Data Preprocessing: из чего 'готовят' данные для LLM
opensourceaillmdatapreprocessingit
Введение: данные — это новое нефть
LLM не лучше своих данных. Качество данных определяет качество модели. Data preprocessing — самый дорогой и важный этап.
Факт: Llama-3 обучался на 15 трлн токенов. Сбор и очистка — 80% времени проекта.
Уровень 1: Откуда берутся данные?
Источники данных
1. Веб-скрейпинг:
- Common Crawl: 400+ млрд веб-страниц
- Wikipedia: качественный текст
- News APIs: статьи, новости
- Reddit, StackOverflow: форумы
2. Книги:
- Project Gutenberg: 70K+ книг
- Books3 (The Pile): книги разных жанров
3. Код:
- GitHub: 200M+ репозиториев
- StarCoder: 80+ языков программирования
4. Диалоги:
- Alpaca, ShareGPT: QA пары
- Anthropic helpful-harmless: диалоги
5. Мультимодальные:
- LAION: изображения + подписи
- YouTube: субтитры + видео
Common Crawl — основа всего
Common Crawl:
400+ млрд URL
100+ PB данных
Обновляется каждые 2 недели
Структура:
WET (Web Entity Text):
<http://example.com>
<date>20230101120000</date>
<text xml:lang=en>Example text...</text>
WARC (Web Archive):
.warc.gz файлы
Полные HTTP ответы
10-50 GB каждый
Уровень 2: Tokenization — текст → числа
Что такое Tokenizer?
Текст → токены → token IDs → модель
"Hello, world!" → ["Hello", ",", " world", "!"]
→ [15496, 11, 1917, 995]
Почему не слова?
"unhappiness" = "un" + "happi" + "ness"
Редкие слова → неизвестны
Subword: баланс между vocab size и coverage
Почему не буквы?
26 букв → 26^50 комбинаций для 50 символов
Слишком долго, слишком много токенов
BPE (Byte-Pair Encoding)
# Алгоритм BPE:
# 1. Начинаем с символов
# 2. Итерируем: сливаем самые частые пары
# Исходные частоты:
# "h": 100, "e": 120, "l": 150, "o": 90
# "he": 80, "el": 70, "ll": 95, "lo": 60
# "lo" и "ll" - самые частые пары
# Шаг 1: сливаем "ll" -> "ll"
# Шаг 2: сливаем "lo" -> "lo"
# ... повторяем K раз (K = 50K для Llama)
# Итоговый vocab: 50K токенов
# Покрытие: 95% английского за 50K токенов
# 99.5% за 100K токенов
SentencePiece (для мультиязычности)
# SentencePiece: токенизация на уровне Unicode
import sentencepiece as spm
sp = spm.SentencePieceProcessor()
sp.train("data.txt", vocab_size=32000, model_type="bpe")
# Результат:
# "Hello, мир!" -> ["Hello", ",", "▁", "мир", "!"]
# -> [15496, 11, 1917, 45231, 995]
# ▁ = space marker
# Работает со всеми языками
Llama Tokenizer
Llama 3: 128K токенов
Специальные токены:
<|begin_of_text|> = 128000
<|end_of_text|> = 128001
<|finetune|> = 128004
<|reserved_special_token_0|> = 128255
Пример:
"Hello, world!"
-> [128000, 2924, 198, 10713, 732, 220]
-> "<|begin_of_text|>Hello", ",", " world", "!"
Уровень 3: Очистка текста
Удаление мусора
def clean_text(text):
# 1. Unicode normalization
text = unicodedata.normalize('NFC', text)
# 2. Remove control characters
text = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', text)
# 3. Remove duplicate characters
text = re.sub(r'(.)\1{7,}', r'\1\1\1', text)
# 4. Remove broken lines
text = re.sub(r'\n{4,}', '\n\n\n', text)
# 5. Remove URLs
text = re.sub(r'http\S+|www\.\S+', '', text)
# 6. Remove email addresses
text = re.sub(r'\S+@\S+', '', text)
return text
Language Detection
# Detect language - filter non-English (if needed)
from langdetect import detect
text = "Это русский текст"
lang = detect(text) # 'ru'
# Filter:
if lang not in ['en', 'ru', 'de', 'fr']:
skip(text) # не хотим этот язык
# FastText language ID (быстрее):
import fasttext
model = fasttext.load_model('lid.176.bin')
lang, prob = model.predict(text)
# __label__ru, 0.99
Perplexity Filter
# Быстрая модель оценивает "качество" текста
# Низкий perplexity = хороший текст
# Высокий perplexity = мусор
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained('gpt2')
tokenizer = AutoTokenizer.from_pretrained('gpt2')
def perplexity(text):
inputs = tokenizer(text, return_tensors='pt')
with torch.no_grad():
outputs = model(**inputs, labels=inputs['input_ids'])
loss = outputs.loss
return torch.exp(loss)
# Примеры:
# perplexity("The cat sat on the mat") = 5.2
# perplexity("asdf qwer zxcv jkl;") = 15000
# perplexity("FREE MONEY CLICK HERE") = 500
Уровень 4: Deduplication
Почему дубликаты — это плохо?
Дубликаты в данных:
Модель "запоминает" дубликаты
Overfitting на конкретных примерах
Плохая generalization
Leak тестовых данных в training
Пример:
Если "Как написать бинарный поиск" встречается 1000 раз
-> Модель запомнит ответ наизусть
-> Не научится reasoning
Exact Deduplication
# Полное удаление дубликатов
from datasketch import MinHash, MinHashLSH
lsh = MinHashLSH(threshold=0.8, num_perm=128)
# Индексируем все документы
for doc_id, doc in documents.items():
m = MinHash(num_perm=128)
for word in doc.split():
m.update(word.encode('utf8'))
lsh.insert(doc_id, m)
# Находим дубликаты
duplicates = {}
for doc_id, doc in documents.items():
close_matches = lsh.query(m)
if close_matches:
duplicates[doc_id] = close_matches
# Удаляем: оставляем только один экземпляр
Fingerprint Deduplication (Llama)
# Llama использует fingerprint deduplication
# Hash of normalized whitespace
def fingerprint(text):
# Normalize whitespace
text = ' '.join(text.split())
return hashlib.sha256(text.encode()).hexdigest()
# Document-level dedup:
# hash(document) -> keep first, drop rest
# Token-level dedup (inside document):
# 128-token sliding window
# hash(window) -> если видел, пропускаем
Уровень 5: Data Mixing
Почему смешиваем данные?
Разные типы данных = разные навыки
Wikipedia: факты, энциклопедические знания
Books: повествование, стиль, длинные контексты
Web: разговорный язык, разнообразие
Code: логика, структура, точность
Math: reasoning, логика
Mix = балансировка разных источников
Примеры Mix'ов
Llama-3 Mix:
85% Web text
5% Books
5% Code
3% Math
2% Dialogues
StarCoder Mix:
70% Code
20% Documentation
10% Comments
RedPajama Mix:
60% Web
15% Books
10% Code
10% Wikipedia
05% Social
Weight Balancing
# Взвешенное смешивание
# Не просто 50/50, а с весами
mix_config = {
'wikipedia': 0.30,
'books': 0.15,
'web': 0.25,
'code': 0.20,
'math': 0.10,
}
# Sampling:
def sample_dataset(mix_config):
weights = list(mix_config.values())
keys = list(mix_config.keys()) return np.random.choice(keys, p=weights)
# На эпохе: перемешиваем весь датасет
# с учетом весов
Уровень 6: Filtering — что убираем
Toxicity Filter
# Убираем токсичный контент
from transformers import pipeline
classifier = pipeline("text-classification",
model="unitary/toxic-bert")
def is_toxic(text):
result = classifier(text)[0]
return result[1]['toxic'] > 0.5
# Filter:
if is_toxic(text):
skip(text) # не хотим в training data
PII Detection
# Убираем персональные данные
import presidio_analyzer
analyzer = AnalyzerEngine()
text = "Мой телефон: +7-999-123-45-67"
results = analyzer.analyze(text, language='ru')
# results:
# Entity: PHONE_NUMBER
# Start: 13, End: 31
# Score: 0.95
# Redact:
text = analyzer.redact(text, results)
# "Мой телефон: [PHONE_NUMBER]"
Quality Scoring
# Multi-factor quality score
def quality_score(text):
score = 0
# 1. Length
if len(text) > 200: score += 1
if len(text) > 1000: score += 1
# 2. Perplexity (GPT-2)
if perplexity(text) < 100: score += 2
# 3. Language confidence
lang, prob = detect_lang(text)
if prob > 0.9: score += 1
# 4. Readability
if readability_score(text) > 0.5: score += 1
# 5. No toxic content
if not is_toxic(text): score += 1
# Total: 0-7
# score >= 5: keep
# score < 3: discard
return score
Уровень 7: Structuring и Formatting
Форматирование для обучения
# Формат для instruction tuning
# Raw data:
{"text": "Как работает градиентный спуск?"}
# Formatted:
{
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Как работает градиентный спуск?"},
{"role": "assistant", "content": "Градиентный спуск - это..."}
]
}
# Для Chat models:
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
You are helpful.<|eot_id|><|start_header_id|>user<|end_header_id|>
How does it work?<|eot_id|><|start_header_id|>assistant<|end_header_id|>
It works by...<|eot_id|>
Dataset Formats
1. Raw Text:
.txt файлы, один документ = одна строка
2. JSONL:
{"text": "..."}
{"text": "..."}
3. Parquet:
columnar format, сжатие
faster I/O для больших датасетов
4. WebDataset:
.tar файлы
для distributed training
Уровень 8: Pipeline — полный цикл
Полный preprocessing pipeline
Input (100 TB raw data)
|
v
[1] Collection -----> 10 TB (filtered sources)
|
v
[2] Dedup -----> 8 TB (duplicates removed)
|
v
[3] Clean -----> 6 TB (toxic, PII removed)
|
v
[4] Tokenize -----> 6 TB (token IDs)
|
v
[5] Quality -----> 3 TB (low quality removed)
|
v
[6] Mix -----> 3 TB (balanced mix)
|
v
[7] Format -----> 3 TB (ready for training)
Пример: свой pipeline
import multiprocessing as mp
def preprocess_pipeline(raw_dir, output_dir):
# Step 1: Collect
raw_files = collect(raw_dir)
print(f"Collected: {len(raw_files)} files")
# Step 2: Clean (parallel)
cleaned = mp.Pool().map(clean_text, raw_files)
print(f"Cleaned: {len(cleaned)} documents")
# Step 3: Dedup
cleaned = minhash_dedup(cleaned)
print(f"After dedup: {len(cleaned)} documents")
# Step 4: Quality filter
cleaned = [t for t in cleaned if quality_score(t) >= 5]
print(f"After quality: {len(cleaned)} documents")
# Step 5: Tokenize
tokenized = [tokenizer.encode(t) for t in cleaned]
print(f"Tokenized: {sum(len(t) for t in tokenized)} tokens")
# Step 6: Save
save(tokenized, output_dir)
print("Done!")
Уровень 9: Metrics — как оцениваем?
Метрики качества данных
1. Coverage:
- Сколько уникальных токенов в vocab
- Llama-3: 128K tokens
2. Dedup rate:
- Сколько % удалили как дубликаты
- Llama-3: ~30% dedup
3. Quality score:
- Средний perplexity
- Средний quality score
4. Token efficiency:
- Сколько % токенов — stop words
- Цель: < 20% stop words
5. Language distribution:
- % каждого языка
- Цель: баланс
Dataset Cards
# Llama-3 Dataset Card
Total tokens: 15 trillion
Documents: 15 trillion / ~4 = 3.75 trillion tokens / doc
Sources:
Web: 85%
Books: 5%
Code: 5%
Math: 3%
Dialogues: 2%
Languages: 30+ languages
Top 5: EN, DE, FR, ES, IT
Dedup:
Document-level: yes
Token-level: yes (128-token windows)
Quality:
Perplexity filter: yes
Toxicity filter: yes
PII removal: yes
Уровень 10: Tools и фреймворки
Популярные инструменты
1. Apache Spark:
- Distributed processing
- 100 TB + данные
- Spark NLP для text processing
2. Ray:
- Distributed Python
- Ray Data для preprocessing
- Easy to use
3. DataBox:
- Amazon DataBox
- Physical data transfer
- 100 TB per device
4. Apache Beam:
- Unified programming model
- Runs on Spark, Flink, etc.
5. DuckDB:
- SQL on JSON/Parquet
- Super fast local queries
Быстрый старт
# Минимальный pipeline за 50 строк
import duckdb
from datasketch import MinHashLSH
from transformers import pipeline
# 1. Load
con = duckdb.connect()
con.execute("""
CREATE TABLE docs AS
SELECT * FROM read_json_auto('data/*.json')
""")
# 2. Clean
con.execute("""
UPDATE docs
SET text = regexp_replace(text, '[\x00-\x1F]', '')
WHERE length(text) > 100
""")
# 3. Dedup
docs = con.execute("SELECT * FROM docs").fetchall()
lsh = MinHashLSH(threshold=0.9)
for i, doc in enumerate(docs):
m = MinHash()
for word in doc[1].split():
m.update(word.encode())
lsh.insert(f'doc_{i}', m)
# 4. Save
clean_docs = [d for i, d in enumerate(docs)
if i not in duplicates]
with open('cleaned.jsonl', 'w') as f:
for doc in clean_docs:
f.write(json.dumps(doc) + '\n')
Summary
Data Preprocessing = 80% работы над LLM
10 уровней:
1. Collection - откуда берем данные
2. Tokenization - текст -> числа
3. Cleaning - убираем мусор
4. Dedup - удаляем дубликаты
5. Mixing - балансируем источники
6. Filtering - toxicity, PII, quality
7. Structuring - формат для обучения
8. Pipeline - полный цикл
9. Metrics - как оцениваем
10. Tools - чем делаем
Главное правило:
Garbage In = Garbage Out
Качество данных > Количество данных