CS336 · 第 14 课

数据过滤、去重、配比与合成数据

第 13 课回答数据从哪里来;这一课把 raw records 变成训练 distribution:先抽取、过滤与去重,再决定不同来源被采样多少,最后理解 mid/post-training 的 synthetic data 如何由任务与 teacher 共同生成。

授课:Percy Liang 预计用时:80 分钟 难度:中等 → 较高

Spring 2026 官方来源:Lecture 14 trace · 课程日程

本页是中文学习导读;讲次主题、教师与材料版本以 Stanford 官方页面和讲义为准。

本讲问题:原始数据规模很大但信号密度不均;处理流水线要在保留有用分布和移除噪声、重复、污染之间做可测量的权衡。

30 秒知识地图

HTML 提取 → 规则 / 模型过滤 → 文档与片段去重 → contamination 检查 → mixture / epoch cap → synthetic prompts 与 responses → 训练验证。

问题与 prerequisite refresher

过滤器不是“质量真相”,而是一个会产生 false positive/negative 的决策器;去重也先要定义 equality 或 similarity。集合 \(A,B\) 的 Jaccard 是 \(|A∩B|/|A∪B|\),MinHash 只是高效估计它,不直接理解语义。

每个门都改变数据分布

HTML → extracted text → normalized document → shingles
             ↓ filter scores             ↓ signatures
        accept / reject             exact / near-duplicate graph
             └──────────────┬──────────────┘
                       selected corpus
                            ↓ mixture → tokenize → train → evaluate

filter score ≠ ground-truth quality
hash collision ≠ guaranteed duplicate

手算一个 Jaccard 反例

A 的 shingles 为 {语言, 模型, 训练},B 为 {语言, 模型, 推理},交集 2、并集 4,所以 Jaccard=0.5。若阈值 0.8,它们不会被判 near-duplicate;若 shingle 太小,常见模板又可能让无关页面相似。MinHash 增加 hash 数只减少估计方差,不修复错误的 document normalization 或 threshold。

为什么“过滤越严,质量越高”会错

用百科风格训练的 classifier 可能删掉口语、方言、代码或少数语言,分数变“干净”却损害目标覆盖。先去重还是先过滤也会影响计算量与代表文档选择。正确验收不是只报 retained ratio,而是审计样本、subgroup recall、contamination 与训练后的下游变化。


数据处理流水线回顾

Raw HTML (Common Crawl WARC)
    ↓ HTML → Text 提取
纯文本
    ↓ 语言识别
目标语言文本
    ↓ 质量过滤
高质量文本
    ↓ 有害内容过滤
安全文本
    ↓ 去重
最终训练数据

通用过滤框架:给定目标数据 T(小量、高质量)和原始数据 R(大量、噪声),找到 R 中与 T 相似的子集 T'。算法需要满足两个要求:(1) 能从 T 泛化(T 和 T' 不完全相同),(2) 极快(要处理海量 R)。


HTML → 文本提取

为什么这很重要?

Common Crawl 存储的是原始 HTML。HTML 包含大量非内容信息:

提取工具

工具方法特点
trafilatura基于内容密度精确,保留结构
jusText基于标点/词比快速,保留更多
readability启发式布局分析类似浏览器阅读模式
BeautifulSoupDOM 解析灵活,需自定义规则
import trafilatura

# 从 HTML 提取主要内容
html = """<html>
<body>
  <nav>Home | About | Contact</nav>
  <article>
    <h1>Main Content</h1>
    <p>This is the actual article text...</p>
  </article>
  <footer>Copyright 2024</footer>
</body>
</html>"""

text = trafilatura.extract(html)
# 结果: "Main Content\nThis is the actual article text..."

# 注意: DCLM 论文显示,trafilatura vs jusText 的选择
# 会影响下游任务精度 1-2 个百分点

WARC 处理示例

Common Crawl WARC 文件结构:
1. WARC header (元数据)
2. HTTP header (Content-Type, 状态码等)
3. HTML content (实际页面)

处理流程:
1. 解析 WARC header
2. 检查 HTTP status (只保留 200)
3. 检查 Content-Type (只保留 text/html)
4. 解码 HTML (处理各种编码)
5. 提取文本
6. 质量/语言/有害过滤
7. 去重


过滤算法

统一框架

所有过滤算法都遵循相同的模式:

给定目标 T 和原始 R
1. 基于 R 和 T 估计一个模型 → 得到评分函数 score(x)
2. 根据 score(x) 保留 R 中的样本

