第 14 讲:数据 II —— 过滤、去重、混合,以及后训练数据

Language Modeling from Scratch · Lecture 14。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应: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 学完本讲,你应该能够

  1. 说出数据流水线的四个环节(转换、过滤、去重、混合),以及每个环节要解决的问题。
  2. 实现 C4 和 Gopher 的规则过滤,并说出它们在哪里会误删、在哪里会漏删。
  3. 用”目标数据 T、原始数据 R”的框架理解所有基于模型的过滤:fastText 分类器、KenLM 困惑度、DSIR 重要性重采样;解释 GPT-3 的随机保留规则。
  4. 解释为什么”目标数据 T 选成什么样,过滤出来的数据就是什么样”,以及为什么过滤阈值应该随训练规模变化。
  5. 用正则表达式做个人信息脱敏,并说出它的误报和漏报;解释词表过滤的”斯肯索普问题”。
  6. 说清精确去重、Bloom 过滤器、MinHash、LSH 的原理,推导 MinHash 的碰撞概率和 LSH 的 S 形曲线,并亲手在语料库上完成近似去重。
  7. 解释数据配比中”有限数据源 → 重复轮数”的问题;说出温度采样、UniMax、基于回归的配比、“模拟重复轮数”的思路。
  8. 描述后训练数据的基本配方(环境 → 任务 → 老师模型的回答),以及 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(百科文章)121high(其中 1 篇其实是被误判为英文的中文网页)
news(新闻)60high
mirror(镜像:原样或略作改动地复制百科文章)30high(内容好,只是重复了)
forum(论坛)60medium(有用,但口语化,还夹着个人信息)
product(商品页)80low
spam(垃圾 SEO 页面)44low
template(模板批量生成的页面)40low
legal(各网站几乎相同的条款页)11low
code(代码页)30code(要不要,取决于你想训练什么)

另外,141 篇文档属于某个”重复组”(镜像和它的原文、模板页、条款页、只改了函数名的代码页)。

【边界】迷你互联网是程序生成的,比真实网页整齐得多:同一类网页来自同一组模板,不同类型之间泾渭分明。所以下面很多方法在这里的表现”好得不真实”。每一节都会指出哪些结论可能偏乐观。


2. 过滤

2.1 最便宜的办法:人工规则

最古老的过滤方法是人工规则:看几个简单的统计量(词数、平均词长、标点……)就决定一篇文档的去留。有不少著名的数据集刻意只用规则:C4、Gopher(MassiveText)、RefinedWeb、FineWeb、Dolma。理由是规则透明、便宜,也不会把”某个模型的偏好”带进数据里。

Gopher 规则(Rae et al. 2021)按整篇文档判断,任何一条不满足就删掉:

  1. 词数在 50 到 100,000 之间;
  2. 平均词长在 3 到 10 个字符之间;
  3. ”#“、省略号的个数各自不超过词数的 10%;
  4. 以项目符号开头的行不超过 90%;
  5. 以省略号结尾的行不超过 30%;
  6. 至少 80% 的词含有字母;
  7. 至少包含 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 保留的字符
article120/121120/12199%
news60/6060/6093%
mirror30/3030/3099%
forum60/6060/6094%
code0/300/300%
product0/800/800%
spam0/440/440%
template40/4040/4091%
legal11/1111/1144%

规则删掉了所有商品页(Gopher:词数不到 50;C4:没有完整的句子)、所有垃圾页(Gopher:不含常用词)、所有代码(词数太少、含大括号);但模板页和条款页一篇也没删——它们都是通顺的英文句子,规则看不出它们”没有价值”或”重复了很多遍”。被删掉的那 1 篇 article 其实是被语言识别误判的中文网页:中文不用空格,split() 只切出 9 个”词”。

规则在这里显得特别准,是因为迷你互联网的类型泾渭分明(商品页恰好都是 45 个词左右,刚好低于 50 的阈值)。真实网页的分布是连续的。所以代码里还有一组手写的”刁钻”例子:

例子GopherC4
简短的新闻快讯(高质量)删(词数)保留
菜谱(一半是列表)保留保留,但标题和配料表全被删掉
数学讲解(高质量,符号多)删(平均词长)保留
带代码的教程(高质量)保留删(大括号)
通顺的广告软文(低质量)保留保留
语法正确的胡言乱语(低质量)保留保留
中文文章(高质量)删(词数)删(少于 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 = 网页。

对过滤算法有两个要求:

  1. 能从 T 推广出去:我们希望 T’ 和 T 不一样——如果只是找出 T 的复制品,那直接用 T 就行了;
  2. 极快:它要在海量的 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 保留
articlehigh0.7645/53
newshigh0.020/50
mirrorhigh0.8017/21
forummedium0.010/44
codecode0.010/24
productlow0.000/56
spamlow0.000/32
templatelow0.000/28
legallow0.010/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)。所以

P(保留∣s)=(1+(1−s))−9=(2−s)−9P(\text{保留} \mid s) = (1 + (1 - s))^{-9} = (2 - s)^{-9}

score理论 (2−s)^−9模拟 20 万次
1.01.00001.0000
0.90.42410.4242
0.80.19380.1920
0.50.02600.0261
0.00.00200.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 模型:

类型比特 / 字节头部 / 中部 / 尾部
article1.1252 / 0 / 1
news4.9231 / 19 / 0
mirror0.9621 / 0 / 0
forum5.790 / 39 / 5
code7.140 / 0 / 24
product6.990 / 0 / 56
spam6.522 / 11 / 19
template5.550 / 28 / 0
legal5.850 / 8 / 0

AUC = 0.987。和分类器比:新闻有 31 篇进了头部(生成模型对”不同体裁的好文本”更宽容);但模板页全在中部,还有 2 篇 “weight loss pills” 垃圾页进了头部——字节级 n-gram 只看局部的拼写,这几个英文单词拼写很普通,重复一百遍也不会被扣分。生成模型奖励的是”局部通顺、常见”的文字;分类器见过负例里的模板页和垃圾页,知道要把它们往下压,生成模型没有这个信息。

2.5 重要性重采样:DSIR

