# Lecture 14 · 数据 II

> **CS336: Language Modeling from Scratch** · Stanford · Spring 2025
> 📅 May 15 · 💻 [可执行讲义](https://github.com/stanford-cs336/spring2025-lectures/blob/main/lecture_14.py) · 讲者 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$。

```mermaid
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$ 是两个分布的密度比。

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

> [!note] 延伸
> 数据选择方法的系统综述见 [A Survey on Data Selection for Language Models](https://arxiv.org/abs/2402.16827)。

---

## 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)$ 随长度线性下降，短文档会被系统性偏爱。困惑度低 = 目标语言模型「觉得顺」= 更像目标语料。

```python
# 用 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 打分的盲区。

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

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

> [!warning] 易错点
> 低困惑度 ≠ 高质量。由常见短语拼成的空洞模板文（SEO 垃圾的典型形态）困惑度也可能很低；反之高质量但风格独特的文本可能被打高分而误删。困惑度过滤本质是**风格匹配**，不是质量判断。

---

## 3. fastText：高速判别式分类器

### 3.1 从 bag-of-words 到 fastText

**朴素基线（bag of words）**：直接为每个词学一个 $K$ 维类别得分向量，对整句取平均。

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

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

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

```python
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 万个桶）：

```python
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 的质量分类器都是这个思想。

> [!note] 分类器不必是 fastText
> 原则上任何分类器（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）本身就没有正确答案**：西英混合的歌词该算哪种语言？

> [!warning] 过滤器不是中立工具
> 语言识别和质量分类的每一个阈值都在决定「谁的文本能进训练集」。这些选择会把偏见**固化**进模型：被误判的方言、低资源语言、少数群体的表达方式，从此在模型的世界里缺席。

---

## 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)}$$

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

> [!example] 数值演算（讲义的玩具例子）
> 词表 $\{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 同量级（都是哈希 + 查表）；
- 两者都能通过更好的密度/判别模型继续改进。

**局限**：受特征表达能力限制；目标数据太小时估计不稳；重采样强度（温度）需要调。

> [!tip] 直觉：为什么「重采样」比「取 top-k」保多样性？
> 判别式过滤取分数最高的文档，结果分布会**塌缩**在目标分布的众数上——全是「最典型」的维基风文本。重要性重采样按 $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，训练基于词特征的线性分类器；
- 保留规则不是硬阈值，而是**随机化保留**：

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

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

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

> [!example] 案例：phi-1 ——「教科书级数据」哲学
> 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 二分类器：**hate** 与 **NSFW**，对页面/段落打分过滤。

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

---

## 6. 去重：为什么重要？

### 6.1 重复从哪来

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

> [!example] 惊人的实例
> 一段婚礼创意网站的商品描述废话，在 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)$，在数十亿文档上完全不可行。**必须用（近似）线性时间算法**，这正是哈希系列技术的用武之地。

```mermaid
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
```

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

---

## 7. 精确去重与哈希函数

### 7.1 哈希函数的取舍

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

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

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

### 7.2 精确去重

设计选择最简单的组合：item = 文档（或段落），精确匹配，保留一份。

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

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

> [!example] C4 的精确去重
> 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 就说明肯定没插入过。

```python
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}$$

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

> [!example] 数值演算
> 每个元素给 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 字节），在**段落级**做精确去重——十亿亿分之一的误删率，几乎等于精确算法。

> [!tip] 直觉
> 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)$。

```python
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$ 增大**（更多带）：更多碰撞机会 → 阈值左移 → **更宽松**。

> [!example] 真实配置算一算
> 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% 的「几乎逐字相同」的文档对才会高概率成为候选。这正对应「模板文/镜像页」类近重复，而不会把内容相近的正常文章误杀。

> [!tip] 直觉：AND-OR 结构的放大作用
> 带内是 **AND**（$r$ 位全中才算，压低弱相似对的碰撞率：$0.5^{450} \approx 0$），带间是 **OR**（$b$ 次机会任一命中即可，抬高强相似对的碰撞率）。AND 压左端、OR 抬右端，一压一抬就把平缓的直线 $P = s$ 掰成了陡峭的 S 曲线——软性的「相似度」被转换成了近似硬性的「阈值判断」。

> [!note] LSH 只产生候选对
> LSH 的输出是**候选重复对**，还可以再对候选精确计算 Jaccard 做确认（两阶段：粗筛 + 精验）。由于候选对数量远小于 $O(n^2)$，整体近似线性时间。

---

## 总结

```mermaid
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 节的指令数据将在那里派上用场。

---

## 复习自测

> [!question]- Q1：KenLM、fastText、DSIR 三种过滤方法的打分函数分别是什么？各自最适合什么场景？
> 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)$，按密度比重采样——适合希望选出子集在分布层面匹配目标、保多样性的场景。三者计算量同级，差别在「好数据」的建模方式。

> [!question]- Q2：算一算：Bloom filter 每个元素分配 10 个 bit（$m/n=10$），最优要用几个哈希函数？假阳性率约多少？如果要 Dolma 的 $10^{-15}$ 呢？
> 最优 $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$ 个哈希函数。可见假阳性率随内存指数下降，压到天文级低也只要每元素几字节。

> [!question]- Q3：为什么 $\Pr[\mathrm{minhash}(A) = \mathrm{minhash}(B)]$ 恰好等于 $J(A,B)$？
> 随机哈希函数等价于给所有元素一个随机排列。MinHash 取的是各集合中「排最前」的元素。两集合的 MinHash 相等，当且仅当并集 $A \cup B$ 中全局排最前的元素落在交集 $A \cap B$ 中（此时双方的最小值都是它；若最前元素只属于一方，两边最小值必不同）。并集中每个元素成为「排最前」的概率相等，故碰撞概率 $= |A \cap B| / |A \cup B| = J(A,B)$。

> [!question]- Q4：LSH 中增大 $r$ 与增大 $b$ 分别把阈值曲线怎么移动？验证配置 $b=20, r=450$ 的相变阈值。
> 增大 $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\%$。

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

---

## 参考资料

- 💻 [lecture_14.py（官方可执行讲义）](https://github.com/stanford-cs336/spring2025-lectures/blob/main/lecture_14.py)
- 📄 [CCNet](https://arxiv.org/pdf/1911.00359)
- 📄 [fastText: Bag of Tricks for Efficient Text Classification](https://arxiv.org/pdf/1607.01759)
- 📄 [DSIR](https://arxiv.org/abs/2302.03169)
- 📄 [Dolma](https://arxiv.org/abs/2402.00159)
- 📄 [Deduplicating Training Data Makes Language Models Better](https://arxiv.org/pdf/2107.06499)
- 📖 [Mining of Massive Datasets: LSH](http://infolab.stanford.edu/~ullman/mmds/ch3n.pdf)
- 📄 [A Survey on Data Selection for Language Models](https://arxiv.org/abs/2402.16827)
- 📄 [OpenWebMath: An Open Dataset of High-Quality Mathematical Web Text](https://arxiv.org/abs/2310.06786)
- 📄 [Textbooks Are All You Need（phi-1）](https://arxiv.org/abs/2306.11644)
- 🌐 [KenLM 官方页面](https://kheafield.com/code/kenlm/)
- 🌐 [fastText 语言识别模型](https://fasttext.cc/docs/en/language-identification.html)
