Skip to content

2. 数据工程

导读:数据是大模型的"燃料",预训练数据的规模与质量直接决定了模型能力的上限。本章系统地梳理预训练数据的来源、清洗流水线、去重算法、分词器以及数据混合策略,并给出工业级实现要点。

2.1 为什么数据决定一切

在 GPT-3 发布之后的几年里,业界逐步形成了一个共识:模型架构的边际收益已经远远小于数据质量与数量的边际收益。LLaMA-1 用 1.4T tokens 在 7B 参数量级追平了 GPT-3 175B 的效果;LLaMA-3 在保持架构基本不变的前提下,仅靠把数据从 2T 扩大到 15T 就把 8B 模型推到了一个全新的性能水位。

这背后有两个根本原因:

  1. Scaling Laws (Chinchilla 2022) 指出在固定计算预算下,模型参数 N 与训练 token 数 D 应当等比例放大,并且经验比例约为 D20N。任何低于这个数据预算的训练都属于 "under-trained"。
  2. 高质量数据上的训练损失(cross-entropy)与下游任务能力高度相关。FineWeb-Edu (2024) 的实验证明,仅用 1.3T 经过教育质量筛选的 token,就能匹配 4-5T 普通 web 数据的下游性能。

因此,数据工程不是预处理的边角活,而是大模型训练中与算法同等重要的核心系统。本章聚焦如下问题:

  • 数据从哪里来?(数据源)
  • 怎么把脏数据洗干净?(清洗流水线)
  • 怎么避免重复 token 浪费算力?(去重)
  • 怎么把文本变成模型能吃的整数序列?(分词器)
  • 各类数据按什么比例喂给模型?(混合策略)

2.2 数据来源

2.2.1 主流公开数据集

数据集规模来源备注
Common Crawl250B+ 网页 / 月全网爬取最大公开网页语料;2008 年至今
C4 (Colossal Clean Crawled Corpus)750GB / ~200B tokensT5 团队基于 CC 单 dump 清洗简单规则过滤
RefinedWeb5T tokensFalcon 团队基于 CC 重洗强力 URL 黑名单 + 模糊去重
The Pile825GBEleutherAI 整合 22 个子集含 PubMed、ArXiv、Books3、StackExchange 等
RedPajama-V11.2T tokensLLaMA-1 配方复现完整开源管线
RedPajama-V230T tokens84 个 CC dump + 40 多种质量信号最大开源 CC 派生集
Dolma3T tokensAI2 OLMo 训练集完整数据卡 + 处理工具链
FineWeb / FineWeb-Edu15T / 1.3T tokensHuggingFace 高质量 CC 子集教育向通过分类器筛选
SlimPajama627B tokensRedPajama-V1 全局去重后质量高于 RP-V1
The Stack v24TBGitHub 代码600+ 编程语言
Wikipedia20GB / 4B tokensdump多语种百科
Books3 / Project Gutenberg~100GB书籍Books3 因版权问题已下架
PG-19~10GBGutenberg 长文本长上下文评测常用

2.2.2 LLaMA-1 的经典数据配方

LLaMA-1 在 1.4T token 上的混合比例值得研究,因为它完全使用公开数据且效果可复现:

数据源占比epochs备注
CommonCrawl67.0%1.105 个 dump,CCNet 流水线清洗
C415.0%1.06补充更高质量 web
GitHub4.5%0.64仅保留 Apache/BSD/MIT 协议
Wikipedia4.5%2.4520 种语言
Books4.5%2.23Gutenberg + Books3
ArXiv2.5%1.06LaTeX 源文件,移除 bibliography
StackExchange2.0%1.03高 score 答案

值得注意:

  • 整体平均 epoch ≈ 1.07,绝大多数 token 只见过一次
  • Wikipedia 和 Books 这类高质量数据 epoch > 2,是常见的"重复采样"做法
  • 代码占比 4.5% 在当时偏低;从 LLaMA-3 开始普遍提到 15-25%
  • ArXiv 和 StackExchange 是后来"reasoning data" 的雏形

2.2.3 现代数据集的趋势

