第 4 章 第一部分:预训练数据

去重——被低估的关键步骤

精确去重、模糊去重、大规模工程挑战与 Benchmark 污染检测

去重 MinHash LSH 数据质量 语义去重

“如果你的训练数据中有 30% 的重复内容,你不是在用 15 万亿 token 训练,你是在用 10 万亿 token 训练——而且那个 ‘10 万亿’ 还有偏差。“


4.1 为什么去重如此重要

重复数据对训练的三重伤害

第一重伤害:记忆化(Memorization)

当一段文本在训练数据中出现多次时,模型不是在”学习”这段文本背后的知识,而是在”背诵”它。这导致模型在面对类似但不完全相同的输入时,倾向于输出记忆中的原文,而非真正理解后的回答。

Lee et al.(2022)的开创性研究《Deduplicating Training Data Makes Language Models Better》系统性地证明了这一点。他们在 C4 数据集上做了精确去重和模糊去重,发现:

  • 去重后的数据集训练出的模型在多个 benchmark 上一致优于未去重版本
  • 去重使模型产生的”逐字重复”(verbatim memorization)大幅减少
  • 即使训练 token 总数因去重而减少,模型性能反而提升——这是一个强有力的证据,说明重复数据的边际价值为负

第二重伤害:泛化能力下降

重复数据会让模型的训练 loss 看起来很好——毕竟它在”记住”的数据上 loss 可以很低。但这是虚假的繁荣。模型在训练数据分布内的表现好,并不意味着它在分布外(即真实使用场景中)也好。重复数据等效于增加了训练分布的”峰度”——让模型过度拟合到某些特定的模式上。

第三重伤害:隐私泄露风险

如果某个人的个人信息(比如一个特定的电话号码+姓名组合)在训练数据中出现了 50 次(比如在多个网站的缓存页面中),模型”记住”这个信息的概率远高于只出现 1 次的情况。去重可以显著降低这种隐私泄露的风险。

互联网数据的重复程度超乎想象

你可能以为互联网上的内容大多是独特的。实际情况是:

  • 完全相同的文档:Common Crawl 的一个快照中,约 20-30% 的文档是完全相同的副本(同一内容通过不同 URL 出现多次,或者被多个网站转载)
  • 近似重复的文档:另外约 10-20% 的文档是近似重复的——内容高度相似但有细微差异(如页眉/页脚不同、时间戳不同、评论数量不同)
  • 段落级重复:即使文档整体不重复,其中的某些段落可能在大量文档中反复出现(如法律声明、版权通知、标准化的产品描述、新闻通稿的传播)

这意味着,如果不做去重,你的”15 万亿 token”训练集中,可能有 5 万亿 token 是重复内容。

定量证据(Lee et al., 2022, “Deduplicating Training Data Makes Language Models Better”):C4 数据集中约 3.04% 的样本在训练集和测试集之间存在重叠——这意味着即使是经过基本去重的主流数据集,仍存在显著的”评估泄露”问题。模型对这些重复内容会产生近乎逐字逐句的机械记忆,解释了为什么”去重是免费的午餐”——减少数据量反而提升性能。


4.2 精确去重

精确去重是最简单也最快的去重方式:找到内容完全相同的文档并移除副本。

URL 去重

方法:对同一 URL 只保留一份文档。

优点:

  • 速度极快——只需要对 URL 做 hash,然后查重
  • 实现简单

局限:

  • 同一内容可以有不同的 URL(http vs https、www. vs 无 www.、URL 参数不同、短链接)
  • 不同 URL 可以指向完全相同的内容(镜像站点、CDN 缓存)
  • 只能作为第一步粗筛,远不够用

实现建议:

from urllib.parse import urlparse, urlunsplit

def normalize_url(url: str) -> str:
    """URL 标准化,合并同一页面的不同 URL 形式"""
    parsed = urlparse(url.lower())
    # 去掉 www.
    netloc = parsed.netloc.replace('www.', '')
    # 去掉常见的无意义参数
    path = parsed.path.rstrip('/')
    # 忽略 scheme (http/https 视为同一页面)
    return urlunsplit(('', netloc, path, '', ''))

文档级精确去重(Hash-based)

方法:计算每个文档的 hash 值(如 SHA-256),hash 相同的文档视为重复。

优点:

  • 100% 精确——不会产生假阳性
  • 速度快——SHA-256 的计算很快
  • 内存友好——可以用 Bloom Filter 来节省内存

