数据清洗工程实践:从 C4 到 FineWeb 的质量革命
系统梳理 LLM 预训练数据清洗的全流程,覆盖去重、质量过滤、毒性检测、PII 脱敏等关键环节,对比主流数据集的清洗策略。
“Garbage in, garbage out” 在大模型时代被赋予了新的含义——预训练数据的质量直接决定了模型的能力上限。本文将系统梳理数据清洗的全流程和工程实践。
一、数据清洗的重要性
1.1 质量差距的量化
相同规模的训练数据,不同质量清洗后训练出的模型性能差异惊人:
| 数据集 | 清洗策略 | 训练后 HellaSwag | 训练后 ARC |
|---|---|---|---|
| Raw Common Crawl | 无清洗 | 45.2 | 32.1 |
| C4 | 基础清洗 | 58.7 | 41.3 |
| RefinedWeb | 中等清洗 | 65.4 | 48.7 |
| FineWeb | 深度清洗 | 68.1 | 52.3 |
| FineWeb-Edu | 教育质量筛选 | 72.6 | 56.8 |
从 Raw Common Crawl 到 FineWeb-Edu,HellaSwag 提升了 27.4 个百分点——仅通过数据清洗,不改变模型架构和训练方法。
1.2 数据清洗的 ROI
| 优化手段 | 相对成本 | 性能提升幅度 |
|---|---|---|
| 增大模型参数 2× | 高(训练+推理成本翻倍) | ~3-5% |
| 增大训练数据 2× | 中(训练成本翻倍) | ~2-4% |
| 提升数据质量 | 低(一次性清洗成本) | ~5-15% |
数据质量是性价比最高的优化手段。
二、数据清洗全流程
2.1 Pipeline 概览
一个完整的数据清洗流水线通常包含以下阶段:
原始数据 (Common Crawl)
↓
[1] URL 过滤 ─── 去除已知低质量域名
↓
[2] 文本提取 ─── HTML → 纯文本
↓
[3] 语言检测 ─── 识别文本语言
↓
[4] 基础过滤 ─── 长度、字符比例等规则
↓
[5] 去重 ─── 精确去重 + 模糊去重
↓
[6] 质量评分 ─── 模型打分或启发式评分
↓
[7] 毒性过滤 ─── 去除有害/违规内容
↓
[8] PII 脱敏 ─── 去除个人敏感信息
↓
[9] Benchmark 去污染 ─── 避免评测数据泄露
↓
清洁数据
2.2 各阶段详解
让我们逐一深入每个阶段。
三、URL 过滤
3.1 黑名单过滤
维护一个低质量 URL 黑名单:
URL 级别:
- 已知的垃圾站点、SEO 农场
- 成人内容站点
- 恶意软件分发站点
域名级别:
- 低质量内容农场的顶级域名
- 自动生成内容的站点
- 大量重复内容的镜像站
3.2 URL 特征过滤
基于 URL 模式的启发式规则:
def url_quality_check(url):
# 路径深度过深通常是动态生成页面
if url.count('/') > 8:
return False
# 过多查询参数
if url.count('&') > 5:
return False
# 常见低质量后缀
low_quality_patterns = [
'/tag/', '/category/', '/page/',
'?replytocom=', '?share=',
]
return not any(p in url for p in low_quality_patterns)
四、文本提取
4.1 HTML → 纯文本
从 HTML 中提取有意义的文本内容:
主流工具:
- Trafilatura:质量最高,FineWeb 使用
- Readability:Mozilla 的算法
- jusText:基于段落分类
- boilerpy3:去除样板文本
Trafilatura vs 其他工具的对比:
| 工具 | 正文提取精度 | 速度 | 样板去除 |
|---|---|---|---|
| Trafilatura | 高 | 中 | 好 |
| Readability | 中高 | 快 | 中 |
| jusText | 中 | 快 | 好 |
| Beautiful Soup | 低(需自定义) | 快 | 无 |
FineWeb 的关键发现:Trafilatura 的文本提取质量比 C4 使用的方法高出很多,这一步对最终模型性能有显著影响。
4.2 特殊内容处理
- 代码块:保留格式和缩进
- 表格:转换为文本表示或 Markdown
- 列表:保持层级结构
- 数学公式:保留 LaTeX 或 Unicode 表示
- 导航/页脚/广告:移除
五、语言检测
5.1 工具和方法
| 工具 | 准确率 | 速度 | 支持语言数 |
|---|---|---|---|
| fastText langid | 98%+ | 极快 | 176 |
| CLD3 | 96%+ | 快 | 107 |
| langdetect | 95%+ | 中 | 55 |
推荐:fastText 的语言检测模型,速度快且准确率高。
5.2 中文特殊处理
中文语言检测的挑战:
- 简体 vs 繁体区分
- 中日韩(CJK)字符共用导致误判
- 夹杂英文的中文文本可能被判为英文
建议:使用 CLD3 + 字符集统计双重验证。
六、基础过滤规则
6.1 文本级别规则
这些是基于简单统计特征的过滤规则:
def basic_text_filter(text):
# 最小长度
if len(text) < 200:
return False
# 最大长度(防止异常数据)
if len(text) > 500_000:
return False
# 平均单词长度异常
words = text.split()
avg_word_len = sum(len(w) for w in words) / max(len(words), 1)
if avg_word_len > 20 or avg_word_len < 2:
return False
# 字母占比过低(可能是乱码)
alpha_ratio = sum(c.isalpha() for c in text) / max(len(text), 1)
if alpha_ratio < 0.4:
return False
# 特殊字符占比过高
special_ratio = sum(c in '!@#$%^&*' for c in text) / max(len(text), 1)
if special_ratio > 0.1:
return False
return True
6.2 行级别规则
def line_level_filter(text):
lines = text.split('\n')
# 过滤以特定模式开头的行
bullet_count = sum(1 for l in lines if l.strip().startswith(('•', '·', '-', '*')))
if bullet_count / max(len(lines), 1) > 0.8:
return False # 可能是列表页
# 短行占比过高
short_lines = sum(1 for l in lines if len(l.strip()) < 20)
if short_lines / max(len(lines), 1) > 0.7:
return False # 可能是导航或目录
# 重复行过多
unique_lines = len(set(l.strip() for l in lines if l.strip()))
if unique_lines / max(len(lines), 1) < 0.5:
return False
return True
6.3 C4 的经典规则
C4(Colossal Clean Crawled Corpus)定义了一组被广泛采用的基础规则:
- 句子必须以标点符号结尾
- 文本至少包含 3 个句子(后来的研究发现这个规则过于严格)
- 删除包含 “bad words” 的页面
- 删除包含 “lorem ipsum” 的页面
- 删除 JavaScript 代码片段
- 每行至少 5 个单词
注意:C4 的规则被后来的研究证明过于激进——它们移除了大量高质量内容(如代码、列表、短文本)。
七、去重
去重是数据清洗中最关键的步骤之一。
7.1 为什么去重如此重要
重复数据的危害:
- 浪费训练计算:重复数据不提供新信息
- 放大偏见:高频重复的内容会在训练中被过度强调
- 记忆而非学习:模型可能逐字记住重复内容,而非学习泛化
- 隐私风险:重复出现的个人信息更容易被模型记住
7.2 精确去重
URL 去重:
- 最简单的方法:相同 URL 的页面只保留一份
- 注意 URL 标准化(去除 tracking 参数、统一大小写等)
文档哈希去重:
- 计算每个文档的哈希值(MD5, SHA256)
- 相同哈希的文档只保留一份
- 对完全相同的文档有效,但无法处理微小变化
7.3 模糊去重(MinHash + LSH)
模糊去重能识别内容相似但不完全相同的文档。
MinHash 原理:
- 将文档表示为 n-gram 集合
- 通过多个哈希函数计算最小哈希值
- MinHash 签名的一致比例近似 Jaccard 相似度
LSH (Locality-Sensitive Hashing):
- 将 MinHash 签名分成多个 band
- 相同 band 的文档被映射到同一个 bucket
- 只需比较同一 bucket 内的文档对
# 典型配置
n_gram = 5 # 5-gram
n_hash = 128 # 128 个哈希函数
n_bands = 20 # 20 个 band
rows_per_band = 6 # 每个 band 6 行
# 这给出约 0.8 Jaccard 相似度时 ~96% 的召回率
工业级工具:
- deduplicate-text-datasets(Google):最常用
- text-dedup(ChenghaoMou):支持多种方法
- Spark/Dask 版本:大规模分布式去重
7.4 子文档级去重
除了文档级去重,还需要段落/句子级去重:
- 段落去重:相同段落在多个文档中出现(如版权声明、Cookie 通知)
- n-gram 去重:长 n-gram 重复表明内容抄袭
- FineWeb 的做法:使用 15-gram 的精确匹配去重,效果优于文档级去重
八、质量评分
8.1 启发式评分
基于文本特征的规则化评分:
Gopher 规则(DeepMind):
def gopher_quality_score(text):
score = 1.0
words = text.split()
# 单词数量
if len(words) < 50 or len(words) > 100000:
score *= 0.1
# 平均单词长度
avg_len = sum(len(w) for w in words) / len(words)
if avg_len < 3 or avg_len > 10:
score *= 0.5
# 符号-单词比例
symbol_count = sum(1 for c in text if c in '#...')
if symbol_count / len(words) > 0.1:
score *= 0.5
# 大写比例
upper_ratio = sum(1 for c in text if c.isupper()) / len(text)
if upper_ratio > 0.2:
score *= 0.7
# 省略号密度
ellipsis_count = text.count('...')
if ellipsis_count / len(words) > 0.02:
score *= 0.7
return score
8.2 模型评分
使用训练好的分类器对文本质量进行评分:
方法 1:困惑度过滤(Perplexity Filtering)
用一个在高质量数据上训练的语言模型来评分:
困惑度低 = 更像高质量文本
方法 2:教育质量分类器
FineWeb-Edu 的核心创新:训练一个分类器来判断文本的 “教育价值”。
训练流程:
- 用 LLaMA-3 70B 对 50 万条样本标注教育质量分数(0-5)
- 用这些标注训练一个小型分类器(如 FastText 或小型 BERT)
- 用分类器对全量数据评分
- 过滤掉低分数据
效果惊人:仅保留教育分数 ≥ 3 的数据,模型在知识密集型任务上提升 5-10 个百分点。
方法 3:DCLM 的数据筛选
DCLM 使用了一种更系统化的方法:
- 训练多个不同的质量分类器
- 在小规模实验中评估每个分类器的效果
- 选择最佳分类器用于大规模筛选
8.3 质量评分的哲学
质量是一个主观概念——不同的应用场景对 “质量” 有不同的定义:
- 通用 LLM:百科式的、知识密集的文本
- 代码模型:高质量的代码和技术文档
- 对话模型:自然、流畅的对话文本
- 科学模型:学术论文、教科书
最佳实践:根据目标场景定义质量标准,然后训练对应的分类器。
九、毒性过滤
9.1 毒性类型
| 类型 | 描述 | 检测难度 |
|---|---|---|
| 显性毒性 | 明确的脏话、侮辱 | 低(关键词即可) |
| 仇恨言论 | 针对特定群体的歧视 | 中 |
| 隐性毒性 | 微妙的偏见和刻板印象 | 高 |
| 有害信息 | 自残、暴力等有害内容 | 中 |
| 虚假信息 | 阴谋论、伪科学 | 高 |
9.2 检测方法
关键词过滤:最基础但召回率有限
分类器过滤:
- Perspective API(Google):毒性分数 0-1
- HateBERT:专门训练的仇恨检测模型
- Detoxify:多维度毒性检测
实践建议:组合使用关键词 + 分类器,设置合理的阈值避免过度过滤。
十、PII 脱敏
10.1 需要脱敏的信息
- 邮箱地址
- 电话号码
- 身份证号/社会安全号
- 银行卡号
- 物理地址
- 姓名(特定上下文中)
- IP 地址
10.2 脱敏策略
策略 1:删除 直接删除包含 PII 的句子或文档。简单但损失信息。
策略 2:替换 用占位符替换 PII:
原文: 请联系张三,邮箱 zhangsan@example.com
脱敏: 请联系[PERSON],邮箱 [EMAIL]
策略 3:混淆 用假的但格式正确的信息替换:
原文: 手机号 13812345678
脱敏: 手机号 13899998888
10.3 工具
- presidio(Microsoft):基于 NER 的 PII 检测
- 正则表达式:针对格式化的 PII(邮箱、电话)
- 自训练 NER 模型:针对特定领域的 PII
十一、Benchmark 去污染
11.1 为什么必须去污染
如果训练数据包含评测集的题目和答案,模型会 “记住” 答案而非学会解题——评测结果会严重虚高。
11.2 去污染方法
n-gram 匹配: 检测训练数据中是否包含评测集的长 n-gram(通常 n=10-13)。
def check_contamination(train_text, benchmark_text, n=13):
train_ngrams = set(get_ngrams(train_text, n))
bench_ngrams = set(get_ngrams(benchmark_text, n))
overlap = train_ngrams & bench_ngrams
contamination_rate = len(overlap) / len(bench_ngrams)
return contamination_rate
哈希指纹:
- 对评测集计算 n-gram 哈希指纹库
- 扫描训练数据,匹配指纹
- 移除匹配的训练样本
11.3 常见评测集
需要去污染的主要评测集:
- MMLU(多学科知识)
- GSM8K(小学数学)
- HumanEval / MBPP(代码)
- HellaSwag(常识推理)
- ARC(科学推理)
- TruthfulQA(真实性)
- C-Eval / CMMLU(中文)
- WinoGrande(常识)
十二、主流数据集对比
| 数据集 | 年份 | 源数据 | 清洗策略 | 规模 | 模型性能 |
|---|---|---|---|---|---|
| C4 | 2020 | CC | 规则过滤 | 750GB | 基线 |
| The Pile | 2021 | 混合 | 中等清洗 | 825GB | +5% |
| RefinedWeb | 2023 | CC | 去重 + 规则 | 5T tokens | +10% |
| RedPajama-v2 | 2023 | CC | 多维质量分 | 30T tokens | +12% |
| FineWeb | 2024 | CC | 深度清洗 | 15T tokens | +15% |
| FineWeb-Edu | 2024 | FineWeb | 教育质量筛 | 1.3T tokens | +20% |
| DCLM | 2024 | CC | 模型筛选 | 3T tokens | +18% |
十三、实践清单
完整的数据清洗 Checklist
数据采集:
- 确认数据来源和许可
- 下载和存储原始数据
- 建立数据版本管理
清洗流水线:
- URL/域名过滤
- HTML → 文本提取(推荐 Trafilatura)
- 语言检测和过滤
- 基础规则过滤(长度、字符比例等)
- 精确去重(URL + 哈希)
- 模糊去重(MinHash + LSH)
- 质量评分和过滤
- 毒性检测和过滤
- PII 脱敏
- Benchmark 去污染
验证:
- 随机抽样人工检查质量
- 统计各阶段数据保留率
- 在小模型上验证清洗效果
- 对比清洗前后的 benchmark 差异
持续改进:
- 建立质量监控 dashboard
- 定期更新过滤规则
- 收集 badcase 反馈
- A/B 测试不同清洗策略
十四、总结
数据清洗是 LLM 训练中投入产出比最高的环节。核心要点:
- 去重是基础:MinHash + LSH 模糊去重是必备技术
- 质量筛选是关键:模型打分 >> 规则过滤
- 文本提取很重要:Trafilatura 比简单的 HTML 解析好很多
- 不要过度清洗:C4 的教训——过于严格的规则会丢掉好数据
- 小实验验证:在小模型上验证清洗策略的效果,再应用到大规模数据
数据清洗不是一次性工作,而是一个持续迭代的过程。每一轮清洗策略的改进,都可能带来模型性能的显著提升。