第 4 章 第一部分:基础篇

预训练数据工程

从数据采集到清洗、去重、质量过滤——构建万亿 token 级数据管线的完整指南

数据 预训练 数据清洗 去重 质量过滤

数据是 LLM 的第一生产力

如果说模型架构是 LLM 的”骨架”,训练算法是”肌肉”,那么数据就是”血液”。在大模型时代,一个被反复验证的共识是:

数据质量对模型能力的影响,往往超过架构改进和训练技巧的总和。

Chinchilla 论文证明了数据量与参数量同等重要。而后续更多的实践表明,在数据量充足的前提下,数据质量才是真正的瓶颈。

本章将系统介绍预训练数据工程的全流程——从原始数据采集到最终的训练数据配方。

数据全景图

现代 LLM 的预训练数据通常包括以下来源:

数据类别典型来源占比估算说明
网页文本Common Crawl, 自主爬取60-80%最大体量,质量参差不齐
代码GitHub, GitLab, Stack Overflow10-20%提升推理和代码能力
书籍Books3, Gutenberg3-5%高质量长文本
学术论文arXiv, Semantic Scholar2-5%学术知识
百科Wikipedia (多语言)2-3%高质量事实知识
对话/论坛Reddit, Stack Exchange2-5%对话能力
数学MATH, GSM8K, 合成数据1-3%数学推理
多语言CC-100, OSCAR, CulturaX5-15%多语言覆盖

⚠️ 多语种存档的可得性会变,引用时要带检查日期。 上表这一行的三个来源,按 2026-10-14 实测 HuggingFace API 的状态如下:

数据集HF 仓库状态备注
CC-100statmt/cc100✅ 公开可下载非 gated,但最后更新 2024-03-05,已两年半无维护
OSCARoscar-corpus/OSCAR-2301🔒 gated: manual需登录并人工审批才能下载;disabled 为 false,数据集本身仍在架上
CulturaXuonlp/CulturaX🔑 gated: auto登录即可,无需人工审批;最后更新 2024-12-16

两个容易踩的坑:

  1. OSCAR 的仓库名是 OSCAR-2301,不带点。 按 OSCAR-23.01(带点)访问会直接返回 401/404,容易被误读成”数据集已下架”。实际存在的是 OSCAR-2301,状态是需授权而非已停止访问。
  2. CulturaX 覆盖不了 OSCAR 的授权门槛。 常见说法是”CulturaX 已覆盖 mC4 与 CC-100,可作为替代”,这是内容覆盖意义上的;在可得性意义上两者不是一回事——CulturaX 登录即可,OSCAR 要人工审批。

若只想要一个无需审批的入口:oscar-corpus/mOSCAR 为非 gated 公开数据集(OSCAR 的多语言版本),可作为 OSCAR 的免授权替代。

主要开源数据集

数据集规模描述
Common CrawlPB 级互联网最大的开放爬虫存档
FineWeb15T tokensHuggingFace 处理的高质量网页数据
FineWeb-Edu1.3T tokensFineWeb 中筛选的教育类高质量内容
RedPajama V230T tokensTogether AI 的大规模开放数据
DCLM3T tokensApple 的 DataComp for Language Models
The Stack V267.5B tokensBigCode 项目的代码数据
Dolma3T tokensAI2 的开源预训练数据集
CulturaX6.3T tokens167 种语言的清洗后数据
Nemotron 3 数据包173B tokens (代码)NVIDIA 开源的 refreshed GitHub 代码 token,另有 50M SFT 样本 + 2M RL 任务 + 55 RL 环境的开源训练配方
Fine PT-PT Web41B tokens2026-09,欧洲葡萄牙语专用网页语料(10 位作者)

上表多语种一行只有 CulturaX,而它是清洗后的通用存档。Fine PT-PT Web 代表另一种做法:对一个在通用存档中长期被低估的语种,单独做一次定向采集与清洗。它的价值不在 41B 这个规模(对 6.3T 的 CulturaX 而言很小),而在范式——单语种独立构建比从多语种存档里切分更可控:阈值可以按该语言定制,质量分布不必迁就其它语言,出问题也更容易定位。这与本章「定向采集 vs 存档过滤」是同一思路在语种维度上的应用。

⚠️ 引用边界:上句”比切分更可控”是本书基于该数据集构建方式的推演,非论文自述结论;论文本身的贡献是数据与构建流程。

从”数据墙”到”质量墙”

2024 年前后,“数据墙逼近”(data wall)的叙事广泛流传——人类产生的高质量文本有限,LLM 训练数据终将耗尽。到 2026 年,这一叙事需要修正:数据墙已被合成数据绕过,但质量墙仍在。

关键证据:

  • 新训练 token 中合成数据占比从 26%(2023) 攀升至 60%+(2026),模型崩溃率(model collapse rate)控制在 <2%
  • DeepSeek V4 以 GPT-5 约 1/30 的推理成本达到前沿性能,验证了低成本高质量数据管线的可行性
  • Nemotron 3 Ultra 的领域特定预训练数据集量化验证了领域数据注入效果:法律合成集 LegalBench 64.6→74.7,Wiki 事实集 SimpleQA 40.2→50.2