局限:

  • 对内容的任何微小变化(多一个空格、时间戳不同)都会产生不同的 hash
  • 只能发现”完全相同”的重复,无法发现近似重复

实现:

import hashlib
from collections import defaultdict

def compute_document_hash(text: str) -> str:
    """计算文档的 SHA-256 hash"""
    # 预处理:统一空白字符,去除首尾空白
    normalized = ' '.join(text.split())
    return hashlib.sha256(normalized.encode('utf-8')).hexdigest()

# 使用 Bloom Filter 节省内存(适用于万亿 token 级别)
# pip install pybloom_live
from pybloom_live import ScalableBloomFilter

class ExactDeduplicator:
    def __init__(self):
        # 初始容量 1 亿,误报率 0.001
        self.seen = ScalableBloomFilter(
            initial_capacity=100_000_000,
            error_rate=0.001
        )
    
    def is_duplicate(self, text: str) -> bool:
        h = compute_document_hash(text)
        if h in self.seen:
            return True  # 可能误报(概率 0.001)
        self.seen.add(h)
        return False

Bloom Filter 的内存优势:

  • 存储 10 亿个 hash:直接存储需要 ~32GB(每个 SHA-256 hash 32 字节),Bloom Filter 只需要 ~2GB
  • 代价是有 0.1% 的误报率(把非重复标记为重复),这在去重场景中是可以接受的

段落/行级精确去重

有时候文档整体不重复,但其中的某些段落是高度模板化的。比如:

  • 法律声明:“本站内容仅供参考,不构成任何法律建议…”(出现在数十万个网站上)
  • Cookie 提示:“我们使用 Cookie 来提升您的体验…”
  • 新闻通稿:同一段通稿被数百家媒体原封不动地发布

方法:对每个段落(或每 N 行)计算 hash,移除出现频率超过阈值的段落。

注意:这个方法要谨慎使用。如果阈值设得太低,可能会误删一些”自然重复”的内容(如常见的问候语、标准化的格式文本)。建议只对长度超过一定阈值(如 > 100 字符)的段落做这种去重。


4.3 模糊去重(Fuzzy Deduplication)

模糊去重的目标是找到内容高度相似但不完全相同的文档。这是去重中最关键也最有挑战的步骤。

MinHash + LSH:当前的主流方案

核心思想:

  1. Shingling:将每个文档转换为一个 n-gram 集合

    • 例如,“今天天气很好” 的 5-gram 集合 = {“今天天气很”, “天天气很好”}
  2. MinHash:用多个 hash 函数对 n-gram 集合做签名压缩

    • 每个文档被压缩为一个固定长度的签名向量(如 128 维)
    • 两个文档的 MinHash 签名的相似度是它们 Jaccard 相似度的无偏估计
  3. LSH(Locality-Sensitive Hashing):用 banding 技术将相似文档映射到同一个桶

    • 将签名分成 b 个 band,每个 band 有 r 个 row(b × r = 签名总长度)
    • 如果两个文档在任意一个 band 上完全匹配,就认为它们是候选相似对
    • 调整 b 和 r 可以控制相似度阈值

参数选择指南:

参数推荐值说明
n-gram 大小 (n)5(英文)/ 3-5(中文)太小会导致假阳性太多,太大会导致召回率下降
签名长度 (num_perm)128-256越长越精确,但计算量越大
band 数 (b)根据目标阈值计算b × r = num_perm
row 数 (r)根据目标阈值计算目标阈值 ≈ (1/b)^(1/r)
相似度阈值0.7-0.8高于这个阈值的文档对被认为是近似重复

n-gram 大小对中文的特殊影响:

中文没有天然的单词边界。英文的 5-gram 可能包含整个短语,但中文的 5-gram 可能只有 5 个字。这意味着:

  • 中文使用较小的 n(如 3-4)更合理——否则 n-gram 太长,不同文档之间的 n-gram 重合率会很低,导致即使内容很相似也检测不到
  • 但 n 太小(如 1-2)又会导致假阳性太多——任何两段中文文本都会有大量的单字或双字重合

推荐:中文使用 n=3 或 n=4,配合 num_perm=128、阈值 0.75。

参数调优实验

不同参数组合对去重效果的影响:

实验设置:100M token 中文数据样本

