预训练数据 Pipeline:爬取→清洗→去重→过滤→配比¶
更新日期:2026-04-26
本文目标:能从零搭建一套工业级预训练数据处理流水线。这是决定模型质量的 80%。本页是综述 + 子页索引;每个环节的工程深读见对应子页。
一、全流程概览¶
flowchart LR
src["原始<br/>数据源"] --> ext["1.爬取<br/>提取"]
ext --> lang["2.语言<br/>检测"]
lang --> rule["3.规则<br/>过滤"]
rule --> qual["4.质量<br/>打分"]
qual --> dedup["5.去重"]
dedup --> safe["6.PII<br/>有害"]
safe --> tok["7.Tokenize"]
tok --> mix["8.配比<br/>打包"]
mix --> bin["9.bin/idx"]
classDef io fill:#f5f3eb,stroke:#bdb9ab,color:#1a1a1a;
classDef op fill:#fff,stroke:#cc785c,color:#1a1a1a;
class src,bin io
class ext,lang,rule,qual,dedup,safe,tok,mix op
9 个阶段细节 — 来源(CC / GitHub / arXiv / Books / 私域),质量打分(分类器 / PPL / RM),去重(exact / MinHash / SemHash),后续小节展开。
子页深读索引¶
| 环节 | 关键问题 | 子页 |
|---|---|---|
| 爬取工程 | 怎么挖深?反检测对抗(CF / TLS / 浏览器指纹)?家庭 IP 池?无头浏览器?爬虫覆盖率 vs Google?蜜罐如何识别? | crawler |
| HTML 解析 | trafilatura / resiliparse / magic-html / goose 怎么选?per-domain 自定义解析器?agentic parser 何时用? | parser |
| 私域数据 | CSDN / Reddit / Twitter / 微信公众号 / GitHub 私库怎么搞? | private-domain |
| 互联网存档 | Common Crawl / Wayback / RedPajama / FineWeb / DCLM 各自数据量、链接、离全网差距多少? | archives |
| 去重 | exact / MinHash / SemHash 关系?分桶分质量 reward model 维度? | dedup |
| 商业供应商 | Together / Datology / HuggingFace / CC Foundation 等付费数据源 | vendors |
| 配比 | 典型方案(CC : code : math : book : 多语言)+ 为什么 | mixture |
二、数据源¶
2.1 Common Crawl 处理¶
# Common Crawl 是 LLM 预训练数据的绝对主体
# 一个 crawl (~每月一次) 约 3-5 PB 压缩后
# 包含 WARC (网页存档) 和 WET (纯文本提取) 格式
# 推荐工具:
# - cc-pyspark: 分布式处理 CC 数据
# - trafilatura: 从 HTML 提取正文(比 CC 自带的 WET 好很多)
# - resiliparse: 高性能 HTML → 文本
# 关键: 不要用 WET 文件! 它的提取质量很差
# 而是从 WARC 文件用 trafilatura 重新提取
class CommonCrawlProcessor:
def process_warc(self, warc_path):
for record in warc_reader(warc_path):
if record.rec_type != 'response':
continue
html = record.content
# 用 trafilatura 提取正文 (比正则好很多)
text = trafilatura.extract(html,
include_tables=True,
include_comments=False,
deduplicate=True)
if text and len(text) > 200:
yield {
'url': record.rec_headers['WARC-Target-URI'],
'text': text,
'timestamp': record.rec_headers['WARC-Date'],
}
三、数据清洗¶
3.1 规则过滤¶
class RuleFilter:
def should_keep(self, doc):
text = doc['text']
# === 长度过滤 ===
if len(text) < 200: return False # 太短
if len(text) > 1_000_000: return False # 太长(可能是数据库 dump)
words = text.split()
if len(words) < 50: return False # 词数太少
# === 字符质量 ===
alpha_ratio = sum(c.isalpha() for c in text) / len(text)
if alpha_ratio < 0.5: return False # 非文本内容(代码/数据除外)
# === 重复检测 ===
lines = text.split('\n')
unique_lines = set(lines)
if len(unique_lines) / len(lines) < 0.3: return False # 大量重复行
# === 特殊模式 ===
if 'lorem ipsum' in text.lower(): return False # 占位文本
if text.count('©') > 5: return False # 版权声明页
if '404' in text[:100] and 'not found' in text[:200].lower():
return False # 404 页面
# === n-gram 重复率 (关键指标) ===
# 计算文档内 n-gram 的重复比例
ngrams_5 = get_ngrams(words, 5)
dup_ratio = 1 - len(set(ngrams_5)) / max(len(ngrams_5), 1)
if dup_ratio > 0.5: return False # 内容高度重复
return True
3.2 质量分类器¶
方法 1:fastText 分类器
-
正样本:Wikipedia、教科书、高质量博客
-
负样本:随机 CC 文本
-
推理极快(百万 doc/min)
quality_model = fasttext.load_model('quality_classifier.bin')
score = quality_model.predict(text)[1][0] # 0-1 分数
if score < 0.5: discard()
方法 2:Perplexity 过滤
用一个在高质量数据上训练的小 LM 计算 PPL。低 PPL 表示数据符合"高质量文本"的分布,高 PPL 则可能是噪声或乱码。
方法 3:多维打分(最佳实践)
同时使用规则过滤 + 质量分类器 + PPL + 教育价值分数,最终分数为加权组合。
3.3 语言检测¶
# 工具: fasttext lid.176.bin (Facebook 的语言检测模型)
# 支持 176 种语言
lang_model = fasttext.load_model('lid.176.bin')
lang, confidence = lang_model.predict(text[:500])
# 按语言分流
if lang == '__label__en' and confidence > 0.8:
en_bucket.add(doc)
elif lang == '__label__zh' and confidence > 0.7:
zh_bucket.add(doc)
# ...
四、去重¶
4.1 去重层次¶
4.2 MinHash + LSH 实现¶
# MinHash: 将文档转为固定大小的签名, 签名相似度 ≈ Jaccard 相似度
def minhash_signature(text, num_hashes=128):
# 将文本分为 n-gram (通常 5-gram)
shingles = set(get_ngrams(text.split(), 5))
signature = []
for i in range(num_hashes):
min_hash = inf
for shingle in shingles:
h = hash_function(shingle, seed=i)
min_hash = min(min_hash, h)
signature.append(min_hash)
return signature
# LSH: 将签名分为 bands, 任一 band 完全匹配 → 候选对
def lsh_dedup(signatures, num_bands=20, rows_per_band=6):
# num_bands × rows_per_band = num_hashes = 120
buckets = defaultdict(list)
for doc_id, sig in enumerate(signatures):
for band in range(num_bands):
start = band * rows_per_band
band_sig = tuple(sig[start:start+rows_per_band])
bucket_key = (band, hash(band_sig))
buckets[bucket_key].append(doc_id)
# 同一 bucket 内的文档是近似重复候选
duplicates = set()
for bucket in buckets.values():
if len(bucket) > 1:
# 保留最早/最长的, 其余标记为重复
for doc_id in bucket[1:]:
duplicates.add(doc_id)
return duplicates
# 工业级工具: datasketch, text-dedup, LSHBloom
4.3 SoftDedup(2024 新方向)¶
# 传统去重: 重复 → 删除
# SoftDedup: 重复 → 降权 (训练时 sampling 概率降低)
# 好处:
# 1. 不丢失信息 (可能某些"重复"文档有独特的上下文)
# 2. 更平滑的数据分布
# 3. 避免去重阈值敏感性
def softdedup_weight(doc, cluster_size):
# cluster_size: 这个文档的近似重复数
weight = 1.0 / sqrt(cluster_size) # 重复越多,权重越低
return weight
五、PII 和有害内容过滤¶
class SafetyFilter:
def filter_pii(self, text):
# 正则移除邮箱、电话、身份证号等
text = re.sub(r'\b[\w.+-]+@[\w-]+\.[\w.]+\b', '[EMAIL]', text)
text = re.sub(r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b', '[PHONE]', text)
text = re.sub(r'\b\d{3}-\d{2}-\d{4}\b', '[SSN]', text) # 美国 SSN
text = re.sub(r'\b\d{17}[\dXx]\b', '[ID]', text) # 中国身份证
return text
def filter_toxic(self, text):
# 用毒性分类器打分
toxicity = toxicity_model.predict(text)
if toxicity > 0.8:
return None # 高毒性 → 丢弃
return text
六、数据配比¶
6.1 典型配比¶
6.2 配比策略¶
七、Tokenize & 打包¶
7.1 Tokenize¶
# 通常使用 SentencePiece 训练 BPE tokenizer
import sentencepiece as spm
# 训练 tokenizer
spm.SentencePieceTrainer.train(
input='sampled_corpus.txt', # 从全量数据采样的子集
model_prefix='tokenizer',
vocab_size=128000, # LLaMA-3: 128K vocab
model_type='bpe',
byte_fallback=True, # 处理未知字符
character_coverage=0.9995,
num_threads=64,
split_by_unicode_script=True, # 按 unicode 脚本分割 (中日韩分开)
treat_whitespace_as_suffix=False,
)
7.2 打包为 Megatron 格式¶
Megatron 预处理脚本:
python tools/preprocess_data.py \
--input data.jsonl \
--output-prefix my_data \
--tokenizer-model tokenizer.model \
--workers 64 \
--append-eod # 文档末尾加 EOD token
输出文件:
-
my_data_text_document.bin— token IDs (uint16/uint32) -
my_data_text_document.idx— 文档索引 (offset + length)
多数据集混合: 训练时用权重参数混合多个数据集:
--data-path "0.7 web_data 0.15 code_data 0.15 math_data"
八、追问延伸¶
| 问题 | 方向 | 为什么 / 原理 | 详见 |
|---|---|---|---|
| 去重应该在 tokenize 前还是后? | 前。在原始文本阶段做去重 | 文本阶段可以用 n-gram / MinHash 高效计算相似度;tokenize 后的 token ID 序列丧失了词汇语义信息,去重效果差且计算浪费 | - |
| 数据配比怎么调? | 先跑小模型实验(1B 规模,不同配比),看下游 eval 分数差异 | Scaling law 研究表明小模型的最优配比趋势可迁移到大模型;用 1B 模型跑 grid search 成本仅为 70B 的 1/70 | D3 |
| 合成数据混入预训练安全吗? | 安全,但需控制比例 (<30%) 避免 model collapse | 过多合成数据导致模型学习自身生成分布的偏差,分布逐代缩窄(model collapse);真实数据是"锚点" | B4 |
| 代码数据需要特殊处理吗? | 需要:代码专用去重 + 编程语言检测 + 许可证过滤 | 代码 fork/template 导致重复率远高于自然语言(可达 70%+);许可证不合规会带来法律风险 | B1 补充 |
| 多语言数据怎么配? | 按目标语言能力需求,中文通常 10-30%,小语种 1-5% | 语言能力与该语言 token 占比强相关;但过度倾斜会导致其他语言退化(零和博弈) | B2 |
参考链接¶
↑ 上级 · B. 数据工程