跳到主要内容
版本:Next

训练数据集工程:寻找、清洗与构建

业界共识:LLM 的差距主要是数据的差距。架构大家都抄 Transformer,真正拉开差距的是数万亿 token 语料的获取、清洗、过滤与配比。本篇按实战流程展开。

一、语料从哪来​

公开语料源​

来源规模特点
Common Crawl数千 TB 原始网页一切预训练的起点,噪声极大,必须重度清洗
C4~800GB 清洗后英文Google 清洗版 Common Crawl
FineWeb / FineWeb-Edu15T token / 1.3THuggingFace 用教育质量分类器筛选,当前英文开源 SOTA
The Pile800GB22 个子集混合(论文、代码、书籍、网页)
RefinedWeb / Dolma5T / 3T tokenFalcon / OLMo 的开放语料
悟道、MNBVC、Skywork中文为主中文场景的重要补充
GitHub / The Stack6TB 代码代码能力与推理能力的重要来源
arXiv / Wikipedia / 书籍小但精高质量知识与长文

自建数据​

  • 网页爬取:自建爬虫 → WARC 存档 → 正文抽取(trafilatura、resiliparse)。
  • PDF 解析:学术论文与书籍是最高质量的长文本来源,用 GROBID、Marker、MinerU 抽取,注意公式与表格的保真。
  • 合成数据:用强模型生成教科书式内容(Phi 系列的"textbook quality"路线、Cosmopedia),成本正快速低于人工标注。
  • 版权合规:书籍、新闻的授权风险真实存在,商用项目务必法务评估;开源项目优先选带明确许可的语料(FineWeb、Dolma 都标注了 license)。

二、清洗流水线(每一步都不能省)​

原始 HTML/WARC
→ 正文抽取(去导航、广告、样板)
→ 语言识别(fastText 分类器,留目标语言)
→ 规则过滤(长度、字符分布、乱码比例)
→ 质量打分(启发式 + 分类器)
→ 去重(精确 + MinHash 近似)
→ 隐私与有害内容过滤(PII、NSFW、域名黑名单)
→ 去污染(剔除与评测集重叠的样本)
→ 混合配比 → 分词 → 打包为二进制 token 流

1. 正文抽取​

网页 90% 的 token 是导航栏、广告、JS。用文本密度 + 标签统计抽正文;保留标题层级与列表结构(用 Markdown 化),结构信息对模型有用。

2. 启发式过滤规则(C4/FineWeb 经典集)​

def keep(doc):
text = doc["text"]
lines = text.split("\n")
if len(text) < 500: return False # 太短
if sum(1 for l in lines if l.endswith(".")) / max(len(lines),1) < 0.3:
return False # 句子终结率太低(多为列表垃圾)
if sum(c.isalpha() for c in text) / len(text) < 0.6:
return False # 字母占比低 → 代码垃圾/乱码
if any(w in text.lower() for w in BAD_WORDS): return False
if "{" in text and text.count("{") / len(text) > 0.001:
return False # JS 泄漏
return True

经验:启发式过滤通常会砍掉 80-95% 的原始网页,这是正常的。

3. 质量分类器​

启发式之上再叠一个学习型过滤器:用 fastText 或小型 BERT,以"Wikipedia/教科书为正样本、随机网页为负样本"训练打分器,只保留高分文档。FineWeb-Edu、Phi 都证明了这一步的收益远超想象——1.3T 精选可以打过 15T 粗选。

4. 去重:三层防线​

  1. URL 去重:同一地址只留最新快照。
  2. 精确去重:文档哈希(SHA)去完全相同文档。
  3. 近似去重(MinHash + LSH):网页间大量"轻微改动的转载",字符级 n-gram 的 Jaccard 相似度 > 0.8 视为重复。
from datasketch import MinHash, MinHashLSH

def minhash(text, num_perm=128, n=5):
m = MinHash(num_perm=num_perm)
for i in range(len(text) - n):
m.update(text[i:i+n].encode("utf-8"))
return m

lsh = MinHashLSH(threshold=0.8, num_perm=128)
for doc_id, text in docs:
m = minhash(text)
if not lsh.query(m): # 没有近似重复
lsh.insert(doc_id, m)

为什么要去重?重复数据等效于"过采样",既浪费算力又诱发模型逐字背诵(memorization),还抬高隐私泄露风险。Pythia 的实验表明去重后模型记忆率可降一个数量级。

5. 去污染(Decontamination)​

基准测试集(MMLU、GSM8K…)的题目经常混入爬取语料,导致评测虚高。标准做法:对每个测试样本取 n-gram(常用 13-gram),与语料碰撞,命中即删。

三、数据配比:配比本身就是超参​

LLaMA-1 的经典配比:

领域占比
Common Crawl 网页67%
C415%
GitHub 代码4.5%
Wikipedia4.5%
书籍4.5%
arXiv 论文2.5%
StackExchange2%

经验法则:

  • 代码占比 5-20%:哪怕做通用模型也要加代码——它对推理能力有正迁移。
  • 高质量数据多轮、低质量数据一轮:Wikipedia 可以训 2-4 epoch,网页只训不到一个 epoch。
  • 退火阶段(annealing):训练末期混入更高比例的教科书/高质量数据并快速衰减 lr,效果显著。
  • DoReMi / 代理模型:用小代理模型学习最优领域权重,再迁移到大模型,避免人肉试错。

四、数据打包与训练格式​

# 把清洗后的文本分词并拼成定长块,供 get_batch 使用
import numpy as np, tiktoken

enc = tiktoken.get_encoding("gpt2")
EOT = enc.eot_token # 文档边界符,防止跨文档注意力污染

def pack(docs, out_path, block=1024):
buf = []
for doc in docs:
buf.extend(enc.encode_ordinary(doc) + [EOT])
arr = np.array(buf, dtype=np.uint16)
arr.tofile(out_path) # 训练时 memmap 随机切片

注意点:

  • EOT 分隔:文档间必须插边界 token,否则模型把两篇不相关文章当一篇续写。
  • 是否屏蔽跨文档注意力:先进实现(如 LLaMA-3 的训练栈)在 packed 序列内用 attention mask 阻止跨文档注意力,小数据集可省略。
  • uint16 vs uint32:vocab < 65536 用 uint16,省一半磁盘。

五、质量检查清单​

  • 抽样 100 篇人工读:有没有导航栏残片、乱码、机器翻译腔?
  • 语言分布、域名分布、时间分布是否符合预期?
  • 去重率报告(一般应去掉 30-50%)?
  • 与目标评测集的 n-gram 碰撞率为 0?
  • 用一小批数据训个小模型,PPL 是否显著优于未清洗数据?

数据就绪后,进入 预训练工程。