参数组合 A (n=5, threshold=0.8):
  - 去重率:12.3%
  - 人工审查准确率(确实是重复):96.5%
  - 召回率估计:65%(有些相似文档没被召回)

参数组合 B (n=3, threshold=0.75):
  - 去重率:18.7%
  - 人工审查准确率:91.2%
  - 召回率估计:82%

参数组合 C (n=3, threshold=0.7):
  - 去重率:24.1%
  - 人工审查准确率:83.7%
  - 召回率估计:89%

参数组合 D (n=3, threshold=0.65):
  - 去重率:31.5%
  - 人工审查准确率:72.4%(开始出现较多误杀)
  - 召回率估计:93%

分析:

  • 参数组合 B 是较好的平衡点——去重率适中,准确率高
  • 如果你更关心”不遗漏重复”(高召回率),选 C
  • 不建议使用 D——72% 的准确率意味着近 30% 的”去重”实际是误杀

SimHash 方案

SimHash 是另一种模糊去重方法。与 MinHash 基于集合相似度不同,SimHash 基于余弦相似度。

原理:

  1. 将文档的每个特征(n-gram)映射为一个固定长度的 hash 值
  2. 对所有特征的 hash 值做加权累加
  3. 最终得到一个固定长度的二进制签名
  4. 两个文档的 SimHash 签名的汉明距离反映了它们的相似度

与 MinHash 的对比:

维度MinHash + LSHSimHash
相似度度量Jaccard余弦
适用场景文档长度差异大时更鲁棒文档长度相近时效果更好
计算效率签名计算较慢,查找快签名计算快,查找也快
内存占用较高(需要存储签名向量)较低(每个文档只需 64-128 bit)
参数调优较复杂(n, num_perm, b, r)较简单(汉明距离阈值)

推荐:

  • 对于大规模预训练数据去重,MinHash + LSH 是更稳健的选择——它对文档长度差异的鲁棒性更好
  • SimHash 适合作为补充方案,特别是在需要快速近似查重的场景

Suffix Array 方案

Suffix Array 去重是一种基于子串匹配的方法。它不是在文档级别判断重复,而是在子串级别查找重复的长片段。

原理:

  1. 将所有文档拼接成一个超长字符串
  2. 构建后缀数组(Suffix Array)
  3. 通过后缀数组快速找到所有长度超过阈值的重复子串

优点:

  • 能发现文档内部的局部重复(如两个文档各有 80% 不同但共享一段 500 字的引用)
  • 不依赖 n-gram 大小等参数

缺点:

  • 内存消耗巨大——需要存储整个文本语料的后缀数组
  • 不适合万亿 token 级别的数据(需要 TB 级内存)
  • 实现复杂度高

适用场景:

  • 数据集较小时(< 100B token)
  • 需要精细的段落级去重时
  • 作为 MinHash 去重后的补充步骤

4.4 大规模去重的工程挑战

当数据规模达到万亿 token 时,去重本身变成了一个严肃的工程问题。

分布式 MinHash 方案

挑战:15T token 的数据包含约 100 亿个文档。为每个文档计算 MinHash 签名,然后在所有文档对之间做比较——这是一个 O(n²) 的问题。

解决方案:

架构设计:

1. 分片计算签名
   - 将数据分成 N 个分片(如 1000 个)
   - 每个分片在一个 worker 上独立计算 MinHash 签名
   - 输出:每个文档的签名向量

2. LSH 分桶
   - 对所有签名做 LSH banding
   - 每个 band 的结果写入一个分布式 hash 表
   - 落入同一个桶的文档是候选相似对

3. 精确比较
   - 对每个桶内的候选对,计算实际的 Jaccard 相似度
   - 超过阈值的标记为重复

4. 连通分量
   - 将所有重复对构建成图
   - 找到连通分量(如果 A≈B,B≈C,则 A、B、C 属于同一组)
   - 每组保留一个代表文档

