Llama 3 的技术报告把架构、超参数、并行策略都写得很细,写到数据只有一句”多种来源,经过处理”。架构可以抄,训练方法可以抄,真正拉开模型差距、又最不愿意公开的是数据。这篇从一个看起来很简单的问题出发:所谓”在整个互联网上训练”,到底是怎么从一堆 HTML 变成十几万亿个干净 token 的?

前置:分词(token 是什么)、资源核算与 Scaling Laws(token 数和模型大小的关系)

“整个互联网”其实拿不到

先纠正一个说法。模型不是在”互联网”上训练的,而是在某个爬虫某个时刻下载下来的一份快照上训练的。web 是一堆在线服务器,要训练就得先把网页抓下来,而很多东西根本抓不到:Discord、wandb 这种应用型网站 URL 不变,内容要点按钮才出现;Facebook、X、NYTimes 要登录甚至付费,平台自己能用,外人拿不到;robots.txt 声明拒绝爬虫,Cloudflare 弹验证码,限速封 IP;服务条款(ToS)禁止机器人下载。而且这些限制在逐年收紧,2023 年 ChatGPT 的爬虫出现后,C4、RefinedWeb 这些数据集里 URL 的 robots.txt 拒绝比例明显上升。

大多数人不自己爬,用的是 Common Crawl:一个 2007 年成立的非营利组织,大约每月爬一轮,每轮几十亿网页。它的爬虫本质是图遍历:从几亿个种子 URL 出发,取出一个下载,把页面里的链接塞回队列,重复。选哪些页(selection)、别把人家服务器压垮(politeness)、多久回访(re-visit)是三个调度策略。难点在于同一个 URL 不同时间内容不同,而大量不同 URL(镜像站)又是同一份内容,这为后面的去重埋下了伏笔。

Common Crawl 发布两种格式:WARC 是原始 HTTP 响应(HTML),WET 是它自己转好的纯文本。直觉上用现成的 WET 最省事,但 DCLM(2024)的实验表明,从 WARC 出发、用 trafilatura 或 resiliparse 自己抽正文,下游准确率更高。原因是 HTML → 文本这一步本身有损:导航栏、广告、页脚要删,嵌套表格要展平,什么算”正文”并没有标准答案,抽取规则的好坏会一路传到模型能力上。PDF 更麻烦:它只描述版面,没有 <h1>、<p> 这样的语义标签,扫描件还得用 VLM 做 OCR,成本比 HTML 高得多;但 PDF 里论文、报告多,质量通常高于网页,所以 Hugging Face 专门做了 FinePDFs。

除了网页,还有几类值得单独收的高质量来源:Wikipedia(直接下载 dump,不要爬)、GitHub(代码,挑 MIT/Apache 这类宽松许可)、arXiv(有 LaTeX 源码)、StackExchange(问答格式天然接近指令数据)。代码被普遍认为不仅提升编程,也有助于推理,不过这没有严格证明。高质量来源也不等于安全:有人在 Wikipedia 生成 dump 前一刻做恶意编辑,编辑随后被回滚,但 dump 里已经带上了,这就是一种数据投毒。

还有一条绕不过去的线:版权。1976 年美国版权法保护”固定在有形媒介上”的原创作品,门槛极低,你的个人博客也自动有版权。所以要用网上的内容,只有两条路:拿 license,或者主张 fair use。2025 年 Bartz v. Anthropic 的简易判决给了一个具体参照:用书训练模型算 fair use,但下载盗版副本不算,最后以 15 亿美元和解。也就是说,“怎么拿到的”和”拿来做什么”是两个独立的问题。

停一下,检查理解单选

一个网站在 robots.txt 里拒绝了某 AI 公司的爬虫。下面哪个说法最准确?

第一把筛子:人工规则

Common Crawl 的绝大部分内容不是有用的自然语言:菜单、商品列表、乱码、模板、SEO 垃圾。最朴素的清洗方法是写规则。2019 年 Google 做 T5 时构建的 C4 就是典型:

输入:一个 Common Crawl 快照,约 1.4 万亿 token
  只留以标点结尾、且至少 5 个词的行
  删掉少于 3 句的页面
  删掉含脏词表中任一词的页面
  删掉含 '{'(多半是代码)、'lorem ipsum' 等的页面
  langdetect 判断英语概率 ≥ 0.99 才留