2024 年后,业界数据策略呈现几个明显趋势:

  • 总量翻倍:从 1-2T 升到 8-15T,DeepSeek-V3 用 14.8T,LLaMA-3 用 15T
  • 代码比例上升:DeepSeek 系列代码占 17%,对推理能力增益显著
  • 数学/科学数据强化:OpenWebMath、Proof-Pile-2、AlgebraicStack 等专项数据集成为标配
  • 多阶段课程:早期通用 + 末期高质量退火,DeepSeek-V3 在 8.1T 通用 token 后,加 1.4T 中英平衡 + 上下文扩展数据
  • 合成数据兴起:Phi-4 几乎全用合成 + 教育数据,挑战"互联网数据为王"

2.3 数据清洗流水线

CommonCrawl 原始 WARC 充满了广告、导航栏、机器生成内容、垃圾文本。清洗流水线把噪声压到 5-10%,是数据质量的关键。我们以 RedPajama / Dolma / RefinedWeb 的现代流水线为例,分阶段拆解:

2.3.1 阶段 1:文本抽取

WARC (Web ARChive) 文件包含 HTTP response 原始字节。文本抽取的核心是从 HTML 中提取主体内容,去掉导航、页脚、广告。

主流工具:

工具特点
trafilaturaPython 库,规则+ML 混合,主体提取效果最好
jusText基于段落分类,速度快
boilerpipeJava,老牌但维护差
ResiliparseC++ 实现,吞吐高

实际生产中,FineWeb 用 trafilatura,RefinedWeb 用 jusText 加自定义规则。提取后保留纯文本,丢弃 HTML 标签、JavaScript、CSS。

2.3.2 阶段 2:语言识别

使用 fastText 的 lid.176.bin 或 Google 的 cld3 进行语言分类。

python
import fasttext
model = fasttext.load_model("lid.176.bin")
labels, probs = model.predict(text.replace("\n", " "), k=1)
lang = labels[0].replace("__label__", "")
confidence = probs[0]
# 通常保留置信度 > 0.65 的样本

陷阱

  • 短文本(< 50 词)置信度普遍偏低,需要更宽松的阈值或先做长度过滤
  • 多语种混合(如中英混合代码)容易误判
  • 古汉语、繁体中文常被误判为日语

LLaMA 等英文模型简单截断为 en;多语种模型(Aya、Qwen-MAX)会保留 fastText 标签作为元数据。

2.3.3 阶段 3:质量过滤

Gopher 启发式规则

DeepMind Gopher (2021) 提出一组简单但极有效的规则:

规则阈值含义
单词数50-100k太短无信息,太长多为机器拼接
平均词长3-10 字符过短像随机字符,过长像数据 dump
哈希符号占比< 10%##### 类装饰内容
省略号结尾占比< 30%截断/低质内容
首字母大写比例< 40%标题党、列表
停用词数≥ 2至少含 "the/of/and/or" 等英语停用词
重复 5-gram 比例< 15%模板化机器文本

这些规则丢弃约 30-40% 的原始 CC 内容。

模型质量分类器

更细粒度的方法是训一个二分类器:

  • 正例:Wikipedia / Books / 高 karma Reddit 帖子 / arXiv 摘要
  • 负例:随机 CC 抽样

模型用 fastText 或简单的小 BERT。对每个文档打分,保留高于阈值的部分(如 P > 0.5)。

FineWeb-Edu 进一步使用 LLM 标注教育价值(0-5 分),训分类器筛选教育向数据,1.3T token 即匹配 5T 通用数据效果。

困惑度过滤

使用一个已训练好的小模型(如 KenLM 5-gram 或 1B-7B 的 Transformer)计算每篇文档的困惑度 PPL,丢弃极高(噪声)和极低(模板化)的文档。

CCNet (Wenzek et al. 2020) 的做法:

  • 在 Wikipedia 上训 KenLM
  • 对 CC 文档打分,按 PPL 分到 head / middle / tail 三个 bucket
  • 高质量训练通常只用 head + middle

2.3.4 阶段 4:去重

去重分精确去重模糊去重

  • 精确去重:检测完全相同的文档/段落(hash 相等)
  • 模糊去重:检测高度相似但不完全相同的文档(如改写、广告变体)

模糊去重是去重的核心,下一节专门讨论。

