斯坦福 CS336 从零构建大模型 (2025 春) - 第十四讲:数据处理 2(质量过滤与去重)

文章目录

斯坦福 CS336 第十四讲的主题是大模型预训练数据的底层处理算法。上一讲介绍了数据处理的整体流程,这一讲深入算法层面,聚焦两大核心问题:如何从海量网络数据中筛选出高质量的子集,以及如何高效地去除重复数据。两者的终极工程挑战是相同的:算法必须能在线性时间内处理万亿级别的 Token,消耗的算力必须远小于训练模型本身。


一、质量过滤:三大算法流派

数据过滤的核心抽象框架是:给定极少量的「目标高质量数据(Target Data, T)」和海量的「原始网络数据(Raw Data, R)」,目标是从 R 中找出与 T 相似的高质量子集 T′。

1. N-gram 统计语言模型(如 KenLM)

原理:统计 N-gram 出现的频次来计算概率,通常结合 Kneser-Ney 平滑算法处理未见过的稀疏词组。

工程应用:用高质量数据训练该模型,然后计算原始网页的困惑度(Perplexity)。困惑度越低,说明越符合目标语法分布。Meta 的 CCNet 就是截取了困惑度得分最高的前 1/3 的网页来构建早期的 LLaMA 数据集。

优缺点:速度极快,但非常粗糙。它只看局部上下文,容易被对抗性文本欺骗——只要打乱段落顺序但保持局部连贯,它依然会给出高分。把它理解为一种剔除「纯粹无意义乱码(True nonsense)」的粗糙工具是最准确的定位。

2. FastText 线性分类器与哈希魔法

原理:FastText 是一种极其高效的线性分类器,通过将词汇映射到较小的隐藏维度进行分类,没有非线性激活函数。

它面临一个核心难题:N-gram 的数量理论上是无上限的,无法建立一张完整的词表。FastText 的解法是哈希映射(Hashing trick)

强行设定固定数量的「桶(Bins)」(如 1000 万个)
把所有 N-gram 哈希放进桶里

"machine learning" → hash → 第 384729 号桶
"gradient descent" → hash → 第 1027483 号桶

虽然必定会发生哈希碰撞(不同 N-gram 映射到同一个桶),但模型在训练时能自动中和这些误差,大幅削减参数量并实现极速分类。

工程应用:通常被用作二分类器(正样本 vs 负样本),速度远快于复杂神经网络,非常适合海量数据的初步清洗。

3. 重要性重采样(Importance Resampling)

原理:相比于分类器「一刀切」的硬性判别边界,这种方法更具统计学严谨性。

它分别在目标数据集 T 和原始数据集 R 上拟合概率分布,然后计算每个文档的重要性权重

w(x) = P(x) / Q(x)

P(x) = 这条数据来自「目标分布」的概率
Q(x) = 这条数据来自「原始数据」的概率

权重越高 → 越像目标分布 → 越应该被选中

直接对原始文本建模概率分布几乎不可能,工程上的解法是用 N-gram 特征来近似:统计两个数据集各自的 N-gram 频率分布,用某条数据的 N-gram 在两个分布中的频率之比来估计 w(x)。

具体例子(目标分布为 arXiv 论文,原始数据为 CommonCrawl):

x = "neural network gradient descent"

N-gram 特征:
  "neural network"  → P 中频率=0.008, Q 中频率=0.003
  "gradient descent"→ P 中频率=0.007, Q 中频率=0.002

P(x) ≈ 0.0067
Q(x) ≈ 0.0020
w(x) = 3.35 → 权重高,被选中 ✓

x = "双十一大促销买鞋打折"

  "买鞋打折" → P 中频率=0.00001, Q 中频率=0.008

w(x) ≈ 0.0015 → 权重极低,被淘汰 ✗

为什么用 N-gram 而不是更复杂的模型?

方案 精度 速度 可行性
用大模型打分 极慢 数据量大时不可行
用 FastText 分类 可行
重要性重采样(N-gram) 极快 万亿 Token 级别可行

参数不是「训练」出来的,而是直接从两个数据集的词频统计中算出来的,这也是它速度极快的原因。


二、三大工业级应用场景

上述算法被广泛应用于以下三条数据清洗流水线:

1. 语言识别(Language Identification)

在算力受限时,混入太多小语种数据会抢占有限的 Token 预算,导致主要语言表现下降(Bloom 模型当年仅包含了 30% 的英语数据)。