这意味着核心挑战不再是”数据够不够”,而是”合成数据的质量控制和多样性如何保障”——数据工程的重心正从”采集-清洗”转向”生成-验证”。

数据处理管线

一个完整的预训练数据管线通常包含以下步骤:

原始数据 (Raw Data)
  ↓ 1. 文本提取 (Text Extraction)
  ↓ 2. 语言检测 (Language Identification)
  ↓ 3. URL/Domain 过滤
  ↓ 4. 启发式规则过滤 (Heuristic Filtering)
  ↓ 5. 模型质量过滤 (Quality Classifier)
  ↓ 6. 去重 (Deduplication)
  ↓ 7. PII 移除 (Personal Information Removal)
  ↓ 8. 毒性过滤 (Toxicity Filtering)
  ↓ 9. 数据混合 & 采样 (Data Mixing)
  ↓
训练数据 (Training Data)

1. 文本提取

对于网页数据,主要挑战是从 HTML 中提取干净的文本内容:

  • trafilatura:目前最流行的网页文本提取工具,支持正文提取、元数据解析
  • resiliparse:高性能的 HTML 解析器
  • jusText:基于段落分类的正文提取
  • Common Crawl 提供的 WET 文件已经做了初步的文本提取,但质量不够好

⚠️ 这三个工具不是同一件事的三种实现,而是三道不同的过滤器。 把它们并列成”常用工具三选一”是本章此前的一处隐含错误。

Beyond a Single Extractor: Re-thinking HTML-to-Text Extraction for LLM Pretraining(Findings of EACL 2026;Apple + Stanford + University of Washington)在 DCLM-BASELINE 的固定后处理管线下(即只换第一道工序、后面完全不动)重跑了三个抽取器,得到:

指标数值
在超过一个抽取器中存活的页面比例仅 39%
只被某一个抽取器唯一保留的页面比例61%
取三者并集的 token 产出提升+71%(再做一轮去重后仍 +58%)
7B 规模数据集规模193B token(仅 resiliparse)→ 283B token(并集)

也就是说:换一个抽取器,你换掉的不是”提取质量”,而是”哪些页面进入语料库”。61% 的页面命运由这一道工序单独决定。论文还指出,对超过四分之一的域名,至少 60% 的存活页面来自单一抽取器——这不是均匀分布的小差异,而是按域名集中的系统性偏差。

当前主流数据集各用了哪一个(这一点比上面的数字更需要记住):

数据集抽取器
DCLMresiliparse(论文原话是”因其速度”而偏好它)
FineWeb / RefinedWebtrafilatura
Nemotron-CCjusText(在跑完 FineWeb-Edu 过滤分类器后产出 token 更多)

本章后面反复引用的 FineWeb 系结论,其第一道工序就是 trafilatura——一个本章此前未加讨论的变量。

可操作结论有三条:

  1. 取并集,而不是挑一个。 并集把 token 产出提高 71%(再去重后 58%),而标准基准性能不降。
  2. 多工具配严格阈值 > 单工具配宽松阈值。 论文明确对比过:把 trafilatura 与 resiliparse 的过滤阈值放宽到 top-15%(让各自产出接近 resiliparse)仍然不如取各抽取器高质量页面的并集。想提高产量,加工具比松阈值更有效。
  3. 抽取器应按内容类型选,而不是全局统一。 三者对表格与代码块的处理差异远大于通用文本:在 WikiTableQuestions 上,7B 模型的成绩是 resiliparse 11.9 / trafilatura 3.7 / jusText 1.6(resiliparse 约为 trafilatura 的 3.2 倍);jusText 常整段删掉代码块,在 HumanEval 上落后最多 3.6 个百分点。论文总结的效应量是:抽取器选择在 WikiTQ 上可造成最多 10 个百分点的差异,在 HumanEval 上约 3 个百分点。

⚠️ 收益的另一面:并集意味着更多页面被纳入,其中也会包含更多个人信息与受版权保护的材料。论文自己在 Limitations 里点名了这一点,并建议使用者配套加强内容过滤与数据使用合规。

最后保留并强化原有一句:WET 不够好。FineWeb 的消融发现从 WARC 用 trafilatura 重新抽取,明显优于直接采用 WET 文件的提取结果。

关键挑战:

  • 模板内容(boilerplate):导航栏、页脚、广告等噪音
  • JavaScript 渲染:SPA 应用的内容可能不在原始 HTML 中
  • 编码问题:非 UTF-8 编码的处理

2. 语言检测

多语言 LLM 需要准确识别每个文档的语言,以控制语言配比。

常用工具:

  • fastText lid.176:Meta 的语言检测模型,支持 176 种语言,速度极快
  • CLD3:Google 的 Compact Language Detector
  • GlotLID:支持 1600+ 种语言的开源检测器

实战经验:单一检测器在短文本(<50 字符)和混合语言文本上容易出错。工业实践中通常 ensemble 多个检测器,或对短文本使用更保守的阈值。

