从0训练LLM · 现代大模型体系 阅读 + 实验 ≈ 35 分钟 页内实验 ×3
C7 数据:从哪来、怎么洗、怎么配 第 2 课

同一段话在语料里出现几万次,会怎样?

去重与 MinHash
重复的模板会被模型背下来,也会被分词器学成 token:我们项目分词器里最长的 token 就是章末套话「且看下文分解」。精确哈希抓不住近重复,MinHash 把两两比较变成查桶;但 FineWeb 发现,去重去得太狠,留下的反而更差。
❓

本课一问:规则留下来的文本里,同一段话可能被转载、镜像、套模板成千上万次。不管它,会怎样?

这一课先不碰:MinHash 为什么能估出相似度的证明(岔路课)、「合法但没营养」的文本(第 3 课)、有意把好数据多喂几遍(第 5 课)。

从已有出发

贯穿任务走到了这里:《三國志演義》的原始文件去掉了英文声明,17,584 行硬换行拼回了 3,697 段,每段都用第 1 课的规则逐条求值过。规则看的是一篇文档自己长什么样;重复看的是文档和文档之间的关系,规则管不到。所以本课先问:这 3,697 段之间有没有重复?网页上的重复又多到什么程度?

从已有出发 · 累计数据流 点已学环节看形状与参数
解码层

模型这条流水线上,本章始终停在最左边的「文字」一格;把这一格展开,就是下面这条数据管线:

从已有出发 · 数据管线 本课其他环节点环节看它做什么、真实数字

先自己设计:不去重,有多少喂多少

最省事的做法是不管重复。Lee 等人在 C4 里找到一句 61 个词的英文句子,重复了 61,036 次;去重之后,模型原样复述训练文本的频率降到原来的 1/10,而 C4 的验证集里有 4.6% 的样本和训练集重叠——这部分「考题」模型其实见过 。

重复还有一个更早、更隐蔽的后果:分词器也会把重复的东西学成 token。我们自己的项目就有现成的例子。

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案不去重:模板重复多少次,就进训练多少次

真实的分词器里已经有这个现象。项目分词器(词表 2,048,只在训练集上训练)里最长的 token 是 「且看下文分解」(id 1639)——章末套话「未知……如何,且看下文分解」的后半句,在训练集里出现 88 次。它比任何人名都长:「司馬懿」「諸葛亮」都只有 3 个字。

放大它。Project Gutenberg 里的每一本书都带同一段英文头部声明和尾部许可证(我们的原始文件里是 752 + 18,437 个字符)。假设把训练集的 107 回当成 107 本独立的电子书,每本原样套上声明(教学构造:文字全是真的,重复次数是构造的),用项目的 minillm/tokenizer.py 重训同样大小的词表:

  • 英文声明占掉的合并:1,034 / 1,791;
  • 中文验证集:58,386 → 71,330 个 token,多 22.2%;
  • 干净条件下学到的 426 个多字中文 token,有 372 个被挤掉。