2.3.5 阶段 5:PII 与有害内容

  • PII (Personally Identifiable Information):邮箱、电话、SSN、信用卡号——正则匹配后用占位符替换
  • NSFW / 暴力:URL 黑名单(如 dsi.ut-capitole.fr/blacklists/)+ 关键词过滤
  • 未成年人保护:CSAM 关键词、robots.txt 强约束
  • 版权敏感:移除付费墙网站、新闻聚合站

这一步通常丢弃 1-5% 的内容,但法律风险远比技术挑战大。

2.3.6 完整流水线示意

WARC.gz


[文本抽取]   trafilatura / jusText        留存 ~50%


[语言识别]   fastText                     留存 ~70%


[Gopher 启发式规则]                       留存 ~60%


[质量分类器]  fastText / 小 BERT          留存 ~50%


[精确去重]   hash 文档                    留存 ~80%


[模糊去重]   MinHashLSH                   留存 ~60%


[PII / 黑名单]                            留存 ~98%


最终干净语料   原始 1TB → ~50GB

关键观察:从原始 CC 到训练用语料,留存率仅 5-10%。这意味着每多 1T 训练 token,就需要 10-20T 的原始爬取数据。


2.4 去重:MinHash 与 SimHash

2.4.1 为什么去重至关重要

Lee et al. (2022) "Deduplicating Training Data Makes Language Models Better" 实验证明:

  • 重复 10 次的文本占 The Pile 的 13%,但训练效果不增反减
  • 去重后的模型在 zero-shot 任务上提升 1-2%
  • 推理时记忆性数据泄露率显著下降

更糟的是,重复数据会让评测集污染训练集(contamination),导致测出虚高分数。

2.4.2 Jaccard 相似度

定义两个文档的 shingle 集合(k-gram 集合):

S(D,k)={Di:i+k:0i|D|k}

Jaccard 相似度:

J(A,B)=|AB||AB|

直接计算所有文档对的 Jaccard 相似度需要 O(n2),不可行。MinHash 提供 O(n) 近似。

2.4.3 MinHash 原理

核心定理:对随机哈希函数 h(将集合元素均匀映射到大整数),有

Pr[minxAh(x)=minyBh(y)]=J(A,B)

直觉:在 AB 中均匀随机选一个元素 xx 落在 AB 的概率正是 |AB|/|AB|minh 的"赢家" 是均匀采样的。

算法

  1. 选择 K 个独立哈希函数 h1,,hK(实际中用一个哈希 + K 对参数 (ai,bi) 的线性变换 hi(x)=(aih(x)+bi)modp
  2. 对每个文档 D,计算签名 sig(D)=(minxS(D)h1(x),,minxS(D)hK(x))
  3. 估计 Jaccard:J^(A,B)=1Ki1[sigi(A)=sigi(B)]

签名只占 K 个 32-bit 整数(如 K=128 → 512 bytes/doc),文档相似度可以在小内存中估计。

2.4.4 LSH(局部敏感哈希)加速

即便有签名,逐对比较仍是 O(n2)Banding LSH 进一步将签名分成 b 个 band,每个 band 含 r 行(K=br)。

  • 两文档在某个 band 上完全相同 → 进入候选对
  • 否则跳过

候选概率为相似度 s 的函数:

P(candidate|s)=1(1sr)b

参数 (b,r) 控制阈值曲线。例如 K=128b=16r=8,相似度 0.7 时候选概率约 0.94,相似度 0.3 时仅 0.001。

python
import numpy as np
from datasketch import MinHash, MinHashLSH

def compute_minhash(doc, num_perm=128, k=5):
    m = MinHash(num_perm=num_perm)
    tokens = doc.split()
    for i in range(len(tokens) - k + 1):
        shingle = " ".join(tokens[i:i+k])
        m.update(shingle.encode("utf8"))
    return m

lsh = MinHashLSH(threshold=0.7, num_perm=128)
for doc_id, doc in corpus:
    m = compute_minhash(doc)
    lsh.insert(doc_id, m)

# 查询
for doc_id, doc in corpus:
    m = compute_minhash(doc)
    duplicates = lsh.query(m)
    # duplicates 包含与该文档 Jaccard >= 0.7 的所有文档 id

生产实现datasketch (Python) 适合数百 GB;TB 级需要 Spark + BigMinHash 或 Google 的 Bigtable 方案。RedPajama-V2 在 Spark 上对 30T token 做 MinHashLSH,单次 run 约 24 小时(千核集群)。

2.4.5 SimHash

Charikar 2002 的另一个 LSH 家族,更适合稠密向量空间:

算法

  1. 对每个 token ti 计算 f-bit 哈希 h(ti)
  2. 维护 f 维向量 v,初始为 0
  3. 对每个 token:第 j 位是 1 → vj+=wiwi 是 token 权重,如 IDF 或 1);第 j 位是 0 → vj=wi
  4. 最终签名:sigj=1[vj>0]

