Пайплайн очистки данных для fine-tuning LLM
Представьте: вы собрали 100 000 примеров для дообучения LLaMA 3, но модель выдаёт бессвязные ответы и галлюцинирует на каждом третьем запросе. Причина — грязные данные: 40% дубликатов, 15% содержат персональные данные, ещё 10% — токсичный контент. Без качественной очистки fine-tuning не даст нужного результата.
Мы разработали пайплайн, который за 10–14 дней превращает сырой датасет в чистый, готовый к обучению. MinHash LSH для дедупликации работает в 10 раз быстрее попарного сравнения при поиске near-duplicates на датасетах из 50 000 примеров. А фильтрация токсичности через Detoxify снижает вероятность нежелательных ответов модели на 25% по сравнению с простым регулярным выражением.
Почему стандартная очистка не подходит для LLM?
Тексты для fine-tuning содержат специфические артефакты: HTML-теги (если собирали с веба), Unicode-вариации, мета-комментарии моделей вроде «As an AI language model...». Простое удаление знаков препинания не решает проблему. Нужна многослойная фильтрация с учётом контекста. Например, PII-детекция требует не только регекспов, но и NER-модели (spaCy), чтобы найти «Джон Доу, ул. Ленина» — это не менее важно, чем номера карт. Перед запуском пайплайна рекомендуется ознакомиться с best practices из документации Hugging Face Datasets.
Как мы строим пайплайн очистки
Пайплайн состоит из последовательных этапов, каждый из которых проверяет и трансформирует пример. Критично не переусердствовать: чрезмерная чистка снижает разнообразие данных.
import re
import unicodedata
from dataclasses import dataclass
@dataclass
class CleaningResult:
original: str
cleaned: str
removed: bool
removal_reason: str = None
class TextCleaner:
def clean(self, text: str) -> CleaningResult:
cleaned = text
# 1. Нормализация Unicode
cleaned = unicodedata.normalize('NFKC', cleaned)
# 2. Удаление HTML/XML тегов
cleaned = re.sub(r'<[^>]+>', ' ', cleaned)
# 3. Очистка URL (опционально — заменяем на placeholder)
cleaned = re.sub(
r'https?://[^\s]+', '[URL]', cleaned
)
# 4. Нормализация пробелов
cleaned = re.sub(r'\s+', ' ', cleaned).strip()
# 5. Удаление повторяющихся символов (ааааааа → а)
cleaned = re.sub(r'(.)\1{4,}', r'\1\1', cleaned)
# Проверка на минимальную длину
if len(cleaned.split()) < 3:
return CleaningResult(text, cleaned, True, "too_short")
return CleaningResult(text, cleaned, False)
class DataFilter:
def __init__(self):
# Токсичность (можно использовать detoxify или fasttext)
from detoxify import Detoxify
self.toxicity_model = Detoxify('multilingual')
def is_toxic(self, text: str, threshold: float = 0.7) -> bool:
result = self.toxicity_model.predict(text)
return result['toxicity'] > threshold
def has_pii(self, text: str) -> bool:
"""Простая эвристика для PII детекции"""
patterns = [
r'\b\d{3}-\d{2}-\d{4}\b', # SSN
r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', # Email
r'\b(?:\+7|8)?[\s-]?\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{2}[\s-]?\d{2}\b', # RU phone
r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b', # Credit card
]
for pattern in patterns:
if re.search(pattern, text):
return True
return False
Пошаговая настройка пайплайна
- Определите пороги фильтрации. Для токсичности используйте threshold 0.7 — это даёт баланс между удалением плохого контента и сохранением полезного. Для дублей установите схожесть 0.8.
- Выберите алгоритм дедупликации. Для точных дублей — exact matching, для near-duplicates — MinHash LSH. SimHash подходит для потоковой обработки, но даёт больше ложных срабатываний.
- Запустите тестовый прогон на 1000 примерах. Проверьте метрики: количество удалённых, тип-токен-рацио, остаточную токсичность. Если всё в норме — запускайте полный датасет.
Очистка output полей
Ответы моделей-помощников часто содержат нежелательные вступления: «Certainly! Here is my response». Алгоритм определяет эти шаблоны и обрезает их, оставляя только полезный контент.
class OutputCleaner:
def clean_output(self, output: str, task_type: str) -> tuple[str, bool]:
cleaned = output.strip()
# Удаление нежелательных фраз модели
unwanted_starts = [
"As an AI language model",
"As a helpful assistant",
"I don't have access to real-time",
"I cannot browse the internet",
"Certainly! Here",
"Of course! I'd be happy to",
]
for phrase in unwanted_starts:
if cleaned.lower().startswith(phrase.lower()):
# Удаляем вступительную фразу
cleaned = cleaned[len(phrase):].lstrip('.,! ')
# Проверка: output не должен содержать meta-комментарии
meta_indicators = [
"Note: This is a fictional",
"[This response was",
"Disclaimer:",
]
for indicator in meta_indicators:
if indicator in cleaned:
idx = cleaned.find(indicator)
cleaned = cleaned[:idx].strip()
# Минимальная длина
if len(cleaned.split()) < 5:
return cleaned, True # Пометить для удаления
return cleaned, False
Детекция дублей разных уровней
Для точных дублей используем хеширование, для near-duplicates — MinHash LSH. Порог схожести 0.8 отсекает почти идентичные примеры, но сохраняет вариативность.
from datasketch import MinHash, MinHashLSH
def find_near_duplicates(texts: list[str],
threshold: float = 0.8) -> list[tuple]:
"""MinHash LSH для эффективного поиска near-duplicates O(n log n)"""
lsh = MinHashLSH(threshold=threshold, num_perm=128)
minhashes = {}
for i, text in enumerate(texts):
m = MinHash(num_perm=128)
for word in text.lower().split():
m.update(word.encode('utf8'))
lsh.insert(f"doc_{i}", m)
minhashes[f"doc_{i}"] = m
duplicates = []
for i, text in enumerate(texts):
key = f"doc_{i}"
result = lsh.query(minhashes[key])
result.remove(key)
if result:
duplicates.append((i, [int(r.split('_')[1]) for r in result]))
return duplicates
Сравнение методов дедупликации
| Метод | Скорость | Точность | Применение |
|---|---|---|---|
| Exact matching | O(n) | 100% | Точные дубли |
| MinHash LSH | O(n log n) | ~95% | Near-duplicates |
| SimHash | O(n) | ~90% | Быстрая оценка |
Статистика после очистки
После пайплайна обязательно проверяем метрики:
| Метрика | Норма | Зачем |
|---|---|---|
| Удалено примеров | 15–30% | Контроль агрессивности очистки |
| Тип токенов | >5 млн | Достаточно для fine-tuning |
| Type-token ratio | >0.5 | Достаточное разнообразие |
| Покрытие задач | >90% | Все нужные сценарии |
| Токсичность | <1% | Безопасность модели |
Типичный результат: из 50 000 сырых примеров после очистки остаётся 35 000–42 000 высококачественных. Снижение объёма на 15–30% — норма, и итоговое качество модели от этого только улучшается. По сравнению с грубой очисткой (только регекспы), точность fine-tuning возрастает на 15–20%. Частая проблема — несбалансированность классов: если в датасете 90% примеров с позитивным тоном, модель не научится отвечать на негативные запросы. Мы применяем стратифицированную выборку и аугментацию редких классов. Также важно удалять стоп-слова, характерные для LLM: 'As a language model', 'I cannot', 'I think'. Это снижает шум на 5–10%.
Что входит в работу
Мы подготавливаем полный пайплайн очистки под ваш датасет:
- Анализ сырых данных (распределение длин, язык, токсичность)
- Настройка фильтров под вашу задачу (RAG, генерация, классификация)
- Дедупликация и удаление PII
- Нормализация и токенизация
- Отчёт с метриками и визуализациями
- Документация пайплайна и конфигурация
- Обучение вашей команды
Сроки — от 10 до 14 рабочих дней в зависимости от объёма. Свяжитесь с нами для оценки вашего проекта — мы гарантируем конфиденциальность и качество результата. Наш опыт: более 5 лет в NLP, более 20 проектов по fine-tuning моделей разного размера. Получите консультацию по очистке датасета — мы подготовим индивидуальный пайплайн.







