数据集构建
高质量数据集是模型能力的上限,模型只能学到数据中包含的知识。数据工程往往是 LLM 项目中投入产出比最高的环节。
在大模型体系中的位置
数据集构建(本章)
│
├── 预训练数据 ──> 预训练阶段(万亿 token 无标注文本)
│
├── SFT 数据 ──> 监督微调阶段(万级~十万级指令-回答对)
│
└── 偏好数据 ──> RLHF/DPO 对齐阶段(万级 chosen/rejected 对)数据贯穿整个 LLM 训练流水线。每个阶段对数据的规模、格式和质量要求截然不同。
预训练数据 vs 后训练数据
| 维度 | 预训练数据 | SFT 数据 | 偏好数据 |
|---|---|---|---|
| 规模 | 1T-15T tokens | 1万-100万条 | 1万-50万条 |
| 格式 | 纯文本(文档级) | 指令-回答对 | chosen/rejected 对 |
| 质量标准 | 宁缺毋滥,去重很关键 | 高质量 >> 大规模 | 对比差异要有意义 |
| 来源 | 网页爬虫、书籍、代码 | 人工标注 + 合成数据 | 人工标注 + 模型生成 |
| 成本 | 主要是清洗和计算成本 | 人工标注成本高 | 对比标注更耗时 |
| 关键挑战 | 去重、质量过滤、配比 | 多样性和难度覆盖 | 确保对比差异有意义 |
一个直觉:预训练数据决定模型"知道什么",SFT 数据决定模型"怎么说话",偏好数据决定模型"说得多好"。
预训练数据集构建
数据来源与规模
| 数据源 | 典型规模(tokens) | 特点 | 代表数据集 |
|---|---|---|---|
| Common Crawl | 数万亿 | 覆盖广、噪声大 | FineWeb, RedPajama |
| Wikipedia | 英文 ~40 亿 / 中文 ~10 亿 | 高质量、结构化 | 20+ 语言版本 |
| GitHub 代码 | 数千亿 | 提升推理和代码能力 | The Stack v2 |
| 书籍语料 | 数百亿 | 长文本、高质量叙述 | Books3, Gutenberg |
| 学术论文 | ArXiv ~300 亿 | 数学和科学推理 | RedPajama-ArXiv |
| StackOverflow | ~100 亿 | 问答格式、实践知识 | Stack Exchange dump |
| 新闻 | 数百亿 | 时事知识 | CC-News |
# GPT-2 预训练数据的加载和处理
import json
def load_jsonl(file_path):
"""加载 JSONL 格式的预训练数据"""
data = []
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
if line.strip():
data.append(json.loads(line)['text'])
return data
data = load_jsonl('./data.jsonl')
print(f"文档数量: {len(data)}") # 5000 篇文档
print(f"示例: {data[0][:80]}...") # "The Technology Report empowers..."
# 预训练的基本流程:文档拼接 -> 分块
data_total = '\n'.join(data)
print(f"总字符数: {len(data_total):,}") # 15,075,691 字符数据清洗流水线
URL 过滤
第一道防线:基于 URL 规则过滤明显的低质量来源。
- 黑名单域名:成人站点、赌博站点、已知 spam 域名
- 白名单域名(高权重):edu, gov, 知名媒体、Wikipedia
- URL 模式过滤:移除过长 URL、含有广告参数的 URL
语言识别 (fastText)
# 使用 fastText 进行语言检测
import fasttext
model = fasttext.load_model('lid.176.bin')
def detect_language(text, threshold=0.65):
"""检测文本语言,返回语言代码和置信度"""
predictions = model.predict(text.replace('\n', ' ')[:500])
lang = predictions[0][0].replace('__label__', '')
score = predictions[1][0]
return lang, score
# 过滤:只保留目标语言且置信度 > 0.65 的文档
lang, score = detect_language("This is an English text.")
# lang='en', score=0.99质量过滤 (perplexity, n-gram, heuristic rules)
基于规则的启发式过滤(FineWeb 使用的部分规则):
| 规则 | 阈值 | 说明 |
|---|---|---|
| 平均行长度 | > 10 字符 | 过滤列表/目录页 |
| 最长行长度 | < 100,000 字符 | 过滤异常文件 |
| 字母数字比例 | > 0.6 | 过滤编码乱码 |
| 重复行比例 | < 0.3 | 过滤模板页面 |
| 特殊字符比例 | < 0.2 | 过滤代码注释/日志 |
| 包含 "lorem ipsum" | 移除 | 占位符文本 |
| 停用词覆盖率 | > 一定比例 | 确保是自然语言 |
基于困惑度的过滤:
用预训练好的 n-gram 语言模型(如 KenLM)计算每篇文档的困惑度。高困惑度意味着文本不像自然语言(可能是乱码、机器生成的 spam)。
基于分类器的过滤(FineWeb-Edu 方法):
训练一个质量分类器,以 Wikipedia 和教科书为正例、随机网页为负例,对每篇文档打分。FineWeb-Edu 使用 Llama 3 对 50 万网页打教育质量分(0-5 分),然后训练 fastText 分类器在全量数据上快速推断。
MinHash 去重
去重是预训练数据清洗中最关键的步骤之一。文档级去重通常使用 MinHash + LSH(局部敏感哈希)。
MinHash 签名的计算原理:
- 将文档转换为 n-gram 集合(如 5-gram)
- 选择
个不同的哈希函数 - 对每个哈希函数,计算所有 n-gram 的哈希值,取最小值作为签名的一个分量
- 两篇文档的 MinHash 签名碰撞概率 = Jaccard 相似度
LSH(局部敏感哈希)加速:
将
越大:越容易匹配(高召回,可能误判) 越大:越难匹配(高精度,可能遗漏)
典型配置:
去污染 (Decontamination)
确保评测集的内容不出现在训练集中,否则评测分数会虚高。
方法:
- 收集所有常用评测集(MMLU, HumanEval, GSM8K, HellaSwag 等)
- 将评测集内容转换为 n-gram 集合
- 在训练数据中搜索高度重叠的段落
- 移除或替换包含评测集内容的训练文档
Llama 3 使用 10-gram 重叠度来判定数据污染,移除了与评测集有显著重叠的训练样本。
SFT 数据格式
Alpaca 格式
最早由 Stanford Alpaca 提出的简洁格式:
{
"instruction": "将以下段落翻译成英文",
"input": "人工智能正在改变世界。",
"output": "Artificial intelligence is changing the world."
}当 input 为空时,只有 instruction 和 output。简单直接,适合单轮任务。
ShareGPT 格式
来自用户分享的 ChatGPT 对话,天然支持多轮:
{
"conversations": [
{"from": "system", "value": "你是一个有帮助的助手。"},
{"from": "human", "value": "什么是机器学习?"},
{"from": "gpt", "value": "机器学习是人工智能的一个分支..."},
{"from": "human", "value": "能举个例子吗?"},
{"from": "gpt", "value": "比如垃圾邮件分类..."}
]
}OpenAI 格式
OpenAI API 风格的 messages 格式,已成为事实标准:
{
"messages": [
{"role": "system", "content": "你是一个有帮助的助手。"},
{"role": "user", "content": "什么是机器学习?"},
{"role": "assistant", "content": "机器学习是人工智能的一个分支..."}
]
}Chat Template
不同模型使用不同的 chat template 来标记角色边界。tokenizer 需要正确处理这些特殊标记。
ChatML 格式(Qwen 系列使用):
<|im_start|>system
你是xiaoming智能体,请安全详细回答用户的问题<|im_end|>
<|im_start|>user
$sin^2x+cos^2x=?<|im_end|>
<|im_start|>assistant
结果为 $\boxed{1}$<|im_end|>Llama 格式:
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
You are a helpful assistant.<|eot_id|><|start_header_id|>user<|end_header_id|>
Hello!<|eot_id|><|start_header_id|>assistant<|end_header_id|>
Hi there!<|eot_id|># Chat Template 的实际使用
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained('Qwen/Qwen3-0.6B', trust_remote_code=True)
messages = [
{'role': 'system', 'content': '你是xiaoming智能体,请安全详细回答用户的问题'},
{'role': 'user', 'content': '"哈基米"翻译成英文'},
{'role': 'assistant', 'content': '"哈基米"翻译成英文通常是 "Hakimi"(人名音译)。'},
]
# apply_chat_template 会自动添加特殊 token
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=False)
print(prompt)
# <|im_start|>system
# 你是xiaoming智能体,请安全详细回答用户的问题<|im_end|>
# <|im_start|>user
# "哈基米"翻译成英文<|im_end|>
# <|im_start|>assistant
# "哈基米"翻译成英文通常是 "Hakimi"(人名音译)。<|im_end|>SFT 训练的关键:只在 assistant 部分计算损失
# SFT 数据处理的核心逻辑
# 构建 labels 时,将非 assistant 部分的 label 设为 -100(忽略)
DEFINIED_SYSTEM_PROMPT = '你是xiaoming智能体,请安全详细回答用户的问题'
messages_list = [
[
{'role': 'system', 'content': DEFINIED_SYSTEM_PROMPT},
{'role': 'user', 'content': '$sin^2x+cos^2x=?'},
{'role': 'assistant', 'content': '结果为 $\\boxed{1}$'},
{'role': 'user', 'content': '为什么?'},
{'role': 'assistant', 'content': '根据勾股定理可证 $sin^2x+cos^2x=1$'},
],
[
{'role': 'system', 'content': DEFINIED_SYSTEM_PROMPT},
{'role': 'user', 'content': '什么是人工智能?'},
{'role': 'assistant', 'content': '人工智能是让机器模拟人类思维的技术。'},
],
]
# 训练时的 label 构造:只计算 assistant 部分的 loss
# input: <system>...<user>...<assistant>回答内容<eos>
# labels: [-100]...[-100]...[回答内容的token_ids]...[eos_id]
# ↑ 只有这部分参与损失计算合成数据生成
Self-Instruct
Wang et al. (2022) 提出的方法,用模型自身生成指令数据:
流程:
1. 准备 175 条人工编写的 seed instructions
2. 从已有 task pool 中随机抽取 6 条作为 few-shot 示例
3. 让模型生成新的 instruction
4. 让模型为新 instruction 生成 input 和 output
5. 质量过滤:去重 + ROUGE-L 过滤相似指令 + 关键词过滤
6. 将通过过滤的样本加入 task pool
7. 重复步骤 2-6Self-Instruct 生成的 52K 数据训练出的模型(Alpaca)在开放指令上已经接近 text-davinci-003 的 performance。
Evol-Instruct (WizardLM)
Xu et al. (2023) 提出的指令进化方法。核心思想:用 LLM 对已有指令进行"进化",增加复杂度和多样性。
两种进化方向:
| 类型 | 策略 | 示例 |
|---|---|---|
| 深度进化 | 增加约束、增加推理步骤、换具体场景 | "排序数组" → "用快速排序算法对包含重复元素的整数数组进行排序,要求空间复杂度 O(1)" |
| 广度进化 | 变换主题、变换任务类型 | "排序数组" → "设计一个数据库索引策略" |
进化 Prompt 示例(深度):
"I want you to act as a Prompt Rewriter.
Your objective is to rewrite a given prompt into a more complex version.
The rewritten prompt must be reasonable, understood by humans, and answerable.
#Given Prompt#:
写一个 Python 函数来计算列表的平均值。
#Rewritten Prompt#:
(让 LLM 生成更复杂的版本)"使用 GPT-4 / Claude 生成数据的最佳实践
- Prompt 设计要具体:明确角色、输出格式、长度要求、质量标准
- temperature 调高以增加多样性:通常 0.7-1.0
- 批量生成 + 后过滤:生成 5x 的量,过滤掉低质量的
- 避免模式坍缩:定期检查生成内容的多样性,用不同的 seed prompt
- 标注质量校验:随机抽样 5-10% 人工检查
Seed Task 设计
好的 seed task 应当覆盖:
| 维度 | 示例 |
|---|---|
| 任务类型 | 分类、生成、翻译、总结、推理、代码、数学 |
| 难度层级 | 基础知识 → 分析应用 → 创造性推理 |
| 输出格式 | 短回答、长文、代码、JSON、表格、列表 |
| 语言风格 | 正式、口语化、技术文档、对话 |
质量把控
合成数据的常见问题及对策:
| 问题 | 检测方法 | 对策 |
|---|---|---|
| 幻觉/事实错误 | 对比知识库、人工抽检 | 增加事实验证步骤 |
| 格式不一致 | 正则表达式检查 | Prompt 中明确格式要求 |
| 多样性不足 | n-gram diversity 统计 | 增加 seed 多样性、升高 temperature |
| 难度分布不均 | 人工抽样评估 | 使用 Evol-Instruct 增加难题 |
| 含有 AI 痕迹 | 检测"As an AI"等套话 | 过滤或 prompt 中禁止 |
数据增强技术
Rejection Sampling
让模型对同一问题生成多个回答,用 reward model 打分,只保留高分回答。
Input: "解释量子纠缠"
├── 回答 1 (reward=0.85) ✓ 保留
├── 回答 2 (reward=0.32) ✗ 丢弃
├── 回答 3 (reward=0.91) ✓ 保留
└── 回答 4 (reward=0.45) ✗ 丢弃Llama 3 在 SFT 阶段就大量使用 rejection sampling 来筛选训练数据。
Chain-of-Thought 扩展
对已有的问答数据,补充推理链(chain-of-thought),提升模型推理能力:
{
"instruction": "一个水池有两个水管,A管每小时注水3吨,B管每小时放水1吨。水池中有10吨水,同时开两管,几小时后水池有22吨水?",
"output_without_cot": "6小时",
"output_with_cot": "让我们一步步思考:\n1. A管注水速度:3吨/小时\n2. B管放水速度:1吨/小时\n3. 净注水速度:3-1=2吨/小时\n4. 需要增加的水量:22-10=12吨\n5. 所需时间:12÷2=6小时\n\n答案是 6 小时。"
}多样性增强 (Persona-driven)
让模型扮演不同角色来回答同一问题,增加回答的多样性:
同一问题:"解释什么是递归"
Persona 1(大学教授):递归是一种函数调用自身的编程技术...(学术风格)
Persona 2(少儿编程老师):想象你站在两面镜子中间...(比喻风格)
Persona 3(面试官):递归需要满足两个条件:基准情形和递归步骤...(面试风格)难度递增 (Auto-Evol)
在 Evol-Instruct 基础上,自动化地逐步增加难度:
Level 1: 写一个函数计算斐波那契数列第 n 项
Level 2: 写一个函数计算斐波那契数列第 n 项,要求时间复杂度 O(log n)
Level 3: 实现广义斐波那契数列的矩阵快速幂解法,支持自定义初始值和递推系数偏好数据构建
Chosen/Rejected pair 的收集
偏好数据的核心形式:对于同一个 prompt,提供一个"好的"回答(chosen)和一个"差的"回答(rejected)。
{
"prompt": "什么是梯度下降?",
"chosen": "梯度下降是一种优化算法。它通过计算损失函数对参数的梯度,沿梯度反方向更新参数,逐步找到损失函数的(局部)最小值。学习率控制每步更新的幅度。",
"rejected": "梯度下降就是让损失变小的方法。"
}好的偏好数据要求 chosen 和 rejected 之间有有意义的质量差异,而不仅仅是长度差异。
人工标注 vs 模型标注
| 方法 | 优势 | 劣势 |
|---|---|---|
| 人工标注 | 质量高、反映真实人类偏好 | 成本高、速度慢、标注者之间一致性差 |
| 模型标注 (AI Feedback) | 成本低、速度快、一致性高 | 可能引入模型偏见、难以超越标注模型能力 |
| 混合方法 | 平衡质量和效率 | 需要设计好的质量控制流程 |
实践建议:用人工标注建立"金标准"子集(~1000 条),用于校准模型标注的质量;剩余大量数据用模型标注 + 人工抽检。
Bradley-Terry 模型的数据需求
RLHF 中的 reward model 通常使用 Bradley-Terry 模型训练:
数据需求:
- 最低规模:~10K 对偏好数据可以训练出有意义的 reward model
- 推荐规模:50K-200K 对
- 质量要求:标注者间一致性(inter-annotator agreement)应 > 70%
- 覆盖要求:prompt 应覆盖多种任务类型和难度级别
数据质量评估
自动评估指标
| 指标 | 适用场景 | 工具 |
|---|---|---|
| 困惑度 (Perplexity) | 预训练数据质量 | KenLM, GPT-2 |
| ROUGE / BLEU | 生成任务的参考对比 | nlg-eval |
| BERTScore | 语义相似度评估 | bert_score 包 |
| 多样性 (Distinct n-gram) | 检测数据单调性 | 自定义脚本 |
| 平均长度 & 长度分布 | 检测长度偏差 | 统计分析 |
Reward Model 打分
用训练好的 reward model 对 SFT 数据进行评分,是一种高效的质量筛选方法:
# 伪代码:使用 reward model 筛选数据
for sample in sft_dataset:
score = reward_model.score(sample['prompt'], sample['response'])
if score > threshold:
filtered_dataset.append(sample)
# 典型做法:保留 top 50-70% 的数据去重与去污染验证
SFT 数据去重:
# 基于编辑距离的去重
from difflib import SequenceMatcher
def is_near_duplicate(text1, text2, threshold=0.85):
ratio = SequenceMatcher(None, text1, text2).ratio()
return ratio > threshold
# 或者使用 embedding 相似度
# cosine_sim = cos(embed(text1), embed(text2))去污染验证清单:
- [ ] 检查训练数据与 MMLU 的 n-gram 重叠
- [ ] 检查训练数据与 HumanEval 的代码重叠
- [ ] 检查训练数据与 GSM8K 的数学题重叠
- [ ] 检查训练数据与 HellaSwag, ARC 等的重叠
- [ ] 使用 13-gram 或更严格的标准匹配
苏格拉底时刻
用 GPT-4 生成的合成数据训练小模型,是否构成"模型坍缩"(Model Collapse)的风险? 是的。如果多代模型都用前代生成的合成数据训练,分布会逐渐退化。解决方案:每一代都混入真实人工数据,保持数据分布的多样性。
数据去重为什么如此重要? 如果某段文本重复出现 100 次,模型会对它过拟合(记忆而非理解),同时挤占其他数据的学习机会。实验表明去重可以让模型在相同计算量下获得更好的泛化能力。
SFT 数据的数量并不需要很大(通常几万条),为什么少量高质量数据反而比大量低质量数据效果更好? 因为 SFT 不是在教模型新知识(那是预训练的工作),而是在"激活"模型已有的能力,教它以正确的格式输出。这更像"调音"而非"教学",因此质量远比数量重要。
在构建偏好数据时,如何确保 chosen 和 rejected 的差异是有意义的? 差异应该体现在事实准确性、完整性、逻辑性等维度,而不是仅仅是长度或格式差异。一个好的做法是让标注者写下"为什么 chosen 更好"的理由。
Chat Template 的特殊 token 如果处理不当会怎样? 模型会无法正确区分用户输入和自己的输出,导致在生成时"角色混乱"。这就是为什么 tokenizer 的
apply_chat_template方法如此重要。
常见问题 & 面试考点
| 问题 | 要点 |
|---|---|
| 预训练数据和 SFT 数据的核心区别? | 预训练用无标注文本学习知识,SFT 用指令数据激活能力 |
| MinHash 去重的原理? | 用 n-gram 集合的最小哈希值近似 Jaccard 相似度,LSH 加速候选对检索 |
| 解释 Self-Instruct | 用模型自身从 seed tasks 出发生成指令-回答对,迭代扩充 |
| Evol-Instruct 的两个进化方向? | 深度进化(增加复杂度)和广度进化(扩展话题) |
| SFT 训练时只在 assistant 部分计算损失,为什么? | 因为 system 和 user 部分是"输入条件",不是模型需要学习生成的内容 |
| 偏好数据的 chosen/rejected 差异太小会怎样? | Reward model 无法学到有意义的偏好信号,RLHF 效果会很差 |
| ChatML 和 Llama chat template 有什么区别? | ChatML 用 <|im_start|>/<|im_end|>,Llama 用 <|start_header_id|>/<|eot_id|> |
| 数据去污染为什么重要? | 防止训练数据泄露评测集内容,导致评测分数虚高 |
推荐资源
- Self-Instruct: Aligning LMs with Self-Generated Instructions - 合成数据的开山之作
- WizardLM: Empowering LLMs to Follow Complex Instructions - Evol-Instruct 方法
- FineWeb 技术报告 - 工业级数据清洗流水线
- Deduplicating Training Data Makes Language Models Better - 去重的重要性
- The RefinedWeb Dataset - 高质量网页数据集构建
- RedPajama - 开源数据集复现
- Llama 3 技术报告 - 数据工程最佳实践
- LIMA: Less Is More for Alignment - 少量高质量 SFT 数据的威力