技术选型:

  • Spark / Ray:适合中等规模(< 1T token),生态成熟

  • 自定义 MapReduce:适合超大规模,可以精细控制内存和 I/O

  • datatrove(HuggingFace):FineWeb 使用的框架,对 MinHash 去重有很好的支持

  • Milvus 2.6 原生 MinHash LSH :向量数据库 Milvus 2.6 原生集成 MinHash LSH 索引,支持百亿级文档近似去重,将去重从”外部工具”变为”数据库基础能力”。相比 MapReduce 方案速度提升 2x+,成本优化 3-5x,简化 MLOps 流程

  • LSHBloom(Khan et al., U Chicago/Argonne, 2025-2026) :MinHashLSH 的内存优化变种,用轻量级布隆过滤器替代 LSHIndex。核心优势:

    • 磁盘空间极低:peS2o(3900 万文档)仅需 MinHashLSH 的 0.6%
    • 速度:peS2o 上 2.7× 加速
    • 假阳性率:1e-5(可忽略)
    • 极大规模优势放大:50B+ 文档级预计 54× 空间优势、2.5×+ 速度优势

    适合场景:单机或小集群预算有限的中小团队;极大规模(数十亿文档)部署。对预算紧但数据量大的学术/初创团队尤其友好。

内存管理

问题:100 亿个文档,每个文档的 MinHash 签名是 128 个 uint32 值,总共需要 128 × 4 × 10^10 = 5.12TB 内存。

解决方案:

  1. 分桶处理:不需要同时在内存中保存所有签名。按 LSH band 分桶处理,每次只加载一个 band 的数据
  2. 磁盘辅助:签名写入磁盘,按需加载
  3. 压缩签名:用 uint16 替代 uint32(精度损失很小),内存减半
  4. 流式处理:签名计算和 LSH 分桶可以流水线化

跨数据集去重 vs 数据集内去重

数据集内去重:在同一个数据集中查找重复。这是标准做法。

跨数据集去重:在你的预训练数据集和 benchmark 评测集之间做去重(benchmark 污染检测),或者在预训练数据和 SFT 数据之间做去重。

跨数据集去重的重要性:

  • 如果预训练数据中包含了 benchmark 的测试题目,模型在 benchmark 上的分数就是”虚高”的
  • 如果 SFT 数据中的内容在预训练中已经大量出现过,SFT 的”激活”效果会被高估

去重顺序的影响

先去重后过滤,还是先过滤后去重?

方案 A:先去重 → 后过滤
  优点:去重在完整数据上进行,覆盖更全面
  缺点:去重的数据量更大,计算成本更高

方案 B:先过滤 → 后去重
  优点:去重的数据量较小,更快
  缺点:某些重复文档可能在过滤阶段有一份被过滤了但另一份保留了,导致去重不完整

推荐:方案 A(先去重后过滤),或至少在粗过滤后去重(去掉明显的垃圾后再去重)

在线增量去重:RAD(Retrieval-Augmented Deduplication)

传统 MinHash LSH 的最大工程痛点是:每次加入新数据批次,都要重新扫描整个已入库语料库——LSH 桶随数据增长而膨胀,每个新文档的候选集扫描成本上升,吞吐量持续下降。RAD(Bore et al., IBM Research, arXiv:2606.03001, 2026-06)提出了在线方案:

  • 核心机制:用 HNSW 图索引替代 LSH 桶,维护已入库文档的增量索引。新文档到达时,在 HNSW 图中检索其最近邻候选集(O(log N)),而非扫描所有 LSH 桶。
  • 关键挑战与解法:Jaccard 相似度在 HNSW 中会出现”距离分数拥挤”(distance score crowding)——大量文档的 Jaccard 距离集中在狭窄区间,使图遍历不可靠。RAD 用 bitmap 表示提供更具区分度的 Jaccard 对齐信号。
  • 实战数据:在 LM1B / C4 / RealNews / Common Crawl 上维持 0.94-0.97 recall(vs LSH 基线),吞吐量提升高达 8×,且吞吐量随数据集增长保持稳定。

适用场景:需要频繁从新 CC 快照增量合并数据的团队。如果你每月都下载最新 CC 快照并合并到已有语料中,RAD 的在线方案可以避免每次全量重新去重的计算开销。


4.5 认知变化:从”越多越好”到”分粒度按需”

去重的认知演变:

2020: "去重?不太需要吧"
     GPT-3 只做了 URL 级别的去重,没有做内容级别的去重

2021: "精确去重就够了"
     一些团队开始做文档级的 hash 去重,但认为模糊去重不值得

2022: "必须做模糊去重!"
     Lee et al. (2022) 的研究提供了强有力的实证证据
     → 去重后模型在多个 benchmark 上一致提升
     → "不去重 = 浪费计算资源" 成为共识

2023: "去重越多越好"
     一度成为主流观点
     → 有团队开始做段落级甚至句子级的去重
     → 追求最大化的去重率