3. 启发式规则过滤

最基础但非常有效的过滤手段。以 FineWeb 的处理流程 为例:

文档级过滤:

  • 文档长度:去掉过短(<50 词)或过长(>100K 词)的文档
  • 重复率:如果文档中 n-gram 重复率过高(如 >50%),说明是模板生成的内容
  • 特殊字符比例:如果非字母字符占比过高(如 >30%),说明可能是乱码或代码
  • 停用词检查:英文文档如果常见停用词(the, is, of)出现频率异常低,可能是低质量内容

行级过滤:

  • 去除全大写行(通常是标题或广告)
  • 去除只包含 URL 的行
  • 去除重复行

来源过滤:

  • 黑名单域名(成人内容、垃圾网站、恶意软件)
  • URL 模式匹配(如 /login, /signup 等非内容页面)

Gopher(Rae et al., 2021)论文详细定义了一组广泛使用的启发式过滤规则,已成为行业标准。

4. 模型质量过滤

启发式规则能去掉明显的低质量内容,但无法判断文本的”教育价值”或”信息密度”。这就需要用模型做质量分类。

困惑度过滤(Perplexity Filtering)

用一个高质量语料(如 Wikipedia)训练的小型语言模型(如 KenLM),计算每个文档的困惑度。困惑度过高说明文本不通顺,过低说明文本可能过于简单/重复。

CCNet(Wenzek et al., 2020)首次大规模使用该方法处理 Common Crawl。

质量分类器(Quality Classifier)

训练一个二分类器来区分”高质量”和”低质量”文档:

  • 正例:Wikipedia、教科书、高质量博客
  • 负例:随机网页数据

FineWeb-Edu 使用 LLaMA-3-70B-Instruct 作为标注器,对文档的教育价值评分(0-5),然后训练一个小的分类模型来大规模过滤。这种”LLM 标注 + 小模型推广”的模式已成为主流范式。

JQL——跨语言质量过滤

JQL(Judging Quality across Languages,arXiv:2505.22232)将 LLM 的教育价值标注能力蒸馏到基于多语言嵌入模型(Snowflake Arctic Embed v2.0)+ 简单回归头的轻量评估器。与 FineWeb-Edu 的”分类器蒸馏”不同,JQL 使用”嵌入蒸馏”路线,覆盖 35 种语言(含跨语言零样本泛化——训练中未见过的阿拉伯语/泰语/中文也表现出色)。效果:比 FineWeb2 启发式方法保留更多数据(+4.8% tokens)且性能更优(+4.27%),推理速度 11,000 docs/min/A100。

数据重要性估计

DSIR(Xie et al., 2023)提出了基于重要性权重(importance resampling)的方法:给定一个目标分布(如高质量数据集),估计每个文档与目标分布的相似度,据此进行加权采样。

DataRater——元学习数据质量评估

DataRater(Google DeepMind)使用元梯度自动学习数据筛选标准——指定训练目标(如验证损失最小化),DataRater 训练数据质量评估器来预测每条数据对训练效果的贡献。与 DSIR 需要预定义目标分布不同,DataRater 通过元学习自动发现数据价值。实验显示:最高 46.6% 净计算收益(减少训练 FLOPs),4 亿参数元训练的评估器可泛化到 5000 万到 10 亿参数模型。能自动识别”符合人类低质量直觉”的数据(文本编码错误、OCR 错误、无关内容)。

质量过滤的代价:多样性与长尾知识

困惑度 → 质量分类器 → 重要性估计 → 元学习,这条链呈现为”方法越来越先进”的单向升级,隐含一个假设:质量分越高越好、过滤越严越好。这个假设有明确的失效边界。

单目标质量过滤的本质是线性剪枝——在一个质量维度上设阈值截断。而质量分与文体/主题强相关:教科书、学术论文、高质量新闻这类文本的教育价值天然高分;代码仓库、方言对话、小众领域文档的分值则系统性偏低。后一类内容会被当作”低质量”按比例截断,此时损失掉的主要不是质量,而是分布多样性与长尾知识。

CuraWeb(arXiv:2607.22662,2026-06,15 位作者)据此提出从线性剪枝转向质量-冗余-多样性三目标联合优化:双轨清洗(rule-based + model-driven,按域定制阈值)+ 混合去重(n-gram 精确 + 语义软去重)+ 多目标采样器(分开评估文本结构与内容价值,兼顾信息质量与分布广度)。论文在 Common Crawl 上构建 2T-token 英文语料,3B 规模实验平均 +1.8%,超过 FineWeb / Dolma3 / DCLM,知识密集与推理任务增益最大。

实践含义:质量过滤应与多样性指标一起做消融,不能只看质量分分布与保留率。建议同步监控”域覆盖数 / 主题熵”这类多样性指标。若长尾领域能力下滑而质量分分布无异常,应优先怀疑过滤阈值,而不是模型或数据规模。

⚠️ 局限:CuraWeb 是单篇论文在单一规模(3B)上的自评结果,尚无独立复现;且与 FineWeb2 多语言管线是互补而非替代关系。本节前述方法并未因此失效,只是需要补上”多样性”这一约束轴。

