过滤框架
目标数据 + 原始数据 → 评分函数 → 保留/拒绝
CS336 · 第 14 课
第 13 课回答数据从哪里来;这一课把 raw records 变成训练 distribution:先抽取、过滤与去重,再决定不同来源被采样多少,最后理解 mid/post-training 的 synthetic data 如何由任务与 teacher 共同生成。
Spring 2026 官方来源:Lecture 14 trace · 课程日程
本页是中文学习导读;讲次主题、教师与材料版本以 Stanford 官方页面和讲义为准。
本讲问题:原始数据规模很大但信号密度不均;处理流水线要在保留有用分布和移除噪声、重复、污染之间做可测量的权衡。
HTML 提取 → 规则 / 模型过滤 → 文档与片段去重 → contamination 检查 → mixture / epoch cap → synthetic prompts 与 responses → 训练验证。
过滤器不是“质量真相”,而是一个会产生 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 duplicateA 的 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)。
Common Crawl 存储的是原始 HTML。HTML 包含大量非内容信息:
| 工具 | 方法 | 特点 |
|---|---|---|
| trafilatura | 基于内容密度 | 精确,保留结构 |
| jusText | 基于标点/词比 | 快速,保留更多 |
| readability | 启发式布局分析 | 类似浏览器阅读模式 |
| BeautifulSoup | DOM 解析 | 灵活,需自定义规则 |
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 个百分点
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 语言模型:
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 数据过滤
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 的核心思想:
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),即线性分类器
# 问题: 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 个桶
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(高维但稀疏的特征)
| 方法 | 评分函数 | 优点 | 缺点 |
|---|---|---|---|
| KenLM | p_T(x) | 极快,简单 | 只看局部,模型容量低 |
| fastText | p(T|x) | 快速,可泛化 | 需要标注数据 |
| DSIR | p_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-3 | Wikipedia, WebText, Books | Common Crawl | 线性分类器 |
| LLaMA | Wikipedia 引用页面 | Common Crawl | 分类器 |
| DCLM | OpenHermes, ELI5 | RefinedWeb | fastText |
| phi-1 | GPT-4 分类为高教育价值 | The Stack | Random 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 保留一个文档
当数据量太大,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 可能)
假设哈希函数独立,给定:
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
检测近似重复:
# 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) 的比例
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 的核心思想:
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%
配比的影响:
- 代码比例高 → 编程能力强,但可能"代码腔"
- 书籍比例高 → 叙事能力强,但可能过时
- 论文比例高 → 推理能力强,但可能"学术腔"
假设 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。
Spring 2026 官方讲义用 UniMax 说明一个简单边界:尽量均匀覆盖来源,但限制任何来源最多重复 (C) 个 epochs。来源用尽上限后,把剩余预算重新分给仍有容量的来源。它优化的是 coverage 与重复风险的可解释约束,不声称自动找到所有下游任务的最优 mixture。
另一条路线是在小模型上尝试多组 mixture,用 regression 预测 mixture → validation/downstream metric,再选择候选配方。但小规模实验会低估大规模训练中的 epoching:同一权重在小预算下可能只看一遍稀缺来源,在大预算下却重复几十遍。官方讲义强调要让小规模 proxy 模拟大规模的 source exhaustion,而不是把拟合结果直接当真理。
用 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
如果只用 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| 来源 | 例子 | 主要风险 |
|---|---|---|
| 真实 | 用户问题、GitHub PR、StackExchange | 隐私、许可、分布偏差与 contamination |
| 半合成 | 真实 repository/environment + 模型生成任务 | 环境搭建失败、任务与真实需求错位 |
| 全合成 | teacher 同时生成 prompt 与 response | teacher blind spots 被复制,diversity 只是表面变化 |
Teacher 更强不保证对每个 domain 都是更好的数据生成器;同一 prompt 多次采样可提高找到好 response 的机会,但也会增加近重复。答案过滤、执行反馈和 reward model 都只是 proxy。验收应同时查看 pass rate、人工样本、difficulty distribution、重复率,以及在未用于生成或筛选的 held-out tasks 上训练后的变化。
若生成 prompts 时读到了 evaluation solutions,后续再对文本做 exact dedup 也可能来不及,因为答案已被 paraphrase 进训练数据。应在 seed、teacher context、generated prompt/response 和最终 corpus 多个阶段记录 lineage,并用 exact、near 与 semantic checks 分层审计。
某来源只有 2B tokens,小实验处理它 1B tokens,大实验按同一 mixture weight 处理 40B tokens。为什么不能说“mixture 没变,所以分布没变”?
答案:该来源从 0.5 epoch 变成 20 epochs;重复、记忆与边际收益都变了。需要显式控制或模拟 epoching。
若只比较 sampling percentage,你忽略了 source size 与 processed tokens 共同决定的 effective epochs。
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:为什么训练数据需要去重?
A={a,b,c,d},B={b,c,e},Jaccard 是多少?阈值 0.5 时是否判 near-duplicate?
答案:交集 2、并集 5,\(J=0.4\),不通过 0.5。
若用 \(2/7\),你把集合并集误算成元素数直接相加,没有扣除交集。
预训练数据决定 base model 的分布;下一课换成更小但目标更明确的 demonstrations 与 preferences,学习“像助手一样回答”而不只是续写。
目标数据 + 原始数据 → 评分函数 → 保留/拒绝
KenLM(生成)、fastText(判别)、DSIR(重要性采样)
精确 → MinHash → SemDedup,从字符到语义
权重必须连同 epochs;生成数据必须连同 provenance 与独立验证