实践上通常直接使用现成的 FastText 语种分类器。AI2 的 Dolma 数据集强硬丢弃了预测为英语概率小于 0.5 的所有网页。

2. 质量过滤(Quality Filtering)的演进

经典做法:GPT-3 使用线性分类器(正样本为 WebText/Wiki/Books,负样本为 Common Crawl);LLaMA 1 将「被维基百科引用的页面」作为高质量正样本。

大小模型协同(Phi-1 案例):微软的 Phi-1 提出了更前卫的范式:

第一步:用昂贵的 GPT-4 给 10 万篇 Python 代码打分(评估「教育价值」)
        → 获得 10 万个高质量正样本 T

第二步:用这 10 万个样本训练一个极便宜的随机森林分类器

第三步:用这个廉价分类器去过滤以亿计的海量原始数据

关键在于:GPT-4 只被用在极小的子集上,成本可控;真正跑大规模过滤的是廉价的小分类器。用极小的算力撬动了极高的数据质量。

3. 毒性过滤(Toxicity Filtering)

利用人工标注的有害评论数据集(如 Jigsaw 的毒性、仇恨、NSFW 等标签),训练 FastText 分类器来精准剔除不安全的网页。


三、数据去重:从 O(N²) 到线性时间

网络中不仅有精确的镜像复制,还有大量的近义重复(Near-duplicates)——无数次被复制的 MIT 开源协议、填入不同地名的垃圾广告。C4 数据集中甚至有一段涂鸦面具的广告重复了高达 6.1 万次

去重的价值:节省算力(避免在同一段文字上浪费 6 万次计算),并防止模型死记硬背引发的隐私与版权问题。

两两对比的暴力做法是 O(N²),对 10 亿文档根本不可行。去重的工程挑战在于必须通过哈希将其转化为线性时间。

1. 精确去重(Exact Deduplication)

Bloom Filter(布隆过滤器) 是处理海量数据精确去重的内存神器。

它的结构极简:一个**位数组(Bit array)**加上 K 个不同的哈希函数。

初始状态:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

插入 "google.com":
  hash1("google.com") % 10 = 2  → 位置2 置1
  hash2("google.com") % 10 = 5  → 位置5 置1
  hash3("google.com") % 10 = 8  → 位置8 置1

查询 "google.com":位置 2、5、8 全为1 → 存在 ✓
查询 "baidu.com":某位置为0 → 一定不存在 ✓

Bloom Filter 有一个非常关键的不对称误差特性

  • 漏报(False Negative)永远不会发生:插入时置的 1 永远不会消失(位只增不减),加上哈希函数是确定性的,所以查询时一定能找到。
  • 误报(False Positive)可能发生:其他元素「顺带」置上的 1 可能让一个从未插入的元素看起来「已存在」。
Bloom Filter 说「不存在」→ 100% 不存在
Bloom Filter 说「存在」  → 可能存在,也可能误报

误报率由三个参数决定:

m = 位数组长度(越大越好)
n = 插入元素数量
k = 哈希函数数量

误报率 ≈ (1 - e^(-kn/m))^k
最优 k ≈ (m/n) × ln2

实际经验:每个元素分配 10 个 bit、用 7 个哈希函数,误报率约 1%;分配 20 个 bit,误报率约 0.01%。Dolma 就是用 Bloom Filter 在段落级别进行精确去重,设定了 10⁻⁵ 的极低误报率。

注意:K 不是越大越好。K 太大时,每次插入会把大量位置置为 1,位数组被过快填满,反而导致误报率飙升。因此必须找到数学上的最优平衡点。

2. 近似去重(Approximate Deduplication):MinHash + LSH

精确去重解决不了「只改了几个字」的近义重复。标准做法是 MinHash + LSH 的组合。

第一步:用 Jaccard 相似度定义「相似」

Jaccard 相似度 = 交集 / 并集,值域 0 到 1:

文本A 的 N-gram 集合:{"机器学习", "学习是", "是子领域"}
文本B 的 N-gram 集合:{"机器学习", "学习是", "是一个", "子领域"}

交集大小 = 2,并集大小 = 5
Jaccard = 2/5 = 0.4

但 10 亿文档两两计算 Jaccard 需要 5×10¹⁷ 次比较,根本不可行。

第二步:MinHash 把集合压缩成签名向量

MinHash 的核心数学性质:对集合的所有元素做随机哈希并取最小值,两篇文档 MinHash 值碰撞(完全相等)的概率,刚好等于它们的 Jaccard 相似度。

