2. 数据工程
导读:数据是大模型的"燃料",预训练数据的规模与质量直接决定了模型能力的上限。本章系统地梳理预训练数据的来源、清洗流水线、去重算法、分词器以及数据混合策略,并给出工业级实现要点。
2.1 为什么数据决定一切
在 GPT-3 发布之后的几年里,业界逐步形成了一个共识:模型架构的边际收益已经远远小于数据质量与数量的边际收益。LLaMA-1 用 1.4T tokens 在 7B 参数量级追平了 GPT-3 175B 的效果;LLaMA-3 在保持架构基本不变的前提下,仅靠把数据从 2T 扩大到 15T 就把 8B 模型推到了一个全新的性能水位。
这背后有两个根本原因:
- Scaling Laws (Chinchilla 2022) 指出在固定计算预算下,模型参数
与训练 token 数 应当等比例放大,并且经验比例约为 。任何低于这个数据预算的训练都属于 "under-trained"。 - 高质量数据上的训练损失(cross-entropy)与下游任务能力高度相关。FineWeb-Edu (2024) 的实验证明,仅用 1.3T 经过教育质量筛选的 token,就能匹配 4-5T 普通 web 数据的下游性能。
因此,数据工程不是预处理的边角活,而是大模型训练中与算法同等重要的核心系统。本章聚焦如下问题:
- 数据从哪里来?(数据源)
- 怎么把脏数据洗干净?(清洗流水线)
- 怎么避免重复 token 浪费算力?(去重)
- 怎么把文本变成模型能吃的整数序列?(分词器)
- 各类数据按什么比例喂给模型?(混合策略)
2.2 数据来源
2.2.1 主流公开数据集
| 数据集 | 规模 | 来源 | 备注 |
|---|---|---|---|
| Common Crawl | 250B+ 网页 / 月 | 全网爬取 | 最大公开网页语料;2008 年至今 |
| C4 (Colossal Clean Crawled Corpus) | 750GB / ~200B tokens | T5 团队基于 CC 单 dump 清洗 | 简单规则过滤 |
| RefinedWeb | 5T tokens | Falcon 团队基于 CC 重洗 | 强力 URL 黑名单 + 模糊去重 |
| The Pile | 825GB | EleutherAI 整合 22 个子集 | 含 PubMed、ArXiv、Books3、StackExchange 等 |
| RedPajama-V1 | 1.2T tokens | LLaMA-1 配方复现 | 完整开源管线 |
| RedPajama-V2 | 30T tokens | 84 个 CC dump + 40 多种质量信号 | 最大开源 CC 派生集 |
| Dolma | 3T tokens | AI2 OLMo 训练集 | 完整数据卡 + 处理工具链 |
| FineWeb / FineWeb-Edu | 15T / 1.3T tokens | HuggingFace 高质量 CC 子集 | 教育向通过分类器筛选 |
| SlimPajama | 627B tokens | RedPajama-V1 全局去重后 | 质量高于 RP-V1 |
| The Stack v2 | 4TB | GitHub 代码 | 600+ 编程语言 |
| Wikipedia | 20GB / 4B tokens | dump | 多语种百科 |
| Books3 / Project Gutenberg | ~100GB | 书籍 | Books3 因版权问题已下架 |
| PG-19 | ~10GB | Gutenberg 长文本 | 长上下文评测常用 |
2.2.2 LLaMA-1 的经典数据配方
LLaMA-1 在 1.4T token 上的混合比例值得研究,因为它完全使用公开数据且效果可复现:
| 数据源 | 占比 | epochs | 备注 |
|---|---|---|---|
| CommonCrawl | 67.0% | 1.10 | 5 个 dump,CCNet 流水线清洗 |
| C4 | 15.0% | 1.06 | 补充更高质量 web |
| GitHub | 4.5% | 0.64 | 仅保留 Apache/BSD/MIT 协议 |
| Wikipedia | 4.5% | 2.45 | 20 种语言 |
| Books | 4.5% | 2.23 | Gutenberg + Books3 |
| ArXiv | 2.5% | 1.06 | LaTeX 源文件,移除 bibliography |
| StackExchange | 2.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 中提取主体内容,去掉导航、页脚、广告。
主流工具:
| 工具 | 特点 |
|---|---|
trafilatura | Python 库,规则+ML 混合,主体提取效果最好 |
jusText | 基于段落分类,速度快 |
boilerpipe | Java,老牌但维护差 |
Resiliparse | C++ 实现,吞吐高 |
实际生产中,FineWeb 用 trafilatura,RefinedWeb 用 jusText 加自定义规则。提取后保留纯文本,丢弃 HTML 标签、JavaScript、CSS。
2.3.2 阶段 2:语言识别
使用 fastText 的 lid.176.bin 或 Google 的 cld3 进行语言分类。
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)计算每篇文档的困惑度
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 集合):
Jaccard 相似度:
直接计算所有文档对的 Jaccard 相似度需要
2.4.3 MinHash 原理
核心定理:对随机哈希函数
直觉:在
算法
- 选择
个独立哈希函数 (实际中用一个哈希 + 对参数 的线性变换 ) - 对每个文档
,计算签名 - 估计 Jaccard:
签名只占
2.4.4 LSH(局部敏感哈希)加速
即便有签名,逐对比较仍是
- 两文档在某个 band 上完全相同 → 进入候选对
- 否则跳过
候选概率为相似度
参数
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 家族,更适合稠密向量空间:
算法
- 对每个 token
计算 -bit 哈希 - 维护
维向量 ,初始为 0 - 对每个 token:第
位是 1 → ( 是 token 权重,如 IDF 或 1);第 位是 0 → - 最终签名:
两文档汉明距离 ≤ 3(在 64-bit 签名上)视为近似重复。
特点:
- 签名极小(64 bit/doc)
- 单次比较 =
popcount(a XOR b),硬件指令一条搞定 - 但只能处理"加权词袋"模型,丢失顺序信息
- 适合短文本(如新闻标题去重)
LLaMA、Dolma 等大型 LLM 数据流水线多用 MinHash,因为它能处理 5-gram 等局部顺序信息。
2.4.6 精确 vs 模糊去重的实践
实践中通常组合使用:
- 句级精确去重:50 字以上完全相同的句子,全局保留 1 份
- 段落级精确去重:md5/sha256 hash
- 文档级模糊去重:MinHashLSH,阈值 0.7-0.8
- 跨域去重:训练数据 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) → Z:Z a b d Z a b a c - 下一轮
(a,b):2 最高 →Y:Z Y d Z Y a c - 下一轮
(Z,Y):2 最高 →X:X d X a c
3 步后词表为 {a, b, c, d, Z=aa, Y=ab, X=ZY=aabb? no, ZY=aaab},序列长度从 11 压到 5。
编码(推理)
把文本拆为基础单元,按 merge rule 顺序贪心合并:
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 tokenizers、tiktoken)用更高效的字典 + 优先级队列,单线程吞吐 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。
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) 提出的另一种子词算法:
- 初始化一个大词表(如 1M 候选 piece)
- 用 EM 算法估计每个 piece 的概率,使语料的 unigram 似然最大化
- 每轮丢掉似然贡献最小的 piece 子集(如 10%)
- 重复直到达到目标词表大小
特点:
- 编码时基于概率,可以做 N-best 解码或采样(subword regularization 训练时用)
- 比 BPE 略慢,但语言建模任务上略好
T5、mT5 用 Unigram;LLaMA、GPT 用 BPE。
2.5.6 Tiktoken (OpenAI)
OpenAI 自研的 BPE 实现,使用正则预分词:
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。
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-2 | byte-level BPE | 50,257 | ~1 字 / 1.5 token | r50k_base |
| GPT-3 | byte-level BPE | 50,281 | 同上 | p50k_base |
| GPT-4 | tiktoken BPE | 100,277 | ~1 字 / 1.0 token | cl100k_base |
| GPT-4o | tiktoken BPE | 199,997 | ~1 字 / 0.7 token | o200k_base |
| LLaMA-1/2 | SentencePiece BPE | 32,000 | ~1 字 / 2.0 token | byte_fallback |
| LLaMA-3 | tiktoken BPE | 128,256 | ~1 字 / 0.85 token | 引入预分词 |
| DeepSeek-V3 | BPE | 128,815 | ~1 字 / 0.6 token | 中文优化 |
| Qwen2 | tiktoken-like BPE | 152,064 | ~1 字 / 0.55 token | 多语种平衡 |
| Gemma | SentencePiece | 256,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 还是两个 - 数字处理:左对齐还是右对齐切分(
12345→123/45vs12/345),影响算术任务 - 代码格式:制表符、4 空格缩进、
\n是否独立 token,影响代码生成质量
LLaMA-3 在数字、代码、中文上做了针对性优化,是教科书级的工程实践。
2.6 数据混合策略
2.6.1 静态加权
最简单的做法:固定每个域的采样比例,在每个 batch 内按比例采样。LLaMA-1 的配方就是静态加权。
工程实现:把每个数据源切成 shard,按权重抽样(采样有放回),混入数据流。
2.6.2 DoReMi (Xie et al. 2023)
核心思想:用一个小参考模型 + 代理模型,通过分布鲁棒优化 (DRO) 自动找最优域权重。
算法:
- 在静态混合上训一个小参考模型
- 训一个相同尺寸的代理模型
,但每个域 的权重 是可学的 - 计算每个域的"超额损失":
- 用 DRO 更新权重
,让超额损失最大的域权重增加(worst-case 优化) - 把学到的
用到大模型训练
DoReMi 在 280B Pile 数据上找到的权重,让下游 1B 模型在 8 个任务上提升 6-10%。
2.6.3 课程学习 (Curriculum)
按"由易到难"或"由通用到专业"组织训练顺序:
- 早期 (前 80% token):通用 web 文本,学语言基本功
- 中期:加大代码、数学、科学数据比例
- 末期 (最后 5-10%):高质量 instruction-style 数据,"退火 (annealing)"
LLaMA-3 405B 训练分两个阶段:
- 14T token 通用预训练,上下文 8K
- 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 数据采集
# 下载 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.jsonl2.7.2 语言过滤
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 质量过滤
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 True2.7.4 MinHash 去重
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 训练分词器
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 二进制化
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 本章小结
本章系统讲解了大模型预训练的数据工程。核心要点:
- 数据决定模型上限。Chinchilla 的 20:1 比例和 LLaMA-3 的 15T tokens 都说明数据规模与质量的重要性。
- 清洗流水线分多阶段:文本抽取 → 语言识别 → 启发式规则 → 模型分类器 → 去重 → PII 过滤,最终留存率仅 5-10%。
- MinHash + LSH 是工业级模糊去重的标准方案,128-bit 签名 + banding LSH 在万亿级数据上可行。
- BPE 是分词器主流,配合 byte-level 或 byte_fallback 可消除 OOV;词表大小是中文/多语种压缩率的关键。
- 数据混合从静态加权演进到 DoReMi、课程学习、退火等动态策略,最后阶段的数据切换至关重要。
下一章我们将深入 Transformer 架构本身,看模型如何高效地消化这些 token。
2.10 思考题
MinHash 参数选择:若你希望在 Jaccard ≥ 0.8 时去重,签名长度
,请推导合适的 参数,使得 0.8 时候选概率 ≥ 0.95,0.5 时候选概率 ≤ 0.05。提示:使用 ,搜索整数解。 分词器影响估算:假设你有 1T 中文字符的训练语料。LLaMA-2 (32K, 1 字 ≈ 2 token) 和 Qwen2 (152K, 1 字 ≈ 0.55 token) 训练同一个 7B 模型,按
的 FLOPs 公式,两者的训练计算量相差几倍?这对硬件预算有何意义? 数据污染检测:你训完一个模型,发现它在 GSM8K 上得分异常高。请设计一个 MinHash + LSH 的方案,定量评估训练数据中包含多少 GSM8K 题目(或其改写版本),并提出阈值与处理策略。