上游一环:先问”池子对不对”,再问”怎么过滤”

上面两步(质量过滤、三目标优化)都在回答同一个问题:给定一个语料池,怎么把它变好。但有个更上游的问题本章此前没有正面处理——这个池子本身对不对?

Data Scout(2026-09-04)给出了一个很难回避的数字:用 FineMath 分类器做探针,定向爬取抓到的页面里 21.9% 是高质量数学内容,是”从同类网页样本里过滤”命中率 0.31% 的约 70 倍;更关键的是——这些页面里有 63.2% 根本不在 Common Crawl 里。

如果目标领域在通用网页中天然稀疏,“先存档后过滤”这条路径在数据入口处就已经漏掉了三分之二的目标内容。过滤器再聪明,天花板也只有 36.8%。

两条路线的取舍:

存档过滤(本章主线)定向采集(Data Scout)
流程通用爬虫存档 → 大规模清洗 / 过滤先定位高价值来源 → 针对性抓取
优点可规模化,一次投入反复使用目标内容命中率高(本例约 70 倍)
代价目标领域内容在入口处即缺失需要先知道”去哪抓”,冷启动成本高,覆盖广度取决于来源清单质量
适用通用能力、需要广覆盖目标领域在通用网页中稀疏(数学、代码、专业垂直领域)

给读者的决策规则:当目标领域在通用网页中天然稀疏时,定向采集的收益远大于继续优化过滤器;当目标是通用能力、需要广覆盖时,存档过滤仍不可替代。两者互补,不是替代——本章前面讲的所有过滤方法都不会因为这条路径而失效,只是它们的适用前提是”池子里有货”。

与 CuraWeb 的交叉引用:CuraWeb 的质量-冗余-多样性三目标优化,假设候选集已经覆盖目标分布。若候选集本身存在系统性缺口(如上面那 63.2%),三目标优化无从谈起——它只能在已有候选上做取舍,无法凭空补回缺失的来源。所以顺序是:先确认覆盖,再谈三目标。

⚠️ 引用边界:63.2% / 21.9% / 0.31% 三个数字已逐字核到论文 HTML 原文(非仅摘要转述),但仍是论文自报结果,截至 2026-09-24 无独立复现。论文的端到端验证规模为:Llama-3.2-3B 在 1.9B Data Scout token 上继续预训练,同等 token 预算下 GSM8K 追平 FineMath 语料——是”追平”,不是”超过”,读的时候别放大。第二条天文学领域的端到端验证中,抓取页面有 51.9% 为相关页面。

5. 去重

去重可能是数据处理中投入产出比最高的环节。Hernandez et al. (2022) 证明,去重可以在不增加任何其他成本的情况下提升模型质量。

先决定去重的边界,再决定去重的阈值

在讨论”怎么去重”之前,有一道更靠前、也更少被写进文档的选择题:去重的比较范围是什么? 是”每个 Common Crawl 转储内部自己比”,还是”跨所有转储一起比”?

这个选择比阈值敏感得多。FineWeb 团队踩过的坑是公开的:

⚠️ 待回源:以下 94% 与对照实验出自 FineWeb 团队公开复盘的二手转述,本轮未能回到 HuggingFace 官方演讲/博客一手页面确认(官方博客空间为 JS 渲染,curl 取不到正文)。不要把这两个数字当作决策依据,它们仅用于说明”作用域选错会发生什么”。

据该复盘,团队最初的策略是”新转储优先”:拿最新转储与上一个转储比对,重复则删旧留新,一路往前推。结果是删掉了全库的 94%。团队用被删数据做对照实验,用剩下 6% 训出的小模型反而更差——跨转储的激进去重删掉的恰好是最有价值的内容。修正方案是每个转储内部自行去重(dump 对自己)。

可以从一手数据卡验证的旁证有三条,它们不依赖上面那段转述:

  1. FineWeb 最终规模是 18.5T token(最初 15T),公开许可 ODC-By 1.0,覆盖 2013 年以来所有 CC 转储。
  2. FineWeb 每条样本都带一个 dump 字段,记录该样本来自哪个 CC 转储——也就是说”转储归属”在数据模型里是一等公民,不是事后可推的元数据。
  3. 其 changelog 记载 v1.1.0 因”在去重上发现一个 bug”而重新处理并重新上传了 11 个转储(CC-MAIN-2021-49 至 CC-MAIN-2023-40)。跨转储去重会出 bug、且 bug 会静默地改掉一批数据的归属,这件事本身就是一个警示。

顺序也是一个变量,不是固定流程。 多数管线的顺序是”先过滤、后去重”,理由是去重计算昂贵、先过滤能缩小规模;但 FineWeb2 刻意反转了这一顺序(先去重再过滤)。本章的管线图(4 过滤 → 5 去重)应按可调项来读,而不是按固定流程来读。

⚠️ 待回源:FineWeb2 反转顺序的说法来自聚合站转述,本轮未回到 HuggingFace 官方发布页确认。