两文档汉明距离 ≤ 3(在 64-bit 签名上)视为近似重复。

特点:

  • 签名极小(64 bit/doc)
  • 单次比较 = popcount(a XOR b),硬件指令一条搞定
  • 但只能处理"加权词袋"模型,丢失顺序信息
  • 适合短文本(如新闻标题去重)

LLaMA、Dolma 等大型 LLM 数据流水线多用 MinHash,因为它能处理 5-gram 等局部顺序信息。

2.4.6 精确 vs 模糊去重的实践

实践中通常组合使用:

  1. 句级精确去重:50 字以上完全相同的句子,全局保留 1 份
  2. 段落级精确去重:md5/sha256 hash
  3. 文档级模糊去重:MinHashLSH,阈值 0.7-0.8
  4. 跨域去重:训练数据 vs 评测数据(避免污染)

SlimPajama 在 RedPajama-V1 (1.2T) 上做全局 MinHashLSH,最终留下 627B。重复率超过 50%——这给了一个直观感受。


2.5 分词器(Tokenizer)

分词器把字符串映射到整数序列。它决定了:

  • 同样字符数的文本占多少 token(压缩率直接影响训练成本)
  • 词表中是否能完整表示某种语言/字符
  • 是否会有 OOV (out-of-vocabulary)

2.5.1 字符 vs 词 vs 子词

最早的尝试:

  • 字符级 (char-level):词表 100-300 个,但序列太长,难以建模长程语义
  • 词级 (word-level):词表 100K+,OOV 严重,无法拆解未见词

子词分词 (subword tokenization) 平衡了二者:高频词作为整体,低频词拆成 morpheme 或字节。BPE、WordPiece、SentencePiece、Unigram 都是子词算法。

2.5.2 BPE 算法

Byte-Pair Encoding (Sennrich et al. 2016) 是最广泛使用的子词算法。

训练

1. 初始化词表 = 256 个字节(或字符)
2. 把语料分成基础单元序列(如 byte 序列)
3. 重复直到词表大小达到目标 V:
   a. 统计所有相邻对 (a, b) 的频率
   b. 选频率最高的对 (a*, b*)
   c. 把语料中所有 a* b* 替换为新 token z
   d. 把 z 加入词表,记录 merge rule (a*, b*) → z

经典示例

语料:aaabdaaabac

  • Step 0:词表 {a, b, c, d},序列 a a a b d a a a b a c
  • 统计相邻对:(a,a):3, (a,b):2, (b,d):1, (d,a):1, (b,a):1, (a,c):1
  • 频率最高 (a,a) → ZZ a b d Z a b a c
  • 下一轮 (a,b):2 最高 → YZ Y d Z Y a c
  • 下一轮 (Z,Y):2 最高 → XX d X a c

3 步后词表为 {a, b, c, d, Z=aa, Y=ab, X=ZY=aabb? no, ZY=aaab},序列长度从 11 压到 5。

编码(推理)

把文本拆为基础单元,按 merge rule 顺序贪心合并:

python
def encode(text, merges):
    tokens = list(text.encode("utf-8"))   # byte 序列
    while True:
        # 找到最早出现的可合并对
        best = None
        for i in range(len(tokens) - 1):
            pair = (tokens[i], tokens[i+1])
            if pair in merges:
                rank = merges[pair]
                if best is None or rank < best[1]:
                    best = (i, rank)
        if best is None:
            break
        i, _ = best
        new_token = merges[(tokens[i], tokens[i+1])]
        tokens = tokens[:i] + [new_token] + tokens[i+2:]
    return tokens

实际生产实现(HuggingFace tokenizerstiktoken)用更高效的字典 + 优先级队列,单线程吞吐 1M token/s 量级。