输出:806 GB,约 1560 亿 token

一个快照、1.4T token 进去,156B 出来,规则一口气删掉了将近九成。DeepMind 的 Gopher 规则思路相同(比如”至少 80% 的词含字母”),Falcon 的 RefinedWeb 和 Hugging Face 的 FineWeb 都在这套规则上继续叠加,最后加上 PII 匿名化(邮箱、IP)。FineWeb 从 95 个 dump 得到 15T token。

规则的好处是不带模型的偏见:它只看表面特征,不会因为”像某种文本”就偏爱它。RefinedWeb 和 Dolma 刻意不用机器学习过滤,就是怕把 web 筛成一个窄的、带着分类器口味的子集。坏处也明显:规则很粗。一篇写得很差、但每行都以句号结尾的文章能过;一首没有标点的好诗会被删。规则能去掉明显的垃圾,却分不出”及格”和”优秀”。

第二把筛子:学一个”好数据长什么样”

要分出优秀,就得先说清楚什么是”好”。数据圈的通用框架是:手里有少量目标数据 T(你认为好的样本)和海量原始数据 R(Common Crawl),想从 R 里找出像 T 的子集。语言识别、质量过滤、有害内容过滤、找数学数据,全是这个框架,只是 T 不同。

这个分类器有两个硬要求。一是要能泛化,找出 T 之外的新数据,否则只找回 T 本身毫无意义。二是要极快,因为它要在整个 R 上跑一遍,R 是几百 T token 的量级。所以这里用的不是大模型,而是两类便宜模型:

打分方式代表训练数据代表数据集
生成式:score(x) = p_T(x)KenLM n-gram只用 TCCNet(Wikipedia 上训练 5-gram,留困惑度低的)
判别式:score(x) = p(T | x)fastText 线性分类器T 当正例,R 中随机采样当负例GPT-3、LLaMA、DCLM

fastText 把文档表示成词和词 bigram 的 embedding 平均,再接一个线性层输出概率。没有 attention、没有深层网络,一个 CPU 核每秒能处理大量文档,这是它能扫完全网的原因。

“好”的定义比分类器本身更关键,而且常常出人意料。几个例子:

  • CCNet 用 Wikipedia 训练 5-gram,留下”读起来像 Wikipedia”的文档。局限也就在这:它只能找到和 Wikipedia 风格相似的文本。
  • LLaMA 换了个角度:正例是”被 Wikipedia 引用过的网页”,而不是 Wikipedia 文章本身。Wikipedia 自身写法太统一,它引用的来源风格各异,但内容大概率靠谱。
  • DCLM 的正例是 OpenHermes-2.5(大多是 GPT-4 生成的指令数据)加 Reddit 的 ELI5 问答,负例是基本没过滤的 RefinedWeb。正例看起来和”好网页”无关,但它在所有试过的方法里效果最好,成了开源社区的标准做法之一。
  • phi-1 用贵模型标注、便宜模型推广:让 GPT-4 判断 10 万条 Python 代码”对学编程的学生有多大教育价值”,用这些标签训练一个 random forest 去筛整个 The Stack。1.3B 模型在筛过的子集上 36K 步拿到 HumanEval 17.68%,原始子集上 96K 步只有 12.19%。

还有一个推论:质量没有通用定义。OpenWebMath 把”好”定义成”是数学”,先用规则查 LaTeX 命令,再用在数学语料上训练的 KenLM 和 fastText 双重打分,得到 14.7B token;用它训练的 1.4B 模型,数学超过了用 20 倍数据训练的模型。你定义什么,就会得到什么。

阈值怎么定?GPT-3 的做法不是硬阈值,而是按分数随机保留:np.random.pareto(9) > 1 - score。numpy 的 pareto 是 Lomax 分布,Pr[X > t] = (1+t)^(-9),所以 score=1 必留,score=0.5 保留概率约 1.5^(-9) ≈ 0.026,score=0 也还有约 0.002 的机会。留一点低分数据,是为了不让分类器的口味完全主导分布。

停一下,检查理解单选

按 GPT-3 的规则 np.random.pareto(9) > 1 - score,一个分类器得分 0.8 的文档被保留的概率约是多少?

过滤多狠才合适:质量和数量的拉锯