用 4 个哈希函数(实际用 100 个,这里简化):

        hash1  hash2  hash3  hash4
文档1:[1821,  3342,  2156,  4401]
文档2:[1821,  3342,  2156,  4401]  ← 签名几乎相同
文档3:[2341,  1234,  3891,  2201]
文档4:[8823,  7234,  9121,  8834]  ← 签名完全不同

这成功将两两对比转化为了单篇文档的哈希求最小值操作。

第三步:LSH 把签名向量变成候选对

即使有了 MinHash 签名,10 亿文档两两比较签名还是 O(N²)。LSH(局部敏感哈希) 的作用是让相似文档自动聚到同一个桶里,跳过两两比较。

做法是将签名向量分成 B 个「带(Band)」,每带含 R 行,每个 band 单独哈希到桶:

签名向量(4维)分成 2 个 band,每个 band 2维:

         band1        band2
文档1:[1821, 3342] [2156, 4401]
文档2:[1821, 3342] [2156, 4401]
文档3:[2341, 1234] [3891, 2201]
文档4:[8823, 7234] [9121, 8834]
文档5:[2341, 1234] [3891, 2201]

band1 分桶:
  文档1、文档2 → 同一桶 ← 碰撞!候选对
  文档3、文档5 → 同一桶 ← 碰撞!候选对
  文档4       → 独立桶

只要有一个 band 落入同一个桶,就成为候选对。LSH 使得最终碰撞概率变成 1-(1-s^R)^B,在图上画出一道 S 型曲线

相似度

1.0 |          ___________
    |         /
0.5 |        /   ← 陡峭的 S 型曲线
    |       /
0.0 |______/
    +----------→ 被选为候选对的概率
         ↑
       阈值(由 B 和 R 决定)
  • 增大 R:全相等的条件更严苛,曲线右移,阈值更高
  • 增大 B:提供更多碰撞机会,曲线左移,高相似度文档更容易被找到

现代大模型常设定 0.99 左右的极严苛阈值来精准秒杀近义网页。


四、一个完整的去重例子

用 5 篇文章走完整个流程:

文档1:「机器学习是人工智能的子领域」
文档2:「机器学习是人工智能的一个子领域」  ← 和文档1几乎一样
文档3:「深度学习是机器学习的子集」
文档4:「今天天气真不错,适合出去玩」
文档5:「深度学习是机器学习的一个子集」    ← 和文档3几乎一样

第一步:转成 N-gram 集合

文档1:{"机器学习", "学习是", "是人工", "人工智能", "智能的", "的子领域"}
文档2:{"机器学习", "学习是", "是人工", "人工智能", "智能的", "的一个", "一个子", "子领域"}
文档3:{"深度学习", "学习是", "是机器", "机器学习", "学习的", "的子集"}
文档4:{"今天天气", "天气真", "真不错", "不错适", "适合出", "出去玩"}
文档5:{"深度学习", "学习是", "是机器", "机器学习", "学习的", "的一个", "一个子", "子集"}

第二步:MinHash 生成签名

        hash1  hash2  hash3  hash4
文档1:[1821,  3342,  2156,  4401]
文档2:[1821,  3342,  2156,  4401]  ← 和文档1签名相同
文档3:[2341,  1234,  3891,  2201]
文档4:[8823,  7234,  9121,  8834]
文档5:[2341,  1234,  3891,  2201]  ← 和文档3签名相同

第三步:LSH 分 Band,产生候选对

分成 2 个 band,每个 band 2 维:

候选对:(文档1, 文档2)、(文档3, 文档5)
文档4 没有碰撞,跳过。

第四步:精确计算 Jaccard

(文档1, 文档2):交集5 / 并集8 = 0.625 > 阈值0.5 → 确认重复!
(文档3, 文档5):交集5 / 并集8 = 0.625 > 阈值0.5 → 确认重复!

第五步:删除重复

保留:文档1、文档3、文档4
删除:文档2(和文档1重复)、文档5(和文档3重复)

为什么三步缺一不可:

只用 Jaccard:    精确,但 10亿文档算不完
只用 MinHash:    压缩了数据,但还是要两两比较
只用 LSH:        分桶快,但没有 MinHash 就无法比较
三者结合:        O(n) 复杂度,近似精确,工业可用

五、一个值得注意的问题:高质量数据要不要去重?

去重主要针对的是预训练阶段那庞大、杂乱的网络爬取数据。对于精心挑选的高质量数据,有时反而希望保留重复,甚至在中期训练(Mid-training)阶段故意对优质数据跑好几个 Epoch。