2.5.3 字节级 BPE (Byte-level BPE)

GPT-2 提出的关键改进:初始词表是 256 个字节,而不是 Unicode 字符。

优点:

  • 完全无 OOV:任何 UTF-8 文本都能被分词
  • 词表初始固定,避免不同语料训出不同基础字符集
  • emoji、罕见汉字也能拆成 byte 序列处理

缺点:

  • 罕见字符占用多个 token(一个生僻汉字 3 byte = 3 token)

LLaMA-3 把词表从 32K 升到 128K,主要就是为了让中、日、韩、阿拉伯语等不被拆得太碎。

2.5.4 SentencePiece

Google 开源工具 (Kudo & Richardson 2018),支持 BPE 与 Unigram 两种算法。两个核心特性:

  • 直接处理原始文本:不需要预分词,把空格视为字符 (U+2581)
  • byte_fallback:罕见字符回退到 UTF-8 字节,保证训练稳定且无 OOV

LLaMA-1/2 使用 SentencePiece BPE,词表 32K。

python
import sentencepiece as spm

spm.SentencePieceTrainer.train(
    input="corpus.txt",
    model_prefix="tokenizer",
    vocab_size=32000,
    character_coverage=0.9995,
    model_type="bpe",
    byte_fallback=True,
    pad_id=0, unk_id=1, bos_id=2, eos_id=3,
)

sp = spm.SentencePieceProcessor()
sp.load("tokenizer.model")
print(sp.encode_as_pieces("Hello, 世界!"))
# ['▁Hello', ',', '▁', '世', '界', '!']

2.5.5 Unigram 算法

Kudo (2018) 提出的另一种子词算法:

  1. 初始化一个词表(如 1M 候选 piece)
  2. 用 EM 算法估计每个 piece 的概率,使语料的 unigram 似然最大化
  3. 每轮丢掉似然贡献最小的 piece 子集(如 10%)
  4. 重复直到达到目标词表大小

特点:

  • 编码时基于概率,可以做 N-best 解码或采样(subword regularization 训练时用)
  • 比 BPE 略慢,但语言建模任务上略好

T5、mT5 用 Unigram;LLaMA、GPT 用 BPE。

2.5.6 Tiktoken (OpenAI)

OpenAI 自研的 BPE 实现,使用正则预分词

python
PAT = r"""'(?i:[sdmt]|ll|ve|re)|[^\r\n\p{L}\p{N}]?+\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]++[\r\n]*|\s*[\r\n]|\s+(?!\S)|\s+"""

正则在 BPE 之前先把文本切成"词",再在每个词内部做 BPE。这样能保证:

  • 数字最多 3 位一组(123 一个 token,12345 拆成 123 45)——避免数学计算时数字编码不一致
  • 标点和空格的语法一致性
  • 不会跨越换行/段落合并

GPT-4 用 cl100k_base(100K 词表);GPT-4o 用 o200k_base(200K);LLaMA-3 改用 tiktoken 风格,词表 128K。

python
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
ids = enc.encode("Hello world! 123456")
print(ids)
print([enc.decode([t]) for t in ids])

2.5.7 主流分词器对比

模型算法词表中文压缩率备注
GPT-2byte-level BPE50,257~1 字 / 1.5 tokenr50k_base
GPT-3byte-level BPE50,281同上p50k_base
GPT-4tiktoken BPE100,277~1 字 / 1.0 tokencl100k_base
GPT-4otiktoken BPE199,997~1 字 / 0.7 tokeno200k_base
LLaMA-1/2SentencePiece BPE32,000~1 字 / 2.0 tokenbyte_fallback
LLaMA-3tiktoken BPE128,256~1 字 / 0.85 token引入预分词
DeepSeek-V3BPE128,815~1 字 / 0.6 token中文优化
Qwen2tiktoken-like BPE152,064~1 字 / 0.55 token多语种平衡
GemmaSentencePiece256,000~1 字 / 0.5 token词表最大

压缩率直接影响训练成本:同样的中文语料,词表 32K 的 LLaMA-2 比 128K 的 LLaMA-3 多用约 2.4 倍 token,训练 FLOPs 也对应增加。这就是为什么 LLaMA-3 特意扩大了词表。