DCLM 先把 Common Crawl 处理成 240T token 的未过滤池(DCLM-pool),经过语言过滤、规则、去重、质量分类器,最终剩 3.8T,保留率 3.8/240 ≈ 1.6%。整条流水线里,过滤是对结果影响最大、删得最多的一步。

删这么多值不值?要看你打算训练多少 token。课上有一组对比:固定一个数据池一直训练,DCLM 过滤后的数据前几个 epoch loss 明显更低,但数据少,很快就开始反复 epoch,然后过拟合;几乎没过滤的数据起点更差,但 epoch 来得晚,loss 能持续往下走。所以没有一个对所有规模都最优的阈值:训练 token 少就要少而精,训练 token 多就得放宽,宁要更多的”还行”也不要反复咀嚼同一批”很好”。

NVIDIA 的 Nemotron-CC 正是站在这一边:它认为 DCLM 和 FineWeb-Edu 删掉九成太狠了。于是 HTML 抽取换成留 token 更多的 jusText;质量打分用 Nemotron-340B 按教育价值给文档评分,再蒸馏成小模型,和 DCLM 分类器做集成;对低质量文档不删,而是用 LM 改写;对高质量文档用 LM 生成问答对等任务。最终 6.3T token,其中高质量子集 1.1T。作为参照,Llama 3 训练了 15T token,Qwen3 训练了 36T。这些训练 token 数可能包含重复的 epoch,和数据集本身的不重复 token 数不能直接比。

停一下,检查理解多选

关于质量过滤,下面哪些说法正确?

去重:从 O(n²) 到线性时间

过滤完的数据里还有大量重复。完全重复好理解:镜像站、GitHub fork。近似重复更常见:到处都是的 MIT license 文本、只把城市名换掉的模板广告、复制时多了一个逗号的同一段话。C4 里有一段防毒面具的商品描述出现了六万多次。

为什么要去重?Lee et al. 2021 的结论是:token 少了,信息没少,训练更高效;同时模型不会死记那些重复很多遍的内容,减少版权和隐私泄露。同一套技术还用于去污染,即确保测试集没混进训练集,这在评估里会再出现。

难点在复杂度。过滤是逐条判断,线性、可并行;去重要拿每篇和其他所有篇比,两两比较是 O(n²),在几十亿文档上不可能。破局点是哈希:把每篇文档单独映射成一个小指纹,相同指纹的自然落到一起,比较就变成了分组。

完全去重最直接:按哈希值分组,每组留一篇。用 MurmurHash 这类快哈希就行,偶尔碰撞无所谓,用不着 SHA-256。它的毛病是太较真:多一个空格、大小写不同,哈希就完全不同。规范化(统一大小写、空白)能缓解,但规范化过强又会把不同内容错误合并。近似重复需要别的工具。

MinHash:让碰撞概率等于相似度

先定义”有多像”。把文档切成 shingle(连续 n 个词的片段)组成集合,两个集合的 Jaccard 相似度是交集大小除以并集大小。

现在想要一个奇怪的哈希函数:两篇文档越像,越容易碰撞,并且碰撞概率恰好等于 Jaccard。MinHash 就是这样一个函数:用一个随机哈希函数打散集合里所有元素,取最小值。

为什么成立?一个随机哈希函数相当于把并集里所有元素随机排一次序,取最小值就是看排第一的那个元素。如果它在交集里,两边的最小值一样,碰撞;如果它只在一边,就不碰撞。每个元素排第一的概率相同,所以碰撞概率 = 交集大小 / 并集大小 = Jaccard。用 k 个不同 seed 就得到一个长度为 k 的签名,签名里相等位置的比例就是 Jaccard 的估计。

LSH:把线性曲线掰成阶跃

单个 MinHash 碰撞只说明”以 Jaccard 的概率碰撞”,没法判断 Jaccard 是否超过阈值。我们要的是:超过阈值的几乎必然碰撞,低于阈值的几乎不碰撞。Locality Sensitive Hashing 的做法是把 n 个 MinHash 分成 b 个 band,每个 band r 个。规则是 band 内 AND、band 间 OR:只要有某一个 band 里 r 个值全相同,就算候选对。

推导一行就够:一个 band 全相同的概率是 s^r,b 个 band 都不全相同是 (1 - s^r)^b,所以