更理想的做法是:统计每份数据出现的频次,取其平方根或对数来按比例保留,而不是简单粗暴地把它删减到只剩一份。


六、核心概念问答(Q&A)

Q1:N-gram 过滤只是确保了词和词之间能连在一起,并不能保证宏观语义合理,它是不是只能用来过滤「纯粹乱码」?

是的。N-gram 确实只关注非常局部的上下文,容易被对抗性地欺骗——只要打乱顺序但保持局部连贯,它依然会给出高分。把它视为一种剔除「纯粹无意义乱码」的粗糙工具是准确的视角。只要网页看起来像符合语法的文字它就能放行,对于粗略清洗网页来说通常足够了。

Q2:Bloom Filter 的最优哈希函数数量 K 为什么存在一个平衡点,而不是越多越好?

K 太大时,每次插入一个新元素都会把位数组里的很多位置变成 1,导致过滤器非常快地被「填满」,随后任何查询都极易碰到全为 1 的情况,误报率反而飙升。K 太小则检查条件不够严苛。因此在给定的内存容量和插入数据量下,存在一个数学最优点 k ≈ (m/n) × ln2。

Q3:现在合成数据越来越多,如果文本被大模型改写过,基于词汇的去重方法还有用吗?

面对改写,如果要捕捉更深层的语义相似性,需要使用文本嵌入(Embeddings)。这完全符合 LSH 的框架,因为 LSH 最初就是为在向量空间中寻找「近似最近邻」而设计的。但有两个代价:第一,计算 Embedding 的算力成本远高于 MinHash;第二,如果基于模糊语义的去重条件设置过于宽松,很容易误删大量虽然语义相近但极其有用的训练数据。

Q4:有没有情况下,我们反而希望保留重复的高质量数据?

完全正确。在中期训练(Mid-training)阶段,对精心挑选的高质量数据确实会故意跑好几个 Epoch。今天讲的去重主要针对预训练阶段庞大杂乱的网络爬取数据。对于高质量数据,更理想的做法是统计其出现频次,取平方根或对数来按比例保留,而不是简单删减到只剩一份。

Q5:像 Phi-1 那样用 GPT-4 给数据打分,成本不会太高吗?

关键在于只在极小的数据子集上使用 GPT-4——比如只标注 10 万个样本作为正样本 T,这笔成本是可控的。然后用这 10 万个样本蒸馏出一个极便宜的分类器(随机森林),最后用这个跑得飞快的廉价分类器去过滤以亿计的海量原始数据。


七、复习题

一、核心过滤算法(Filtering Algorithms)

  1. 过滤的抽象框架:数据质量过滤的核心抽象目标是什么?在算力开销上有什么严格的前提要求?
  2. N-gram 语言模型:使用 N-gram 语言模型(如 KenLM)进行数据过滤的核心原理是什么?它在评估网页质量时存在什么致命的局限性?
  3. FastText 分类器的哈希魔法:为什么 FastText 线性分类器在处理无限数量的 N-gram 词表时依然能够极其高效?它采用了什么核心技术来控制参数量?
  4. 重要性重采样(Importance Resampling):与训练一个简单的二分类器(判断网页是「好」还是「坏」)相比,采用「重要性重采样」方法进行数据过滤在统计学理论上有什么优势?

二、过滤算法的实际应用(Applications)

  1. 大小模型协同(Phi-1 的案例):微软的 Phi-1 模型在进行代码质量过滤时,采用了一种怎样结合大模型和小模型的「前卫」范式?它是如何用极小的算力成本撬动高质量过滤的?

三、数据去重(Deduplication)与精确匹配

  1. 去重的两大核心目的:为什么在预训练阶段对海量数据进行去重(Deduplication)极其重要?请列举至少两个主要原因。
  2. 布隆过滤器的误差权衡(Bloom Filters):在进行精确去重时,布隆过滤器的误差特性是什么(会漏报还是会误报)?如果一味地增加哈希函数的数量 K,会对它的准确性产生什么反噬影响?

四、近似去重与局部敏感哈希(MinHash + LSH)

  1. MinHash 的数学奇迹:在检测近义/近似重复文档(Near-duplicates)时,MinHash 算法的核心数学定理是什么?(提示:关于它的碰撞概率与某种相似度之间的等式)
  2. LSH 阈值塑形(Locality Sensitive Hashing):为什么单独使用 MinHash 算法不够,必须结合 LSH 将哈希值分组为多个「带(Bands)」和「行(Rows)」?LSH 是如何重塑碰撞概率曲线的?
  3. 调节 LSH 曲线:在 LSH 算法中,如果增大参数 R(每个带的行数)和增大参数 B(带的数量),分别会对判定两篇文档是否重复的「S型概率曲线」产生向左移还是向右移的影响?