2.5.8 词表设计的工程细节

  • 特殊 token<bos>, <eos>, <pad>, <unk>,以及聊天模板的 <|im_start|>, <|im_end|>
  • 保留 token:预留 256 个未使用 token,未来可以用于工具调用、特殊任务而无需扩词表
  • 空格规范化:决定 "hello"" hello" 是一个 token 还是两个
  • 数字处理:左对齐还是右对齐切分(12345123/45 vs 12/345),影响算术任务
  • 代码格式:制表符、4 空格缩进、\n 是否独立 token,影响代码生成质量

LLaMA-3 在数字、代码、中文上做了针对性优化,是教科书级的工程实践。


2.6 数据混合策略

2.6.1 静态加权

最简单的做法:固定每个域的采样比例,在每个 batch 内按比例采样。LLaMA-1 的配方就是静态加权。

工程实现:把每个数据源切成 shard,按权重抽样(采样有放回),混入数据流。

2.6.2 DoReMi (Xie et al. 2023)

核心思想:用一个小参考模型 + 代理模型,通过分布鲁棒优化 (DRO) 自动找最优域权重。

算法:

  1. 在静态混合上训一个小参考模型 θref
  2. 训一个相同尺寸的代理模型 θ,但每个域 k 的权重 αk 是可学的
  3. 计算每个域的"超额损失":k(θ)k(θref)
  4. 用 DRO 更新权重 αk,让超额损失最大的域权重增加(worst-case 优化)
  5. 把学到的 α 用到大模型训练

DoReMi 在 280B Pile 数据上找到的权重,让下游 1B 模型在 8 个任务上提升 6-10%。

2.6.3 课程学习 (Curriculum)

按"由易到难"或"由通用到专业"组织训练顺序:

  • 早期 (前 80% token):通用 web 文本,学语言基本功
  • 中期:加大代码、数学、科学数据比例
  • 末期 (最后 5-10%):高质量 instruction-style 数据,"退火 (annealing)"

LLaMA-3 405B 训练分两个阶段:

  1. 14T token 通用预训练,上下文 8K
  2. 1T token 长上下文 + 高质量数据,上下文扩到 128K

2.6.4 退火 (Annealing) / 数据切换

最后阶段把数据分布从"通用"切换到"高质量+任务相关",同时把学习率从峰值衰减到接近 0。

类比物理:高温让模型探索(学到广泛知识),低温让模型固化(聚焦关键能力)。

DeepSeek-V3 的退火设计:

  • 8.1T token 主预训练
  • 1T token "decay" 阶段,含更多代码、数学、推理数据
  • 0.3T token 长上下文扩展(YaRN)

2.6.5 数据调度的内存约束

数据混合不只是逻辑问题,还涉及 IO 工程:

  • 全量数据可能 30-50TB,单机放不下,需要分布式存储(如 S3 / GCS)
  • 每个 worker 流式拉取自己 shard,避免重复
  • 跨 epoch 重复采样需要保证不同 worker 不重复 token
  • 训练中途修改混合比例需要保持随机种子兼容

主流框架:

  • HuggingFace datasets:流式 + 分布式
  • NVIDIA Megatron-LM data-mixing:基于 MMap 的 binary index 文件 (.bin + .idx),加载快
  • Mosaic StreamingDataset:S3 友好

2.7 实战:构建一个最小预训练数据集

我们以一个 100GB 中文+英文小型预训练为例,给出完整流程。

2.7.1 数据采集

bash
# 下载 CC dump 索引(CC-MAIN-2024-10)
wget https://data.commoncrawl.org/crawl-data/CC-MAIN-2024-10/wat.paths.gz
zcat wat.paths.gz | head -100 > my_paths.txt

# 流式下载并抽取
python download_warc.py my_paths.txt | trafilatura > raw.jsonl

2.7.2 语言过滤

python
import fasttext
model = fasttext.load_model("lid.176.bin")

with open("raw.jsonl") as fin, open("zh_en.jsonl", "w") as fout:
    for line in fin:
        doc = json.loads(line)
        labels, probs = model.predict(doc["text"][:2000].replace("\n", " "), k=1)
        if probs[0] > 0.65 and labels[0] in ["__label__zh", "__label__en"]:
            doc["lang"] = labels[0][9:]
            fout.write(json.dumps(doc) + "\n")