精确去重(Exact Deduplication)

  • URL 去重:同一 URL 的多次爬取只保留最新版本
  • 文档级哈希:对文档内容计算哈希值(如 SHA-256),完全相同的文档去重
  • 行级去重:去除在数据集中出现次数过多的行(如版权声明、cookie 提示)

模糊去重(Fuzzy Deduplication)

精确去重无法处理稍有差异的近似重复。模糊去重是核心技术:

MinHash + LSH(Locality-Sensitive Hashing):

  1. 将文档表示为 n-gram 集合
  2. 用 MinHash 生成紧凑的签名(signature)
  3. 用 LSH 将相似签名映射到同一桶中
  4. 桶内文档做精确比较

这种方法可以在近似 O(n)O(n) 时间复杂度内完成全库去重。

参数选择的经验值:

  • n-gram 大小:5(太小会误判,太大会漏判)
  • MinHash 签名长度:128-256
  • LSH band 数量 × 行数 = 签名长度,阈值取决于期望的 Jaccard 相似度
  • 相似度阈值:0.7-0.8(FineWeb 使用 0.75)

LSHBloom——Bloom Filter 替代 LSHIndex

LSHBloom用轻量 Bloom Filter 替代 MinHashLSH 中的 LSHIndex,解决了 LSHIndex 存储随数据规模急剧膨胀的问题。去重性能与 MinHashLSH 相同,假阳性率可控制在 1e-5,运行速度提升 270%(peS2o 数据集),磁盘空间仅需 MinHashLSH 的 0.6%。在数十亿文档规模下,预期 250% 加速和 54x 空间优势。

语义去重与”降权替代删除”

除精确去重与模糊去重之外,工业管线普遍把语义去重作为第三个层级:对文档生成嵌入,用 ANN 检索找出语义相近的簇,再在簇内处理冗余。SemDeDup(Abbas et al., 2023)在 LAION 子集上的实验显示,语义去重可移除约 50% 的数据而性能损失极小,等效于把训练时间减半。

处理冗余簇时有两条路线,工程含义差别很大:

  • 硬删除:簇内只保留代表文档。数据量下降明显,模型对高频内容过拟合的风险降低;但阈值调错时会误删独特文档,且数据集的统计分布被一并改变。
  • 软降权(SoftDedup 思路):保留全部文档,但降低冗余簇内文档的采样权重——例如 100 份近似的热门新闻各自采样概率降为 1/100,而不是删掉 99 份。数据量不减少,冗余影响同样下降,同时保留原始分布的完整性,阈值调错的代价更低。

权衡:语义去重需要为全量数据生成嵌入并做 ANN 检索,计算成本显著高于前两级。GPU 预算有限时建议只做精确 + 模糊两级,把语义层留给高质量子集;预算充足时优先考虑软降权而非硬删除,以降低过度去重损害多样性的风险。

下面用 Python 实现一个简化版 MinHash + LSH 去重管线:

import hashlib
import struct
from typing import List, Set, Tuple
from collections import defaultdict

class MinHashDedup:
    """MinHash + LSH 模糊去重
    
    工业级实现可用 datasketch 库,这里展示核心原理。
    """
    def __init__(self, num_perm: int = 128, bands: int = 16,
                 ngram_size: int = 5, threshold: float = 0.75):
        self.num_perm = num_perm
        self.bands = bands
        self.rows = num_perm // bands  # 每个 band 的行数
        self.ngram_size = ngram_size
        self.threshold = threshold
        # 随机哈希函数参数: h(x) = (ax + b) mod p
        self._a = [hash(f"a_{i}") % (2**61 - 1) for i in range(num_perm)]
        self._b = [hash(f"b_{i}") % (2**61 - 1) for i in range(num_perm)]
        self._p = (1 << 61) - 1  # Mersenne prime

    def _get_ngrams(self, text: str) -> Set[str]:
        """提取字符级 n-gram"""
        tokens = text.lower().split()
        ngrams = set()
        for i in range(len(tokens) - self.ngram_size + 1):
            ngrams.add(" ".join(tokens[i:i + self.ngram_size]))
        return ngrams

    def _minhash(self, ngrams: Set[str]) -> List[int]:
        """计算 MinHash 签名"""
        signature = [float('inf')] * self.num_perm
        for gram in ngrams:
            h = int(hashlib.md5(gram.encode()).hexdigest(), 16)
            for i in range(self.num_perm):
                val = (self._a[i] * h + self._b[i]) % self._p
                signature[i] = min(signature[i], val)
        return signature

    def _lsh_buckets(self, signature: List[int]) -> List[str]:
        """LSH 分桶: 将签名分成 bands,每个 band 哈希到一个桶"""
        buckets = []
        for b in range(self.bands):
            start = b * self.rows
            band = tuple(signature[start:start + self.rows])
            bucket_id = f"b{b}_{hash(band)}"
            buckets.append(bucket_id)
        return buckets

    def deduplicate(self, documents: List[str]) -> List[int]:
        """对文档集合执行去重,返回保留的文档索引"""
        bucket_to_docs = defaultdict(set)
        signatures = []

        # Step 1: 计算所有文档的 MinHash 签名并 LSH 分桶
        for idx, doc in enumerate(documents):
            ngrams = self._get_ngrams(doc)
            if len(ngrams) < 2:
                signatures.append(None)
                continue
            sig = self._minhash(ngrams)
            signatures.append(sig)
            for bucket in self._lsh_buckets(sig):
                bucket_to_docs[bucket].add(idx)

        # Step 2: 找到候选重复对并验证
        duplicates = set()
        checked = set()
        for bucket, doc_ids in bucket_to_docs.items():
            doc_list = sorted(doc_ids)
            for i in range(len(doc_list)):
                for j in range(i + 1, len(doc_list)):
                    pair = (doc_list[i], doc_list[j])
                    if pair in checked:
                        continue
                    checked.add(pair)
                    # 用签名估算 Jaccard 相似度
                    s1, s2 = signatures[pair[0]], signatures[pair[1]]
                    if s1 and s2:
                        sim = sum(a == b for a, b in zip(s1, s2)) / self.num_perm
                        if sim >= self.threshold:
                            duplicates.add(pair[1])  # 保留前者,标记后者

        # 返回去重后保留的索引
        kept = [i for i in range(len(documents))
                if i not in duplicates and signatures[i] is not None]
        print(f"去重: {len(documents)} → {len(kept)} 篇 "
              f"(移除 {len(documents) - len(kept)} 篇, "
              f"{(len(documents) - len(kept)) / len(documents) * 100:.1f}%)")
        return kept