2024: "等等,过度去重也有害"
     FineWeb 的实验发现:
     → 去重阈值设得太低(如 0.6)会误删大量不是真正重复的内容
     → 过度去重减少了训练数据的多样性
     → 某些"看起来重复"的内容其实来自不同语境,各有价值

2025-2026: "分粒度去重,按需调节"
     当前最佳实践:
     → URL 去重(基础)
     → 文档级精确去重(必须)
     → 文档级模糊去重(推荐,阈值 0.75-0.80)
     → 段落级去重(可选,对模板化内容有效)
     → 不建议在 n-gram 层面做过细的去重

4.6 Benchmark 污染检测

什么是 Benchmark 污染

如果你的预训练数据中包含了 benchmark 评测集的题目或答案,模型在 benchmark 上的得分就不能反映其真实能力——它可能只是”背诵”了答案。

这个问题比你想象的严重。因为:

  • 很多 benchmark 的题目最初来源于互联网(如 HellaSwag 的数据来自 WikiHow、ARC 的数据来自科学教材)
  • Common Crawl 的爬取范围极广,完全有可能爬到这些来源
  • 一些 benchmark 本身就是开源的,任何人都可以把题目发布到网页上

检测方法

N-gram 重叠检测:

def check_contamination(train_text: str, test_text: str, n: int = 13) -> float:
    """检测训练文本和测试文本之间的 n-gram 重叠率"""
    train_ngrams = set()
    for i in range(len(train_text) - n + 1):
        train_ngrams.add(train_text[i:i+n])
    
    test_ngrams = []
    for i in range(len(test_text) - n + 1):
        test_ngrams.append(test_text[i:i+n])
    
    if not test_ngrams:
        return 0.0
    
    overlap = sum(1 for ng in test_ngrams if ng in train_ngrams)
    return overlap / len(test_ngrams)

Llama 3 使用的方法:Llama 3 使用 token 级别的 n-gram(n=10)来检测 benchmark 污染。如果训练数据中某个文档与 benchmark 题目的 10-gram 重叠率超过一定阈值,该文档被标记为受污染并从训练集中移除。

编辑距离方法:计算训练文本和测试文本之间的编辑距离,适用于检测经过轻微修改的泄露(如换了几个词但意思相同)。

Embedding 相似度方法:用 embedding 模型将训练文本和测试文本映射到向量空间,计算余弦相似度。适用于检测语义级别的泄露(如同一道题被改写为不同的措辞)。

DiffMIA(差分成员推断攻击):无需参考数据集,通过对代码做语义保持变换(如变量重命名、循环展开),然后比较模型对原始版本和变换版本的损失差异来检测污染。如果模型对原始代码的损失显著低于变换版本,说明模型可能记住了原始代码(APSEC 2025)。适用于代码基准污染检测。

BeyondBench(动态抗污染基准):从根本上避免静态基准污染——通过算法化动态生成数学问题,组合空间超过 10^15 种,使任何固定题库都无法覆盖。101 个模型的评测显示,即使最强模型在 Hard Suite 上也仅达 56%,有效区分真实能力与记忆化(ICLR 2026)。

Self-Critique(策略塌陷检测):针对 RL 后训练阶段的新型污染检测方法。核心观察是”策略塌陷”现象——当模型在污染问题上训练后,其推理路径会固化。检测方法:要求模型给出同一问题的不同解法,如果多条解法高度相似(而非预期的多样化推理路径),则表明可能存在污染。这是首个专门针对 RL 阶段污染的检测方法(PKU, 2025)。

CDD(峰度检测法):通过分析模型输出分布的峰度(kurtosis)来检测小模型的数据污染——受污染样本的输出分布更为尖锐。但需注意该方法在 LoRA 微调下检测失效,这是一个重要的实践限制(arXiv: 2603.03203)。

建立去污染管线的最佳实践

1. 收集所有需要保护的 benchmark 数据
   - 主流评测集:MMLU, HumanEval, GSM8K, ARC, HellaSwag, ...
   - 中文评测集:C-Eval, CMMLU, ...
   - 注意同时包含 train/val/test split

2. 多层检测
   - 第一层:13-gram 精确匹配(快速、高精度)
   - 第二层:编辑距离检测(捕获轻微修改)
   - 第三层(可选):embedding 相似度(捕获语义相似的变体)

