跳转至

预训练数据 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 则可能是噪声或乱码。

ppl = small_lm.perplexity(text)
if ppl > 1000: discard()  # 阈值需要根据数据调整

方法 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. 数据工程