# 示例
docs = [
    "大语言模型是人工智能领域的重要突破 它改变了自然语言处理的范式",
    "大语言模型是人工智能领域的关键突破 它改变了自然语言处理的范式",  # 近似重复
    "预训练数据的质量对模型性能有决定性影响 高质量数据可以显著提升效果",
    "Transformer 架构采用了自注意力机制 实现了完全并行的序列建模",
    "预训练数据质量对模型性能有决定性的影响 高质量数据能显著提升效果",  # 近似重复
]
deduper = MinHashDedup(num_perm=64, bands=8, threshold=0.5)
kept_indices = deduper.deduplicate(docs)
for i in kept_indices:
    print(f"  [{i}] {docs[i][:40]}...")

去重的规模

工业级数据集的去重规模惊人。以 Common Crawl 为例:

  • 原始数据:约 250B 个网页
  • URL 去重后:约 100B
  • 精确去重后:约 50B
  • MinHash 模糊去重后:约 15-25B

去重可能移除 60-80% 的数据,但这些”减少”实际上大幅提升了数据质量。

口径限定:上面的 60-80% 是硬删除范式下的移除比例。若改用上文的软降权(降采样权重而非删除),数据量不减少,但冗余对训练的影响同样下降——两个数字不可直接比较。

⚠️ 这个区间是结果,不是目标。 60-80% 是几十条工业管线事后统计出来的分布,不是一个应当逼近的 KPI。判断顺序应该是:先确认去重的作用域设对了(见本节开头的”先决定边界”),再看阈值。

一条可操作的止损线:一旦去重率逼近 90%,第一反应应该是”作用域设错了”,而不是”阈值还能再调”。 上一小节那个 94% 的失败案例(见该处的待回源标记)正是这一类错误——它把”跨转储重复”当成了”冗余”,而跨转储重复恰恰是高频优质内容的特征。

6. PII 移除与安全过滤

PII(个人可识别信息)移除

预训练数据中包含大量个人信息(邮箱、电话号码、地址等),如不移除可能导致隐私泄露。

常用方法:

  • 正则表达式:匹配邮箱、电话号码等固定格式
  • NER 模型:识别人名、地址等非固定格式的 PII
  • Presidio:Microsoft 的开源 PII 检测和匿名化工具

实践中通常采用”替换”而非”删除”策略——用特殊标记(如 [EMAIL], [PHONE])替换 PII,保留文本的语义完整性。

毒性过滤

去除有害内容(仇恨言论、暴力、色情等):

  • Perspective API:Google 的毒性检测 API
  • 关键词列表:维护领域特定的敏感词列表
  • 分类器:训练专门的毒性检测模型

注意:过度的毒性过滤可能导致代表性不足(under-representation),移除某些群体的正常讨论。需要在安全和公平之间取得平衡。

数据混合(Data Mixing)

确定不同来源数据的混合比例是预训练数据工程中最关键的决策之一。

经典方法

按数据量比例:直接按各来源的原始数据量做配比。简单但粗暴,大体量的低质量网页数据会淹没高质量的小数据集。

手动调优:基于经验和 ablation 实验手动调整配比。LLaMA 论文中的配比:

来源占比采样权重
CommonCrawl67%1.10
C415%1.06
GitHub4.5%0.64
Wikipedia4.5%2.45
Books4.5%2.23
ArXiv2.5%1.06
StackExchange2%1.03

注意 Wikipedia 和 Books 的采样权重远大于 1,意味着这些高质量数据会被多次采样(多 epoch)。

数据混合的理论

