对应:Stanford CS336 Spring 2026 Lecture 14(Data II,Percy Liang 的可执行讲义
lecture_14.py)。Spring 2025 的第 14 讲内容相近,有公开录像。 配套代码:code/lecture14/预计阅读时间:3 小时 硬件说明:01~05 号脚本只需要 Python 标准库和 numpy,每个几秒钟;06 号脚本用 PyTorch 在 CPU 上训练 18 个小模型,约 30 秒。所有实验都在第 13 讲的”迷你互联网”上进行,每个网页都带着标准答案。
0. 本讲导航
0.1 学完本讲,你应该能够
- 说出数据流水线的四个环节(转换、过滤、去重、混合),以及每个环节要解决的问题。
- 实现 C4 和 Gopher 的规则过滤,并说出它们在哪里会误删、在哪里会漏删。
- 用”目标数据 T、原始数据 R”的框架理解所有基于模型的过滤:fastText 分类器、KenLM 困惑度、DSIR 重要性重采样;解释 GPT-3 的随机保留规则。
- 解释为什么”目标数据 T 选成什么样,过滤出来的数据就是什么样”,以及为什么过滤阈值应该随训练规模变化。
- 用正则表达式做个人信息脱敏,并说出它的误报和漏报;解释词表过滤的”斯肯索普问题”。
- 说清精确去重、Bloom 过滤器、MinHash、LSH 的原理,推导 MinHash 的碰撞概率和 LSH 的 S 形曲线,并亲手在语料库上完成近似去重。
- 解释数据配比中”有限数据源 → 重复轮数”的问题;说出温度采样、UniMax、基于回归的配比、“模拟重复轮数”的思路。
- 描述后训练数据的基本配方(环境 → 任务 → 老师模型的回答),以及 OpenThoughts、SWE-smith、SWE-Zero、SWE-rebench 的做法。
0.2 先修知识
| 知识点 | 用在哪里 | 最低要求 |
|---|---|---|
| 第 13 讲 | 全讲 | 数据漏斗、迷你互联网、正文抽取、语言识别 |
| 第 12 讲 | 第 2.4 节 | n-gram 语言模型、困惑度 |
| 第 9 讲 | 第 2.7、4 节 | 重复数据的价值(约 4 轮以内接近新数据) |
| 概率基础 | 第 3 节 | 独立事件的概率相乘、“至少一次”= 1 −“一次都没有” |
| 逻辑回归 | 第 2.3 节 | 知道 sigmoid 和交叉熵即可 |
0.3 本讲地图
第1节 流水线全景:从原始数据到训练数据
第2节 过滤:规则 → 目标数据框架 → 分类器 / 生成模型 / 重要性重采样 → 个人信息与有害内容
第3节 去重:哈希 → 精确去重 → Bloom 过滤器 → Jaccard 与 MinHash → LSH
第4节 数据混合:重复轮数、温度采样、UniMax、尺度相关效应、模拟重复轮数、基于回归的配比
第5节 后训练数据:OpenThoughts、SWE-smith、SWE-Zero、SWE-rebench
第6节 小结
1. 流水线全景:从原始数据到训练数据
上一讲的结论是:数据不会从天上掉下来。从在线服务(例如 GitHub)到原始数据(例如 GitHub Archive)再到处理后的数据集(例如 The Stack),中间隔着一整条流水线:
原始数据(HTML、PDF、代码仓库)
│ 转换:HTML → 文本(第 13 讲 4.2 节)
▼
文本
│ 过滤:语言识别、质量过滤、有害内容过滤、个人信息脱敏(本讲第 2 节)
▼
干净的文本
│ 去重:精确去重、近似去重(本讲第 3 节)
▼
不重复的文本(分成很多个数据源:网页、维基、代码、论文……)
│ 混合:每个数据源各占多少比例(本讲第 4 节)
▼
训练数据
转换在第 13 讲已经讲过:原始数据不是文本,把 HTML 变成文本要去掉样板(导航、广告),天然有损(表格、图片怎么办?),而且抽取的好坏会实实在在地影响模型(DCLM 的实验)。PDF 更难:FinePDFs 从 Common Crawl 里找出 PDF(被截断的大文件要重新抓取),用视觉语言模型做 OCR(RolmOCR)或用 Docling 解析,再做大量清洗和过滤——排版信息依然大量丢失。
本讲的代码从第 13 讲数据漏斗的输出开始(corpus.py):礼貌爬虫抓到的网页 → 去除样板、抽取正文 → 语言识别保留 p(英文) > 0.65,最后得到 476 篇”英文文档”:
| 类型 | 篇数 | 我们心目中的质量 |
|---|---|---|
| article(百科文章) | 121 | high(其中 1 篇其实是被误判为英文的中文网页) |
| news(新闻) | 60 | high |
| mirror(镜像:原样或略作改动地复制百科文章) | 30 | high(内容好,只是重复了) |
| forum(论坛) | 60 | medium(有用,但口语化,还夹着个人信息) |
| product(商品页) | 80 | low |
| spam(垃圾 SEO 页面) | 44 | low |
| template(模板批量生成的页面) | 40 | low |
| legal(各网站几乎相同的条款页) | 11 | low |
| code(代码页) | 30 | code(要不要,取决于你想训练什么) |
另外,141 篇文档属于某个”重复组”(镜像和它的原文、模板页、条款页、只改了函数名的代码页)。
【边界】迷你互联网是程序生成的,比真实网页整齐得多:同一类网页来自同一组模板,不同类型之间泾渭分明。所以下面很多方法在这里的表现”好得不真实”。每一节都会指出哪些结论可能偏乐观。
2. 过滤
2.1 最便宜的办法:人工规则
最古老的过滤方法是人工规则:看几个简单的统计量(词数、平均词长、标点……)就决定一篇文档的去留。有不少著名的数据集刻意只用规则:C4、Gopher(MassiveText)、RefinedWeb、FineWeb、Dolma。理由是规则透明、便宜,也不会把”某个模型的偏好”带进数据里。
Gopher 规则(Rae et al. 2021)按整篇文档判断,任何一条不满足就删掉:
- 词数在 50 到 100,000 之间;
- 平均词长在 3 到 10 个字符之间;
- ”#“、省略号的个数各自不超过词数的 10%;
- 以项目符号开头的行不超过 90%;
- 以省略号结尾的行不超过 30%;
- 至少 80% 的词含有字母;
- 至少包含 the、be、to、of、and、that、have、with 中的两个。
每一条背后都有一个直觉:太短的没内容;平均词长太短像符号堆、太长像网址;全是列表的是导航;省略号结尾的是”阅读更多…”式的摘要页;不含常用词的根本不是正常的英文句子。
C4 规则(Raffel et al. 2019)不同:它会修改文档。先逐行清洗——只留以句末标点结尾、至少 5 个词的行,删掉含 “javascript”(“请启用 JavaScript”)和 “terms of use”、“privacy policy”、“uses cookies” 等字样的行;再整篇判断——含 “lorem ipsum”(占位文字)、含大括号 ”{“(代码的标志)、清洗后少于 3 句话,就整篇删掉。(C4 还会删掉含”脏话列表”中任何词的网页,见 2.8 节。)
01_rule_filters.py 在语料库上运行两套规则:
| 类型 | Gopher 保留 | C4 保留(篇数) | C4 保留的字符 |
|---|---|---|---|
| article | 120/121 | 120/121 | 99% |
| news | 60/60 | 60/60 | 93% |
| mirror | 30/30 | 30/30 | 99% |
| forum | 60/60 | 60/60 | 94% |
| code | 0/30 | 0/30 | 0% |
| product | 0/80 | 0/80 | 0% |
| spam | 0/44 | 0/44 | 0% |
| template | 40/40 | 40/40 | 91% |
| legal | 11/11 | 11/11 | 44% |
规则删掉了所有商品页(Gopher:词数不到 50;C4:没有完整的句子)、所有垃圾页(Gopher:不含常用词)、所有代码(词数太少、含大括号);但模板页和条款页一篇也没删——它们都是通顺的英文句子,规则看不出它们”没有价值”或”重复了很多遍”。被删掉的那 1 篇 article 其实是被语言识别误判的中文网页:中文不用空格,split() 只切出 9 个”词”。
规则在这里显得特别准,是因为迷你互联网的类型泾渭分明(商品页恰好都是 45 个词左右,刚好低于 50 的阈值)。真实网页的分布是连续的。所以代码里还有一组手写的”刁钻”例子:
| 例子 | Gopher | C4 |
|---|---|---|
| 简短的新闻快讯(高质量) | 删(词数) | 保留 |
| 菜谱(一半是列表) | 保留 | 保留,但标题和配料表全被删掉 |
| 数学讲解(高质量,符号多) | 删(平均词长) | 保留 |
| 带代码的教程(高质量) | 保留 | 删(大括号) |
| 通顺的广告软文(低质量) | 保留 | 保留 |
| 语法正确的胡言乱语(低质量) | 保留 | 保留 |
| 中文文章(高质量) | 删(词数) | 删(少于 3 句) |
规则在两个方向上都会犯错:误删高质量内容(短新闻、数学、代码教程、非英语);放过低质量内容(规则只看”形式”,不看”意思”)。所以数学、代码这类专门的数据要用专门的流水线。OpenWebMath(Paster et al. 2023)就是一个例子:先用规则(例如是否含 LaTeX 命令)筛选,再用在 ProofPile 上训练的 KenLM 过滤(困惑度 < 15000 才保留),再用一个判断”是不是数学写作”的 fastText 分类器(阈值:含数学符号的网页 0.17,不含的 0.8)。最终得到 147 亿 token,用它训练的 1.4B 模型胜过了用 20 倍数据训练的模型。
结论:规则是便宜的第一道筛子,负责删掉明显的垃圾;“好不好”要交给模型。
2.2 通用框架:目标数据 T 与原始数据 R
CS336 把几乎所有过滤问题都归结成同一块”积木”:
给定少量的目标数据 T(我们想要的样子)和大量的原始数据 R,找出 R 中与 T 相似的子集 T’。
- 语言识别:T = 英文,R = 所有语言;
- 质量过滤:T = 高质量文本,R = 网页;
- 有害内容过滤:T = 无害文本,R = 网页。
对过滤算法有两个要求:
- 能从 T 推广出去:我们希望 T’ 和 T 不一样——如果只是找出 T 的复制品,那直接用 T 就行了;
- 极快:它要在海量的 R 上运行(几十亿篇文档)。用一个大模型给每篇文档打分,代价可能和训练本身差不多。
通用的做法分两步:(1) 用 R 和 T 估计一个模型,得到打分函数 score(x);(2) 按分数决定去留(阈值,或者按分数随机保留)。打分函数有三种:
| 类型 | 打分函数 | 代表 | 需要负例吗? |
|---|---|---|---|
| 判别式分类器 | p(T | x) | fastText:GPT-3、LLaMA、DCLM | 需要(通常就用 R 的随机样本) |
| T 的生成模型 | p_T(x) | KenLM:CCNet | 不需要 |
| 重要性权重 | p_T(x) / p_R(x) | DSIR | 需要 R 的分布 |
有的数据集刻意不用基于模型的过滤(C4、Gopher、RefinedWeb、FineWeb、Dolma),有的用(GPT-3、LLaMA、DCLM)——后者正在成为主流。
02_model_filters.py 把三种方法都实现了一遍,实验设置是:
- T = 40 篇百科文章(扮演”维基百科”);
- 负例 = 从其余文档中随机抽的 120 篇(扮演”随机的 Common Crawl 网页”,其中 28 篇恰好也是百科文章——随机网页里本来就有好文章);
- 测试集 = 剩下的 316 篇。关键问题:新闻和论坛从来没有当过正例,分类器会怎么对待它们?
2.3 判别式分类器:fastText
fastText 分类器的结构非常简单:把文档中的词和 n-gram 映射成短向量,取平均,再接一个线性层。它本质上是一个线性分类器,这正是它能在几十亿篇网页上运行的原因。两个关键技巧:
- 哈希技巧:词表可能有几百万个词、几十亿个二元组,不可能给每个都分配编号。直接用哈希函数把每个 n-gram 映射到固定数量的桶里(fastText 默认 200 万个);偶尔会冲突,但影响很小。
- 取平均:长文档和短文档的特征”音量”一样。
我们的代码用的是它的”满秩”版本:在 2^18 个哈希桶(一元词和二元词)上做逻辑回归。
历史上的几个配方:
- GPT-3:正例 = {维基百科、WebText2、Books1、Books2} 的样本,负例 = Common Crawl 的样本;按分数随机保留(见下文);
- LLaMA / RedPajama:正例 = 被维基百科引用的网页,负例 = Common Crawl 的样本;保留被判为正例的网页;
- DCLM:正例 = 指令数据(OpenHermes 2.5)+ Reddit ELI5(“像给五岁孩子解释”)的回答,负例 = 随机网页;只保留分数最高的 10%。
实验结果(测试集):
| 类型 | 质量 | 平均 p(T|x) | p > 0.5 保留 |
|---|---|---|---|
| article | high | 0.76 | 45/53 |
| news | high | 0.02 | 0/50 |
| mirror | high | 0.80 | 17/21 |
| forum | medium | 0.01 | 0/44 |
| code | code | 0.01 | 0/24 |
| product | low | 0.00 | 0/56 |
| spam | low | 0.00 | 0/32 |
| template | low | 0.00 | 0/28 |
| legal | low | 0.01 | 0/8 |
按排序看,分类器几乎完美(AUC = 0.993:随机取一篇高质量文档和一篇低质量文档,前者得分更高的概率)。但按阈值看,新闻和论坛全军覆没——它们是好文本,只是不像百科文章。
这是本讲最重要的一个教训:分类器学到的是”像不像 T”,而不是”好不好”。T 选成什么样,过滤出来的数据就是什么样。 这就是 GPT-3、LLaMA、DCLM 都在精心挑选正例的原因:正例越多样,推广得越好。(百科文章的测试分数高得”不真实”:迷你互联网的百科文章来自同一组句子模板,测试集和 T 太像了。)
GPT-3 的随机保留规则。GPT-3 不用固定阈值,而是:
def keep_document(score):
return np.random.pareto(9) > 1 - score
numpy 的 pareto(a) 是 Lomax 分布,P(X > x) = (1 + x)^(−a)。所以
| score | 理论 (2−s)^−9 | 模拟 20 万次 |
|---|---|---|
| 1.0 | 1.0000 | 1.0000 |
| 0.9 | 0.4241 | 0.4242 |
| 0.8 | 0.1938 | 0.1920 |
| 0.5 | 0.0260 | 0.0261 |
| 0.0 | 0.0020 | 0.0019 |
为什么要随机?让低分文档也有一点点机会留下来——保住多样性,也避免数据集完全变成”分类器的口味”。但它救不了分数只有 0.02 的新闻(保留概率约 0.2%);而且这个规则相当严格:分数 0.9 的文档也只有 42% 的机会留下。
2.4 生成模型:KenLM 与 CCNet
另一种思路:只用 T 训练一个语言模型,用它给每篇文档算困惑度——困惑度低 = “读起来像 T”。CCNet(Wenzek et al. 2019)在维基百科上训练 KenLM(C++ 写的 n-gram 语言模型工具),按困惑度把每种语言的网页三等分:头部、中部、尾部。它不需要负例,只需要”好的样子”。
我们用第 12 讲的 NGramLM 在 T 上训练一个字节级 5-gram 模型:
| 类型 | 比特 / 字节 | 头部 / 中部 / 尾部 |
|---|---|---|
| article | 1.12 | 52 / 0 / 1 |
| news | 4.92 | 31 / 19 / 0 |
| mirror | 0.96 | 21 / 0 / 0 |
| forum | 5.79 | 0 / 39 / 5 |
| code | 7.14 | 0 / 0 / 24 |
| product | 6.99 | 0 / 0 / 56 |
| spam | 6.52 | 2 / 11 / 19 |
| template | 5.55 | 0 / 28 / 0 |
| legal | 5.85 | 0 / 8 / 0 |
AUC = 0.987。和分类器比:新闻有 31 篇进了头部(生成模型对”不同体裁的好文本”更宽容);但模板页全在中部,还有 2 篇 “weight loss pills” 垃圾页进了头部——字节级 n-gram 只看局部的拼写,这几个英文单词拼写很普通,重复一百遍也不会被扣分。生成模型奖励的是”局部通顺、常见”的文字;分类器见过负例里的模板页和垃圾页,知道要把它们往下压,生成模型没有这个信息。
2.5 重要性重采样:DSIR
DSIR(Xie et al. 2023)换了一个目标:不是”留下好的”,而是让抽出来的数据的分布匹配 T 的分布。做法:
- 把每篇文档表示成”哈希 n-gram 特征的袋子”(例如 1 万个桶);
- 分别在 T 和 R 上估计特征的分布 p_T、p_R(多项分布);
- 每篇文档的对数重要性权重 = Σ_特征 [log p_T(特征) − log p_R(特征)];
- 按权重不放回地抽样(代码用 Gumbel-top-k 技巧:给每个对数权重加一个 Gumbel 噪声,取最大的 k 个)。
一个实现细节:T 很小,很多特征在 T 里一次都没出现。如果简单地”每个桶加 1”做平滑,这些没见过的特征反而会拿到正的权重(桶太多、T 的总数太少,平滑把概率抬高了)。代码改用和 p_R 插值:p_T’ = 0.5 × p_T + 0.5 × p_R,没见过的特征的权重就是 log 0.5 < 0。
结果:按”每个特征的平均对数权重”排序,AUC = 0.998。但按权重抽 100 篇时,除了 52 篇百科和 21 篇镜像,还抽到了 21 篇代码页:
| 类型 | 平均对数权重 | 平均特征个数 |
|---|---|---|
| article | +100.8 | 317 |
| news | −104.3 | 275 |
| code | −42.5 | 92 |
测试集里”像 T 的”文档只有 74 篇,剩下的名额给了权重”最不负”的文档——最短的代码页:特征少,对数权重的和离 0 最近。另外,对数权重之间动辄相差几十上百(e 的几十次方倍),“按权重抽样”实际上几乎等于”取权重最大的”。这是把对数权重直接相加的副作用:长文档的权重更极端。实际使用时要注意长度的影响。
2.6 换一个”目标”:用大模型当标注者
2.3 节的教训是”T 决定一切”。那能不能直接定义”什么是好”?phi-1(Gunasekar et al. 2023)的做法:
- 原始数据 R = The Stack 的 Python 部分;
- 用 GPT-4 给其中 10 万个样本打标签,提示词是”判断它对一个想学基本编程概念的学生有没有教育价值”;
- 在预训练代码模型的输出向量上训练一个随机森林分类器,用它去筛选整个 R;
- 结果(HumanEval):1.3B 模型在原始 Python 数据上训练 96K 步,12.19%;在筛选后的数据上训练 36K 步,17.68%——步数更少,效果更好。
FineWeb-Edu 用同样的思路处理网页:用 Llama-3-70B 给大量网页的”教育价值”打 0~5 分,再训练一个小分类器给所有网页打分。
代码的演示 5 用同样的 160 篇训练文档,只把标签换成”标注者认为是不是高质量”(用迷你互联网的标准答案扮演标注者):
| 类型 | 正例 = 百科 | 正例 = 标注者说好 |
|---|---|---|
| article | 45/53 | 52/53 |
| news | 0/50 | 50/50 |
| mirror | 17/21 | 21/21 |
| 其余类型 | 0 | 0 |
换了”目标”的定义,新闻就全部留下来了。用大模型当标注者,等于把大模型对”好数据”的理解蒸馏进一个又快又便宜的分类器。代价是标注者的偏好(和偏见)也被一起继承了;“教育价值”高的数据也不一定对所有下游任务都好。(这里 AUC = 1.000,是因为迷你互联网的类型太分明了;真实的标注有噪声,类别之间也没有清楚的界限。)
2.7 过滤阈值应该随规模变化
过滤没有唯一的最优阈值(Goyal et al. 2024 在图文数据上系统地研究了这一点):
- 训练得短(算力少):只要最好的数据——过滤得狠一点;
- 训练得长(算力多):最好的那部分很快就会被重复很多轮(第 9 讲:重复约 4 轮以内接近新数据,之后价值迅速衰减),这时宁可要更多”次好”的数据——过滤得松一点。
这也是第 13 讲提到的 Nemotron-CC 的动机:它认为 DCLM、FineWeb-Edu 过滤得太狠(去掉了 90% 的数据),对于要训练十几万亿 token 的模型,“更多的 token”更重要。第 4 节的数据混合实验会看到同一个现象。
2.8 个人信息与有害内容
个人信息(PII)。模型会记住训练数据(第 12、13 讲),训练数据里的邮箱、电话可能被”背”出来。常见做法是脱敏——用占位符替换,而不是删掉整篇文档。作业 4 的约定:
邮箱 → |||EMAIL_ADDRESS||| 电话 → |||PHONE_NUMBER||| IPv4 地址 → |||IP_ADDRESS|||
03_pii_toxicity.py 用正则表达式实现这三种脱敏。在语料库上,论坛帖子里埋着 36 个邮箱、36 个电话、16 个 IP 地址,全部被找到,没有误报(商品页的价格、SKU 编号都没被误认)。但这只是因为迷你互联网里的个人信息都是标准格式。手写的例子暴露了问题:
| 例子 | 结果 |
|---|---|
[email protected] | 识别为邮箱 ✓ |
anna dot lee at example dot org、anna.lee [at] example.org | 漏报 |
(555) 234-5678、+1 555 234 5678 | 识别为电话 ✓ |
英国号码 +44 20 7946 0000 | 漏报 |
订单号 5552345678 | 误报为电话 |
版本号 1.2.3.4 | 误报为 IP 地址(只看字符串根本分不出来) |
999.1.1.1 | 正确地不当作 IP |
| “My name is Anna Lee and I live at 12 River Road.” | 名字、住址完全处理不了 |
名字和住址需要命名实体识别(NER)模型(例如 Presidio),成本高得多,而且”名人的名字”该不该删又是一个需要人来定的问题。个人信息过滤永远是”尽力而为”:能大幅减少,但不能消除。
有害内容。C4 删掉了含”脏话列表”(400 多个词)中任何一个词的网页。代码用一个很温和的示例列表(hell、damn、crap、ass、idiot)演示它的问题:
- 子串匹配:在语料库上(476 篇,其中根本没有脏话)会删掉 43 篇——被误伤的词是 “passes”(27 次,来自百科文章里的 “as water passes through it”)和 “hello”(16 次,来自论坛帖子的开头)。这就是斯肯索普问题:英国小镇 Scunthorpe 的名字里藏着一个脏话,当地居民长期无法在很多网站上注册账号;
- 整词匹配:语料库上一篇也不删;但它依然不看上下文(挪威真实存在一个叫 Hell 的村子;“讨论为什么不该骂人是 idiot”的文章也会被删),而稍作变形的脏话(h3ll、i d i o t)却能溜过去;
- 系统性偏差:Dodge et al. 2021 发现,C4 的词表过滤不成比例地删掉了非裔美国人英语、西班牙裔相关的文本,以及关于 LGBTQ+ 群体的(完全无害的)文本。
所以现代流水线用分类器。Dolma 用在 Jigsaw 有害评论数据集(2018)上训练的 fastText 分类器。这个数据集来自维基百科讨论页的评论(项目的初衷是”帮助人们在网上更好地讨论”),人工标注了 toxic、severe_toxic、obscene、threat、insult、identity_hate 六类。分类器能看上下文,但也会从标注数据里学到偏见(例如看到某些身份词就判为有害)。阈值怎么定,又是一个取舍。
2.9 过滤小结
- 过滤对构建好模型至关重要;
- 配方:定义目标数据(好的样子),再把它推广到原始数据上;
- 规则便宜透明,但只看形式;模型能看”像不像”,但”像什么”完全由 T 决定;
- 语言识别、质量过滤、有害内容过滤是同一个框架的不同实例;
- 最优的过滤强度取决于训练规模。
3. 去重
3.1 为什么要去重?
网上有两类重复:
- 完全重复:镜像网站(例如古登堡计划有很多镜像)、GitHub 的 fork;
- 近似重复:只差几个 token 的文本。例如服务条款和开源许可证(MIT 许可证出现在无数个仓库里)、套话(复制粘贴或模板生成的文字)、复制粘贴时产生的格式差异。
C4 里有一段商品描述(“通过结合奇妙的创意、有趣的布置和最新的潮流……”)重复出现了 61,036 次。迷你互联网的模板页就是仿照它的风格写的。
去重的好处(Lee et al. 2021,“Deduplicating Training Data Makes Language Models Better”):
- 训练更高效:token 更少,同样的算力看到更多不同的内容;
- 减少背诵:缓解版权和隐私问题(重复次数越多的文本,越容易被模型逐字背出来)。
3.2 设计空间
- 单位是什么?句子、段落、整篇文档?
- 怎样算匹配?完全相同?有共同的片段?共同片段的比例超过阈值?
- 匹配后做什么动作?全部删掉?只留一份?
核心难点:去重本质上是”每一项和其他所有项比较”。n 篇文档有 n²/2 对,几十亿篇文档时是 10^18 量级——不可能。我们需要线性时间的算法。答案是哈希。
3.3 哈希函数
哈希函数 h 把一项(可能很长)映射成一个短得多的哈希值(整数或字符串)。h(x) = h(y) 但 x ≠ y 叫碰撞。效率和抗碰撞性之间有取舍:
- 加密哈希(SHA-256):几乎不可能找到碰撞,但慢(比特币用它);
- 非加密哈希(DJB2、MurmurHash、CityHash):快,但不抗碰撞(哈希表用它们)。
04_exact_dedup_bloom.py 演示 1 对 20 万个随机字符串分别计算 SHA-256 和 CRC32(32 位):SHA-256 碰撞 0 次;CRC32 碰撞 7 次。按”生日悖论”,n 个随机值落在 2³² 个桶里,碰撞次数的期望是 n²/(2 × 2³²) ≈ 4.7——32 位对去重来说太短了。实践中用 64 位或 128 位的快速哈希:足够快,碰撞概率可以忽略。
【Python 小知识】不要用 Python 自带的
hash()做去重或特征哈希:它对字符串的结果每次启动 Python 都不一样(为了安全故意随机化),实验就没法复现了。代码里用hashlib或zlib.crc32。
3.4 精确去重
最简单的例子:单位 = 字符串,匹配 = 完全相同,动作 = 只留一份。官方讲义的写法是”按哈希值排序、分组、每组留一个”:
hash_items = itertools.groupby(sorted(items, key=h), key=h)
deduped = [next(group) for _, group in hash_items]
这是 MapReduce 的写法:map(算哈希)→ shuffle(按哈希值分组)→ reduce(每组留一个),每一步都能拆开并行,能扩展到几十亿篇文档。优点:简单、语义清楚、精确率高。缺点:对近似重复无能为力。
在语料库上,这个缺点暴露得非常彻底:完全相同的文档 0 组——可语料库里明明有 15 篇原样复制的镜像!原因是正文抽取时残留了每个网页随机的 “See also …” 链接文字,原文和镜像的结尾不一样了。先把结尾的链接文字去掉再比较,就能找到 16 组(15 组”原文 + 镜像”,1 组 11 个网站的条款页),可以删掉 25 篇。规范化有用,但另外 15 篇”略作改动”的镜像、模板页、代码页,无论怎么规范化都不会完全相同。
更细的单位。精确去重也可以在更小的单位上做:
- 逐行去重(作业 4 的做法):数出每一行在整个语料库里出现了几次,出现超过一次的行,所有副本都删掉;
- C4 的三句话片段去重:任何一个”连续三句话的片段”出现不止一次,只保留第一次出现的那一份。
| 类型 | 逐行去重删掉的字符 | 三句话片段去重删掉的字符 |
|---|---|---|
| article | 9% | 23% |
| news | 0% | 5% |
| mirror | 31% | 0% |
| forum | 55% | 22% |
| code | 33% | 0% |
| product | 37% | 0% |
| spam | 21% | 0% |
| template | 0% | 68% |
| legal | 73% | 83% |
出现次数最多的行是商品页的 “Free shipping on orders over $50”、“Add to cart”、“Add to wishlist”、“In stock”(各 80 次)和论坛里的”万能回复”(最多 22 次)。两种做法都能清掉大量样板,但都有副作用:
- 逐行去重”所有副本都删掉”:被镜像的百科原文也失去了和镜像相同的段落;论坛帖子只剩下个别行,回答全没了,剩下的内容不再连贯;
- C4 的片段去重”只留第一次出现的那一份”。可”第一次”取决于处理顺序:文档按网址排序,mirror.example 排在 wiki.example 前面,于是镜像完好无损(mirror 0%),被镜像过的 30 篇百科原文反而平均被删掉了 89%(其余百科文章一句没删)!
- 从文档中间删掉几句话,剩下的内容可能前言不搭后语——C4 论文自己也承认这一点。
这就是设计空间里”单位”和”动作”两个维度的取舍。真实的流水线往往用”质量更高 / 更早出现的那一份”作为保留的副本,但这需要额外的信息。
3.5 Bloom 过滤器
如果只想回答”这个段落以前见过吗?“,而数据大到集合放不进内存怎么办?Bloom 过滤器是一个近似的集合:
- 一个 m 位的位数组(全是 0)和 k 个哈希函数;
- 加入 x:把 h₁(x), …, h_k(x) 这 k 个位置都设成 1;
- 查询 x:看这 k 个位置是不是全是 1。
性质:说”不在”就一定不在(没有假阴性);说”在”可能是误判(假阳性:别的元素恰好把这 k 个位置都设成了 1)。
假阳性率的推导。加入 n 个元素,每次设 k 个位置,某一位始终没被设成 1 的概率是 (1 − 1/m)^(kn) ≈ e^(−kn/m)。一个新元素的 k 个位置全是 1 的概率:
k 太小,一个位置撞上就误判;k 太大,位数组很快被填满。对 k 求导可得最优的 k = (m/n)·ln 2,此时 FP ≈ 0.6185^(m/n)。
实测(加入 1 万个元素,查询 10 万个从没加入过的元素):
| 每个元素的位数 m/n | k | 理论 | 实测 |
|---|---|---|---|
| 8 | 1 | 0.1175 | 0.1185 |
| 8 | 2 | 0.0489 | 0.0504 |
| 8 | 4 | 0.0240 | 0.0236 |
| 8 | 6 | 0.0216 | 0.0227 |
| 8 | 8 | 0.0255 | 0.0256 |
| 4 | 3 | 0.1469 | 0.1479 |
| 16 | 11 | 0.0005 | 0.0004 |
理论和实测吻合;m/n = 8 时最优的 k ≈ 5.5。代码用”双重哈希”生成 k 个哈希函数:只算两个哈希 h₁、h₂,第 i 个 = (h₁ + i·h₂) mod m。
内存:每个元素只占 m/n 位,和元素本身的长度无关。在语料库上流式地做行去重(2361 行,其中 1572 行不同):把不同的行存进集合,光文字就要 240 KB;而 1.9 KB 的 Bloom 过滤器(每行 10 位,k = 7)只误删了 1 个从没出现过的行(0.8 KB 时误删 54 个,0.2 KB 时误删 589 个)。Dolma 就用 Bloom 过滤器做段落级去重:在几万亿 token 上,精确的集合放不进内存,Bloom 过滤器可以。
3.6 Jaccard 相似度与 MinHash
要处理近似重复,先得定义”相似”。把每篇文档看成一个集合——通常是 n 个连续词组成的”瓦片”(shingle)的集合,然后用 Jaccard 相似度:
例如 A = {1, 2, 3, 4},B = {1, 2, 3, 5},J = 3/5 = 0.6。近似重复:Jaccard ≥ 某个阈值。
算法上的挑战依然是:怎样在线性时间内找到所有近似重复的对?
MinHash:一个随机哈希函数 h,使得
通常我们希望不同的东西哈希成不同的值;这里反其道而行之:越相似,越容易碰撞。
定义:minhash(S) = min_{x∈S} h(x)。为什么碰撞概率恰好是 Jaccard? 把 A ∪ B 的所有元素写成”特征矩阵”:
元素 | A | B
1 | 1 | 1
2 | 1 | 1
3 | 1 | 1
4 | 1 | 0
5 | 0 | 1
一个随机哈希函数相当于把这 5 行随机排个序。看排在最前面的那一行:
- 如果是 1、2、3(A ∩ B 中的元素),它同时是 A 和 B 的第一名 → minhash(A) = minhash(B);
- 如果是 4 或 5,A 和 B 的第一名不同。
每一行排在最前面的概率相同,所以 P[相等] = |A∩B| / |A∪B| = 3/5。
05_minhash_lsh.py 验证:用 10,000 个不同的随机哈希函数,minhash(A) = minhash(B) 的比例是 0.5947。代码用”通用哈希函数族” h_i(x) = (a_i·x + b_i) mod p(p = 2³¹ − 1)来模拟随机排列。
用 n 个哈希函数,MinHash 签名就是 n 个最小值;两个签名相同位置取值相同的比例就是 Jaccard 的估计。它是无偏的,标准差是 √(J(1−J)/n):
| 哪一对 | 真实 Jaccard | n = 10 | n = 100 | n = 1000 |
|---|---|---|---|---|
| 镜像 | 0.806 | 0.808 ± 0.130 | 0.808 ± 0.040 | 0.806 ± 0.013 |
| 模板页 | 0.600 | 0.615 ± 0.151 | 0.601 ± 0.050 | 0.599 ± 0.014 |
| 代码页 | 0.550 | 0.557 ± 0.147 | 0.557 ± 0.049 | 0.551 ± 0.016 |
(镜像那一对的理论标准差:0.125、0.040、0.013。)
但 MinHash 本身还没有解决”两两比较”的问题:它只是把每篇文档压缩成一个短签名。一次碰撞也不能告诉我们 J(A, B) 是否超过阈值。
3.7 局部敏感哈希(LSH)
只用一个 MinHash 时,P[碰撞] = J,是一条直线:相似的更容易碰撞,但随机性太大。我们希望的是:J 超过阈值就碰撞,低于阈值就不碰撞——需要把概率曲线”锐化”。
做法:用 n = b × r 个哈希函数,分成 b 个”带”(band),每带 r 个:
h1 h2 h3 h4 | h5 h6 h7 h8 | h9 h10 h11 h12 (n = 12, b = 3, r = 4)
规则:只要某一个带里的 r 个哈希值全部相同,A 和 B 就成为候选对。
这是一个”与-或”结构:
- 某一个带全部相同(“与”):s^r(r 越大越难);
- b 个带中至少有一个全部相同(“或”):
实现上,每个带只需要把文档按”这个带的取值”分桶(哈希表),同一个桶里的文档才两两配对——不需要两两比较所有文档。
课件的例子:s = 0.8,b = 5,r = 10 → P = 0.4333。更多数字:
| (b, r) | 0.5 | 0.7 | 0.75 | 0.8 | 0.85 | 0.9 | 0.95 | 0.98 | 阈值 ≈ (1/b)^(1/r) |
|---|---|---|---|---|---|---|---|---|---|
| (1, 1) | 0.500 | 0.700 | 0.750 | 0.800 | 0.850 | 0.900 | 0.950 | 0.980 | — |
| (10, 10) | 0.010 | 0.249 | 0.440 | 0.679 | 0.888 | 0.986 | 1.000 | 1.000 | 0.794 |
| (10, 20) | 0.000 | 0.008 | 0.031 | 0.109 | 0.327 | 0.726 | 0.988 | 1.000 | 0.891 |
| (20, 20) | 0.000 | 0.016 | 0.062 | 0.207 | 0.546 | 0.925 | 1.000 | 1.000 | 0.861 |
- 增大 r:曲线变陡,并向右移(更难碰撞);
- 增大 b:曲线向左移(更容易碰撞)。
阈值在哪里? 相变发生在”某一个带匹配的概率 = 1/b”的地方:s^r = 1/b,即 s ≈ (1/b)^(1/r)。代码用构造的集合对(Jaccard 恰好为 s)模拟了 (b, r) = (20, 5) 的 S 形曲线,和公式吻合(例如 s ≈ 0.5:公式 0.464,模拟 0.453;s = 0.6:公式 0.802,模拟 0.820)。
Lee et al. 2021 的设置:n = 9000,b = 20,r = 450。阈值 (1/20)^(1/450) = 0.9934——只找极其相似的文档。在阈值处,某一个带匹配的概率 = 1/b,成为候选对的概率 = 1 − (1 − 1/b)^b = 0.6415(b 很大时趋近于 1 − 1/e ≈ 0.632)。
3.8 在语料库上完整地做一遍
作业 4 的流程:规范化(小写、去掉重音符号、去掉标点、合并空白)→ 5-gram 瓦片 → MinHash 签名 → LSH 找候选对 → 对候选对算真实的 Jaccard 确认 → 聚类(A 像 B、B 像 C → 一类)→ 每类留一篇。
先看标准答案(476 篇,共 113,050 对,其中 915 对是重复):
| 重复对 | 对数 | Jaccard 最小 / 中位数 / 最大 |
|---|---|---|
| 原文 − 镜像 | 30 | 0.74 / 0.86 / 0.95 |
| 条款页 | 55 | 0.85 / 0.87 / 0.90 |
| 模板页 | 780 | 0.57 / 0.61 / 0.77 |
| 代码页(只改了函数名) | 50 | 0.42 / 0.51 / 0.55 |
| 不是重复的对 | — | 最大 0.49(两篇新闻) |
两两比较、精确计算时,阈值 0.5 的精确率 1.00、召回率 0.98;阈值 0.7 时召回率只剩 0.16(模板页和代码页都没到 0.7)。
再看 MinHash + LSH(100 个哈希函数),候选对再用真实 Jaccard ≥ 0.5 确认:
| (b, r) | 阈值 ≈ | 候选对 | 占全部的对 | 候选对覆盖的真重复 | 确认后精确率 | 召回率 |
|---|---|---|---|---|---|---|
| (50, 2) | 0.14 | 8633 | 7.6% | 100% | 1.00 | 0.98 |
| (25, 4) | 0.45 | 1175 | 1.0% | 99% | 1.00 | 0.97 |
| (20, 5) | 0.55 | 878 | 0.8% | 88% | 1.00 | 0.87 |
| (10, 10) | 0.79 | 163 | 0.1% | 18% | 1.00 | 0.18 |
- LSH 只把很少一部分对交给”真实 Jaccard”去确认。规模越大,省得越多:几十亿篇文档时两两比较不可能,而 LSH 只需要把每篇文档放进 b 个桶;
- LSH 的阈值要设得比你想要的 Jaccard 阈值低一些:(20, 5) 的阈值 ≈ 0.55,模板页的 Jaccard 大多在 0.57~0.61,正好落在 S 形曲线的”斜坡”上,漏掉了 94 对模板页;(25, 4) 的阈值 ≈ 0.45,只漏掉 20 对代码页(Jaccard 本来就低于 0.5)和 4 对模板页;
- 精确率都是 1.00:最后一步用真实 Jaccard 确认过。
用 (25, 4) 去重:删掉 93 篇(模板页 39、百科 30、代码 14、条款 10);39 个重复组中 36 组正好剩 1 篇,另外 3 组代码页剩 3~5 篇(它们之间的 Jaccard 低于 0.5);没有误删任何没有副本的文档。但有一个熟悉的问题:每一类只留”排在最前面”的那一篇——留下的是 mirror.example 上的 30 篇副本,被删掉的是 wiki.example 上的原文。真实的流水线会按质量分数、抓取时间或网站的可信度来决定保留哪一篇。
【边界】(b, r)、瓦片长度、确认阈值都要根据数据来调。这里代码页之间只改了函数名,Jaccard 却只有 0.42
0.55——算不算”重复”本身就是一个需要人来定的问题。另外,聚类有传递性:AB、B~C 时 A 和 C 会被归为一类,哪怕它们并不相似;在真实数据上,这可能把一长串”逐渐变化”的文档连成一个巨大的类。
去重小结:哈希让我们能在海量数据上做(近似的)匹配——精确去重用普通哈希;集合成员查询用 Bloom 过滤器;近似去重用 MinHash + LSH,把”相似度超过阈值”变成”在某个桶里碰撞”。
4. 数据混合
4.1 问题
语言模型的训练数据来自很多个数据源。The Pile 有 22 个领域;Marin 项目的 token 查看器列出了几十个数据源。关键问题:每个数据源占多大比例? 例如 {维基百科: 0.3, Common Crawl: 0.5, GitHub: 0.2}。
基线做法:
- 凭感觉:手动设定(相当常见);
- 均匀:p(s) ∝ 1;
- 按比例:p(s) ∝ 数据源 s 的 token 数。
直觉告诉我们:应该多用高质量的数据源。但是——
- 我们还想要多样性:文学、代码、论文这些”没法比较”的数据源都要有;
- 每个数据源都是有限的。给一个小数据源太多权重,就得把它重复很多轮(epoch)。
4.2 有限的数据源与重复轮数
第 2 点很重要,也有点微妙。课件的例子(06_data_mixing.py 演示 1):
- 低质量数据 10T token(很多),高质量数据 10B token(稀缺);
- 天真地按 50/50 混合,训练 1T token;
- 低质量数据只用了 0.05 轮,高质量数据要重复 50 轮——会过拟合!
4.3 温度采样与 UniMax
多语言模型面临同样的问题(英语数据是约鲁巴语的上万倍)。常见的折中是温度采样:p(s) ∝ size(s)^α,α ∈ [0, 1]——α = 1 是按比例,α = 0 是均匀(mT5 用 α = 0.3)。
UniMax(Chung et al. 2023)的思路更直接:尽量均匀地分配,但任何数据源最多重复 C 轮,即 p(s) × 训练 token 数 ≤ C × size(s)。算法:从最小的数据源开始,它的份额 = min(剩余预算 / 剩下的数据源个数, C × 它的大小);小数据源用不完的预算自动留给更大的数据源。
演示 2(训练 2T token;每格是”配比 / 重复轮数”):
| 数据源 | 大小 | α = 1 | α = 0.5 | α = 0.3 | α = 0 | UniMax (C = 4) |
|---|---|---|---|---|---|---|
| 英语 | 10T | 88.45% / 0.2 | 65.84% / 0.1 | 49.90% / 0.1 | 20% / 0.0 | 32.97% / 0.1 |
| 中文 | 1T | 8.85% / 0.2 | 20.82% / 0.4 | 25.01% / 0.5 | 20% / 0.4 | 32.97% / 0.7 |
| 德语 | 300B | 2.65% / 0.2 | 11.40% / 0.8 | 17.43% / 1.2 | 20% / 1.3 | 32.97% / 2.2 |
| 斯瓦希里语 | 5B | 0.04% / 0.2 | 1.47% / 5.9 | 5.10% / 20.4 | 20% / 80 | 1.00% / 4.0 |
| 约鲁巴语 | 500M | 0.00% / 0.2 | 0.47% / 18.6 | 2.56% / 102.3 | 20% / 800 | 0.10% / 4.0 |
α = 1 时小语种几乎学不到;α = 0 时约鲁巴语要重复 800 轮;中间的 α 两头都不理想;UniMax 让每种语言都尽量多,但不超过 4 轮。
4.4 最优配比会随规模变化
这是数据混合中最微妙的一点。用第 9 讲的玩具任务做实验(演示 3):
- 高质量数据:干净的标签,但数量有限(一个固定的数据池,用完就从头再来一轮);
- 低质量数据:60% 的标签被换成随机的 token,但要多少有多少;
- w = 每个批次中高质量数据的比例。表中是可约损失(越低越好)和高质量数据被重复的轮数。
| w | 小规模(训练 20 万条,高质量 2 万条) | 大规模(训练 160 万条,高质量 2 万条) | 模拟重复轮数(训练 20 万条,高质量 2500 条) |
|---|---|---|---|
| 0 | 0.885(0 轮) | 0.761(0 轮) | 0.897(0 轮) |
| 0.25 | 0.624(2.5 轮) | 0.518(20 轮) | 0.643(20 轮) |
| 0.5 | 0.414(5 轮) | 0.385(40 轮) | 0.539(40 轮) |
| 0.75 | 0.246(7.5 轮) | 0.354(60 轮) | 0.604(60 轮) |
| 0.9 | 0.166(9 轮) | 0.459(72 轮) | 0.806(72 轮) |
| 1.0 | 0.149(10 轮) | 0.952(80 轮) | 1.580(80 轮) |
- 小规模:高质量数据最多只重复 10 轮,越多越好,最优 w = 1;
- 大规模:同样的数据池,训练量大 8 倍。w = 1 意味着重复 80 轮——严重过拟合,损失 0.952,比完全不用高质量数据(0.761)还差!最优配比变成了混合(w = 0.75)。
这就是课件说的尺度相关效应:在小模型上调出来的配比(偏向高质量数据),直接用到大模型上,会把高质量数据重复很多遍,然后过拟合。
4.5 模拟重复轮数
Simulated epoching(课件引用的 2025 年的工作)的思路是本课程反复出现的主题:让小规模实验”看起来像”大规模。做小规模实验时,把每个数据源都按”小规模训练量 / 大规模训练量”的比例缩小——这里是 1/8,高质量数据从 20,000 条缩到 2,500 条。这样,小实验中每个数据源的重复轮数就和大规模时一样;重复太多的配比在小实验里也会”原形毕露”。
结果:模拟实验的最优点 w = 0.5,比天真的小实验(w = 1)更接近大规模的答案(w = 0.75)。但它不完全一样:数据池小了 8 倍,模型更容易把它”背下来”,所以模拟实验的最优点比大规模的更保守。
4.6 基于回归的配比
当数据源有几十个时,不可能用网格搜索。RegMix(Liu et al. 2024)等基于回归的方法:
- 定义一个配比的分布(例如狄利克雷分布),从中抽很多种配比;
- 每种配比训练一个小模型,测下游指标(小心不要过拟合到评测集上!);
- 用回归模型(线性回归、梯度提升树……)拟合”配比 → 指标”;
- 在回归模型上找最优配比,用它训练大模型。
这和规模定律(第 9、11 讲)是一个思路:在小规模上估计”配比 → 损失”,优化,再外推。它依赖两个”希望”:
- 希望 1:回归模型在最优点附近是准的;
- 希望 2:小规模的最优配比能迁移到大规模。
演示 4 用二次曲线拟合 log(损失) 和 w 的关系:
| 用哪组小实验拟合 | 拟合误差(log 损失,最大) | 预测的最优 w | 拿到大规模上的损失 |
|---|---|---|---|
| 小规模 | 0.069 | 1.00 | 0.952(最差) |
| 模拟重复轮数 | 0.180 | 0.45 | 0.385(w = 0.5;大规模最优是 0.354) |
不做”模拟重复轮数”时,希望 2 彻底落空;做了之后,预测的配比在大规模上离最优不远。
混合小结:问题是怎样给不同的数据源(维基、网页、代码……)加权;基于回归的配比在小规模上估计”配比 → 损失”再优化(和规模定律类似);要特别注意重复轮数和过拟合——解决办法是设上限(UniMax)或模拟(simulated epoching)。
【边界】玩具实验只有两个数据源、一个变量,而且”低质量”就是随机标签——真实的低质量数据有自己的用处(多样性、覆盖面)。结论的方向(最优配比随规模变化、模拟重复轮数有帮助)是可靠的,具体数字不要外推。
5. 后训练数据
前面讲的都是预训练数据。中期训练和监督微调(SFT)的数据长得像评测题:一个问题(提示)加一个好的回答。基本配方:
- 定义一组环境(例如数学、代码仓库);
- 定义一组任务 / 提示;
- 用一个强模型(老师)生成回答。
5.1 OpenThoughts
OpenThoughts(Guha et al. 2025)构造推理数据:
- 用 QwQ-32B 当老师,生成了 120 万条样本;
- 问题来自 27 个人工和合成的来源(StackExchange、NuminaMath、化学……);
- 几个发现:
- 每个提示采样多个(16 个)回答是有帮助的;
- 更强的模型不一定是更好的老师:QwQ-32B 比 DeepSeek-R1 更适合当老师;
- 按答案对错过滤回答并没有帮助;
- 少量高质量的来源(例如 OpenMath-2-Math)比大量多样的来源更好。
5.2 SWE 任务:代码仓库里的真实工作
软件工程(SWE)任务的数据比数学难得多:任务有沉重的依赖(不像数学题或编程竞赛题那样自成一体),给成千上万个仓库搭好 Docker 环境是一场”基础设施噩梦”。几种构造方式:
- SWE-smith(Yang et al. 2025):给定一个仓库,让语言模型往里面引入 bug,从而生成任务。128 个 GitHub 仓库产生了 5 万个任务;
- SWE-rebench(Badertdinov et al. 2025):从 GitHub 和 GitHub Archive 收集 45 万个 PR,用 Qwen2.5-72B-Instruct 安装依赖、评估 PR 质量,得到来自 3400 个仓库的 2.1 万个可交互的 Python SWE 任务;
- SWE-Zero(2026):一个观察——强模型在没有执行反馈的情况下也能解决很多任务,因为它们对代码语义有一个内在的”世界模型”。于是:
- 从 15 万个 GitHub PR 出发,生成 30 万条不需要在具体仓库中执行的智能体轨迹;
- 用 OpenHands 框架,去掉”未来的” git 提交,防止智能体”git 作弊”(直接从历史里找到答案);
- 从 Qwen3-Coder-480B 蒸馏,并做过滤(还是会尽量尝试执行);
- 另有 SWE-Hero:1.3 万条需要执行反馈的轨迹。
- SWE-ZERO-12M-trajectories:把 SWE-Zero 扩大到 1200 万条轨迹,使用 SWE-rebench-v2 的任务(3.2 万个可执行 + 12 万个不可执行),用一个很小的模型 mini-coder-1.7b(pass@100 为 50.4)和 mini-swe-agent 框架生成。
后训练数据小结:
- 提示的来源:完全合成、半合成(真实环境 + 合成任务)、真实(GitHub PR);
- 回答:来自能力强、也适合当老师的模型;
- 代码环境很痛苦;
- 到处都是过滤和各种细节。
6. 小结
- 过滤:训练分类器(语言识别、质量、有害内容),定义”好的样子”。目标数据 T 决定了一切;规则便宜但只看形式;过滤强度应随训练规模变化。
- 去重:哈希让模糊匹配能扩展到海量数据。精确去重简单但脆弱;Bloom 过滤器用极少的内存回答”见过吗”;MinHash + LSH 把”相似度超过阈值”变成”在某个桶里碰撞”。
- 混合:在小规模上尝试不同配比,外推到最优配比和大规模;小心重复轮数(上限或模拟)。
- 后训练数据:长得像评测题,大量使用合成数据(老师模型)。
- 大量的数据工作是领域相关的,需要亲眼看例子。 本讲几乎每一个”意外”(链接文字让镜像不再相同、“passes” 被当成脏话、处理顺序决定留下副本还是原文、代码页短所以 DSIR 权重高)都是打印出例子之后才发现的。
7. 与作业 4 的关系
作业 4 要求你从 Common Crawl 出发构建预训练数据集。本讲的代码对应其中的大部分组件:
| 作业 4 的组件 | 本讲代码 | ||||||
|---|---|---|---|---|---|---|---|
| Gopher 质量规则(词数、平均词长、省略号行、含字母的词) | 01_rule_filters.py 的 gopher_filter | ||||||
| 质量分类器(正例:被维基百科引用的网页;负例:Common Crawl) | 02_model_filters.py 的 HashedLogisticRegression(作业里用 fastText) | ||||||
| 个人信息脱敏(` | EMAIL_ADDRESS | ` 等占位符) | 03_pii_toxicity.py 的 mask_emails 等 | ||||
| 有害内容 / NSFW 分类(Dolma 的 Jigsaw fastText 分类器) | 03_pii_toxicity.py 讨论了词表过滤的问题 | ||||||
| 精确行去重 | 04_exact_dedup_bloom.py 的 dedup_lines | ||||||
| MinHash + LSH 去重(规范化、n-gram、b、r、Jaccard 阈值、聚类) | 05_minhash_lsh.py 的 near_dedup |
建议:作业里的数据是真实的,比迷你互联网乱得多。每做一步都随机抽 20 个例子看一看被删掉的和被留下的——这比任何指标都更能告诉你规则是不是在做你以为它在做的事。
8. 练习
练习 1(GPT-3 的保留规则):一篇文档的分类器分数是 0.7,它被保留的概率是多少?分数 0.95 呢?如果所有文档的分数都是 0.5,最后大约留下多少?
参考答案
P = (2 − s)^(−9)。s = 0.7:1.3^(−9) ≈ 0.094;s = 0.95:1.05^(−9) ≈ 0.645;s = 0.5:1.5^(−9) ≈ 0.026,约留下 2.6%。可见这个规则很严格,只有分数接近 1 的文档才大概率留下。
练习 2(目标数据):你用”被维基百科引用的网页”当正例训练了质量分类器,发现过滤后的数据里几乎没有菜谱、论坛讨论和诗歌。为什么?你会怎样修改?
参考答案
分类器学到的是”像不像正例”,而维基百科很少引用菜谱、论坛和诗歌,所以它们得分低(本讲 2.3 节新闻 0/50 的现象)。修改:让正例更多样(加入高质量的问答、书籍、指令数据,像 GPT-3、DCLM 那样);或者用标注者(人或大模型)按”质量 / 教育价值”标注,而不是按”来源”定义正例(2.6 节);或者按领域分别过滤;或者用随机保留规则、降低阈值来保住多样性。
练习 3(Bloom 过滤器):你要对 10 亿个段落做去重,希望假阳性率约 1%。需要多少内存?k 取多少?
参考答案
最优 k 时 FP ≈ 0.6185^(m/n),即 m/n = −ln(0.01) / (ln 2)² ≈ 9.59 位。内存约 10⁹ × 9.59 / 8 ≈ 1.2 GB;k = 9.59 × ln 2 ≈ 6.6,取 7(此时 FP ≈ 1.00%)。对比:把 10 亿个段落的 64 位哈希值存进集合至少要 8 GB,存原文就更多了。
练习 4(LSH):b = 20,r = 5。(a) 阈值大约是多少?(b) Jaccard 为 0.3、0.5、0.8 的两篇文档成为候选对的概率各是多少?(c) 想让阈值约为 0.8,应该怎样改 b 和 r?
参考答案
(a) (1/20)^(1/5) ≈ 0.549。(b) 1 − (1 − s⁵)²⁰:s = 0.3 → 0.047;s = 0.5 → 0.470;s = 0.8 → 0.9996。(c) 需要 (1/b)^(1/r) ≈ 0.8:例如 b = 10、r = 10(阈值 0.794,见 3.7 节的表),或 b = 20、r = 13(0.794)。r 越大曲线越陡,但需要的哈希函数也越多(n = b·r)。
练习 5(MinHash 的精度):两篇文档的真实 Jaccard 是 0.5。要让 MinHash 估计的标准差不超过 0.01,需要多少个哈希函数?这对 LSH 意味着什么?
参考答案
√(J(1−J)/n) ≤ 0.01 → n ≥ 0.25 / 0.0001 = 2500。直接用 MinHash 估计 Jaccard 来判断”是否超过阈值”需要很多哈希函数;LSH 的思路不同:它不精确估计 Jaccard,只用”与-或”结构让超过阈值的对以高概率碰撞,再对少量候选对算真实的 Jaccard 确认(3.8 节)。
练习 6(去重的”动作”):(a) 为什么”所有副本都删掉”的逐行去重,会让被镜像的百科原文也丢失内容?(b) C4 的”只留第一次出现”为什么反而让原文被删掉了 89%?(c) 你会怎样设计”留哪一份”?
参考答案
(a) 原文和镜像中相同的行在语料库里出现了 2 次,“出现超过一次就全部删掉”会同时删掉原文和镜像中的这些行。(b) “第一次”取决于处理顺序;文档按网址排序时 mirror.example 排在 wiki.example 前面,镜像先被处理,它的所有片段都”先出现”,原文的片段就全成了重复。(c) 在每个重复类中按一个有意义的优先级保留:例如网站的可信度(维基百科优先于镜像站)、质量分类器的分数、最早的抓取时间、文档长度(保留最完整的版本)。
练习 7(数据配比):你有 100B token 的网页、10B 的代码、1B 的数学,训练 300B token,用 UniMax(C = 4)。每个数据源的比例和重复轮数是多少?如果改用均匀配比呢?
参考答案
UniMax 从最小的开始:数学的均匀份额 100B,上限 4 × 1B = 4B → 分配 4B(4 轮);剩余 296B 分给 2 个数据源,代码的均匀份额 148B,上限 40B → 分配 40B(4 轮);剩余 256B 全给网页(2.56 轮)。比例:网页 85.3%、代码 13.3%、数学 1.3%。均匀配比:每个 100B,数学重复 100 轮、代码 10 轮、网页 1 轮——数学严重过拟合。
练习 8(模拟重复轮数):大规模训练 2T token,其中数学数据源 50B,配比 10%(重复 4 轮)。你想用 20B token 的小实验来找最优配比。按”模拟重复轮数”的做法,小实验中数学数据源应该有多大?为什么不能直接用完整的 50B?
参考答案
缩小比例 = 20B / 2T = 1%,数学数据源缩到 500M token。这样 10% 的配比在小实验中是 2B token = 4 轮,和大规模一致。如果直接用 50B,小实验里 10% 的配比只用了 0.04 轮,看不到任何重复的代价,小实验会倾向于给数学更高的权重——到了大规模就会重复太多轮(4.4 节的现象)。
练习 9(动手):
(a) 在 01_rule_filters.py 中把 Gopher 的最小词数从 50 改成 40,商品页会怎样?这说明了阈值的什么问题?
(b) 在 02_model_filters.py 的 split 中把新闻也加进正例 T,重新看演示 1 的表格。论坛的分数会变吗?
(c) 在 05_minhash_lsh.py 中把瓦片长度从 5 改成 3 和 8,重复对和非重复对的 Jaccard 分布怎样变化?(25, 4) 还是好的选择吗?
(d) 在 06_data_mixing.py 中把低质量数据的噪声从 0.6 改成 0.3,大规模实验的最优配比会怎样移动?
9. 延伸阅读
- CS336 Spring 2026 Lecture 14(
stanford-cs336/lectures中的lecture_14.py)。 - 数据选择综述:Albalak et al.,A Survey on Data Selection for Language Models(2024)。
- 规则:Raffel et al.(2019,C4);Rae et al.(2021,Gopher / MassiveText 附录 A);Dodge et al.,Documenting Large Webtext Corpora(2021)。
- 基于模型的过滤:Brown et al.(2020,GPT-3 附录 A);Touvron et al.(2023,LLaMA);Wenzek et al.(2019,CCNet);Xie et al.,Data Selection for Language Models via Importance Resampling(2023,DSIR);Li et al.(2024,DCLM);Gunasekar et al.,Textbooks Are All You Need(2023,phi-1);Paster et al.,OpenWebMath(2023);Goyal et al.,Scaling Laws for Data Filtering(2024)。
- 有害内容:Soldaini et al.(2024,Dolma);Jigsaw Toxic Comment Classification Challenge(Kaggle,2018)。
- 去重:Lee et al.,Deduplicating Training Data Makes Language Models Better(2021);Leskovec, Rajaraman, Ullman,Mining of Massive Datasets 第 3 章(MinHash 与 LSH 的经典教材);Broder,On the Resemblance and Containment of Documents(1997,MinHash 的起源)。
- 数据混合:Gao et al.(2020,The Pile);Chung et al.,UniMax(2023);Liu et al.,RegMix(2024)。
- 后训练数据:Guha et al.,OpenThoughts(2025);Yang et al.,SWE-smith(2025);Badertdinov et al.,SWE-rebench(2025)。
10. 下一讲预告
到这里,我们已经知道怎样得到一个好的基座模型:好的架构和系统(第 18、10 讲),好的超参数和规模(第 9、11 讲),好的数据(第 1214 讲)。但基座模型只会”续写”。下一讲(中期训练与后训练:SFT 与 RLHF)讨论怎样把它变成有用的助手:
- 中期训练:高质量数据、长上下文;
- 监督微调(SFT):指令数据、对话模板、只对回答部分计算损失——本讲第 5 节的后训练数据就是它的原料;
- RLHF:奖励模型(Bradley–Terry,第 12 讲的 Chatbot Arena 用的也是它)、PPO,以及 DPO 的推导。
下一讲之前,建议你:跑通 code/lecture14/ 下的 6 个脚本,在每个脚本里至少打印 5 个”被删掉的例子”亲眼看一看,完成练习 1、3、4、7。