DSIR(Xie et al. 2023)换了一个目标:不是”留下好的”,而是让抽出来的数据的分布匹配 T 的分布。做法:

  1. 把每篇文档表示成”哈希 n-gram 特征的袋子”(例如 1 万个桶);
  2. 分别在 T 和 R 上估计特征的分布 p_T、p_R(多项分布);
  3. 每篇文档的对数重要性权重 = Σ_特征 [log p_T(特征) − log p_R(特征)];
  4. 按权重不放回地抽样(代码用 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.8317
news−104.3275
code−42.592

测试集里”像 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 篇训练文档,只把标签换成”标注者认为是不是高质量”(用迷你互联网的标准答案扮演标注者):

类型正例 = 百科正例 = 标注者说好
article45/5352/53
news0/5050/50
mirror17/2121/21
其余类型00

换了”目标”的定义,新闻就全部留下来了。用大模型当标注者,等于把大模型对”好数据”的理解蒸馏进一个又快又便宜的分类器。代价是标注者的偏好(和偏见)也被一起继承了;“教育价值”高的数据也不一定对所有下游任务都好。(这里 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 设计空间

  1. 单位是什么?句子、段落、整篇文档?
  2. 怎样算匹配?完全相同?有共同的片段?共同片段的比例超过阈值?
  3. 匹配后做什么动作?全部删掉?只留一份?

核心难点:去重本质上是”每一项和其他所有项比较”。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 的三句话片段去重:任何一个”连续三句话的片段”出现不止一次,只保留第一次出现的那一份。
类型逐行去重删掉的字符三句话片段去重删掉的字符
article9%23%
news0%5%
mirror31%0%
forum55%22%
code33%0%
product37%0%
spam21%0%
template0%68%
legal73%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 的概率:

FP≈(1−e−kn/m)k\text{FP} \approx \left(1 - e^{-kn/m}\right)^k

k 太小,一个位置撞上就误判;k 太大,位数组很快被填满。对 k 求导可得最优的 k = (m/n)·ln 2,此时 FP ≈ 0.6185^(m/n)。

实测(加入 1 万个元素,查询 10 万个从没加入过的元素):

每个元素的位数 m/nk理论实测
810.11750.1185
820.04890.0504
840.02400.0236
860.02160.0227
880.02550.0256
430.14690.1479
16110.00050.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 相似度:

J(A,B)=∣A∩B∣∣A∪B∣J(A, B) = \frac{|A \cap B|}{|A \cup B|}

例如 A = {1, 2, 3, 4},B = {1, 2, 3, 5},J = 3/5 = 0.6。近似重复:Jaccard ≥ 某个阈值。

算法上的挑战依然是:怎样在线性时间内找到所有近似重复的对?

MinHash:一个随机哈希函数 h,使得

P[h(A)=h(B)]=J(A,B)P[h(A) = h(B)] = J(A, B)

通常我们希望不同的东西哈希成不同的值;这里反其道而行之:越相似,越容易碰撞。

定义: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):

哪一对真实 Jaccardn = 10n = 100n = 1000
镜像0.8060.808 ± 0.1300.808 ± 0.0400.806 ± 0.013
模板页0.6000.615 ± 0.1510.601 ± 0.0500.599 ± 0.014
代码页0.5500.557 ± 0.1470.557 ± 0.0490.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 个带中至少有一个全部相同(“或”):

P(候选对)=1−(1−sr)bP(\text{候选对}) = 1 - (1 - s^r)^b

实现上,每个带只需要把文档按”这个带的取值”分桶(哈希表),同一个桶里的文档才两两配对——不需要两两比较所有文档。

课件的例子:s = 0.8,b = 5,r = 10 → P = 0.4333。更多数字:

(b, r)0.50.70.750.80.850.90.950.98阈值 ≈ (1/b)^(1/r)
(1, 1)0.5000.7000.7500.8000.8500.9000.9500.980—
(10, 10)0.0100.2490.4400.6790.8880.9861.0001.0000.794
(10, 20)0.0000.0080.0310.1090.3270.7260.9881.0000.891
(20, 20)0.0000.0160.0620.2070.5460.9251.0001.0000.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 最小 / 中位数 / 最大
原文 − 镜像300.74 / 0.86 / 0.95
条款页550.85 / 0.87 / 0.90
模板页7800.57 / 0.61 / 0.77
代码页(只改了函数名)500.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.1486337.6%100%1.000.98
(25, 4)0.4511751.0%99%1.000.97
(20, 5)0.558780.8%88%1.000.87
(10, 10)0.791630.1%18%1.000.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.420.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 数。

直觉告诉我们:应该多用高质量的数据源。但是——

  1. 我们还想要多样性:文学、代码、论文这些”没法比较”的数据源都要有;
  2. 每个数据源都是有限的。给一个小数据源太多权重,就得把它重复很多轮(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α = 0UniMax (C = 4)
英语10T88.45% / 0.265.84% / 0.149.90% / 0.120% / 0.032.97% / 0.1
中文1T8.85% / 0.220.82% / 0.425.01% / 0.520% / 0.432.97% / 0.7
德语300B2.65% / 0.211.40% / 0.817.43% / 1.220% / 1.332.97% / 2.2
斯瓦希里语5B0.04% / 0.21.47% / 5.95.10% / 20.420% / 801.00% / 4.0
约鲁巴语500M0.00% / 0.20.47% / 18.62.56% / 102.320% / 8000.10% / 4.0

α = 1 时小语种几乎学不到;α = 0 时约鲁巴语要重复 800 轮;中间的 α 两头都不理想;UniMax 让每种语言都尽量多,但不超过 4 轮。

4.4 最优配比会随规模变化

这是数据混合中最微妙的一点。用第 9 讲的玩具任务做实验(演示 3):

  • 高质量数据:干净的标签,但数量有限(一个固定的数据池,用完就从头再来一轮);
  • 低质量数据:60% 的标签被换成随机的 token,但要多少有多少;
  • w = 每个批次中高质量数据的比例。表中是可约损失(越低越好)和高质量数据被重复的轮数。
w小规模(训练 20 万条,高质量 2 万条)大规模(训练 160 万条,高质量 2 万条)模拟重复轮数(训练 20 万条,高质量 2500 条)
00.885(0 轮)0.761(0 轮)0.897(0 轮)
0.250.624(2.5 轮)0.518(20 轮)0.643(20 轮)
0.50.414(5 轮)0.385(40 轮)0.539(40 轮)
0.750.246(7.5 轮)0.354(60 轮)0.604(60 轮)
0.90.166(9 轮)0.459(72 轮)0.806(72 轮)
1.00.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)等基于回归的方法:

  1. 定义一个配比的分布(例如狄利克雷分布),从中抽很多种配比;
  2. 每种配比训练一个小模型,测下游指标(小心不要过拟合到评测集上!);
  3. 用回归模型(线性回归、梯度提升树……)拟合”配比 → 指标”;
  4. 在回归模型上找最优配比,用它训练大模型。

这和规模定律(第 9、11 讲)是一个思路:在小规模上估计”配比 → 损失”,优化,再外推。它依赖两个”希望”:

  • 希望 1:回归模型在最优点附近是准的;
  • 希望 2:小规模的最优配比能迁移到大规模。

演示 4 用二次曲线拟合 log(损失) 和 w 的关系:

用哪组小实验拟合拟合误差(log 损失,最大)预测的最优 w拿到大规模上的损失
小规模0.0691.000.952(最差)
模拟重复轮数0.1800.450.385(w = 0.5;大规模最优是 0.354)

不做”模拟重复轮数”时,希望 2 彻底落空;做了之后,预测的配比在大规模上离最优不远。

混合小结:问题是怎样给不同的数据源(维基、网页、代码……)加权;基于回归的配比在小规模上估计”配比 → 损失”再优化(和规模定律类似);要特别注意重复轮数和过拟合——解决办法是设上限(UniMax)或模拟(simulated epoching)。

【边界】玩具实验只有两个数据源、一个变量,而且”低质量”就是随机标签——真实的低质量数据有自己的用处(多样性、覆盖面)。结论的方向(最优配比随规模变化、模拟重复轮数有帮助)是可靠的,具体数字不要外推。


5. 后训练数据

前面讲的都是预训练数据。中期训练和监督微调(SFT)的数据长得像评测题:一个问题(提示)加一个好的回答。基本配方:

  1. 定义一组环境(例如数学、代码仓库);
  2. 定义一组任务 / 提示;
  3. 用一个强模型(老师)生成回答。

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. 小结

  1. 过滤:训练分类器(语言识别、质量、有害内容),定义”好的样子”。目标数据 T 决定了一切;规则便宜但只看形式;过滤强度应随训练规模变化。
  2. 去重:哈希让模糊匹配能扩展到海量数据。精确去重简单但脆弱;Bloom 过滤器用极少的内存回答”见过吗”;MinHash + LSH 把”相似度超过阈值”变成”在某个桶里碰撞”。
  3. 混合:在小规模上尝试不同配比,外推到最优配比和大规模;小心重复轮数(上限或模拟)。
  4. 后训练数据:长得像评测题,大量使用合成数据(老师模型)。
  5. 大量的数据工作是领域相关的,需要亲眼看例子。 本讲几乎每一个”意外”(链接文字让镜像不再相同、“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。

← 返回课程目录