三种实例化:
- KenLM: score(x) = p_T(x)(生成模型)
- fastText: score(x) = p(T|x)(判别分类器)
- DSIR: score(x) = p_T(x) / p_R(x)(重要性采样)

KenLM:N-gram 语言模型

KenLM 是数据过滤中最常用的工具之一:

N-gram 语言模型:
p(in | the cat) = count(the cat in) / count(the cat)

问题: 很多 n-gram 的 count 为 0
解决: Kneser-Ney 平滑
- 插值: p(in | the cat) 也依赖于 p(in | cat)
- 折扣: count > 0 的 n-gram 的计数"分给" count=0 的

KenLM 的特点:
- 极快(C++ 实现)
- 内存效率高(基于 trie 和量化)
- 广泛用于机器翻译和 LM 数据过滤

KenLM 用于质量过滤

import kenlm

# 在目标数据(如 Wikipedia)上训练 KenLM
model = kenlm.Model("wikipedia.arpa.bin")

def compute_perplexity(text):
    """计算文本在模型下的 perplexity"""
    score = model.score(text)
    num_tokens = len(text.split())
    perplexity = math.exp(-score / num_tokens)
    return perplexity

# 高质量文本 → 低 perplexity
print(compute_perplexity("Stanford University was founded in 1885."))
# → perplexity ≈ 50

# 低质量文本 → 高 perplexity
print(compute_perplexity("asdf asdf asdf asdf asdf"))
# → perplexity ≈ 5000

# CCNet 策略: 按 perplexity 排序,保留 top 1/3

KenLM 的局限:N-gram 模型只看局部上下文(5-gram = 5 个词),无法捕捉长距离的语义质量。一个句子内部通顺但语义荒谬的文本,KenLM 可能给高分。

fastText 分类器

fastText 是一个快速的文本分类器:

fastText 的核心思想:
1. 把文本表示为词嵌入的平均(bag of embeddings)
2. 通过线性分类器分类

模型结构:
x = [word1, word2, ..., wordN] (token 序列)
embed = mean(W[x] for x in tokens) (平均词嵌入)
y = softmax(U @ embed) (分类)

参数: W (V × H) + U (H × K) = H(V + K)
对比 bag-of-words: V × K 参数

对于质量过滤(K=2 类):
参数 = H(V + 2),即线性分类器

Bag of n-grams 和 Hashing Trick

# 问题: n-gram 数量可能无限
# 解决: Hashing trick

import mmh3

num_bins = 10_000_000  # 10M 哈希桶

def hash_ngrams(text, n=2):
    """将文本的 n-gram 哈希到固定数量的桶"""
    tokens = text.split()
    ngrams = [tokens[i:i+n] for i in range(len(tokens)-n+1)]
    hashed = [mmh3.hash(" ".join(ngram)) % num_bins for ngram in ngrams]
    return hashed

# 这样无论有多少唯一 n-gram,都映射到 10M 个桶

DSIR:重要性重采样

Data Selection for Language Models via Importance Resampling:

核心思想: 用重要性采样从 R 中采样出 T 的分布

步骤:
1. 在目标 T 上估计分布 p
2. 在原始 R 上估计分布 q
3. 对 R 中每个样本 x,计算权重 w(x) = p(x) / q(x)
4. 按权重 w(x) 重新采样

问题: T 太小,无法估计好的 p 和 q
解决: 使用 hashed n-grams(高维但稀疏的特征)

三种过滤方法对比

方法评分函数优点缺点
KenLMp_T(x)极快,简单只看局部,模型容量低
fastTextp(T|x)快速,可泛化需要标注数据
DSIRp_T(x)/p_R(x)理论优雅实现复杂

过滤应用

语言识别

# fastText 语言识别模型
# - 支持 176 语言
# - 基于 Wikipedia、Tatoeba、SETimes 训练
# - 开箱即用

import fasttext

model = fasttext.load_model("lid.176.bin")

# 预测
print(model.predict("The quick brown fox."))
# → ('__label__en', [0.98])

print(model.predict("Auf dem Wasser zu singen"))
# → ('__label__de', [0.95])

print(model.predict("The quadratic formula is $x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}$"))
# → 可能被误判(包含大量符号)

# 注意事项:
# - 短文本难以识别
# - 低资源语言效果差
# - 代码和公式可能被误判
# - 代码切换(中英混合)困难

质量过滤

不同的模型使用不同的质量过滤策略:

模型正例负例方法
GPT-3Wikipedia, WebText, BooksCommon Crawl线性分类器
LLaMAWikipedia 引用页面Common Crawl分类器
DCLMOpenHermes, ELI5RefinedWebfastText
phi-1GPT-4 分类为高教育价值The StackRandom Forest

GPT-3 的质量过滤方法:
1. 训练分类器区分 {Wikipedia, WebText, Books} vs Common Crawl
2. 对每个文档得到质量分数 s ∈ [0, 1]
3. 按 Pareto 分布随机保留:
keep if np.random.pareto(9) > 1 - score

效果: 高质量文档有更高概率被保留,但不是确定性过滤

有害内容过滤

Dolma 使用 Jigsaw Toxic Comments 数据集训练分类器

两类分类器:
1. Hate speech: positive = {unlabeled, obscene}, negative = 其他
2. NSFW: positive = {obscene}, negative = 其他

使用 fastText 在大ram 上训练

注意: "I love strawberries" vs "I hate strawberries"
简单的词袋模型可能误判包含 "hate" 的文本


去重

为什么要去重?

两种重复

类型来源检测方法
精确重复镜像站点、GitHub fork哈希
近似重复模板文本、小修改MinHash/LSH

惊人的事实:C4 中有一条产品描述被重复了 61,036 次。Common Crawl 中大量内容是模板生成的(免责声明、许可证、导航栏)。

去重的设计空间

三个关键决策:

1. 什么是"项"(item)?
- 句子?段落?文档?
- 越小越精细,越大越高效

2. 如何匹配?
- 精确匹配(哈希)
- 存在公共子项
- 公共子项比例(Jaccard 相似度)

3. 匹配后如何处理?
- 删除所有重复项
- 只保留一个
- 保留"最好"的一个

精确去重

# 简单方法: 哈希 + 去重
import mmh3

def exact_dedup(documents):
    """精确去重"""
    seen = set()
    unique = []
    for doc in documents:
        h = mmh3.hash(doc)  # 计算哈希
        if h not in seen:
            seen.add(h)
            unique.append(doc)
    return unique

# C4 的方法:
# - 项: 3 个句子的 span
# - 匹配: 精确匹配
# - 处理: 删除所有但一个
# 注意: 删除中间的 span 可能导致文档不连贯

# 可以 MapReduce 并行化:
# Map: 每个文档 → (hash, document)
# Reduce: 每个 hash 保留一个文档

Bloom Filter

当数据量太大,set 放不进内存时:

# Bloom Filter: 概率性集合成员检测

# 特点:
# - 内存高效(每个元素 ~10 bits)
# - 如果返回"不在" → 100% 正确
# - 如果返回"在" → 大概率正确(有 false positive)
# - 无法删除

# 结构:
# - 一个 bit array(m 位)
# - k 个哈希函数

# 操作:
# - 插入: 对 item 计算 k 个哈希,将对应位置 1
# - 查询: 对 item 计算 k 个哈希,检查是否全为 1

from bitarray import bitarray
import mmh3

class BloomFilter:
    def __init__(self, size, num_hashes):
        self.size = size
        self.num_hashes = num_hashes
        self.table = bitarray(size)
        self.table.setall(0)

    def add(self, item):
        for seed in range(self.num_hashes):
            h = mmh3.hash(item, seed) % self.size
            self.table[h] = 1

    def might_contain(self, item):
        for seed in range(self.num_hashes):
            h = mmh3.hash(item, seed) % self.size
            if not self.table[h]:
                return False
        return True  # 可能在(false positive 可能)

False Positive Rate

假设哈希函数独立,给定:
m = bit array 大小
k = 哈希函数数量
n = 已插入元素数量

False positive rate:
f = (1 - (1 - 1/m)^(k*n))^k
近似: f ≈ (1 - e^(-kn/m))^k

最优 k(给定 m/n):
k = ln(2) * m / n

例: Dolma 设置 false positive rate = 1e-15
每个元素约需 50 bits

MinHash 与 Jaccard 相似度

检测近似重复:

# Jaccard 相似度:
# J(A, B) = |A ∩ B| / |A ∪ B|
# 范围: [0, 1]

# 例:
# A = {"the", "cat", "in", "the", "hat"}
# B = {"the", "cat", "on", "the", "mat"}
# J(A, B) = 3 / 7 ≈ 0.43

