跳转至

去重:exact / MinHash / SemHash / 质量分桶

更新日期:2026-04-26

去重是数据 pipeline 决定模型质量第二大杠杆(仅次于规模)。Common Crawl 的原始 unique 文本在 2-5T tokens 量级;如果不去重,"训了 10T tokens" 实际可能只见过 1.5T 独立内容。本页拆三层:精确去重 / 近似去重(MinHash + SemHash)/ 质量分桶。


一、为什么必须去重

flowchart LR
    raw["原始爬取<br/>(每条都进训练)"] -- 不去重 --> ovef["over-fit<br/>+ memorization"]
    raw -- exact 去重 --> half["半-unique<br/>(URL 重复消除)"]
    half -- MinHash 近似去重 --> qual["quality-tier<br/>(boilerplate 消除)"]
    qual -- SemHash --> sem["真正语义去重<br/>(同义改写消除)"]

    classDef pos fill:#fff,stroke:#cc785c;
    class raw,half,qual,sem pos
    classDef neg fill:#f5f3eb,stroke:#bdb9ab;
    class ovef neg

实证

  • LLaMA-1 论文:去重后 token 量从 2.4T 降到 1.4T,但 perplexity 改善
  • DCLM-Baseline:MinHash + Bloom 把 240T 池子压到 4T,模型质量比直接训 240T 更好
  • The Pile / RedPajama 都把"去重失败"列为头号 lesson learned

去重不是可选项 —— 不去重训练就是把同一个 fact 反复塞进模型,长 context 容易 verbatim 复述(版权风险 + memorization 攻击面)。


二、三层去重

2.1 Exact Dedup(URL / Hash 级)

最简单也最先做

# URL 级去重
seen_urls = set()
def keep(doc):
    canonical = canonicalize_url(doc.url)  # 去 query string trailing slash 等
    if canonical in seen_urls:
        return False
    seen_urls.add(canonical)
    return True

# 内容 hash 级(处理同 content 不同 URL 的镜像站)
def content_hash(text):
    # 标准化前处理
    normalized = re.sub(r'\s+', ' ', text.strip().lower())
    return hashlib.sha256(normalized.encode()).hexdigest()

效果:CC 单 snapshot 内 URL 去重消 ~10-15%;跨 snapshot 累计去重消 ~70-80%(因为同 URL 月月被抓)。

工具:直接 Python set / Redis SET / Bloom filter(节省内存:1B 文档用 12-24GB Bloom filter,false positive ~1% 完全够用)。

2.2 Near-Dup(MinHash + LSH)

问题:站点 A 转载站点 B 的内容,URL 不同、内容 99% 一样、boilerplate 不同 → exact dedup 检测不到。

思路:把每个文档转成固定大小的"指纹",指纹相似度 ≈ 文档相似度。

MinHash 数学

文档 → n-gram 集合(shingles)→ 用 K 个独立 hash 函数取最小值 → K 维签名向量。

两个文档的 MinHash 签名重合率 ≈ 它们 n-gram 集合的 Jaccard 相似度

理论保证:E[相同位数 / K] = |A ∩ B| / |A ∪ B|

# 简化实现
def minhash_signature(text, num_hashes=128):
    shingles = set(get_ngrams(text.split(), 5))  # 5-gram 主流
    signature = []
    for i in range(num_hashes):
        min_h = min(murmurhash(s, seed=i) for s in shingles)
        signature.append(min_h)
    return signature

LSH(Locality-Sensitive Hashing)

128 维签名两两比较是 O(N²),1B 文档 → 1e18 次比较,不可行。

LSH 思路:把 128 维签名分成 b 个 bands,每 band 内 r 维。任一 band 内完全相同 → 候选近似对

P[两文档进同 bucket] = 1 - (1 - s^r)^b
其中 s = Jaccard 相似度

b, r 控制阈值的"陡峭度":

相似度阈值 推荐 (b, r)
0.7(去重宽松) (b=8, r=16)
0.8(典型 LLaMA / FineWeb 设置) (b=14, r=9)
0.9(保守,仅去高度相似) (b=20, r=6)

实操

from datasketch import MinHash, MinHashLSH

# 1B 文档级别推荐 datasketch + redis 后端
lsh = MinHashLSH(threshold=0.8, num_perm=128, storage_config={
    'type': 'redis',
    'redis': {'host': 'localhost', 'port': 6379},
})

for doc in docs:
    m = MinHash(num_perm=128)
    for shingle in get_ngrams(doc.text, 5):
        m.update(shingle.encode())
    candidates = lsh.query(m)
    if not candidates:
        lsh.insert(doc.id, m)
    else:
        mark_dup(doc.id, candidates[0])

