第 14 讲:Data Processing(下)
本讲延续上一讲的数据处理主题,重点讨论 deduplication(去重)技术和数据混合策略。去重是数据处理 pipeline 中最具技术挑战性的环节之一,直接影响模型的训练效率和最终性能。
核心问题
互联网上存在大量重复和近似重复的内容。如果不进行去重,模型会对这些重复内容进行过度记忆(memorization),不仅浪费训练计算资源,还会导致模型在某些内容上产生过拟合,降低生成多样性。
1. Deduplication 的重要性
1.1 为什么需要去重
去重对 LLM 训练的影响体现在多个方面:
- 减少 Memorization:重复数据会被模型记忆。Carlini et al. (2022) 的研究表明,数据重复次数与模型记忆该数据的概率呈正相关。去重可以显著降低模型逐字输出训练数据的风险。
- 提升多样性:去重后,相同的训练 budget 可以覆盖更多不同的文本内容,提升模型的知识广度。
- 提高训练效率:去除冗余数据意味着每个 training step 都在学习新知识,而不是反复强化已知内容。
- 改善下游性能:Lee et al. (2022) 的实验证明,在去重后的数据上训练的模型在多项 benchmark 上表现更好。
1.2 重复的类型
网页数据中的重复可以分为几种类型:
- 精确重复 (Exact Duplicates):完全相同的文档,通常来自同一内容的多次爬取
- 近似重复 (Near Duplicates):内容高度相似但不完全相同的文档,例如同一文章的不同转载版本、带有不同广告的同一网页
- 段落级重复:不同文档中包含相同的段落或片段,例如法律声明、隐私政策、新闻通稿的引用
- 模板化重复:结构相同、仅部分内容不同的文档,如电商产品页面
2. 精确去重 (Exact Deduplication)
精确去重是最基本的去重方式,目标是找出完全相同的文档或文档片段。
2.1 基于 Hash 的去重
最直接的方法是计算每个文档的 hash 值,相同 hash 值的文档被认为是重复的:
import hashlib
def exact_dedup(documents):
seen_hashes = set()
unique_docs = []
for doc in documents:
# Normalize whitespace before hashing
normalized = " ".join(doc.split())
doc_hash = hashlib.sha256(normalized.encode()).hexdigest()
if doc_hash not in seen_hashes:
seen_hashes.add(doc_hash)
unique_docs.append(doc)
return unique_docs
2.2 文档级 vs 行级去重
精确去重可以在不同粒度上进行:
- 文档级 (Document-level):对整个文档计算 hash。简单高效,但无法处理部分重叠的文档。
- 行级 / 段落级:对每一行或每一段计算 hash。可以去除跨文档的重复片段(如 boilerplate),但计算开销更大。
- N-gram 级:对固定长度的 n-gram 序列计算 hash。粒度最细,但 hash 集合可能非常大。
Suffix Array 方法
Lee et al. (2022) 提出使用 suffix array 进行高效的子字符串去重。Suffix array 可以在 O(n) 时间内找到所有重复的子字符串。这种方法可以识别任意长度的重复片段,但需要将整个语料库加载到内存中(或使用外存 suffix array),对内存要求很高。
3. 近似去重 (Fuzzy Deduplication)
近似去重是更复杂但也更实用的技术。现实中大量的重复内容并非完全相同,精确去重无法捕捉这些 near-duplicates。
3.1 Jaccard Similarity
近似去重的基础概念是集合相似度。给定两个文档,将其转化为 n-gram 集合后,使用 Jaccard similarity 来衡量相似度:
其中 A 和 B 是两个文档的 n-gram 集合。Jaccard similarity 的值在 [0, 1] 之间,值越大表示两个文档越相似。
然而,直接计算所有文档对的 Jaccard similarity 的时间复杂度是 O(n²),对于数十亿文档的语料库来说完全不可行。这就需要 MinHash 和 LSH 技术。
4. MinHash 算法详解
MinHash 是一种 locality-sensitive hashing 技术,可以高效地估计两个集合的 Jaccard similarity。
4.1 MinHash 的核心思想
MinHash 的关键洞察是:对于一个随机 hash 函数 h,两个集合 A 和 B 的 MinHash 值相同的概率恰好等于它们的 Jaccard similarity:
这意味着我们可以通过比较 MinHash 值来估计 Jaccard similarity,而不需要直接比较两个集合的所有元素。
4.2 MinHash Signature
为了获得更准确的估计,我们使用多个独立的 hash 函数(通常 64-256 个),为每个文档生成一个 MinHash signature(签名向量):
import numpy as np
from datasketch import MinHash
def create_minhash(text, num_perm=128):
"""Create a MinHash signature for a document."""
m = MinHash(num_perm=num_perm)
# Convert text to shingles (n-grams)
shingles = set()
words = text.split()
for i in range(len(words) - 4): # 5-gram shingles
shingle = " ".join(words[i:i+5])
shingles.add(shingle)
# Add shingles to MinHash
for shingle in shingles:
m.update(shingle.encode('utf-8'))
return m
# Estimate Jaccard similarity
m1 = create_minhash(doc1)
m2 = create_minhash(doc2)
estimated_jaccard = m1.jaccard(m2)
两个 MinHash signature 中相同位置上值相等的比例,就是 Jaccard similarity 的无偏估计。使用 k 个 hash 函数时,估计误差大约为 O(1/√k)。
4.3 Shingle 的选择
将文档转化为集合时,"shingle"(也叫 n-gram)的选择很重要:
- Word-level shingles:以 word 为单位的 n-gram,例如 5-word shingles。对自然语言文本效果好。
- Character-level shingles:以字符为单位的 n-gram,例如 13-character shingles。对短文本或非英文文本更稳健。
- Shingle 太短会导致大量 false positive(不相关的文档也可能有较高的 similarity)
- Shingle 太长会导致对微小差异过于敏感
5. Locality-Sensitive Hashing (LSH)
即使有了 MinHash,对所有文档对进行比较仍然是 O(n²) 的。LSH 技术通过只比较"可能相似"的文档对,大幅降低计算量。
5.1 LSH 的原理
LSH 的核心思想是将 MinHash signature 分成多个 "band"(段)。如果两个文档在任意一个 band 中的值完全相同,它们就被视为候选重复对,进入进一步比较。
具体来说,假设 MinHash signature 有 k 个值,将其分为 b 个 band,每个 band 包含 r 个值(k = b × r):
def lsh_buckets(signature, num_bands, rows_per_band):
"""Split MinHash signature into bands and hash each band."""
bands = []
for i in range(num_bands):
start = i * rows_per_band
end = start + rows_per_band
band = tuple(signature[start:end])
band_hash = hash(band)
bands.append(band_hash)
return bands
# Two documents are candidate duplicates if they share
# the same hash in ANY band
def find_candidates(all_signatures, num_bands, rows_per_band):
buckets = {} # band_index -> hash -> list of doc_ids
candidates = set()
for doc_id, sig in enumerate(all_signatures):
for band_idx in range(num_bands):
start = band_idx * rows_per_band
end = start + rows_per_band
band_hash = hash(tuple(sig[start:end]))
key = (band_idx, band_hash)
if key in buckets:
for other_id in buckets[key]:
candidates.add((other_id, doc_id))
buckets.setdefault(key, []).append(doc_id)
return candidates
5.2 参数调优:b 和 r 的选择
b(band 数量)和 r(每个 band 的行数)的选择决定了 LSH 的行为特征:
两个 Jaccard similarity 为 s 的文档被识别为候选对的概率为:
这个函数呈现 S 形曲线特征:
- 大 b、小 r:更敏感,能捕捉较低 similarity 的 near-duplicates,但 false positive 更多
- 小 b、大 r:更严格,只捕捉高 similarity 的 near-duplicates,但可能遗漏一些
- 典型配置:b=20, r=5(对应 threshold 约 0.5)或 b=10, r=10(对应 threshold 约 0.8)
工程注意事项
在处理 Common Crawl 级别的数据时,LSH 的 bucket 数据结构需要分布式存储。常用方案包括将 bucket 存储在分布式文件系统(如 HDFS)或分布式数据库中,或者使用 MapReduce / Spark 框架进行分布式 LSH 计算。
6. Train-Test Overlap 检测与移除
除了训练数据内部的去重,还需要确保训练数据与评估数据(test set)之间没有重叠。如果 benchmark 的测试样本出现在训练数据中,模型的评估结果就会虚高,无法反映真实能力。
6.1 Contamination 问题
GPT-3 论文首次系统性地研究了 benchmark contamination 问题。他们发现训练数据中确实包含一些 benchmark 数据的片段,这对部分评估结果产生了影响。
常见的 contamination 检测方法:
- N-gram overlap:检查训练文档中是否包含与测试样本高度重叠的 n-gram 序列
- Exact match:直接搜索测试样本的完整文本或关键片段
- Fuzzy matching:使用 MinHash/LSH 检测近似匹配
6.2 Contamination 移除策略
def check_contamination(train_doc, test_examples, n=13):
"""Check if a training document overlaps with test examples
using n-gram matching."""
train_ngrams = set(get_ngrams(train_doc, n))
for test_ex in test_examples:
test_ngrams = set(get_ngrams(test_ex, n))
overlap = len(train_ngrams & test_ngrams)
if overlap / len(test_ngrams) > 0.8: # 80% overlap threshold
return True # Document is contaminated
return False
在实践中,通常对所有主要 benchmark(如 MMLU, HellaSwag, ARC, WinoGrande, GSM8K 等)的测试集进行 contamination 检测,并移除有重叠的训练文档。
7. 数据混合策略
Pre-training 数据通常来自多个来源(如第 13 讲所述)。如何确定不同来源数据的混合比例,对模型的最终性能有显著影响。
7.1 基本原则
数据混合需要平衡多个目标:
- 多样性:确保模型学习到广泛的知识和能力
- 质量:高质量来源应该有更高的采样权重
- 比例:某些来源虽然数量少但价值高(如 Wikipedia),可以 upsample(重复采样)
- 领域平衡:避免某个领域的数据过度主导训练
7.2 典型的数据混合比例
| 数据来源 | LLaMA 比例 | GPT-3 比例 | 说明 |
|---|---|---|---|
| Common Crawl(清洗后) | 67% | 60% | 最大数据源,提供广泛覆盖 |
| Books | 4.5% | 16% | 长文本质量高 |
| Wikipedia | 4.5% | 3% | 高质量百科知识 |
| GitHub / Code | 4.5% | - | 代码能力 |
| ArXiv | 2.5% | - | 科学知识 |
| StackExchange | 2% | - | 技术问答 |
| C4 | 15% | - | 清洗后的 Common Crawl 子集 |
| WebText | - | 22% | Reddit 高赞链接 |
8. DoReMi: 基于模型的数据 Reweighting
手动设定数据混合比例需要大量的实验和人工调优。DoReMi (Xie et al., 2023) 提出了一种基于模型的方法来自动学习最优的数据混合比例。
8.1 核心思想
DoReMi 使用 distributionally robust optimization (DRO) 的思路:训练一个小的 proxy model 来学习数据权重,使得模型在所有 domain 上的 worst-case loss 最小化。
直觉上,如果模型在某个 domain 上的 loss 较高,DoReMi 会增加该 domain 的采样权重,让模型花更多时间学习这个"薄弱"领域。
8.2 算法流程
- 训练 Reference Model:用均匀混合比例训练一个小的 reference model
- 训练 DRO Proxy Model:用 reference model 作为参考,通过 group DRO 优化数据权重。每一步更新中:
- 计算 proxy model 在每个 domain 上的 excess loss(相对于 reference model 的额外 loss)
- 增加 excess loss 较高的 domain 的权重
- 使用更新后的权重采样数据,更新 proxy model
- 提取最终权重:从训练过程中收集的 domain 权重中取平均
- 训练大模型:使用学到的权重比例训练最终的大型模型
DoReMi 的效果
实验表明,DoReMi 学到的数据权重与人工调优的权重有显著差异。例如,DoReMi 通常会给 Wikipedia 更高的权重,并大幅降低低质量网页数据的比重。使用 DoReMi 权重训练的 280M 模型在 The Pile benchmark 上的 perplexity 优于使用默认权重训练的模型。
9. 面向特定能力的 Continued Pre-training
除了通用的 pre-training,有时需要通过 continued pre-training(在已有 checkpoint 上继续训练)来增强模型的特定能力。
9.1 长上下文 (Long Context)
标准 pre-training 通常使用较短的 context length(如 2048 或 4096 token)。要支持更长的上下文窗口(如 32K, 128K 甚至更长),需要:
- 收集或合成长文本数据(长文档、书籍、长对话)
- 逐步增加 context length 进行 continued pre-training
- 调整 RoPE (Rotary Position Embedding) 的 base frequency 以支持更长序列
- Yarn、NTK-aware scaling 等 position interpolation 方法
9.2 代码能力增强
要显著提升模型的代码能力,可以在代码数据上进行 continued pre-training:
- 收集高质量代码数据:GitHub 仓库、编程竞赛、高质量教程
- 需要注意代码与自然语言的比例平衡,避免灾难性遗忘
- Code Llama 就是在 LLaMA 2 基础上用代码数据 continued pre-training 的成功案例
9.3 数学能力增强
数学推理能力的提升同样依赖专门的数据:
- 数学教科书、数学竞赛题目、解题过程
- 合成数学数据:使用 GPT-4 等强模型生成数学题目和解答
- Minerva (Google) 使用 ArXiv 和数学网站数据增强数学推理
- Llemma (Azerbayev et al.) 专门针对数学领域进行 continued pre-training
10. 数据处理 Pipeline 的工程实践
在工程层面,构建一个高效的数据处理 pipeline 面临诸多挑战。
10.1 典型 Pipeline 架构
一个完整的 pre-training 数据处理 pipeline 通常包含以下阶段:
# Typical data processing pipeline stages
pipeline = [
"1. Download raw data (WARC files from Common Crawl)",
"2. HTML to text extraction (trafilatura / resiliparse)",
"3. Language detection (fasttext lid)",
"4. Rule-based quality filtering",
"5. Perplexity filtering (KenLM)",
"6. Harmful content filtering",
"7. PII removal",
"8. Exact deduplication (hash-based)",
"9. Fuzzy deduplication (MinHash + LSH)",
"10. Train-test contamination removal",
"11. Data mixing and sampling",
"12. Tokenization and packing",
"13. Shuffle and save to binary format",
]
10.2 规模挑战
处理 Common Crawl 级别的数据面临巨大的工程挑战:
- 数据量:单个 crawl 的 WARC 文件总量约 80-100TB,压缩后约 40-50TB
- 计算资源:处理一个完整 crawl 可能需要数千 CPU 核 × 数天的时间
- 分布式处理:需要使用 Spark、Ray 或 Dask 等分布式计算框架
- 中间存储:每个阶段的中间结果都需要大量存储空间
- 容错机制:长时间运行的 pipeline 必须能从失败中恢复
10.3 Tokenization 和数据打包
处理后的文本数据需要 tokenize 并打包成适合训练的格式:
import numpy as np
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b")
def pack_documents(documents, max_length=2048):
"""Pack multiple documents into fixed-length sequences
with end-of-document tokens."""
all_tokens = []
for doc in documents:
tokens = tokenizer.encode(doc)
all_tokens.extend(tokens)
all_tokens.append(tokenizer.eos_token_id) # Document separator
# Split into fixed-length chunks
packed = []
for i in range(0, len(all_tokens) - max_length, max_length):
packed.append(all_tokens[i:i + max_length])
return np.array(packed, dtype=np.uint16) # Save memory with uint16
打包后的数据通常以 memory-mapped binary 格式存储(如 NumPy .npy 或自定义的 binary format),以支持高效的随机访问和 data loading。
10.4 开源数据处理工具
目前有多个开源的数据处理框架可供使用:
- DCLM (DataComp for Language Models):提供标准化的 benchmark 和工具来评估数据处理策略
- dolma:Allen AI 开发的数据处理 pipeline,用于构建 OLMo 的训练数据
- RedPajama:Together.ai 开源的数据处理工具,复现 LLaMA 的数据处理流程
- CCNet:Meta 开源的 Common Crawl 处理 pipeline
本讲小结
本讲覆盖了数据处理的下半部分,核心内容包括:
- Deduplication 的重要性:减少 memorization,提升多样性和训练效率
- 精确去重:基于 hash 的方法,简单但无法处理 near-duplicates
- MinHash 算法:通过随机 hash 函数高效估计 Jaccard similarity
- LSH:通过 banding 技术将候选对发现从 O(n²) 降低到近线性
- Train-Test 去污染:确保评估结果的可靠性
- 数据混合策略:人工设定比例 vs DoReMi 自动优化
- Continued Pre-training:针对长上下文、代码、数学等特定能力的数据增强
- Pipeline 工程:分布式处理、容错、tokenization 打包等实践问题
至此,数据处理模块结束。下一讲将进入 Alignment 模块,讨论 SFT 和 RLHF。