这条 S 形曲线陡升的位置约在 。增大 r 让 s^r 衰减更快,曲线右移、更难匹配;增大 b 给更多次机会,曲线左移、更容易匹配。下面把整套流程跑一遍:四篇短文,a 和 b 只差最后一个词,c 改了几个词,d 是 MIT license 的开头。

import hashlib
from collections import defaultdict
 
def h(x, seed):
    # 用 seed 区分出一族"独立"的哈希函数;blake2b 跨进程稳定,结果可复现
    return int.from_bytes(hashlib.blake2b(f"{seed}|{x}".encode(), digest_size=8).digest(), "big")
 
def shingles(text, n=3):
    w = text.lower().split()
    return {" ".join(w[i:i+n]) for i in range(len(w)-n+1)}
 
def jaccard(a, b):
    return len(a & b) / len(a | b)
 
def signature(S, k):
    return [min(h(x, s) for x in S) for s in range(k)]
 
docs = {
    "a": "the quick brown fox jumps over the lazy dog near the old river bank today",
    "b": "the quick brown fox jumps over the lazy dog near the old river bank tonight",
    "c": "the quick brown fox leaps over the lazy cat near the new river bank today",
    "d": "permission is hereby granted free of charge to any person obtaining a copy",
}
S = {k: shingles(v) for k, v in docs.items()}
sig = {k: signature(v, 200) for k, v in S.items()}
for x, y in [("a","b"), ("a","c"), ("a","d")]:
    est = sum(p == q for p, q in zip(sig[x], sig[y])) / 200
    print(f"{x}-{y}  真实 Jaccard {jaccard(S[x], S[y]):.3f}  MinHash 估计 {est:.3f}")
 
b, r = 20, 10
buckets = defaultdict(set)
for k, s in sig.items():
    for i in range(b):
        # band 内 r 个值拼成 key:全相同才进同一个桶(AND);任一 band 同桶即候选(OR)
        buckets[(i, tuple(s[i*r:(i+1)*r]))].add(k)
cands = {tuple(sorted(v)) for v in buckets.values() if len(v) > 1}
print("阈值约", round((1/b)**(1/r), 3), " 候选对:", sorted(cands))
for s_ in (0.5, 0.7, 0.8, 0.9):
    print(f"s={s_}  碰撞概率 {1-(1-s_**r)**b:.3f}")
a-b  真实 Jaccard 0.857  MinHash 估计 0.850
a-c  真实 Jaccard 0.182  MinHash 估计 0.150
a-d  真实 Jaccard 0.000  MinHash 估计 0.000
阈值约 0.741  候选对: [('a', 'b')]
s=0.5  碰撞概率 0.019
s=0.7  碰撞概率 0.436
s=0.8  碰撞概率 0.897
s=0.9  碰撞概率 1.000

200 个哈希的签名把 Jaccard 估得很准;b=20、r=10 时阈值在 0.74 附近,0.5 以下几乎不会成为候选,0.9 以上几乎一定是。c 只改了三个词,Jaccard 却只有 0.18,因为每改一个词会破坏它所在的 n 个 shingle。这就是 shingle 长度的取舍:n 太小(比如 1)时任意两篇英文都共享大量常用词,Jaccard 虚高;n 太大时改一个词就破坏一大片,近似重复反而认不出。

真实配置参考:FineWeb 用 5-gram shingle、112 个哈希、b=14、r=8,阈值 (1/14)^(1/8) ≈ 0.72。Lee et al. 用 b=20、r=450,阈值约 0.993,只去掉几乎一模一样的。存储也要算:每篇 112 个 32 位哈希约 448 字节,十亿篇就是 448 GB 签名,要分片并按 band 做 MapReduce 式的 groupby。LSH 只产出候选对,之后通常再用精确 Jaccard 复核,每簇留一篇。

停一下,检查理解单选

用 MinHash LSH 去重,发现很多 Jaccard 只有 0.5 左右的文档也被当成重复删掉了。应该怎么调?

数据配比:别让小来源被嚼 50 遍

每个来源都洗干净了,最后要决定混合比例:网页、代码、论文、书各占多少。混合就是来源上的一个分布,比如 {web: 0.5, code: 0.2, wiki: 0.3}。训练时,batch 里每个位置按这个分布抽一个来源,再从该来源取一条序列。