DoReMi(Xie et al., 2023)提出了一个优雅的自动化方法:

  1. 先用均匀配比训练一个小的参考模型
  2. 用参考模型计算每个数据域的 excess loss
  3. 用在线优化算法(Distributionally Robust Optimization)自动调整配比
  4. 用优化后的配比训练最终模型

实验表明,DoReMi 自动找到的配比优于人工调优。

数据重复(Multi-epoch)的影响

当高质量数据不足以支撑足够的训练步数时,不可避免要重复使用数据。Muennighoff et al. (2023) 系统研究了数据重复的影响:

  • 4 epoch 以内:与独立数据的损失差异不显著,且不体现为下游任务差异
  • 4-16 epoch:仍然有正收益,但回报快速衰减
  • 超过约 16 epoch:回报衰减极快,趋近于零

论文把这个规律写成了显式公式:无论重复多少次,损失都不会优于”在 UD+UDRD∗U_D + U_D R_D^* 个全新 token 上跑 1 个 epoch”所能达到的结果——重复数据有一个硬上限,它不是免费的训练步数。参数侧有对称的同类公式(模型规模远超可用数据时,每参数回报同样递减)。论文在数据受限区给出的建议是:新增算力要一并加给参数量和 epoch 数,且 epoch 应略微加得更快一些。

实战建议:如果你的数据量有限,优先保证高质量数据多 epoch,低质量数据 1 epoch 即可。代码和数学数据的 epoch 容忍度通常高于网页文本。

🔴 跨章提醒:本节结论有适用区制,别当成通则。 上面 Muennighoff 的”epoch 应略微加得更快一些”,描述的是收益递减;它无法表示”loss 因过拟合而上升”的区制。2026 年的一篇 scaling-law 工作(arXiv:2605.01640,COLM 2026)在 15M–1B 参数、最多 16 epoch 上测得重复损害是超线性的,并据此给出方向相反的建议:过阈值后应停止加 epoch,把算力转投模型容量。

完整论证、两条建议的分水岭、以及该论文的规模限定(上限 1B 参数/6B 唯一 token,未确认可外推到前沿规模)见第五章「超越 Chinchilla:Over-Training」。在你确定自己处在哪个区制之前,不要单独依据本节做 epoch 预算。

构建数据处理管线

将上面的各个步骤整合为一个可复用的数据处理管线:

import re
from dataclasses import dataclass, field
from typing import List, Optional, Callable

@dataclass
class Document:
    """单个文档的数据结构"""
    text: str
    url: str = ""
    language: str = ""
    quality_score: float = 0.0
    metadata: dict = field(default_factory=dict)

@dataclass
class FilterStats:
    """过滤统计"""
    total: int = 0
    passed: int = 0
    filter_reasons: dict = field(default_factory=lambda: {})

    def report(self):
        print(f"\n📊 过滤报告: {self.passed}/{self.total} 通过 "
              f"({self.passed/self.total*100:.1f}%)")
        for reason, count in sorted(self.filter_reasons.items(),
                                     key=lambda x: -x[1]):
            print(f"  ❌ {reason}: {count} ({count/self.total*100:.1f}%)")

class DataPipeline:
    """预训练数据处理管线
    
    支持链式添加过滤器,自动统计过滤结果。
    """
    def __init__(self):
        self.filters: List[tuple] = []  # (name, filter_fn)
        self.stats = FilterStats()

    def add_filter(self, name: str, fn: Callable[[Document], bool]):
        """添加过滤器: fn 返回 True 表示保留"""
        self.filters.append((name, fn))
        return self  # 支持链式调用

    def process(self, docs: List[Document]) -> List[Document]:
        """处理文档列表"""
        self.stats = FilterStats(total=len(docs))
        result = []
        for doc in docs:
            passed = True
            for name, fn in self.filters:
                if not fn(doc):
                    self.stats.filter_reasons[name] = \
                        self.stats.filter_reasons.get(name, 0) + 1
                    passed = False
                    break
            if passed:
                result.append(doc)
        self.stats.passed = len(result)
        return result

# === 常用过滤器 ===

def length_filter(min_words=50, max_words=100000):
    """文档长度过滤"""
    def _filter(doc: Document) -> bool:
        n_words = len(doc.text.split())
        return min_words <= n_words <= max_words
    return _filter

def repetition_filter(max_ratio=0.3):
    """行级重复率过滤(Gopher 规则)"""
    def _filter(doc: Document) -> bool:
        lines = doc.text.strip().split("\n")
        if len(lines) < 2:
            return True
        line_counts = {}
        for line in lines:
            line = line.strip()
            if line:
                line_counts[line] = line_counts.get(line, 0) + 1
        dup_lines = sum(c - 1 for c in line_counts.values() if c > 1)
        return dup_lines / len(lines) < max_ratio
    return _filter

def special_char_filter(max_ratio=0.3):
    """特殊字符比例过滤"""
    def _filter(doc: Document) -> bool:
        if not doc.text:
            return False
        special = sum(1 for c in doc.text if not c.isalnum() and not c.isspace())
        return special / len(doc.text) < max_ratio
    return _filter