# MinHash 的核心思想:
# - 对集合 A,计算其 minhash 签名
# - 两个集合的 minhash 签名相同的概率 = Jaccard 相似度
# - 这样可以用固定大小的签名近似 Jaccard 相似度

def minhash_signature(shingles, num_hashes=128):
    """计算集合的 minhash 签名"""
    signature = []
    for i in range(num_hashes):
        min_hash = float('inf')
        for s in shingles:
            h = mmh3.hash(s, seed=i)
            min_hash = min(min_hash, h)
        signature.append(min_hash)
    return signature

# 两个集合的 Jaccard 估计:
# J ≈ (signature1 == signature2) 的比例

Locality-Sensitive Hashing (LSH)

MinHash 签名仍然需要两两比较。LSH 将相似签名分到同一个桶:

LSH 的核心思想:
- 将 minhash 签名分成 b 个 band
- 每个 band 有 r 行(b × r = num_hashes)
- 如果两个签名在某个 band 完全相同 → 进入同一个候选桶
- 只在候选桶内做精确比较

参数选择:
- b 个 band, 每个 r 行
- 相似度为 s 的文档成为候选的概率:
P = 1 - (1 - s^r)^b

例: num_hashes=128, b=16, r=8
s=0.5: P ≈ 0.004(不太可能被选中)
s=0.8: P ≈ 0.99(几乎一定被选中)

这样可以在 O(n) 时间内找到所有相似度 > 阈值的文档对

SemDedup

语义层面的去重(更先进的方法):

SemDedup 的核心思想:
1. 用 embedding 模型编码每个文档
2. 聚类相似的文档
3. 在每个聚类内随机保留一个

步骤:
1. 编码: embedding = model.encode(documents)
2. 聚类: clusters = kmeans(embedding, k=...)
3. 去重: 每个 cluster 只保留一个

优势:
- 能检测语义重复(不同措辞但相同含义)
- 比基于字符串的方法更强大

劣势:
- 需要 embedding 模型(计算成本高)
- 聚类质量影响去重效果

去重策略对比

方法检测类型速度内存精度
精确去重完全相同极快中精确
Bloom Filter完全相同极快低概率
MinHash近似重复快中估计
MinHash + LSH近似重复很快中估计
SemDedup语义重复慢高语义

数据配比

为什么需要配比?

不同来源的数据有不同的能力和偏差。Mixture 不是静态饼图,而是一条 sampling policy:在总训练 token 数 (D) 固定时,权重 (p_s) 决定来源 (s) 被处理多少 token,以及有限数据会重复多少个 epoch。

The Pile 的配比(按 tokens):
- Pile-CC (网页): 22.0%
- Books3 (书籍): 10.3%
- OpenWebText2 (Reddit): 8.1%
- ArXiv (论文): 7.8%
- GitHub (代码): 7.6%
- PubMed Central (医学): 6.3%
- Wikipedia: 3.9%
- 其他 20+ 来源: 34%

配比的影响:
- 代码比例高 → 编程能力强,但可能"代码腔"
- 书籍比例高 → 叙事能力强,但可能过时
- 论文比例高 → 推理能力强,但可能"学术腔"

先算 epoching:高质量不等于可以无限重复

假设 low-quality web 有 10T tokens,high-quality books 只有 10B tokens;训练 1T tokens,并把两者各采样 50%。books 会被处理 (0.5T / 10B = 50) 个 epochs,而 web 只用 0.05 epoch。这个 mixture 表面“质量更高”,却可能让小来源过拟合和记忆。Mixture 的 contract 必须同时写 sampling weight 与 per-source epoch cap。

UniMax:先给稀缺来源设 epoch 上限

Spring 2026 官方讲义用 UniMax 说明一个简单边界:尽量均匀覆盖来源,但限制任何来源最多重复 (C) 个 epochs。来源用尽上限后,把剩余预算重新分给仍有容量的来源。它优化的是 coverage 与重复风险的可解释约束,不声称自动找到所有下游任务的最优 mixture。

从小规模 mixture 实验迁移到大规模

另一条路线是在小模型上尝试多组 mixture,用 regression 预测 mixture → validation/downstream metric,再选择候选配方。但小规模实验会低估大规模训练中的 epoching:同一权重在小预算下可能只看一遍稀缺来源,在大预算下却重复几十遍。官方讲义强调要让小规模 proxy 模拟大规模的 source exhaustion,而不是把拟合结果直接当真理。

DoReMi:自动数据配比

用 reference model 指导数据配比:

DoReMi 的核心思想:
1. 训练一个小的 reference model
2. 用 reference model 的 excess loss 作为权重
3. 优化配比使 reference model 的 loss 最小

算法:
1. 用均匀配比训练 reference model
2. 计算每个 domain 的 excess loss
3. 用 excess loss 更新配比权重
4. 用新配比训练 target model

反例:small-scale 最优配比不一定能直接放大

如果只用 1B tokens 比较 mixtures,小型高质量来源尚未重复;把同一配方用于 1T-token run,effective epochs 已完全不同。失败的不是 regression 公式,而是训练分布随预算发生了变化。必须记录 source size、sampled tokens、epochs 与目标尺度。


合成数据:生成什么,谁来验证

Spring 2026 Lecture 14 把 synthetic data 放在 mid-training 与 SFT 场景中:不是凭空制造“更多 token”,而是先定义目标任务,再生成 prompts、responses 或交互 trajectories,并用规则、执行环境、teacher 或人工审计筛选。

合成数据的对象链

task distribution / real seed data
+ teacher or environment
+ generation policy
+ filters / verifier
+ mixture weight
+ provenance
+ decontamination
↓
train → held-out evaluation

synthetic response quality ≠ teacher benchmark score
verifier pass ≠ general correctness
more trajectories ≠ more independent information

三类 prompt 来源

来源例子主要风险
真实用户问题、GitHub PR、StackExchange隐私、许可、分布偏差与 contamination
半合成真实 repository/environment + 模型生成任务环境搭建失败、任务与真实需求错位
全合成teacher 同时生成 prompt 与 responseteacher blind spots 被复制,diversity 只是表面变化

为什么生成以后还要 filtering

Teacher 更强不保证对每个 domain 都是更好的数据生成器;同一 prompt 多次采样可提高找到好 response 的机会,但也会增加近重复。答案过滤、执行反馈和 reward model 都只是 proxy。验收应同时查看 pass rate、人工样本、difficulty distribution、重复率,以及在未用于生成或筛选的 held-out tasks 上训练后的变化。

Contamination 是 pipeline invariant

若生成 prompts 时读到了 evaluation solutions,后续再对文本做 exact dedup 也可能来不及,因为答案已被 paraphrase 进训练数据。应在 seed、teacher context、generated prompt/response 和最终 corpus 多个阶段记录 lineage,并用 exact、near 与 semantic checks 分层审计。

Concept Check · 配比放大

某来源只有 2B tokens,小实验处理它 1B tokens,大实验按同一 mixture weight 处理 40B tokens。为什么不能说“mixture 没变,所以分布没变”?

答案:该来源从 0.5 epoch 变成 20 epochs;重复、记忆与边际收益都变了。需要显式控制或模拟 epoching。

若只比较 sampling percentage,你忽略了 source size 与 processed tokens 共同决定的 effective epochs。

Concept Check · synthetic evidence

Teacher 生成的代码都通过公开 tests,是否足以证明这批 synthetic data 高质量?

答案:不足。Tests 的 coverage、hard-coded exploit、重复任务和 held-out 泛化都未被证明;还要做 hidden/independent evaluation 与 sample audit。

若认为 verifier pass 就是真实质量,你把可观测 proxy 当成了完整目标。


知识检查

问题 1:KenLM 用于质量过滤的主要局限是什么?

问题 2:Bloom filter 的 false positive 和 false negative 特性是什么?

问题 3:MinHash 为什么能估计 Jaccard 相似度?

问题 4:为什么训练数据需要去重?


Concept Check · Jaccard 迁移题

A={a,b,c,d},B={b,c,e},Jaccard 是多少?阈值 0.5 时是否判 near-duplicate?

答案:交集 2、并集 5,\(J=0.4\),不通过 0.5。

若用 \(2/7\),你把集合并集误算成元素数直接相加,没有扣除交集。

你现在应该能够解释

通往第 15 课

预训练数据决定 base model 的分布;下一课换成更小但目标更明确的 demonstrations 与 preferences,学习“像助手一样回答”而不只是续写。

如果只能记住 5 件事

过滤框架

目标数据 + 原始数据 → 评分函数 → 保留/拒绝

三种算法

KenLM(生成)、fastText(判别)、DSIR(重要性采样)

去重层次

精确 → MinHash → SemDedup,从字符到语义

配比与合成

权重必须连同 epochs;生成数据必须连同 provenance 与独立验证


延伸阅读

Reference · 延伸阅读与一手资料