真分词器 + 真训练的对照实验 · 旧版条目 M1-34 的兑现
我们项目的分词器(词表 2048,只在训练集上训练)里最长的 token
    对照实验:模板重复得越多,词表被它占得越多

    🎯

    试三件事:① 读最上面一行,找出排第一的 token 是什么、在训练集里出现了多少次;② 在三种条件之间切换,看堆叠条里红色(英文声明)的部分怎么从 0 涨到一半以上;③ 在「每一回都带一份声明」下读「还在不在词表里」:「諸葛亮」被挤掉了,「司馬懿」还在——出现得越少的词,越先被挤掉。

    朴素方案二:精确哈希去重

    那就删掉重复的。最直接的办法是给每篇文档算一个哈希值(或者比 URL),哈希相同就只留一篇。它能删掉一字不差的拷贝,但网页上的重复大多是近似的:转载时加一行「来源:某某网」,改几个字,换个日期。改一个字,整篇的哈希值就完全不同,精确去重认为它们毫无关系。下面实验台的第 ⓪ 步就是这个。

    改进:shingle + Jaccard → MinHash → 分桶

    第一步,把「像不像」变成一个数。把文档切成连续的片段(shingle,这里是连续 5 个字),两篇文档的相似度定义为两个片段集合的 Jaccard:交集大小除以并集大小。一字不差是 1,完全无关是 0;转载时改几个字、加一行来源,Jaccard 仍然很高(下面实验台的默认例子就是这种)。

    第二步的麻烦是配对数。两两算 Jaccard,n 篇文档要比 n(n−1)/2 次。我们整本书只有 3,697 段,已经是 6,832,056 对;一个 Common Crawl 快照有几十亿个网页,两两比较根本不可能。

    MinHash 的做法:对每个哈希函数,只留下一篇文档所有 shingle 里哈希值最小的那个。两篇文档的最小值相等的概率,恰好等于它们的 Jaccard(为什么?见岔路课)。于是每篇文档压缩成 k 个数的「签名」,签名逐位相等的比例就是 Jaccard 的估计。

    分桶(LSH)解决配对数:把 k 个数切成 b 个桶、每桶 r 个。只有在某个桶里 r 个数全部相等的两篇文档才会被拿出来比较——查桶是按键找邻居,不用两两比。代价是它变成了一个概率筛:

    一对文档被分桶选为「候选重复」的概率
    = 1 − (1 − )
    👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
    用 1 减去「全都没中」,就是「至少中一个」。曲线最陡的地方大约在 s ≈ (1/b)1/r:FineWeb 14 × 8 是 0.719,RefinedWeb 450 × 20 是 0.737。

    代入 FineWeb 的 14 × 8(共 112 个哈希函数):s = 0.7、0.75、0.8、0.85 时,被选中的概率是 56.5%、77.2%、92.4%、98.8%,与 FineWeb 附录 E.1 给的 56%、77%、92%、98.8% 一致 。RefinedWeb 用 450 × 20 = 9,000 个哈希,同样四个点是 30.2%、76.1%、99.5%、100.0%——更陡,但每篇文档要算、存、比 9,000 个数。FineWeb 认为省下的算力和存储值得换那一点点阈值附近的不确定性。

    算法模拟器 · MinHash 与分桶,浏览器里当场算
      整本《三國志演義》上真跑一遍(Python,3,697 段)
      🎯

      试三件事:① 默认的「原文 vs 转载改写版」:看第 ⓪ 步的指纹完全不同,而第 ① 步的 Jaccard 仍然很高,第 ③ 步把它捞了出来;② 在文档 B 里多删几句,把 Jaccard 拖到 0.7 附近,多试几次——它有时被选中、有时没有,这就是 S 曲线在阈值附近的样子;③ 切到「整本书里最像的一对」,再把分桶切成 RefinedWeb 450 × 20,读这对在两条曲线上的概率差。

      在整本书上真跑一遍的结论写在实验台最下面:3,697 段里,Jaccard ≥ 0.75 的段落对是 0 对,MinHash-LSH 给出的候选也是 0 对;最像的一对只有 0.5833,是两句章末套话「未知勝負如何,且看下文分解。」和「未知負如何,且看下文分解。」。一部精心编校的书几乎没有近重复——网页才是去重的主战场。

      新的失败:去重越狠越好吗?

      有了 MinHash,FineWeb 的第一反应是把全部快照放在一起去重:重复就删,删得越多越干净。先猜一下结果:

      背景:FineWeb 最初把全部 96 个 Common Crawl 快照放在一起做 MinHash 去重——从最新的快照开始,逐个往更老的快照推进,老快照里凡是和已处理数据重复的都删掉。最老的快照被删掉了 90% 以上。

      实验:取 2013-48 这个老快照,拿两份数据各训一个同样的 1.71B 模型:
      ① 全局去重后留下的约 31B token;
      ② 被全局去重删掉的约 460B token,只在它们内部再去一次重,剩 171B token。

      你猜哪一份训出的模型更好?

      ⚠

      常见误解一:「去重越狠越好。」跨 96 个快照的全局去重只留下 4T token,训出的模型几乎没有变好;每个快照内部独立去重留下 20T,追平 RefinedWeb。删掉的不只是垃圾,还有被反复转载的好内容。

      常见误解二:「精确哈希去重就够了。」它只能删一字不差的拷贝。转载改一个字、加一行来源,哈希就完全不同;近重复必须用 shingle 级的相似度。

      常见误解三:「去重 = 每段话只能出现一次。」去重针对的是网页上成千上万份的无意重复。有意把高质量数据多喂几遍是另一回事,4 个 epoch 以内几乎不亏(第 5 课)。

      🔍 FineWeb、Gopher、我们的 shingle 为什么不一样?

      FineWeb 对英文按「词」取 5-gram(先用英文分词器切词);Gopher 的 MinHash 用 13-gram、Jaccard 超过 0.8 算重复。n 越大,shingle 越「挑剔」:一处改动会毁掉 n 个 shingle,相似度掉得越快。中文没有空格,我们按字取 5 个连续字符——5 个汉字大约是英文 2–3 个词的信息量,和 FineWeb 的设置在同一个量级。这是本课的设计选择,没有做消融。出处:FineWeb §3.4 与附录 E.1;Gopher 附录 A.1.1。

      放回演进图里

      去重和规则属于同一代:都在不理解内容的前提下删东西。下一代才开始让模型「读」数据。

      预训练数据的演进:每一代都被上一代的某个真数字逼出来
      1. 起点 有多少用多少:原始网页直接进训练 Common Crawl 原始快照
        解决了量几乎无限:Common Crawl 定期抓取全网,FineWeb 用了 96 个快照。
        卡在WET 纯文本里样板和菜单太多(FineWeb §3.2);我们的原始文件不还原硬换行就跑 C4 句末规则,整本书只剩 17.02% 的字。
      2. 第一代 让人当过滤器:只取被点过赞的链接 GPT-2 WebText,2019
        解决了Reddit 上至少 3 karma 的外链,4,500 万个链接,清洗后约 800 万文档、40 GB。
        卡在量太小:C6 算出的需求是十几 T token;而且只覆盖被分享过的那一小部分网页。
      3. 第二代 只看长相的规则过滤 C4 2019 · Gopher 2021
        解决了句末标点、词数、平均词长、停用词、重复行……便宜,能在全网快照上跑。
        卡在绑语言、绑求值单位:英文原版在整本《三國志演義》3,697 段上通过 0 段;C4 句末标点规则删掉约 30% 的 token,FineWeb 最后没用它。
      4. 第二代 近重复去重:MinHash + 分桶 Broder 1997 → Lee 2021 → FineWeb 2024 你在这里
        解决了C4 里一句话重复 61,036 次;去重后模型复述训练文本的频率降到 1/10。MinHash 把两两比较变成查桶。
        卡在去得越狠未必越好:跨 96 个快照全局去重只剩 4T,训不出更好的模型;每快照独立去重留 20T,才追平 RefinedWeb。
      5. 第三代 让模型给数据打分 CCNet 2019 · GPT-3 2020 → FineWeb-Edu、DCLM 2024
        解决了判断「有没有营养」:FineWeb-Edu 让 Llama-3-70B 标 46 万页、蒸馏成分类器,MMLU 33% → 37%。
        卡在参考语料的困惑度偏向「像参考」:我们模型写的胡话困惑度 93.417,低于真原文 273.209;阈值 3 删掉 92%,长程训练不够用。
      6. 第四代 配比:从手工权重到代理模型搜索 The Pile 2020 → DoReMi 2023
        解决了DoReMi 用 280M 代理模型搜域权重,8B 模型下游平均 +6.5 个点,达到基线快 2.6 倍。
        卡在权重一变就要重复:按 DoReMi 的权重花完 The Pile 的有效体量,YouTube 字幕要走约 16.9 遍(按字节近似 token,只看量级)。
      7. 第五代 数据墙:重复与合成 Muennighoff 2023 · WRAP 2024 · Phi-4 2024
        解决了重复 4 遍以内几乎等于新数据;改写真实网页(WRAP)预训练约快 3 倍;Phi-4 约 10T token 里合成占 40%。
        卡在让模型只吃自己生成的数据,分布的尾巴一代代消失:在三国演义真实字频上重采样,只出现 1 次的字每一代有约 36.8% 的概率丢掉,丢了就回不来。
      8. 第六代 按时机投放:退火 → mid-training MiniCPM、Llama 3 2024 → OLMo 2、Qwen3、GLM-5
        解决了最好的数据放到学习率衰减段(C5 的 WSD)加量;再发展成独立阶段,顺便拉长上下文。
        卡在同名不同方:Qwen3 的高质量阶段占 13.9%,Llama 3 的退火只占 2.6e-6;收益与学习率调度缠在一起,很难单独归因。
      数字口径:我们语料上的数字由 gen_c07_corpus.py 在《三國志演義》上实算(c07-*.json);FineWeb、GPT-2 数字出自本地 PDF;DoReMi、Muennighoff、WRAP、Phi-4、Qwen3、Llama 3 出自原文(核实记录见设计卡);「约 16.9 遍」「36.8%」「13.9%」在构建时现算。

      证据三问

      问本课的回答
      ① 它解决什么问题?删掉网页上的大量重复:它们让模型死记硬背、污染验证集,还会占用分词器的词表
      ② 数学上为什么可能有效?MinHash 相等的概率 = Jaccard,分桶的候选概率 = 1 − (1 − sr)b,都可以严格推出
      ③ 实验上为什么这样做?去重的收益来自 Lee 2021 的实验;「每个快照独立去重」与「14 × 8 的参数」来自 FineWeb 的消融

      一句话

      📌

      去重要删的是被复制成千上万份的大簇,不是所有「相似」;MinHash 把两两比较变成查桶,但删到多狠,要让训练出来的模型说了算。

      🚦

      先停在这里:记住三件事——精确哈希抓不住近重复;MinHash + 分桶是一条 S 形的概率筛;全局去重反而更差。
      再深一层:去岔路课亲手做 1,000 次随机置换,看「最小值相等的频率」怎么收敛到 Jaccard。

      下一个缺口

      不重复、也合法,但很多文本「没营养」,怎么筛? → 第 3 课:规则都过了、也不重复,没营养的文本怎么筛?

      你可能会问