3. 处理策略
   - 对于高度匹配(>80% overlap):从训练集中移除
   - 对于中度匹配(50-80% overlap):标记并在评估时特别关注
   - 对于低度匹配(<50% overlap):通常可以保留

4. 评估时的处理
   - 在模型评估时,除了标准分数外,同时报告去污染后的分数
   - 如果两者差异显著(>2%),说明存在严重的数据泄露

<!-- edit: 2026-05-10 新增语义级去重专节,Soft Contamination 揭示 n-gram 方法盲区 -->

## 4.7 语义级去重与 Soft Contamination

2026 年 2 月发表的 arXiv: 2602.12413 揭示了一个颠覆传统认知的现象:**Soft Contamination(软污染)**——训练数据泄漏了 benchmark 测试题的语义近重复(sentences with equivalent/near-equivalent content but not close in string space)。传统 n-gram 去污染无法检测这类泄漏,因为字符串层面差异很大,模型通过浅层模式匹配而非真正的分布外推理来"作弊"。

### 什么是语义近重复

用不同措辞表达相同或高度相似内容的文档,在 n-gram 层面差异可能只有 30-40%,但语义完全等价。典型案例:

- "如何重置密码?" vs "忘记密码怎么办?"
- "解决内存泄漏的方法" vs "修复 out-of-memory 错误"
- 同一道数学题的不同表述方式

这类内容会让模型通过浅层模式匹配记住解题套路,而非真正学会推理。

### 为什么现在必须处理

合成数据比例的急剧增加带来了双重压力:

1. **合成数据更容易产生语义近重复**——同一个知识点用不同 prompt 生成的内容,在措辞上千差万别但语义相同
2. **语义去重正在从"补充"走向"必须"**——Prem 公司 2026 年的实践指南已明确将"语义去重(SemDeDup)"列为标准步骤

### 技术方案

**MinHashLSH vs SemDeDup 工程权衡(2026 工业实践)**:

| 维度 | MinHashLSH | SemDeDup |
|:---|:---|:---|
| 计算复杂度 | O(N) | O(N) embedding + O(N²) 聚类 |
| 内存 | 整型签名(低) | float32 向量(高) |
| 句法精度 | 极高 | 高 |
| 语义召回 | 极低 | 高 |
| 硬件 | CPU 集群 | GPU(embeddings + Faiss) |
| 可扩展性 | PB 级 | TB 级(受限于显存) |

**推荐组合**:先 MinHashLSH 过滤 95%+ 明显非重复数据,再对剩余做 SemDeDup。这能避免 SemDeDup 的 GPU 成本爆炸——直接对全量做 SemDeDup 在亿级以上语料上不现实。

<!-- edit: 2026-07-02 补充 MinHashLSH vs SemDeDup 工程权衡表(计算/内存/句法/语义/硬件/扩展性) -->

**多粒度融合:SemHash-LLM(2026-07)** <!-- edit: 2026-07-10 新增 SemHash-LLM 作为 MinHashLSH vs SemDeDup 的融合方案,采纳书籍提案建议 1(中置信度,论文极新未经独立复现) --> 尝试打破"MinHashLSH vs SemDeDup"的二选一困境。其核心思路是**门控融合**——同时从字符级(n-gram MinHash)、token 级(注意力加权)、文档级(蒸馏 LLM embedding 哈希)三个粒度提取信号,通过可学习的门控机制融合。关键创新包括:(1) 用蒸馏 LLM embedding 空间学习紧凑二值码做语义投影哈希;(2) 注意力加权 MinHash 自动抑制模板/导航等 boilerplate 文本;(3) 自适应决策边界配合不确定性估计,提升对模板污染、短文本扰动、包含重复、病毒片段四种困难场景的鲁棒性。关键效率:<1% 的神经验证成本即可达到强去重质量——既不抛弃 MinHash 的效率优势,又引入了语义信号,是"先 MinHash 后 SemDeDup"两阶段的潜在替代方案。注意:该论文(arXiv:2607.01601, Fang et al., 2026-07-02)发表仅 8 天,尚未经过独立复现验证,建议标注为"待验证前沿"。

**Sentence-BERT / Embedding + 聚类(SemDeDup)**:

```python
from sentence_transformers import SentenceTransformer
import numpy as np

def semantic_dedup(documents: list[str], threshold: float = 0.88) -> list[str]:
    """基于 embedding 的语义去重"""
    # 1. 编码所有文档
    model = SentenceTransformer('shibing624/text2vec-base-chinese')
    embeddings = model.encode(documents, show_progress_bar=True)

    # 2. 归一化并计算相似度矩阵
    norms = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)
    similarity = norms @ norms.T

    # 3. 找出高相似度对
    n = len(documents)
    seen = set()
    unique_docs = []

    for i in range(n):
        if i in seen:
            continue
        unique_docs.append(documents[i])
        # 标记相似文档
        for j in range(i + 1, n):
            if similarity[i, j] > threshold:
                seen.add(j)

    return unique_docs

阈值建议:0.85-0.90(太低会误杀语义相近但内容不同的文档,太高则检测不到真正的语义重复)

计算成本:约为 MinHash 的 10 倍,但召回率显著更高。实践建议:

  • 在 SFT/RL 数据上优先部署(数据量相对较小,语义质量更关键)
  • 对预训练语料,先用 MinHash 筛掉 95%+ 的明显非重复数据,再做语义去重

后训练阶段的污染动态

arXiv: 2601.06103 系统研究了后训练(SFT/RL)对 benchmark 污染的影响——污染发生在预训练阶段,但后训练可能放大或掩盖这种污染效应。向预训练数据注入 5 份 GSM8K 和 MBPP 测试题后,SFT 阶段可能进一步放大污染效应,使 benchmark 分数虚高。

这意味着:benchmark 污染检测需要贯穿整个训练流程,而非只在预训练阶段做一次。


4.8 跨源去重的质量信号价值

传统去重流程将跨源重复视为纯粹的”噪声”——需要在合并多个数据源时消除的冗余。但 MixMinMatch(Abdelaziz et al., 2025, arXiv: 2512.18834)提出了一个颠覆性视角:跨源重复本身是免费、无模型的质量信号。

核心发现

当多个独立的 Web 语料处理 pipeline(如 FineWeb、C4、RedPajama)各自独立地从原始 Common Crawl 中筛选和去重后,如果同一文档被多个 pipeline 同时保留,说明它通过了多个独立的质量标准——这等价于一个”共识投票”机制。

关键数据:

  • 多源 Web 语料间约 40%+ 的 token 存在跨源重复
  • 被多源独立恢复的文档质量显著更高
  • 阿拉伯语 4.5% 提升(vs ArabicWeb24),土耳其语 5.5% 提升(vs FineWeb-2)
  • 同时提供 4x 更多独特 token——因为跨源共识帮助识别出各源独有的高质量内容

方法流程

跨源共识质量信号流程:

1. 收集多个独立处理过的Web语料库
   └─ 每个语料库已有自己的质量过滤和去重pipeline

2. 跨数据集MinHash去重
   └─ 识别被多个语料库同时保留的文档

3. 共识信号提取
   └─ 被N个独立源保留的文档 → 共识强度=N
   └─ 共识强度越高,质量信号越强

4. 数据增强
   └─ 利用共识信号加权或筛选,而非简单去重

对去重流程的启示

这一发现要求我们重新审视去重的目标:

  1. 去重副产品有价值:跨源去重识别出的重复文档不应被简单丢弃,其重复模式本身就是质量评估的依据
  2. 合并策略升级:多源数据合并时,从”去重保留一份”升级为”利用共识信号加权”——被更多源确认的文档应获得更高权重
  3. 与质量过滤的协同:跨源共识信号可以作为质量分类器的补充或替代,在无需额外模型的情况下提供质量评估

这一工作标志着去重从”纯清洗步骤”向”质量发现工具”的认知跃迁。

多源独立处理管线的工程价值:阿拉伯语 4.5% 相对提升(vs ArabicWeb24),土耳其语 5.5% 相对提升(vs FineWeb-2),同时提供 4× 更多独特 token——跨源共识帮助识别出各源独有的高质量内容。实操建议:当你合并多个数据集(自建 + FineWeb + CC-Main)时,不要简单去重——用 MixMinMatch 的共识信号作为质量权重。被多源独立恢复的文档显著更高质量,这一”免费信号”无需额外模型训练。


4.9 动手环节:构建一个简单的去重管线

"""
一个简化的去重管线示例
目标:对一组中文文档进行精确去重 + 模糊去重
"""

import hashlib
from datasketch import MinHash, MinHashLSH

# === 第一步:精确去重 ===

def exact_dedup(documents: list[str]) -> list[str]:
    """基于 SHA-256 的精确去重"""
    seen_hashes = set()
    unique_docs = []
    
    for doc in documents:
        normalized = ' '.join(doc.split())
        h = hashlib.sha256(normalized.encode('utf-8')).hexdigest()
        
        if h not in seen_hashes:
            seen_hashes.add(h)
            unique_docs.append(doc)
    
    print(f"精确去重: {len(documents)} → {len(unique_docs)} "
          f"(去除 {len(documents) - len(unique_docs)} 篇)")
    return unique_docs


# === 第二步:模糊去重 ===

def create_minhash(text: str, n: int = 3, num_perm: int = 128) -> MinHash:
    """为文本创建 MinHash 签名"""
    m = MinHash(num_perm=num_perm)
    # 中文使用字符级 n-gram
    for i in range(len(text) - n + 1):
        ngram = text[i:i+n]
        m.update(ngram.encode('utf-8'))
    return m


def fuzzy_dedup(documents: list[str], threshold: float = 0.75) -> list[str]:
    """基于 MinHash + LSH 的模糊去重"""
    # 创建 LSH 索引
    lsh = MinHashLSH(threshold=threshold, num_perm=128)
    
    unique_docs = []
    
    for i, doc in enumerate(documents):
        m = create_minhash(doc)
        
        # 查询是否已有相似文档
        result = lsh.query(m)
        
        if not result:
            # 没有相似文档,保留并加入索引
            try:
                lsh.insert(f"doc_{i}", m)
                unique_docs.append(doc)
            except ValueError:
                pass  # 极少数情况下可能有 hash 冲突
        # else: 有相似文档,跳过(去重)
    
    print(f"模糊去重: {len(documents)} → {len(unique_docs)} "
          f"(去除 {len(documents) - len(unique_docs)} 篇)")
    return unique_docs


# === 组合使用 ===

def dedup_pipeline(documents: list[str]) -> list[str]:
    """完整的去重管线"""
    print(f"输入文档数: {len(documents)}")
    
    # Step 1: 精确去重
    docs = exact_dedup(documents)
    
    # Step 2: 模糊去重
    docs = fuzzy_dedup(docs, threshold=0.75)
    
    print(f"最终保留: {len(docs)} ({len(docs)/len(documents)*100:.1f}%)")
    return docs


# === 测试 ===
if __name__ == "__main__":
    test_docs = [
        "大语言模型的训练数据质量非常重要,它直接决定了模型的最终表现。",
        "大语言模型的训练数据质量非常重要,它直接决定了模型的最终表现。",  # 完全重复
        "大语言模型的训练数据质量特别重要,它直接影响了模型的最终效果。",  # 近似重复
        "今天的天气非常好,适合出去散步。",  # 不同内容
        "预训练阶段的数据配比是一个关键决策,不同的配比会导致模型表现差异巨大。",
    ]
    
    result = dedup_pipeline(test_docs)
    for i, doc in enumerate(result):
        print(f"保留 [{i}]: {doc[:50]}...")

本章要点回顾

  1. 重复数据的三重伤害:记忆化(影响泛化)、虚假的训练 loss(掩盖真实学习进度)、隐私泄露风险
  2. 互联网数据的重复程度超乎想象——未经去重的 Common Crawl 中,20-30% 是完全重复,另有 10-20% 是近似重复
  3. 精确去重(hash-based)是必须的,配合 Bloom Filter 可以在有限内存中处理百亿级文档
  4. 模糊去重(MinHash + LSH)是关键步骤,中文推荐 n=3-4、阈值 0.75-0.80
  5. 认知演变:从”不需要”到”越多越好”到”适度”——过度去重会伤害数据多样性
  6. 大规模去重是一个严肃的工程问题——需要分布式计算、内存管理、流式处理
  7. Benchmark 污染检测是去重的延伸——确保训练数据不包含评测题目,防止分数虚高
  8. 语义级去重是 2026 年的新前沿——Soft Contamination 揭示了 n-gram 方法的盲区,embedding 检测成为必须(阈值建议 0.85-0.90)
  9. 跨源去重的质量信号价值——去重副产品(跨源共识重复)本身是免费的质量信号,标志去重从”纯清洗”向”质量发现”的认知跃迁
  10. 污染检测方法持续演进——DiffMIA(代码污染)、BeyondBench(动态抗污染)、Self-Critique(RL阶段污染)、CDD(小模型污染)等新方法扩展了检测场景覆盖