工具链

工具 规模 备注
datasketch(Python) <100M docs 经典选择,Redis backend
text-dedup(HF) <1B docs 流水线化封装
LSHBloom 1B+ docs Bloom filter 优化的 LSH,arXiv:2411.04257
Slimpajama-DC dedup 1T+ docs Cerebras 开源的工业级
Spark + datasketch UDF 任意规模 工业自建主流

5-gram vs 13-gram

  • 5-gram:宽松,捕捉"段落级相似"(boilerplate + 转载)
  • 13-gram(GPT-3 / FineWeb 用):严格,只捕捉"几乎逐字重复"

LLaMA-1 用 5-gram,DCLM 用 13-gram。13-gram 不丢类似主题的不同表述,更适合保留多样性;5-gram 去得更狠。

2.3 SemHash / Semantic Dedup

MinHash 的局限:同义改写("猫坐垫上" vs "垫上有猫")n-gram 重合度低,MinHash 视为不同 → 训练里都进。

SemHash 思路:用 embedding 模型把文档投到向量空间,向量近邻 = 语义相似。

# 用 Sentence-Transformers / E5 / BGE 等
from sentence_transformers import SentenceTransformer
import faiss

model = SentenceTransformer('BAAI/bge-large-en-v1.5')
# 文档前 512 token 嵌入(全文 embed 太贵)
embeddings = model.encode([doc.text[:2048] for doc in docs])

# FAISS 构 index 找近邻
index = faiss.IndexFlatIP(embeddings.shape[1])
index.add(embeddings)

# 阈值 0.95 cosine = 几乎肯定语义重复
for i, emb in enumerate(embeddings):
    D, I = index.search(emb.reshape(1, -1), k=5)
    for j, sim in zip(I[0], D[0]):
        if i != j and sim > 0.95:
            mark_semantic_dup(i, j)

SemHash vs MinHash

维度 MinHash SemHash
计算开销 低(hash) 高(embedding 模型 forward)
1B docs 单机 几小时 数天(GPU)
抓"同义改写" 不抓
抓"机翻 / 转译" 不抓 部分抓
标准做法 主流默认 加在 MinHash 之后做 second pass

Pipeline 实践:MinHash 作主去重(处理 99% 工作量),SemHash 在 MinHash 留下的"幸存者"上做精筛 —— 不是替代,是叠加

论文参考

  • SemDeDup(Abbas et al. 2023, arXiv:2303.09540):embedding-based dedup,证明在 LAION 上去 50% 数据但模型 zero-shot 提升
  • D4: Improving LLM Pretraining via Document De-Duplication(Tirumala 2023, arXiv:2308.12284):把 SemDeDup 推到文本,进一步证明语义去重收益
  • DataComp-LM 用 Bloom + LSH,未上 SemDeDup(速度优先)

三、去重的"边界 / 噪声"问题

3.1 阈值敏感性

设 0.8 还是 0.85 直接影响留多少。LLaMA-1 论文用 0.8,删 ~30% 内容;某 frontier lab 内部用 0.85 保留更多多样性 —— 没有"普世正确"阈值,要 ablate

3.2 跨文档结构重复

文档自身全句不重复,但 100 万个文档结构都一样(template 生成的 stub 页)—— MinHash 部分捕捉,需要再加"结构指纹"

# 把所有非锚名字的 token 替换成 mask,看模板
template = re.sub(r'[A-Z][a-z]+', '[NAME]', doc.text)
template = re.sub(r'\d+', '[NUM]', template)
template_hash = hash(template[:500])
# 同一 template_hash 出现 >100 次 → 模板生成

3.3 SoftDedup(不删,只降权)

新方向:重复文档不删,但训练采样时降低概率:

# 在 cluster 里:cluster_size 越大,每个文档采样权重越小
def softdedup_weight(cluster_size, alpha=0.5):
    return 1.0 / (cluster_size ** alpha)

好处

  • 不丢"罕见上下文"(比如同一 fact 在不同主题下出现,每次都有独特上下文)
  • 平滑数据分布,避免阈值断崖
  • 配合 RHO / cluster-aware sampling 可以做更细的训练 dynamics

参考:SoftDedup(Liang et al. 2023,arXiv:2407.06654)。FineWeb / DCLM 暂未默认采纳,但 frontier 内部正在 ablate。


四、Quality Reward Model:分桶 + 多维打分

去重之后,剩下的文档质量参差。不是简单"留 / 弃"二元,而是"打分 → 分桶 → 训练时按桶配比"。

4.1 单维度打分(旧)

历史方案:fastText 二分类(高质量 / 低质量)。LLaMA-1 / GPT-3 / OPT 都用。