八、参考答案

1. 过滤的抽象框架?

给定极少量的「目标高质量数据(Target Data, T)」和海量的「原始网络数据(Raw Data, R)」,目标是从 R 中找出与 T 相似的高质量子集 T′。因为网络数据极其庞大,过滤算法必须在线性时间内运行,消耗的算力必须远小于训练大模型本身的算力。

2. N-gram 语言模型的原理与局限?

原理是通过统计 N-gram 出现的频次来计算句子的概率,从而算出整个网页的困惑度(Perplexity),困惑度越低代表越符合目标语法分布。其局限性是它只关注极其局部的上下文连贯性,容易被对抗性文本(例如打乱段落顺序但局部连贯的句子)欺骗,它无法保证宏观语义的合理性,主要作用仅仅是剔除「纯粹的无意义乱码(True nonsense)」。

3. FastText 分类器的哈希魔法?

为了处理数量可能无限大的 N-gram 词表,FastText 采用了**哈希映射(Hashing trick)**技术。它强行设定固定数量的「桶(Bins)」(如 1000 万个),把所有提取出的词组哈希放进这些桶里。虽然这必然会产生哈希碰撞,但模型在优化损失时会自动中和这些误差,从而大幅削减参数量并实现极速分类。

4. 重要性重采样(Importance Resampling)的理论优势?

普通二分类器是硬性的判别边界,而重要性重采样分别在目标数据集 T 和原始数据集 R 上拟合生成分布 P 和 Q。通过计算重要性权重比值(P/Q)并以此为概率进行重采样,该方法在理论上能更好地匹配目标数据的真实多样性分布,而不仅仅是硬性截断。

5. 大小模型协同(Phi-1 的案例)?

Phi-1 先使用非常昂贵的 GPT-4 对 10 万篇 Python 代码打分(评估其教育价值)作为正样本 T。接着,利用这 10 万个样本训练一个极其便宜且跑得极快的随机森林分类器(Random Forest Classifier)。最后,使用这个廉价的随机森林分类器去过滤以亿计的海量原始代码数据,实现了算力与质量的完美杠杆。

6. 去重的两大核心目的?

提升训练效率:消除海量毫无意义的重复数据(例如出现 6.1 万次的同一段面具广告),节省昂贵的计算算力(Flops)和 Token 预算。

防止模型死记硬背(Memorization):减少同一数据在训练集中出现的频次,从而降低模型一字不差地背诵原文所引发的版权(Copyright)和隐私侵权风险。

7. 布隆过滤器的误差权衡(Bloom Filters)?

布隆过滤器绝对不会漏报(False Negative)(如果它说不存在,就一定不存在),但会产生极小的误报率(False Positive)(可能把新文档误认为已存在)。如果哈希函数数量 K 设置得太大,每次插入元素都会把大量位置置为 1,导致位数组被过快填满,随后的任何查询都极易发生误报。因此必须找到一个数学最优的 K 值以平衡误报率。

8. MinHash 的数学奇迹?

MinHash 将文档的所有元素进行随机哈希打乱并取最小值。其核心数学性质是:两篇文档的 MinHash 值发生碰撞(完全相等)的概率,刚好严丝合缝地等于它们之间的 Jaccard 相似度(交集除以并集)。这成功将时间复杂度极高的两两比对,转化为了单篇文档的哈希操作。

9. LSH 阈值塑形(Locality Sensitive Hashing)?

单个 MinHash 的碰撞概率是线性的,不够严苛。LSH 将所有哈希值分为 B 个「带」,每带含 R 行,并规定:只有当两篇文档在某一个完整的带里面所有的 R 个哈希值都完全相等时,才判定为碰撞。这使得整体碰撞概率变成了 1-(1-s^R)^B,在数学图表上画出了一道完美的 S 型曲线(Sigmoid),从而实现了只对极高相似度(如 0.99)才触发拦截的「严苛阈值开关」。

10. 调节 LSH 曲线?

增大 R(行数):会使全相等的条件变得更加严苛,S 型曲线会向右移,阈值变得更高。

增大 B(带的数量):会给文档提供更多触发碰撞的机会,S 型曲线会向左移,从而提高高相似度情况下的成功拦截率。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