工作台课程

CS336 · 从零构建语言模型

Lecture 14 · 数据 II

Lecture 14 · 数据 II

CS336: Language Modeling from Scratch · Stanford · Spring 2025
📅 May 15 · 💻 可执行讲义 · 讲者 Percy Liang


承上启下

上一讲(Lecture 13 · 数据 I
- 梳理了训练数据的来源与演进:Common Crawl、Wikipedia、代码、书籍、指令数据;从 WebText、C4、The Pile 一路到 DCLM、Nemotron-CC。
- 数据的转化链条是「在线服务(GitHub)→ 原始快照(GH Archive)→ 处理后数据(The Stack)」,每一步都提到了「过滤」「去重」这些操作,但没有展开它们到底怎么实现
- 数据是开源模型最不透明的部分,也是能力差异与法律风险的核心。

本讲(数据 II:处理算法)
- 从「数据来源」深入到「数据处理的机制(mechanics)」。
- 三块内容:过滤算法(KenLM、fastText、DSIR)、过滤应用(语言识别、质量过滤、毒性过滤)、去重(精确哈希、Bloom filter、MinHash、LSH)。
- 学完本讲,上一讲每个数据集卡片里的「用 KenLM 过滤」「MinHash 去重」就都能落到具体算法与公式上。
- 数据准备完毕后,Lecture 15 · 对齐 I:SFT 与 RLHF 进入后训练。

本讲一句话总览:数据处理不是写几条正则,而是在 PB 级文本上做高速筛选和去重。过滤的统一框架是:给定目标数据 $T$ 和原始数据 $R$,训练一个足够快的打分器,从 $R$ 里选出更像 $T$ 的子集。


1. 过滤问题的统一框架

设:
- $T$:小而高质量的目标数据(target data)——你希望训练数据长成的样子;
- $R$:巨大但粗糙的原始数据(raw data)——如 Common Crawl;
- 目标:从 $R$ 中找出与 $T$ 相似的子集 $T' \subseteq R$。

graph LR
    T["Target data T<br/>小而高质"] --> S["Scoring model<br/>打分器"]
    R["Raw data R<br/>大而粗糙"] --> S
    S --> F["Filtered subset T'"]

对过滤器的两条核心要求(desiderata):
- 要能泛化:我们想要的 $T'$ 应当与 $T$ 不同——如果只会挑出和 $T$ 一模一样的文本,直接用 $T$ 就好了,过滤就失去了意义;
- 要足够快:打分器要在整个 $R$(百 TB 到 PB 级)上逐文档运行,稍慢一点乘以文档数就是灾难,这就是为什么本讲的主角全是 n-gram 模型与线性分类器,而不是 BERT。

三种算法都是同一框架的实例化——先估计一个打分函数 $s(x)$,再按分数决定去留:

方法 打分函数 $s(x)$ 保留规则 代表应用
生成式(KenLM) $\log p_T(x)$ 分数超过阈值(可随机化) CCNet / LLaMA
判别式(fastText) $p(T \mid x)$ 分数超过阈值(可随机化) GPT-3 / DCLM
重要性重采样(DSIR) $\hat p_T(x) / \hat p_R(x)$ 以正比于分数的概率重采样 DSIR 论文

其中 $p_T$ 是在目标数据上拟合的分布,$p(T \mid x)$ 是「文档 $x$ 属于目标类」的判别概率,$\hat p_T / \hat p_R$ 是两个分布的密度比。

提示

三种方法回答同一个问题——「这篇文档像不像目标数据?」——只是建模视角不同:生成式问「$T$ 的语言模型觉得它顺不顺」,判别式问「分类器能不能把它和垃圾区分开」,重要性重采样问「它在目标分布里比在原始分布里常见多少倍」。第三种最讲究:它不是选「最像 $T$ 的那批」,而是让选出的子集整体分布趋近 $T$,因此天然保多样性。

说明

数据选择方法的系统综述见 A Survey on Data Selection for Language Models


2. KenLM:快而粗的 n-gram 语言模型

KenLM 是一个高速 n-gram 语言模型实现(最初为机器翻译开发),因为「只需数数和归一化」而极快,成为数据过滤的常用工具。n-gram 语言模型的建模基础可回看 CS221 的 Lecture 17 · 语言模型(未找到对应页面)

2.1 n-gram 语言模型与 Kneser-Ney 平滑

三元(trigram)模型用最大似然估计:条件概率就是计数之比。

$$p(w_3 \mid w_1 w_2) = \frac{c(w_1 w_2 w_3)}{c(w_1 w_2)}$$

其中 $c(\cdot)$ 是该词串在训练语料中的出现次数。例如 $p(\text{in} \mid \text{the cat}) = c(\text{the cat in}) / c(\text{the cat})$。

稀疏性问题:$n$ 越大,绝大多数 n-gram 在语料中从未出现,计数为 0——按上式它们的概率是 0,整篇文档的概率就会被一个未见 n-gram 直接打成 0。

Kneser-Ney 平滑是 n-gram 时代的最强方案,两个思想:
- 回退(backoff):高阶 n-gram 没见过时,回退用低阶模型——$p(\text{in} \mid \text{the cat})$ 估不准时参考 $p(\text{in} \mid \text{cat})$ 乃至 $p(\text{in})$;
- 延续概率(continuation probability):低阶概率不用原始词频,而用「该词能接在多少种不同上下文后面」。经典例子:「Francisco」词频很高但几乎只出现在「San」后面,作为回退候选它应该得低分——原始词频会高估它,延续计数则正确地压低它。

2.2 用于数据过滤:以困惑度为质量分

在目标语料(如维基百科)上训练 KenLM,对每篇候选文档 $x = (w_1, \dots, w_n)$ 计算困惑度(perplexity)

$$\mathrm{ppl}(x) = p_T(x)^{-1/n} = \exp\!\left(-\frac{1}{n} \log p_T(x)\right)$$

按 $1/n$ 归一化是关键:不归一化的话 $\log p(x)$ 随长度线性下降,短文档会被系统性偏爱。困惑度低 = 目标语言模型「觉得顺」= 更像目标语料。

# 用 KenLM 打分(对应讲义可执行代码)
model = kenlm.Model("wikipedia_5gram.arpa.bin")
score = model.score(text)                      # log p(text)
ppl = math.exp(-score / num_tokens)            # 长度归一化 → 困惑度
if ppl < threshold:
    keep(text)

对几类文本的定性预期(讲义现场演示):
- 「Stanford University was founded in 1885 …」——百科式陈述句,困惑度
- 课程规章类文本——正常英语但风格偏离维基,困惑度中等
- 「asdf asdf asdf …」——乱码,困惑度极高
- 「the the the the …」——单个高频词重复,n-gram 模型对这种退化文本的困惑度未必高,暴露了 n-gram 打分的盲区。

例子

CCNet 的处理单位是段落:用维基百科训练的 KenLM 给每段算困惑度,按困惑度升序排序,把语料分成 head / middle / tail 三档,保留最像维基的头部 1/3。这个流水线后来被直接用于 LLaMA 的 Common Crawl 处理(见 Lecture 13 · 数据 I)。

优点:极快(数数而已)、简单、可在全量 Common Crawl 上运行。
缺点:表达能力有限,只看局部词序;对风格/领域高度敏感——「不像维基」不等于「质量差」(论坛、代码、口语都会被误伤)。

注意

低困惑度 ≠ 高质量。由常见短语拼成的空洞模板文(SEO 垃圾的典型形态)困惑度也可能很低;反之高质量但风格独特的文本可能被打高分而误删。困惑度过滤本质是风格匹配,不是质量判断。


3. fastText:高速判别式分类器

3.1 从 bag-of-words 到 fastText

朴素基线(bag of words):直接为每个词学一个 $K$ 维类别得分向量,对整句取平均。

V, K, L = 8192, 64, 32               # 词表大小、类别数、句长
W = nn.Embedding(V, K)               # 每个词一个 K 维得分向量
y = softmax(W(x).mean(dim=0))        # 词向量取平均 → 类别分布

参数量为 $V \times K$——词表和类别数一大就爆炸。

fastText 的改进:先把词嵌入到低维隐空间 $H$,再线性映射到类别。

H = 16                                # 隐藏维度
W = nn.Embedding(V, H)                # 词嵌入:V × H
U = nn.Linear(H, K)                   # 分类头:H × K
y = softmax(U(W(x).mean(dim=0)))      # 平均词向量 → 线性分类

即 $y = \mathrm{softmax}\!\left(U \cdot \frac{1}{L}\sum_{i=1}^{L} W_{x_i}\right)$,参数量从 $VK$ 降到 $H(V+K)$;配合并行异步 SGD,训练与推理都极快,论文的卖点正是「效果与慢得多的神经网络分类器相当」。

n-gram 特征与哈希技巧(hashing trick):只用词袋会丢词序,于是把 bigram(如 “the cat”、”cat in”)也当特征。但 bigram 数量无界,解决办法是哈希进固定数量的桶(实践中约 1000 万个桶):

hashed_x = [mmh3.hash(bigram) % num_bins for bigram in bigrams]

3.2 质量过滤

质量过滤就是 $K=2$(好/坏)的二分类——此时 fastText 退化为一个线性分类器
- 正例:高质量文本(Wikipedia、被维基引用的页面、指令数据等,见 Lecture 13 · 数据 I 中各数据集的选择);
- 负例:Common Crawl 随机样本。

$$s(x) = p(\text{good} \mid x), \qquad \text{keep if } s(x) \ge \tau$$

其中 $\tau$ 是保留阈值。GPT-3、LLaMA、DCLM 的质量分类器都是这个思想。

说明

原则上任何分类器(BERT、Llama)都能当过滤器,只是更慢。规模决定选择:在 240T tokens 的 DCLM-pool 上,只有 fastText 量级的速度才跑得起;Nemotron-CC 想用大模型打分,就必须先把它蒸馏成小模型再上线(见 Lecture 13 · 数据 I)。

3.3 语言识别(language identification)

fastText 官方发布的语言识别模型是数据流水线的标配第一关:
- 支持 176 种语言,就是一个多分类 fastText;
- 训练语料来自多语言站点:Wikipedia、Tatoeba(翻译例句站)、SETimes(东南欧新闻);
- 用法:给文档打出各语言概率,按阈值筛选(如 Dolma 保留 $p(\text{English}) \ge 0.5$ 的页面,FineWeb 用 0.65)。

已知弱点(讲义现场用例子演示):
- 短文本难判:「Hello!」判英语容易,「Bonjour!」这种单词级输入置信度很低;
- 低资源语言容易误杀:训练数据少,判别边界差;
- 英语方言可能被当成「非英语」过滤掉——过滤器会系统性地删掉特定人群的声音;
- 相近语言难分:马来语 vs. 印尼语;
- 语码混用(code-switching)本身就没有正确答案:西英混合的歌词该算哪种语言?

注意

语言识别和质量分类的每一个阈值都在决定「谁的文本能进训练集」。这些选择会把偏见固化进模型:被误判的方言、低资源语言、少数群体的表达方式,从此在模型的世界里缺席。


4. DSIR:重要性重采样

DSIR(Data Selection via Importance Resampling)把数据选择当成概率建模问题来做。

4.1 重要性采样入门

设定:想要来自目标分布 $p$ 的样本,但手里只有来自提议分布(proposal) $q$ 的样本。三步走:

  1. 从 $q$ 采样 $x_1, \dots, x_n$;
  2. 给每个样本算重要性权重并归一化:

$$w(x) = \frac{p(x)}{q(x)}, \qquad \tilde w_i = \frac{w(x_i)}{\sum_{j=1}^{n} w(x_j)}$$

  1. 按概率 $\tilde w_i$ 对这批样本有放回重采样——重采样结果就近似服从 $p$。

例子

词表 $\{0,1,2,3\}$,目标 $p = [0.1, 0.2, 0.3, 0.4]$,提议 $q = [0.4, 0.3, 0.2, 0.1]$。
各取值的权重 $p/q$ 分别为 $0.25,\ 0.67,\ 1.5,\ 4$——在 $q$ 中被高估的 0 被降权,被低估的 3 被放大 4 倍。从 $q$ 采 100 个样本(0 最多),按归一化权重重采样后,样本频率就翻转成 3 最多——分布被「扳」回了 $p$。

4.2 在文本上怎么做?

对应到数据选择:目标数据集 $D_p$(小,如维基+书籍),原始数据集 $D_q$(大,如 Common Crawl)。

直接做法(Take 1):分别在 $D_p$、$D_q$ 上拟合语言模型 $\hat p$、$\hat q$,然后重要性重采样。问题:$D_p$ 太小,拟合不出可靠的完整语言模型。

DSIR 的做法(Take 2):用哈希 n-gram(hashed n-grams)特征上的词袋模型近似——
- 把文档的 n-gram 哈希进 $m$ 个桶(与 fastText 的哈希技巧同款);
- 在每个桶上估计一个简单的多项分布:文档概率近似为 $\hat p(x) = \prod_{j} \theta_{h_j(x)}$,其中 $h_j(x)$ 是文档第 $j$ 个 n-gram 的桶编号,$\theta$ 是桶频率;
- 目标侧与原始侧各估一个 $\hat\theta^{(p)}, \hat\theta^{(q)}$,按比值 $\hat p(x)/\hat q(x)$ 重采样。

模型糙得惊人(连词序都基本不看),但两个糙模型的比值已足以刻画「这篇文档更像哪边」。

结果:在 GLUE 基准上,DSIR 选的数据比启发式 fastText 分类略好。

与 fastText 的比较
- 建模分布之比更有原则,且能保留多样性(不会所有选中文档都挤在「最像 $T$」的尖上);
- 计算复杂度与 fastText 同量级(都是哈希 + 查表);
- 两者都能通过更好的密度/判别模型继续改进。

局限:受特征表达能力限制;目标数据太小时估计不稳;重采样强度(温度)需要调。

提示

判别式过滤取分数最高的文档,结果分布会塌缩在目标分布的众数上——全是「最典型」的维基风文本。重要性重采样按 $p/q$ 比值成比例地抽取:目标分布里罕见但存在的文本类型也会按其应有比例入选。目标是「让 $T'$ 的分布像 $T$」,而非「让每篇文档都最像 $T$」。顺带一提,重要性权重 $p/q$ 这个构件在 RL 的离策略校正中还会出现(Lecture 16 · 对齐 II:RLVR 中 PPO 的比值项)。


5. 过滤应用

同一套打分-保留机制,换不同的 $T$ 就是不同的应用:$T$ = 英语 → 语言过滤;$T$ = 高质量文本 → 质量过滤;$T$ = 毒性文本(反着用)→ 毒性过滤;$T$ = 数学文本 → 领域提取。

5.1 语言过滤(Language Filtering)

先回答一个前置问题:为什么不干脆全语言通吃?
- 数据侧:每种语言的清洗、质量过滤、去重都要单独调校(正例语料、阈值、脏话表都语言相关),策展成本随语言数线性增长;
- 算力侧:算力固定时,多一种语言就摊薄一份 token 预算,每种语言都可能训练不充分。

实证:BLOOM 的训练数据中英语只占约 30%,结果英语能力明显受损(欠训练);而 GPT-4、Claude、Gemini、Qwen 等前沿模型是在总量足够大的前提下才做到高度多语言的。

操作:fastText 语言识别 + 阈值(Dolma:$p(\text{en}) \ge 0.5$);对低资源语言可单独放宽阈值;代码、数学等「非自然语言」需要另立规则,否则会被语言过滤误杀。

5.2 质量过滤(Quality Filtering)

上一讲的历史在这里可以总结成两大阵营:

阵营 数据集 理由
刻意不用模型过滤 C4、Gopher、RefinedWeb、FineWeb、Dolma 规则可解释,避免分类器偏见
使用模型过滤 GPT-3、LLaMA、DCLM 上限更高;正逐渐成为主流

GPT-3 的做法(细节补全上一讲):
- 正例 = {Wikipedia, WebText2, Books1, Books2},负例 = Common Crawl,训练基于词特征的线性分类器;
- 保留规则不是硬阈值,而是随机化保留

keep = (np.random.pareto(9) > 1 - score)   # score ∈ [0,1]

分数高的文档几乎必进,分数低的文档也有小概率入选——Pareto 分布的重尾给「分类器看走眼的好文档」留了活路,兼顾质量与多样性。

LLaMA 的做法:正例 = 「被维基百科引用的页面」,负例 = 随机 Common Crawl;被判正例的文档保留。用维基编辑的引用行为当人工背书,比「像维基百科正文」更能容纳多样风格。

例子

phi-1(Microsoft,2023)的理念:用教科书质量的数据训练小模型(1.3B)。做法:

  1. 原始池 $R$ = The Stack 的 Python 子集;
  2. 用 GPT-4 按提示「判断这段代码对学习基础编程概念的学生的教育价值」标注 10 万个样本,作为目标 $T$;
  3. 在预训练代码模型的输出嵌入上训练随机森林分类器,推广到全量 $R$。

结果(HumanEval):在未过滤 Python 子集上训练 → 12.19%(9.6 万步);在过滤后子集上训练 → 17.68%(仅 3.6 万步)。数据更少、步数更少、成绩更高——「少而精」对小模型尤其有效。

5.3 领域提取案例:OpenWebMath

从 Common Crawl 中挖数学文本,是「多级过滤组合拳」的好样本:
1. 规则:保留含 LaTeX 命令等数学标志的页面;
2. KenLM:用 ProofPile(数学语料)训练的 n-gram 模型打分,保留困惑度 < 15000 的页面(阈值宽松——数学网页风格千奇百怪);
3. fastText:训练「是否数学写作」分类器,分级阈值:页面已含 LaTeX 时阈值放宽到 0.17,不含时收紧到 0.8——先验证据强的候选可以宽进。

结果:14.7B tokens 的数学语料;用它训练的 1.4B 模型超过了在 20 倍通用数据上训练的同规模模型——领域数据的密度远比总量重要。

5.4 毒性过滤(Toxicity Filtering)

Dolma 为例:
- 标注数据来自 Jigsaw Toxic Comments(2018):Wikipedia 讨论页评论,人工标注 {toxic, severe_toxic, obscene, threat, insult, identity_hate} 六类——项目初衷是「帮助人们更好地在线讨论」;
- 用它训练两个 fastText 二分类器:hateNSFW,对页面/段落打分过滤。

难点
- 毒性定义与文化、语境强相关,一个数据集的标注标准未必普适;
- 过强的过滤会删掉少数群体讨论自身身份的正常文本(与 C4 脏话表同款副作用,见 Lecture 13 · 数据 I);
- 目标权衡:训练安全模型未必等于「训练时从没见过敏感文本」——完全没见过毒性文本的模型也无法识别毒性。


6. 去重:为什么重要?

6.1 重复从哪来

两类重复:
- 精确重复(exact duplicates):镜像站点(如古腾堡的全球镜像)、GitHub fork、原样转载;
- 近重复(near duplicates):只差几个 token 的文本——各种 license 与服务条款模板、模板生成的页面(换个地名的「本地新闻」)、复制粘贴时的格式差异。

例子

一段婚礼创意网站的商品描述废话,在 C4 里出现了 61,036 次。不去重的话,模型等于把这段垃圾「背诵」了六万遍。

6.2 去重的收益

Lee et al. (2021)《Deduplicating Training Data Makes Language Models Better》的结论:
- 训练更高效:同样算力见到更多不同的信息(token 预算不浪费在重复上);
- 减少记忆(memorization):模型逐字背诵训练数据的概率随重复次数急剧上升——去重直接缓解版权与隐私风险(呼应 Lecture 13 · 数据 I 的法律部分);
- 防止评测虚高:基准测试泄漏如果被重复多次,污染会被反复强化(见 Lecture 12 · 评估)。

6.3 设计空间与核心挑战

设计维度 可选项
item 是什么? 句子 / 段落 / 文档
怎么算匹配? 精确匹配 / 存在公共子片段 / 公共子项比例(Jaccard)
匹配后怎么办? 全删 / 保留一份 / 按质量挑着留

核心挑战:去重本质是「拿每个 item 和其他所有 item 比较」——朴素做法是 $O(n^2)$,在数十亿文档上完全不可行。必须用(近似)线性时间算法,这正是哈希系列技术的用武之地。

flowchart TD
    DUP["去重技术栈"] --> EXACT["精确匹配<br/>哈希表 / MapReduce"]
    DUP --> MEMB["集合成员判断<br/>Bloom filter(近似、省内存)"]
    DUP --> NEAR["近重复检测<br/>Jaccard 相似度"]
    NEAR --> MH["MinHash<br/>碰撞概率 = Jaccard"]
    MH --> LSH["LSH 分带<br/>b × r 锐化阈值"]

    style DUP fill:#f3e5f5,stroke:#7b1fa2
    style EXACT fill:#e1f5fe,stroke:#0277bd
    style MEMB fill:#fff9c4,stroke:#f57f17
    style NEAR fill:#ffe0b2,stroke:#e65100
    style MH fill:#c8e6c9,stroke:#2e7d32
    style LSH fill:#c8e6c9,stroke:#2e7d32

提示

整个去重技术栈就是在回答一个问题:如何不做两两比较就发现重复? 答案是让「相同/相似的东西哈希到一起」,把比较变成查表。精确重复用普通哈希就够;近重复的难点在于——普通哈希对一个字符的差异都极其敏感,于是需要 MinHash 这种「越相似越容易碰撞」的反常识哈希


7. 精确去重与哈希函数

7.1 哈希函数的取舍

哈希函数 $h$ 把 item 映射为一个短得多的整数/字符串;$h(x) = h(y)$ 而 $x \neq y$ 称为碰撞(collision)。选型是速度与抗碰撞性的权衡

类型 例子 特点 场景
密码学哈希 SHA-256 抗碰撞,慢 比特币、安全
快速哈希 MurmurHash、DJB2、CityHash 不抗恶意碰撞,极快 哈希表、数据去重

数据清洗不怕对手恶意构造碰撞,所以一律用快速哈希(本讲代码用 MurmurHash / mmh3)。

7.2 精确去重

设计选择最简单的组合:item = 文档(或段落),精确匹配,保留一份。

seen = set()
for doc in docs:
    key = mmh3.hash(doc)
    if key not in seen:
        keep(doc)
        seen.add(key)

大规模实现天然适合 MapReduce:map 阶段算哈希,shuffle 按哈希分组(相同文档必然同组),reduce 每组保留一个——完全并行、线性时间。

例子

C4 的 item 是「连续三句话的片段」,精确匹配,重复片段只留一份。
副作用警告:从文档中间抠掉一个三句片段后,剩下的文本可能语义断裂不连贯——去重粒度与文档完整性是有张力的。

局限:只能抓完全一样的文本;改一个标点、加一个空格就漏网。这就需要近重复检测(第 9 节)。


8. Bloom Filter:内存高效的集合成员判断

精确去重要维护「见过的哈希」集合,数十亿文档的集合本身就是内存负担。Bloom filter 用极少内存做近似的成员判断:

  • 内存极省(每个元素约几字节甚至更少);
  • 支持插入,不支持删除(多个元素共享 bit,删除会误伤别人);
  • 回答「不在」一定正确(无假阴性);
  • 回答「」有小概率错误(假阳性)——即可能把没见过的文档误判为重复。

8.1 工作机制

一个长度为 $m$ 的 bit 数组 + $k$ 个独立哈希函数:
- 插入 item:把 $h_1(x), \dots, h_k(x)$ 这 $k$ 个位置全部置 1;
- 查询 item:看这 $k$ 个位置是否全为 1——有一个为 0 就说明肯定没插入过。

table = bitarray(m)
def insert(x):
    for seed in range(k):
        table[mmh3.hash(x, seed) % m] = 1
def query(x):
    return all(table[mmh3.hash(x, seed) % m] for seed in range(k))

8.2 假阳性率的推导

设已插入 $n$ 个 item。查询一个未插入的 item,它被误判的概率是多少?

逐步推(假设哈希独立均匀):
- 插入 1 个 item、用 1 个哈希后,某个特定 bit 仍为 0 的概率是 $1 - \frac{1}{m}$;
- 插入 $n$ 个 item、每个用 $k$ 个哈希,总共置位 $kn$ 次,该 bit 仍为 0 的概率是 $\left(1-\frac{1}{m}\right)^{kn}$;
- 查询时要 $k$ 个位置全部为 1 才误判,所以假阳性率

$$f = \left(1 - \left(1 - \frac{1}{m}\right)^{kn}\right)^{k} \approx \left(1 - e^{-kn/m}\right)^{k}$$

最优哈希个数:固定内存预算比 $m/n$ 时,对 $k$ 求最优(此时每个 bit 恰好约一半是 1):

$$k^{*} = \frac{m}{n}\ln 2, \qquad f^{*} = 2^{-k^{*}} \approx 0.6185^{\,m/n}$$

假阳性率随内存比指数下降——这就是「可以用内存换准确率」的含义。

例子

每个元素给 10 个 bit($m/n = 10$):$k^* = 10 \ln 2 \approx 7$ 个哈希函数,假阳性率 $f \approx 2^{-6.9} \approx 0.8\%$。
Dolma 实际把假阳性率压到 $10^{-15}$(需要 $m/n \approx 72$,即每段约 9 字节),在段落级做精确去重——十亿亿分之一的误删率,几乎等于精确算法。

提示

Bloom filter 的聪明之处在于放弃存储 item 本身,只存「指纹的痕迹」。$k$ 个哈希相当于给每个 item 盖 $k$ 个不同位置的章:查询时所有章都在才算见过。章可能被别的 item 恰好盖满(假阳性),但少一个章就铁定没见过(无假阴性)。对去重来说假阳性 = 误删一篇文档,在 PB 级语料里完全可以接受。


9. 近重复:Jaccard、MinHash、LSH

9.1 Jaccard 相似度

把文档表示为 n-gram 集合(shingles),两个文档 $A, B$ 的 Jaccard 相似度定义为交集占并集的比例:

$$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$。

定义:$J(A,B) \ge$ 阈值时称两文档为近重复(near duplicates)

问题:直接算所有文档对的 Jaccard 是 $O(n^2)$,PB 级数据上不可行——需要「相似的文档自动撞到一起」的哈希。

9.2 MinHash:碰撞概率恰好等于相似度

MinHash 是一个随机哈希:用随机哈希 $h$ 给集合中每个元素打分,取最小值作为签名。

$$\mathrm{minhash}(A) = \min_{x \in A} h(x)$$

它的神奇性质:

$$\Pr[\mathrm{minhash}(A) = \mathrm{minhash}(B)] = J(A, B)$$

为什么成立(置换论证):随机哈希等价于给全体元素随机排了个序。「两个集合的 MinHash 相等」当且仅当并集 $A \cup B$ 中排最前的那个元素恰好落在交集 $A \cap B$ 里。并集每个元素排第一的概率均等,所以这个概率就是 $\frac{|A \cap B|}{|A \cup B|} = J(A,B)$。

def minhash(S: set[str], seed: int):
    return min(mmh3.hash(x, seed) for x in S)

# 用 100 个独立 MinHash 估计 Jaccard:数一数多少个签名位相等
matches = [minhash(A, seed) == minhash(B, seed) for seed in range(100)]
estimated_jaccard = sum(matches) / len(matches)   # ≈ 0.6

单个 MinHash 碰撞与否随机性太大;用 $n$ 个独立 MinHash 组成签名向量,相等比例就是 Jaccard 的无偏估计。但还差一步:我们要的不是「估计相似度」,而是「相似度超阈值的对高概率被捞出来」。

9.3 LSH:用分带把概率曲线变陡

局部敏感哈希(Locality Sensitive Hashing, LSH)
- 取 $n = b \times r$ 个 MinHash,分成 $b$ 个带(band),每带 $r$ 个哈希;
- 把每个带的 $r$ 个签名拼起来作为该带的键,任何一个带完全相同的两文档进入同一候选桶。

设 $s = J(A,B)$,则:
- 某个固定带全部 $r$ 位都碰撞的概率:$s^r$;
- 至少一个带碰撞(成为候选对)的概率:

$$P(\text{collision}) = 1 - (1 - s^{r})^{b}$$

这是一条 S 形曲线:$s$ 小的时候几乎不碰撞,$s$ 大的时候几乎必碰撞,中间有个陡峭的相变。相变点(阈值)近似在:

$$s^{*} \approx \left(\frac{1}{b}\right)^{1/r}$$

且在 $s = s^*$ 处的碰撞概率约为 $1 - (1 - \tfrac{1}{b})^{b} \approx 1 - \tfrac{1}{e} \approx 0.63$。

调参直觉
- $r$ 增大(每带更多哈希):单带全中变难 → 阈值右移、曲线更陡 → 更严格
- $b$ 增大(更多带):更多碰撞机会 → 阈值左移 → 更宽松

例子

Lee et al. (2021) 的去重配置:$n = 9000$,$b = 20$,$r = 450$。
阈值 $s^* = (1/20)^{1/450} = e^{-\ln 20 / 450} \approx e^{-0.00666} \approx 0.993$——只有 Jaccard 高达 99.3% 的「几乎逐字相同」的文档对才会高概率成为候选。这正对应「模板文/镜像页」类近重复,而不会把内容相近的正常文章误杀。

提示

带内是 AND($r$ 位全中才算,压低弱相似对的碰撞率:$0.5^{450} \approx 0$),带间是 OR($b$ 次机会任一命中即可,抬高强相似对的碰撞率)。AND 压左端、OR 抬右端,一压一抬就把平缓的直线 $P = s$ 掰成了陡峭的 S 曲线——软性的「相似度」被转换成了近似硬性的「阈值判断」。

说明

LSH 的输出是候选重复对,还可以再对候选精确计算 Jaccard 做确认(两阶段:粗筛 + 精验)。由于候选对数量远小于 $O(n^2)$,整体近似线性时间。


总结

mindmap
  root((数据 II))
    过滤框架
      目标数据 T
      原始数据 R
      打分器 + 保留规则
    过滤算法
      KenLM 生成式
      fastText 判别式
      DSIR 重采样
    过滤应用
      语言识别
      质量过滤
      毒性过滤
      数学等领域提取
    去重
      精确哈希 MapReduce
      Bloom filter
      Jaccard 相似度
      MinHash
      LSH 分带

关键要点

  1. 过滤是目标分布匹配:给定小而好的 $T$ 和大而糙的 $R$,训练快速打分器选出像 $T$ 的子集——生成式($\log p_T$)、判别式($p(T \mid x)$)、重采样($p_T/p_R$)是同一框架的三种实例。
  2. KenLM 快但粗:n-gram + Kneser-Ney 平滑,用困惑度做风格匹配式过滤(CCNet 保留最像维基的 1/3);盲区是「低困惑度 ≠ 高质量」。
  3. fastText 是数据清洗的实用王者:均值词嵌入 + 线性头 + 哈希 n-gram,参数量 $H(V+K)$,速度快到能跑 240T tokens;语言识别、质量分类、毒性过滤通吃。
  4. DSIR 用重要性重采样保留多样性:按密度比 $\hat p_T/\hat p_R$ 重采样,让选出子集的整体分布趋近目标,而不是人人挤向众数。
  5. 去重必须近似线性时间:精确重复用哈希 + MapReduce;内存紧张用 Bloom filter(假阳性率 $f^* \approx 0.6185^{m/n}$ 随内存指数下降);近重复用 MinHash(碰撞率 = Jaccard)+ LSH 分带($P = 1-(1-s^r)^b$ 锐化阈值)。
  6. 数据直觉靠花时间看样本:本讲给的是 mechanics(工具与公式),真正的数据品味来自反复 inspection——这是讲者反复强调的收尾之言。

下一讲预告(Lecture 15 · 对齐 I:SFT 与 RLHF

预训练数据让模型学会语言和知识;接下来要让模型听指令、符合偏好和安全约束。下一讲进入 SFT、RLHF、偏好数据、PPO 与 DPO——上一讲 4.5 节的指令数据将在那里派上用场。


复习自测

题目

KenLM(生成式):$s(x) = \log p_T(x)$,在目标语料上训 n-gram 模型按困惑度筛——适合有一份风格统一的参考语料(如维基)、要极致速度的场景。fastText(判别式):$s(x) = p(T \mid x)$,训二分类器——适合能构造好的正负例对比(如 DCLM 用指令数据当正例)。DSIR(重采样):$s(x) = \hat p_T(x)/\hat p_R(x)$,按密度比重采样——适合希望选出子集在分布层面匹配目标、保多样性的场景。三者计算量同级,差别在「好数据」的建模方式。

题目

最优 $k^* = (m/n)\ln 2 = 10 \times 0.693 \approx 7$ 个哈希函数,假阳性率 $f = 2^{-k^*} = 2^{-6.93} \approx 0.8\%$。要达到 $10^{-15}$:由 $f = 0.6185^{m/n}$ 解得 $m/n = \ln(10^{-15})/\ln(0.6185) \approx 72$ bit(约 9 字节/元素),对应 $k^* \approx 50$ 个哈希函数。可见假阳性率随内存指数下降,压到天文级低也只要每元素几字节。

题目

随机哈希函数等价于给所有元素一个随机排列。MinHash 取的是各集合中「排最前」的元素。两集合的 MinHash 相等,当且仅当并集 $A \cup B$ 中全局排最前的元素落在交集 $A \cap B$ 中(此时双方的最小值都是它;若最前元素只属于一方,两边最小值必不同)。并集中每个元素成为「排最前」的概率相等,故碰撞概率 $= |A \cap B| / |A \cup B| = J(A,B)$。

题目

增大 $r$(带内 AND 更多):单带全中更难,阈值 $s^* = (1/b)^{1/r}$ 右移、曲线更陡,去重更严格;增大 $b$(带间 OR 更多):碰撞机会更多,阈值左移,更宽松。代入 $b=20, r=450$:$s^* = (1/20)^{1/450} = e^{-\ln 20/450} \approx e^{-0.00666} \approx 0.993$——即只把 Jaccard ≥ 99.3% 的「几乎逐字相同」文档视为近重复,且在阈值处的捕获概率约 $1 - 1/e \approx 63\%$。

题目

效率:重复 token 不带来新信息,去重后同样算力覆盖更多不同内容。记忆:模型逐字背诵一段文本的概率随其在训练集中的重复次数急剧上升(C4 中重复六万次的商品描述必然被背下来),去重从源头压低背诵概率,从而缓解版权(复述受保护文本)与隐私(吐出个人信息)风险。评测:测试集若泄漏且被重复强化,模型「背题」导致分数虚高——去重(含训练-测试重叠去除)是数据侧的防污染手段。


参考资料