问题:单一维度 = 单一审美。"教育性强"的文档不一定"逻辑清晰","信息密度高"的不一定"语言流畅"。

4.2 多维 Reward Model(FineWeb-Edu / DCLM 路线)

FineWeb-Edu:用 LLaMA-3-70B 给样本打 0-5 分"教育价值",训一个小 BERT 分类器,再对全集打分。单维度,但用 LLM 打的"细维度"

RedPajama V2 路线:30+ 维度信号,不用一个分数过滤,作为 metadata 留给下游用户。

# RedPajama V2 每个文档伴随的 quality signals (节选)
signals = {
    'doc_length': 1234,
    'word_count': 250,
    'mean_word_length': 4.5,
    'lang_id_score': 0.99,
    'perplexity_kenlm': 215.4,            # KenLM 5-gram PPL
    'wikipedia_classifier_score': 0.82,    # FastText 分类器
    'oh_classifier_score': 0.74,           # OpenHermes 风格分类器
    'nsfw_score': 0.02,                    # NSFW 检测
    'fraction_lines_ending_with_punct': 0.91,
    'fraction_chars_in_dupe_5grams': 0.12,
    'fraction_lines_dupe_5grams': 0.08,
    'fraction_chars_in_top_2gram': 0.05,
    'fraction_chars_in_top_3gram': 0.03,
    # ... 30+ 项
}

下游用户挑维度组合阈值,不同模型规模 / 目标用不同子集。

4.3 维度家族

家族 维度示例 信号源
结构 长度、段落数、平均句长、标点比例、列表 / 表格占比 规则
语言 language id、语言连贯性(PPL)、字符分布 fasttext / KenLM
重复 dupe-line ratio, top-n-gram 占比 自计算
教育性 是否像教材、解释性、有 example LLM-judge
逻辑 argument 结构、引证密度 LLM-judge
领域 code / math / news / fiction / forum 分类 多类 fasttext
安全 NSFW、toxicity、PII 专用分类器
新颖 跨语料相似度(与 Wikipedia / 教科书的距离) embedding

4.4 训练时的桶用法

# 训练数据 sampler 按桶分别加权
def get_batch():
    sources = {
        'high_edu': 0.3,       # FineWeb-Edu top tier
        'medium_edu': 0.2,
        'code': 0.15,          # The Stack
        'math': 0.1,           # OpenWebMath
        'multilingual': 0.1,
        'general_web': 0.15,   # FineWeb 剩余
    }
    return weighted_sample(sources)

桶配比的 ablation 是 frontier 训练的关键 hyperparameter,详见 mixture.md


五、典型 dedup pipeline(FineWeb 风格)

flowchart LR
    raw["WARC/extracted text"] --> exact["exact dedup<br/>(content hash)"]
    exact --> mh["MinHash<br/>(5-gram, threshold 0.8)"]
    mh --> qc["quality classifier<br/>(fasttext + Edu LLM-judge)"]
    qc --> bucket["分桶<br/>(low / mid / high)"]
    bucket --> sem["SemHash<br/>(only high tier)"]
    sem --> mix["mixture sampling"]

    classDef step fill:#fff,stroke:#cc785c;
    class raw,exact,mh,qc,bucket,sem,mix step

实操数字(仅供参考,需各家自家 ablate):

  • exact dedup 留 ~70% (消跨 snapshot 重复)
  • MinHash 0.8 留 ~50% (消近似重复)
  • quality 50%-percentile 阈值留 ~50%(中位数以上)
  • 综合后从 240T → 4T(DCLM-Baseline ratio)≈ 1.7%

六、追问延伸

问题 方向
为什么不在 token 级去重? token 序列 ID 没有词汇 / 语义信息,相似度计算无意义。所有 dedup 必须在原始文本阶段做
去重应该在 quality filter 之前还是之后? 之前。先去重再筛质量,避免对同一文档反复打分
1T 文档 MinHash 工程上能做吗? 能。Spark + LSHBloom 30+ 节点单 day 跑完 1T 级
怎么验证去重做对了? sample 1000 对"被判定相似"的文档,人工 / LLM-judge 打 precision;sample 1000 对"被判定不相似"的,看是否真不相似算 recall
RedPajama V2 不删数据,下游怎么用? 用户选维度阈值过滤。FineWeb-Edu = 在 V2 池上加 educational threshold
GPT-4 / Claude 训练用什么 dedup? 不公开。猜测都包含 MinHash + SemHash + 质量分桶;DatologyAI 类公司提供专业服务

参考链接


上级 · B1. 预训练数据 Pipeline:爬取→清洗→去重→过滤→配比