两个朴素基线:均匀(每个来源一样多)不看大小和质量;按比例(p ∝ token 数)会让最大、往往也最脏的来源占掉大头。直觉上应该给高质量来源更高权重,但这里有个算术陷阱。假设有 10T 低质量 token、10B 高质量 token,按 50/50 训练 1T token:

低质量 epoch 数 = 0.5 × 1e12 / 10e12 = 0.05   只用到 5%
高质量 epoch 数 = 0.5 × 1e12 / 10e9  = 50     同一批数据重复 50 遍

高质量数据被反复训练 50 遍,好的情况浪费算力,坏的情况过拟合。规矩是:定比例时一定要算出每个来源实际过几个 epoch。

几种修正:

  • UniMax(多语言场景):按来源均匀采样,但给每个来源设 epoch 上限,到上限就停,预算分给别的来源。
  • 回归混合(RegMix 等):在小模型上用很多组随机权重各训一次,拟合”混合权重 → loss”的回归模型,再在回归模型上找最优混合,拿去训大模型。思路和 资源核算与 Scaling Laws 一样,用便宜的小实验推出答案。风险是优化会把解推到回归模型没见过的极端区域,以及目标指标太窄时会过拟合 eval(eval 里代码多,就会给代码巨大权重)。
  • Simulated epoching:上面的回归有一个规模陷阱。小模型只训 10B token 时,给高质量来源 0.9 的权重也不到 1 个 epoch,看起来最优;放到 1T token 就是 90 个 epoch。修正办法是让小实验”看起来像”大实验:每个来源按 小实验 token / 大实验 token 同比例缩小。缩小后小实验也会遇到反复 epoch 的惩罚,选出的混合更均衡。

配比也可以在一个数据集内部做:Nemotron 把 Common Crawl 按主题分组、再按质量分档,得到”领域 × 质量”的二维网格,每格是一个混合成分。

停一下,检查理解单选

大规模训练计划用 500B token,来源是低质量 10T + 高质量 10B;小规模实验只训 10B token。按 simulated epoching,小实验里两个来源应缩成多大?

从网页到合成数据

把这些年的开源数据集按时间排开,能看到方法的演进:

数据集年份过滤方式规模
WebText2019Reddit karma ≥ 3 的外链(人当分类器)40 GB
C42019人工规则156B token
The Pile2021按来源挑选 22 个领域约 275B token
RefinedWeb2023Gopher 规则,刻意不用模型5T token
FineWeb2024规则 + MinHash15T token
DCLM-baseline2024fastText 质量分类器3.8T token
Nemotron-CC2024LLM 打分蒸馏 + 合成改写6.3T token

主线是从”人工规则”走到”学一个质量模型”,再走到”用 LM 改写和生成”。合成数据在后训练阶段已经是主流:开源社区的指令和推理数据几乎都是强模型生成的。OpenThoughts 用 QwQ-32B 当 teacher 生成 120 万条推理样本,发现每个 prompt 多采几个回答有帮助,而更强的模型不一定是更好的 teacher(QwQ-32B 比 DeepSeek-R1 更合适)。这部分怎么用,见 后训练。

另一个方向是只用许可明确的数据。Common Pile(2025)收集了 8 TB 宽松许可数据,结果发现检查许可远比想象难:有人把受版权保护的作品重新发布并标成 CC BY(license laundering);数据集整体的许可(如 Dolma 的 ODC-By)不等于里面每篇都是宽松许可;开源模型生成的合成数据,模型本身可能就用了未授权数据。工程上的建议是:每篇文档至少记下来源、快照日期、许可、抽取工具版本、被过滤的原因和内容哈希,否则出了污染、删除请求或质量回归时无从追查。

记住这三件事

  1. 预训练数据的流水线是”抓取 → 抽取 → 过滤 → 去重 → 配比”,其中过滤删掉 98% 以上,影响最大;而”好”的定义决定了你得到什么。
  2. 去重靠 MinHash(碰撞概率等于 Jaccard)加 LSH(1-(1-s^r)^b,阈值约 (1/b)^(1/r)),把 O(n²) 的两两比较变成线性时间的分组。
  3. 配比时一定算清每个来源过了几个 epoch;过滤阈值和混合比例都随训练规模变化,小实验要按比例模拟大规模。

延伸阅读