2.7.3 质量过滤

python
def gopher_filter(doc):
    text = doc["text"]
    words = text.split()
    if len(words) < 50 or len(words) > 100000:
        return False
    avg_len = sum(len(w) for w in words) / len(words)
    if avg_len < 3 or avg_len > 10:
        return False
    if text.count("#") / len(text) > 0.1:
        return False
    return True

2.7.4 MinHash 去重

python
from datasketch import MinHash, MinHashLSH

lsh = MinHashLSH(threshold=0.7, num_perm=128)
seen = set()
for doc_id, doc in enumerate(docs):
    m = compute_minhash(doc["text"], num_perm=128, k=5)
    if not lsh.query(m):
        lsh.insert(doc_id, m)
        seen.add(doc_id)

2.7.5 训练分词器

python
import sentencepiece as spm

spm.SentencePieceTrainer.train(
    input="cleaned.txt",
    model_prefix="my_tokenizer",
    vocab_size=64000,
    character_coverage=0.9999,
    model_type="bpe",
    byte_fallback=True,
    num_threads=64,
)

2.7.6 二进制化

python
import numpy as np

sp = spm.SentencePieceProcessor()
sp.load("my_tokenizer.model")

with open("train.bin", "wb") as f, open("train.idx", "wb") as fidx:
    offsets = [0]
    for doc in docs:
        ids = sp.encode(doc["text"]) + [sp.eos_id()]
        arr = np.array(ids, dtype=np.uint16)
        f.write(arr.tobytes())
        offsets.append(offsets[-1] + len(arr))
    np.save("offsets.npy", np.array(offsets, dtype=np.int64))

训练时 mmap 读取 train.bin,按 offsets 切 sequence,吞吐可达 GB/s。


2.8 数据工程的常见陷阱

陷阱症状对策
评测污染benchmark 分数虚高,实际能力差训练数据与评测集做交叉去重
过度过滤训练 loss 平稳但下游能力差保留多样性,避免风格单一
Tokenizer 不匹配中文/数字编码极长训练前评估压缩率,词表至少 64K
数据顺序固定loss spike 反复出现全局打散,每 epoch 重新洗牌
长尾域被吞数学/代码数据被通用 web 淹没上采样小域(epoch > 1)
缺乏数据卡出问题难以定位每个 shard 标注来源、清洗版本、quality score

2.9 本章小结

本章系统讲解了大模型预训练的数据工程。核心要点:

  1. 数据决定模型上限。Chinchilla 的 20:1 比例和 LLaMA-3 的 15T tokens 都说明数据规模与质量的重要性。
  2. 清洗流水线分多阶段:文本抽取 → 语言识别 → 启发式规则 → 模型分类器 → 去重 → PII 过滤,最终留存率仅 5-10%。
  3. MinHash + LSH 是工业级模糊去重的标准方案,128-bit 签名 + banding LSH 在万亿级数据上可行。
  4. BPE 是分词器主流,配合 byte-level 或 byte_fallback 可消除 OOV;词表大小是中文/多语种压缩率的关键。
  5. 数据混合从静态加权演进到 DoReMi、课程学习、退火等动态策略,最后阶段的数据切换至关重要。

下一章我们将深入 Transformer 架构本身,看模型如何高效地消化这些 token。


2.10 思考题

  1. MinHash 参数选择:若你希望在 Jaccard ≥ 0.8 时去重,签名长度 K=128,请推导合适的 (b,r) 参数,使得 0.8 时候选概率 ≥ 0.95,0.5 时候选概率 ≤ 0.05。提示:使用 P(s)=1(1sr)b,搜索整数解。

  2. 分词器影响估算:假设你有 1T 中文字符的训练语料。LLaMA-2 (32K, 1 字 ≈ 2 token) 和 Qwen2 (152K, 1 字 ≈ 0.55 token) 训练同一个 7B 模型,按 C=6ND 的 FLOPs 公式,两者的训练计算量相差几倍?这对硬件预算有何意义?

  3. 数据污染检测:你训完一个模型,发现它在 GSM8K 上得分异常高。请设计一个 MinHash + LSH 的方案,定量评估训练数据中包含多少 GSM8K 题目(或其改写版本),并提出阈值与处理策略。

基于 MIT 协议发布