def pii_redact(doc: Document) -> Document:
    """PII 脱敏(正则替换)"""
    text = doc.text
    text = re.sub(r'\b[\w.+-]+@[\w-]+\.[\w.-]+\b', '[EMAIL]', text)
    text = re.sub(r'\b1[3-9]\d{9}\b', '[PHONE]', text)  # 中国手机号
    text = re.sub(r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b', '[PHONE]', text)  # US
    doc.text = text
    return doc

# === 组装管线 ===

pipeline = (DataPipeline()
    .add_filter("too_short", length_filter(min_words=50))
    .add_filter("too_long", length_filter(max_words=100000))
    .add_filter("high_repetition", repetition_filter(max_ratio=0.3))
    .add_filter("special_chars", special_char_filter(max_ratio=0.3))
)

# 示例运行
sample_docs = [
    Document("这是一个太短的文档", url="http://a.com"),
    Document(" ".join(["高质量的预训练数据"] * 80), url="http://b.com"),
    Document("正常文档 " * 60 + "包含丰富的技术内容和多样化的表述", url="http://c.com"),
    Document("!!@#$%^&*" * 100, url="http://d.com"),
]
clean_docs = pipeline.process(sample_docs)
pipeline.stats.report()

数据处理的工程实践

分布式处理框架

处理 TB 级数据需要高效的分布式框架:

  • Spark:经典大数据处理框架,适合批处理
  • Ray:灵活的分布式计算框架,适合 ML 工作负载
  • datatrove:HuggingFace 开源的数据处理工具包,专为 LLM 预训练数据设计
  • dolma:AI2 开源的数据处理工具包

数据格式

格式优点缺点使用场景
JSONL人类可读,灵活体积大,解析慢中间处理
Parquet列式存储,压缩好不支持流式追加数据存储和分析
Arrow/IPC零拷贝读取,内存映射文件较大训练时高效读取
MDS(MosaicML)流式读取,分布式友好生态较新大规模训练

数据溯源与可审计性

随着数据合规要求越来越严格,数据溯源(data provenance)变得至关重要:

  • 记录每条数据的来源 URL、爬取时间、处理步骤
  • 支持根据 URL/域名回溯和删除数据(应对版权要求)
  • Data Provenance Initiative 推动了数据来源的标准化

补一个此前缺失的字段:抽取器的名称、版本与配置。

上面三条里”处理步骤”通常被理解为”过滤了什么、去重了什么”,但本章 §「1. 文本提取」给出的证据说明第一道工序才是覆盖面最大的那一刀:三个抽取器只有 39% 的页面重叠,换一个工具就换掉一批页面。如果不记录用的是哪个抽取器、哪个版本、什么配置,事后就无法解释两个同源语料为什么不同,也无法复现任何一条结论。

这条的成本极低(数据卡上多几行),但它决定了后续所有对比实验能否归因。EACL’26 那篇论文自己就把配置逐条写进了实验设置——resiliparse 用 main_content=True,trafilatura 与 jusText 用默认参数——这正是”配置要记录”的示范。论文同时提醒:这三个库都仍在活跃开发中,新版本的行为会变。

实践口径:数据卡里至少记三个字段——extractor(名称)、extractor_version(版本)、extractor_config(非默认参数的键)。本章 §1 已说明 trafilatura 的默认值与 main_content=True 会给出不同的页面集合,这一行配置就是可复现性的分界线。

中文数据的特殊挑战

中文网页质量

中文互联网的数据质量问题尤为突出:

  • SEO 垃圾内容占比高
  • 采集/伪原创内容泛滥
  • 多平台相互转载导致高重复率
  • 繁体/简体混合,拼音/混合编码

中文特有的处理步骤

  1. 繁简转换:统一为简体或繁体(使用 OpenCC)
  2. 中文分词:用于 n-gram 统计和去重(如 jieba)
  3. 全角/半角统一:标准化标点和数字
  4. 乱码检测:中文 UTF-8 编码错误很常见,需要专门的检测逻辑

推荐数据集

数据集规模说明
WuDaoCorpora200GB智源研究院,中文多来源
ChineseWebText1.4T tokens中文网页高质量数据
SkyPile150B tokens昆仑万维的中文数据

数据质量评估

如何知道你的数据处理管线是否有效?

下游任务评估

最直接的方法:用处理后的数据训练一个小模型(如 1B 参数),在 benchmark 上评估。

常用 benchmark:

内在指标

不训练模型也能评估数据质量的方法:

  • 困惑度分布:用参考模型计算数据的困惑度,分析分布形态
  • Token 覆盖率:检查词表的利用情况
  • 语言/域分布:确保配比符合预期
  • 重复率:文档级和 token 级的重复比例

DCLM 评估框架

Apple 的 DataComp for Language Models 提供了一个标准化的数据质量评估框架:

  1. 固定模型架构和训练配方
  2. 只改变数据
  3. 在标准 benchmark 集上评估
  4. 数据质量的改进可以被公平比较

这种方法使得不同团队的数据处理技术可以在同一框架下对比。


📖 延伸阅读路线