Пайплайн очистки данных для 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 моделей разного размера. Получите консультацию по очистке датасета — мы подготовим индивидуальный пайплайн.







