27届大模型面试准备(二十三):数据工程与合成数据------配方、去重、质量过滤与数据飞轮
上一篇《推理时扩展与强化学习新范式》讲的是如何在推理阶段榨取更多能力,这一篇回到源头:模型能力的上限,其实早在数据准备阶段就被决定了。这是本系列第二十三篇。过去两年有个越来越明确的共识------在架构趋同、算力可买的今天,数据是唯一还没有被商品化的差异来源。Llama 3 相比 Llama 2 的主要提升来自 15T token 的数据规模与更严的过滤,而不是架构改动;DeepSeek、Qwen 的技术报告里,数据配方的篇幅普遍超过模型结构。可惜的是,绝大多数候选人对数据的理解停留在"清洗一下、去个重",一问"你怎么判断某批数据该不该加进去"就答不上来。本文按"数据在能力链条中的位置 → 预训练数据配方 → 去重的三个层次 → 质量过滤流水线 → 数据配比与课程安排 → 合成数据方法谱系 → 蒸馏与模型坍缩 → SFT 数据的选择 → 数据污染检测 → 数据飞轮"展开,结尾给面试速答和高频追问清单。
一、先建立框架:数据决定了什么
1.1 三个阶段的数据,服务三个不同目标
数据在模型生命周期中的三段分工
┌────────────────┬────────────────┬────────────────┐
│ 预训练数据 │ SFT 数据 │ 偏好/RL 数据 │
├────────────────┼────────────────┼────────────────┤
│ 规模: 10T~30T │ 规模: 1万~100万 │ 规模: 1万~50万 │
│ 单位: token │ 单位: 对话样本 │ 单位: 偏好对 │
│ 目标: 世界知识 │ 目标: 指令遵循 │ 目标: 价值对齐 │
│ + 语言建模 │ + 格式规范 │ + 风格偏好 │
│ 决定: 能力上限 │ 决定: 能力释放 │ 决定: 体验下限 │
│ 成本: 千万美元 │ 成本: 十万美元 │ 成本: 十万美元 │
└────────────────┴────────────────┴────────────────┘
│ │ │
▼ ▼ ▼
"知不知道" "会不会说" "愿不愿意说"
这张图是面试里最值钱的一张。它能立刻回答一个高频问题:"SFT 能不能给模型注入新知识?"
答案是基本不能。预训练决定了知识的存量,SFT 只是教模型如何把已有知识以特定格式取出来。用 SFT 硬灌新知识,模型会学到"要自信地回答这类问题"的表层模式,但没有对应的内部表示支撑,结果就是加剧幻觉。这个现象在论文里叫 superficial alignment hypothesis(表层对齐假说),LIMA 那篇用 1000 条精选样本就达到接近 RLHF 的效果,正是这个假说的实证支持。
1.2 数据质量与规模的权衡曲线
早期的 Scaling Law(Kaplan 2020)只谈参数量和 token 数,把数据当成同质的。Chinchilla(2022)修正了配比。但真正的转折是 phi 系列 证明了第三个维度:在同等 token 预算下,教科书级质量的数据能让 1.3B 模型在代码任务上超过 16B 模型。
| 路线 | 代表 | 数据规模 | 数据质量 | 结论 |
|---|---|---|---|---|
| 大力出奇迹 | GPT-3 | 300B token | 弱过滤 | 有效但低效 |
| 计算最优 | Chinchilla | 1.4T token | 中等过滤 | 参数/数据配比优化 |
| 质量优先 | phi-1/2/3 | 7B~3T token | 极严+合成 | 小模型高密度 |
| 规模+质量 | Llama 3 | 15T token | 严格多级过滤 | 当前主流 |
| 超训练 | Qwen2.5/3 | 18T~36T token | 严格+领域增强 | 远超 Chinchilla 最优 |
值得注意的是,现在的主流做法(Llama 3、Qwen)已经远远偏离 Chinchilla 最优点。Chinchilla 说 8B 模型配 160B token 最优,Llama 3 8B 却训了 15T token,是 90 倍。原因很简单:Chinchilla 优化的是"训练算力固定下的最终 loss",而工业界优化的是"推理算力固定下的最终能力"。模型要服务上亿次请求,多训一次省下的每一分推理成本都会被放大无数倍。这个视角差异是面试里区分"读过论文"和"做过工程"的分水岭。
二、预训练数据配方
2.1 典型的数据源构成
一个 15T token 级预训练语料的典型构成
网页文本 (CommonCrawl 等) ████████████████████ 50~60%
代码 (GitHub/Stack) ██████ 15~20%
书籍/论文 (Books/arXiv) ███ 5~8%
百科 (Wikipedia 多语言) ██ 3~5%
问答/论坛 (StackExchange) ██ 3~5%
数学 (OpenWebMath 等) ██ 3~5%
多语言 ███ 5~10%
合成数据 ██ 2~10%(上升中)
注意:代码占比远超其直觉重要性。
即使目标不是代码模型,代码数据也被证明能显著提升
逻辑推理、结构化输出和长程依赖能力。
"为什么纯语言模型也要喂大量代码"是高频面试题。三个层面的答案:
- 形式层:代码有严格的语法树结构和长程依赖(函数定义与调用可能隔几百行),逼迫模型学习精确的结构化建模能力。
- 语义层:代码是"可执行的逻辑",注释与实现的配对天然是"自然语言到形式化"的对齐数据。
- 实证层:多篇消融显示,去掉代码数据后,模型在 BBH、GSM8K 这类需要多步推理的任务上大幅下降,即使这些任务本身与代码无关。
2.2 网页数据的处理管线
CommonCrawl 原始数据大约 100T+ token,最后能进入训练的往往不到 10%。完整管线:
CommonCrawl WARC (原始网页)
│
▼ [1] 文本抽取:trafilatura / resiliparse
│ 去掉导航栏、广告、页脚样板
▼
纯文本
│
▼ [2] 语言识别:fastText langid,按语种分流
│ 置信度 < 0.65 直接丢弃
▼
语种分桶
│
▼ [3] 规则过滤:启发式质量信号(见 2.3)
▼
规则通过
│
▼ [4] 去重:URL → 文档级 MinHash → 段落级精确去重
▼
去重后
│
▼ [5] 模型过滤:质量分类器 / 困惑度 / 教育价值打分
▼
高质量子集
│
▼ [6] 安全过滤:毒性、PII、版权、基准污染
▼
最终训练语料 (约原始的 5~10%)
关键工程细节:步骤顺序不能随便换。去重必须在昂贵的模型过滤之前,否则会浪费大量算力给重复内容打分。但语言识别又必须在去重之前,因为跨语种做 MinHash 没有意义且会拖慢。
2.3 启发式质量信号
这一层是最便宜的,能过滤掉 60%~70% 的垃圾。Gopher 论文和 RefinedWeb 给出了一套被广泛采用的规则:
| 信号 | 阈值(英文,中文需调整) | 拦截的典型垃圾 |
|---|---|---|
| 文档词数 | < 50 或 > 100000 | 碎片/爬虫拼接 |
| 平均词长 | < 3 或 > 10 | 乱码/base64 |
符号词比(#、...) |
> 0.1 | 目录页/代码残渣 |
| 省略号结尾行占比 | > 0.3 | 列表页/摘要聚合页 |
| 含停用词的行占比 | < 0.8 | 关键词堆砌 SEO 页 |
| 重复行占比 | > 0.3 | 模板生成页 |
| 首字母大写行占比 | > 0.9 | 导航菜单 |
| 词表内单词比例 | < 0.8 | 乱码/非自然文本 |
中文场景有额外的坑:中文没有空格分词,词数统计要先分词或改用字数;停用词表要重建;而且中文互联网的样板文本("本文由XX整理发布"、"未经授权禁止转载")比英文更泛滥,需要额外的样板句黑名单。
python
import re
from collections import Counter
CN_BOILERPLATE = [
"未经授权禁止转载", "版权归原作者所有", "本文由", "点击上方蓝字",
"扫码关注", "免责声明", "如有侵权请联系删除", "阅读原文",
]
def heuristic_filter_zh(text: str) -> tuple[bool, str]:
"""中文网页启发式过滤,返回 (是否保留, 拒绝原因)"""
n_char = len(text)
if n_char < 100:
return False, "too_short"
if n_char > 300_000:
return False, "too_long"
lines = [ln.strip() for ln in text.split("\n") if ln.strip()]
if not lines:
return False, "empty"
# 重复行占比:模板生成页的典型特征
counts = Counter(lines)
dup_ratio = 1 - len(counts) / len(lines)
if dup_ratio > 0.3:
return False, "dup_lines"
# 中文字符占比:过低说明不是中文正文
cn_chars = len(re.findall(r"[\u4e00-\u9fff]", text))
if cn_chars / n_char < 0.3:
return False, "low_cn_ratio"
# 标点密度:正常中文正文约 0.03~0.12
punct = len(re.findall(r"[,。!?;:、]", text))
if not (0.02 < punct / max(cn_chars, 1) < 0.20):
return False, "abnormal_punct"
# 样板句
hit = sum(1 for b in CN_BOILERPLATE if b in text)
if hit >= 3:
return False, "boilerplate"
# 以省略号/截断符结尾的行(聚合页特征)
trunc = sum(1 for ln in lines if ln.endswith(("...", "...", "查看更多", "阅读全文")))
if trunc / len(lines) > 0.3:
return False, "truncated_lines"
return True, "ok"
面试时如果能说出"中文要单独设计过滤规则,因为英文那套阈值直接套过来会误杀大量正常中文",会比背 Gopher 规则更有说服力。
三、去重:三个层次与工程实现
3.1 为什么去重如此重要
去重不只是省算力。Deduplicating Training Data Makes Language Models Better 那篇给出了三个硬结论:
- 去重后模型困惑度更低,且用更少的训练步数达到同等效果;
- 未去重的模型会逐字背诵训练数据,输出中约 1% 的 token 是训练集的直接拷贝,去重后降低 10 倍;
- 训练集与测试集的重叠会严重高估评测结果。
第二点直接关系到隐私和版权风险,是产品上线前的合规红线。
3.2 三个层次
去重的三个层次与代价
层次1: URL / 精确哈希去重
方法: SHA256(normalized_text) 建索引
代价: O(N),最便宜
命中: 完全相同的文档(镜像站、转载)
典型去除率: 30%~50%
│
▼
层次2: 模糊去重 (Near-dedup)
方法: MinHash + LSH,Jaccard 相似度阈值 0.8
代价: O(N) 但常数大,需要分布式
命中: 改动少量文字的转载、模板化生成页
典型去除率: 再去 20%~40%
│
▼
层次3: 子串级去重 (Substring)
方法: 后缀数组,去除长度 > 50 token 的重复子串
代价: O(N log N),内存压力最大
命中: 文档不同但含相同长段落(引用、免责声明)
典型去除率: 再去 5%~15%
大多数团队只做前两层,第三层因为后缀数组的内存开销(约为语料的 8~10 倍)而放弃。但第三层对减少背诵最有效。
3.3 MinHash + LSH 的实现要点
python
import hashlib
import numpy as np
from collections import defaultdict
class MinHashLSH:
"""
文档级模糊去重。
num_perm=128, bands=16, rows=8 时,
相似度 0.8 的文档对被召回概率约 0.98,
相似度 0.5 的被误召回概率约 0.06。
"""
def __init__(self, num_perm=128, bands=16):
assert num_perm % bands == 0
self.num_perm = num_perm
self.bands = bands
self.rows = num_perm // bands
rng = np.random.RandomState(42)
# 全局一致的置换参数,必须固定种子否则分布式各节点不一致
self.a = rng.randint(1, 2**31 - 1, size=num_perm, dtype=np.int64)
self.b = rng.randint(0, 2**31 - 1, size=num_perm, dtype=np.int64)
self.prime = (1 << 61) - 1
self.buckets = defaultdict(list)
@staticmethod
def shingles(text, k=5):
"""中文用字符 5-gram;英文建议用词级 5-gram"""
text = "".join(text.split()) # 去掉所有空白,抗排版差异
return {text[i:i+k] for i in range(max(len(text)-k+1, 1))}
def signature(self, text):
sh = self.shingles(text)
if not sh:
return None
# 每个 shingle 先哈到 64bit
hs = np.array(
[int.from_bytes(hashlib.blake2b(s.encode(), digest_size=8).digest(), "big")
for s in sh], dtype=np.uint64
).astype(np.int64) & 0x7FFFFFFFFFFFFFFF
# 一次性向量化算 num_perm 个最小哈希
perm = (np.outer(hs, self.a) + self.b) % self.prime
return perm.min(axis=0)
def add(self, doc_id, sig):
"""按 band 分桶;同桶的文档才需要精确比对"""
for i in range(self.bands):
band = sig[i*self.rows:(i+1)*self.rows]
key = (i, hashlib.blake2b(band.tobytes(), digest_size=8).hexdigest())
self.buckets[key].append(doc_id)
def candidate_pairs(self):
seen = set()
for ids in self.buckets.values():
if len(ids) < 2 or len(ids) > 5000: # 超大桶多半是模板页,单独处理
continue
for i in range(len(ids)):
for j in range(i+1, len(ids)):
p = (ids[i], ids[j]) if ids[i] < ids[j] else (ids[j], ids[i])
if p not in seen:
seen.add(p)
yield p
三个容易被追问的实现坑:
- 置换参数必须全局固定。分布式跑的时候如果各 worker 各自初始化随机数,签名就不可比,去重完全失效。这是实际项目里最常见的低级错误。
- 超大桶要特判。某些模板页会让一个 band 桶里塞进几十万文档,两两比较直接爆炸。做法是设上限,超过就整桶按"疑似模板"降权或抽样保留。
- 保留哪一个副本有讲究。不能随机留一个。合理策略是留 URL 域名权威度最高的、或文本最长的(通常样板最少)、或抓取时间最新的。
3.4 跨数据集去重的顺序问题
当有多个数据源(网页、书籍、论文)时,去重顺序影响最终配比:
错误做法:全部混一起去重
→ 网页里的书籍盗版内容可能"赢过"正版书籍数据
→ 高质量源被低质量源挤掉
正确做法:按质量优先级分层去重
① 先内部去重:书籍内部、论文内部、网页内部
② 再跨层去重:以高质量源为基准,
从低质量源中删除与之重复的内容
优先级:书籍 > 论文 > 百科 > 问答 > 网页
四、模型化质量过滤
4.1 三种主流方法
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 分类器过滤 | 用高质量样本作正例、随机网页作负例训 fastText | 极快,10万文档/秒 | 只学到"像不像维基",偏向特定风格 |
| 困惑度过滤 | 用小模型算 PPL,保留中间区间 | 无需标注 | PPL 低的可能是重复模板 |
| LLM 打分 | 用 LLM 给"教育价值"打 0~5 分,再蒸馏成小分类器 | 语义准确 | 直接打分太贵,必须蒸馏 |
FineWeb-Edu 的做法是当前最优实践,值得作为面试的标准答案:用 Llama-3-70B 给 50 万个网页样本打"教育价值 0~5 分",然后训练一个基于 BERT 的回归模型作为廉价代理,用它扫全量 15T token,保留分数 ≥ 3 的部分。结果是数据量减少到 8%,但在 MMLU、ARC 上的表现反而大幅提升。
python
# 用 LLM 打标 -> 蒸馏小模型 的核心 prompt 设计
EDU_SCORE_PROMPT = """以下是一段网页文本。请按照以下加分制评估它对
小学到大学阶段学习者的教育价值,总分 0 到 5 分:
- 如果包含一些基础信息(即使混杂广告或无关内容),加 1 分。
- 如果涉及教育相关主题,但内容零散、组织混乱,加 1 分。
- 如果对教育主题有连贯的介绍,适合学习者阅读,加 1 分。
- 如果表述清晰、结构完整、类似教科书或教程章节,加 1 分。
- 如果内容准确、深入、无冗余,达到优秀教材水准,加 1 分。
文本:
<text>
{text}
</text>
先用不超过 60 字说明理由,最后一行输出:"得分:X"
"""
为什么必须先"打理由"再"给分数"?这是评分类 prompt 的通用技巧------直接输出数字会让模型走捷径,分数分布严重集中在 3 分;强制先生成理由相当于给了推理空间,分数区分度明显变好。同样的道理适用于所有 LLM-as-Judge 场景(参见本系列 A20 评测体系那篇)。
4.2 质量过滤的失效模式
过滤不是越严越好,有三个典型陷阱:
陷阱1: 风格偏见
分类器用维基百科作正例 → 学到"百科腔"
→ 误杀口语化但信息密度高的论坛技术讨论
→ 模型丧失对话能力和长尾知识
陷阱2: 多样性坍缩
只留高分数据 → 数据分布变窄
→ 模型在 benchmark 上涨分,实际泛化变差
→ 缓解:分桶采样而非硬阈值截断
陷阱3: 语种/文化偏见
过滤器多在英文上开发 → 低资源语言被系统性过滤
→ 多语言能力崩塌
→ 缓解:每个语种独立训过滤器、独立定阈值
第二个陷阱的缓解方案值得展开:不要用硬阈值,而是按分数分桶做加权采样 。比如把分数 0~5 分成 6 桶,采样权重设为 [0, 0.1, 0.4, 1.0, 1.0, 1.0],这样低分数据不是完全丢弃而是降采样,保留了分布的长尾。
五、数据配比与课程安排
5.1 配比怎么定
暴力网格搜索不可行(一次预训练几百万美元)。工业界的做法是用小模型做代理实验:
配比搜索的标准流程
① 定义 K 个数据域(网页/代码/数学/多语言/...)
② 在 1B 参数、10B token 的小规模上跑 N 组不同配比
(N 通常 20~50 组,用拉丁超立方或 Sobol 采样)
③ 每组测一批下游任务,拟合
loss_domain = f(配比向量) 的回归模型(DoReMi / RegMix 思路)
④ 用回归模型外推到目标规模,解出最优配比
⑤ 在中等规模(7B, 100B token)验证一次
⑥ 上大规模
风险:小规模最优配比不一定在大规模最优。
已知规律:规模越大,越应该提高代码和数学的占比,
因为这些数据的收益在小模型上被容量瓶颈掩盖了。
最后一句是很好的加分点------数据配比与模型规模存在交互效应,这解释了为什么小模型上做的消融不能无脑外推。
5.2 课程学习与退火
现在主流做法不是全程固定配比,而是分阶段:
| 阶段 | token 占比 | 数据特点 | 学习率 |
|---|---|---|---|
| 通用阶段 | 70%~85% | 大规模、多样性优先 | 峰值恒定/缓降 |
| 能力增强 | 10%~20% | 提高代码/数学/推理占比 | 开始衰减 |
| 退火(annealing) | 3%~5% | 极高质量 + 少量指令数据 | 快速降到接近 0 |
| 长上下文扩展 | 1%~5% | 长文档、书籍、代码仓库 | 低学习率,调 RoPE base |
退火阶段是这两年最重要的经验之一。Llama 3 和 MiniCPM 都报告了:在训练末期用极小学习率、只喂最高质量数据(含少量 SFT 格式数据),能带来显著的 benchmark 提升,成本却只占总训练的几个百分点。直观解释是:学习率高时模型在参数空间大幅移动,最后阶段的低学习率相当于让模型"精调"到高质量数据附近的局部最优。
另外注意最后一行------长上下文扩展是单独一个阶段,不是全程用长序列训。全程长序列的注意力开销是平方级的,浪费巨大。标准做法是 95% 的训练用 4K/8K 短序列,最后几个百分点切到 32K/128K,同时调大 RoPE 的 base(这部分是明天 A24 的主题)。
六、合成数据:方法谱系
6.1 为什么必须用合成数据
两个现实压力:
- 高质量自然数据快见底了。Epoch AI 的估算是高质量英文文本存量约 10T~20T token,而单个前沿模型的训练量已经到 15T~36T。继续 scaling 只能靠合成。
- 自然数据的分布不匹配需求。互联网上几乎没有"完整的多步推理过程",人们只写结论不写思考链;也没有"函数调用轨迹"这种 Agent 训练需要的格式。这些必须造。
6.2 五类合成方法
合成数据方法谱系(按信息来源分类)
① 蒸馏式 (Distillation)
强模型 → 弱模型。Alpaca / Vicuna / OpenHermes
信息来源:教师模型的参数化知识
风险:法律条款 + 能力天花板受限于教师
② 自举式 (Self-Instruct / Bootstrap)
模型自己生成指令 → 自己回答 → 过滤 → 训自己
信息来源:模型自身分布 + 过滤器引入的信号
风险:模型坍缩(见 6.4)
③ 改写式 (Rephrasing / WRAP)
把已有语料改写成多种风格(QA体/教科书体/儿童体)
信息来源:原始语料(信息量不增加,但可学习性提高)
优点:不引入新幻觉,性价比最高
④ 程序化 / 可验证式 (Programmatic)
用代码生成题目和标准答案;用编译器/单测/形式化验证器打标
信息来源:外部符号系统(真正的新信息)
优点:零幻觉,规模无上限。数学和代码的主力方案
⑤ 环境交互式 (Environment Grounded)
Agent 在真实/模拟环境里执行,用执行结果作监督
信息来源:环境反馈
优点:唯一能产生"真实世界因果"的方式,最贵
这个分类的核心洞察是"信息从哪来"。①②本质上不引入新信息,只是重新组织;③提高可学习性;④⑤才真正注入了模型原本没有的信息。面试时能讲清这一层,比罗列十个方法名有价值得多。
6.3 可验证合成数据的实现
以数学为例,程序化生成 + 验证的骨架:
python
import random
from fractions import Fraction
import sympy as sp
def gen_linear_system(n_var=2, coef_range=9):
"""生成有唯一整数解的线性方程组,附完整解题步骤"""
syms = sp.symbols(f"x0:{n_var}")
sol = [random.randint(-9, 9) for _ in range(n_var)]
eqs, lines = [], []
while len(eqs) < n_var:
coefs = [random.randint(-coef_range, coef_range) for _ in range(n_var)]
if all(c == 0 for c in coefs):
continue
rhs = sum(c * s for c, s in zip(coefs, sol))
expr = sum(c * x for c, x in zip(coefs, syms))
cand = eqs + [sp.Eq(expr, rhs)]
# 保证方程组线性无关(否则无唯一解)
M = sp.Matrix([[e.lhs.coeff(x) for x in syms] for e in cand])
if M.rank() == len(cand):
eqs = cand
lines.append(" + ".join(f"{c}*x{i}" for i, c in enumerate(coefs)) + f" = {rhs}")
verified = sp.solve(eqs, syms, dict=True)
# 关键:用符号求解器复核,不匹配就丢弃这条样本
if not verified or [verified[0][s] for s in syms] != sol:
return None
question = "解下列方程组:\n" + "\n".join(lines)
steps = _elimination_steps(eqs, syms) # 生成消元过程文本
answer = ", ".join(f"x{i} = {v}" for i, v in enumerate(sol))
return {"question": question, "reasoning": steps, "answer": answer,
"verified": True, "difficulty": n_var}
def _elimination_steps(eqs, syms):
"""用 sympy 逐步消元,把中间过程转成自然语言"""
M = sp.Matrix([[e.lhs.coeff(x) for x in syms] + [e.rhs] for e in eqs])
rref, pivots = M.rref()
out = ["将方程组写成增广矩阵:", sp.pretty(M),
"做初等行变换化为行最简形:", sp.pretty(rref),
"由行最简形直接读出各变量的值。"]
return "\n".join(out)
这段代码体现的三个原则,是面试里讲"如何保证合成数据质量"的标准答案:
- 生成时反向构造:先定答案再造题目,保证解一定存在且是整数(可控难度)。
- 生成后独立验证:用 sympy 重新求解并比对,不一致就丢。验证器必须独立于生成器。
- 过程也要合成:只有 question-answer 对训不出推理能力,必须把中间步骤也生成出来。
6.4 模型坍缩:合成数据的最大风险
递归训练导致的模型坍缩 (Model Collapse)
真实分布 p(x) 第1代 第n代
╭─────────╮ ╭───────╮ ╭──╮
╱ ╲ ╱ ╲ ╱ ╲
╱ 长尾 长尾 ╲ ╱ 长尾变薄 ╲ ╱ 尾巴 ╲
──────────────── ────────────── ─────没了───
机制:
① 统计误差:有限采样必然丢失低概率事件(早期坍缩)
② 表达误差:模型容量有限,无法完美拟合(晚期坍缩)
③ 优化误差:采样时的 top-p / temperature 进一步砍尾巴
表现:方差单调递减 → 多样性丧失 → 最终退化为少数模式
Nature 2024 那篇 "AI models collapse when trained on recursively generated data" 让这个问题出圈了。但面试时如果只会说"合成数据会导致坍缩"是不够的,要能说出坍缩的前提条件和规避方法:
坍缩发生的前提是完全替换:第 n 代只用第 n-1 代的输出训练。而实际工程中:
| 规避手段 | 原理 |
|---|---|
| 数据累积而非替换 | 保留全部真实数据,合成数据只做增量。后续研究证明累积模式下坍缩不发生 |
| 外部验证器把关 | 用编译器/单测/符号求解器过滤,引入了模型分布之外的信息 |
| 人类数据锚定 | 保持真实数据占比不低于某阈值(经验值 50%+) |
| 提高采样温度 | 不用贪心/低温采样,保留分布尾部 |
| 多教师混合 | 用多个不同模型生成,避免单一分布 |
| 多样性监控 | 监控 n-gram 熵、embedding 空间的平均两两距离,下降即告警 |
python
def diversity_metrics(texts, model=None):
"""合成数据多样性监控:跑在每一批生成后,趋势下降就报警"""
from collections import Counter
import math
# 1) distinct-n:不同 n-gram 数 / 总 n-gram 数
def distinct_n(n):
grams = []
for t in texts:
toks = list(t) # 中文按字,英文改成分词
grams += [tuple(toks[i:i+n]) for i in range(len(toks)-n+1)]
return len(set(grams)) / max(len(grams), 1)
# 2) 语料级 n-gram 熵:比 distinct-n 更敏感
def entropy_n(n):
c = Counter()
for t in texts:
toks = list(t)
c.update(tuple(toks[i:i+n]) for i in range(len(toks)-n+1))
total = sum(c.values())
return -sum((v/total) * math.log(v/total) for v in c.values())
out = {f"distinct_{n}": distinct_n(n) for n in (1, 2, 3, 4)}
out.update({f"entropy_{n}": entropy_n(n) for n in (2, 3)})
# 3) 语义多样性:embedding 两两余弦距离均值
if model is not None:
import numpy as np
emb = model.encode(texts, normalize_embeddings=True)
sim = emb @ emb.T
n = len(texts)
out["mean_pairwise_dist"] = 1 - (sim.sum() - n) / (n * (n - 1))
return out
七、SFT 数据:少而精
7.1 数量与质量的实证
| 工作 | 样本量 | 关键发现 |
|---|---|---|
| Alpaca | 52K | 蒸馏 GPT-3.5,证明低成本可行 |
| LIMA | 1K | 精心挑选 1000 条,接近 RLHF 效果 |
| AlpaGasus | 9K(从 52K 筛) | 筛掉 82% 后效果反而更好 |
| Deita | 6K | 复杂度+质量+多样性三维打分选样 |
| LESS | 5% 子集 | 用梯度相似度选与目标任务最相关的样本 |
结论一致:SFT 阶段数据质量的边际收益远大于数量,而且低质量样本是负收益。AlpaGasus 那个"删掉 82% 反而更好"的结果特别适合在面试里引用。
7.2 三维选样
python
def select_sft_data(pool, budget=6000, judge=None, embedder=None):
"""
Deita 风格的三维选样:复杂度 x 质量 x 多样性
pool: [{"instruction":..., "output":...}, ...]
"""
import numpy as np
# 维度1&2:用 LLM 分别给指令复杂度、回答质量打分(1~6)
for s in pool:
s["complexity"] = judge.score_complexity(s["instruction"])
s["quality"] = judge.score_quality(s["instruction"], s["output"])
s["evol_score"] = s["complexity"] * s["quality"]
# 维度3:贪心选择,用 embedding 距离保证多样性
pool.sort(key=lambda x: -x["evol_score"])
embs = embedder.encode([s["instruction"] for s in pool],
normalize_embeddings=True)
selected, sel_emb = [], []
THRESH = 0.9 # 与已选样本相似度超过此值则跳过
for i, s in enumerate(pool):
if len(selected) >= budget:
break
if sel_emb:
if float(np.max(np.asarray(sel_emb) @ embs[i])) > THRESH:
continue # 太相似,跳过,保证覆盖面
selected.append(s)
sel_emb.append(embs[i])
return selected
注意这里的贪心顺序很重要:先按质量排序再逐个用多样性剔除,等价于"在保证质量下界的前提下最大化覆盖"。反过来先聚类再每簇取最优,会引入大量低质量簇的代表。
7.3 SFT 数据的常见质量问题
真实项目里,SFT 数据出问题的形态很固定:
| 问题 | 表现 | 检测方法 |
|---|---|---|
| 拒答污染 | 大量"作为AI模型我无法..." | 正则统计拒答模板占比 |
| 长度偏置 | 全是长回答,模型学会啰嗦 | 看输出长度分布,与人类偏好长度对比 |
| 格式单一 | 全是 markdown 列表 | 统计 markdown 标记密度 |
| 身份泄露 | 蒸馏数据里带教师模型自称 | 关键词扫描("我是由XX训练的") |
| 事实错误 | 教师幻觉被固化 | 抽样人工核查 + 事实性 judge |
| 指令-回答不匹配 | 答非所问 | 用 LLM 判 relevance |
| 基准泄露 | 含测试集原题 | n-gram 与评测集比对(见第八节) |
"身份泄露"这一条特别容易在面试里被问到实际经验:用 GPT-4 蒸馏的数据训出来的模型,问它"你是谁"会说自己是 OpenAI 开发的。这不是玄学,就是数据里有这些句子。处理办法是建立自我认知数据的独立子集,并在通用蒸馏数据里做关键词清洗和替换。
八、数据污染检测
8.1 污染的三种形态
① 直接污染:训练集里含测试集原题原答案
检测:n-gram 精确匹配(13-gram 是常用阈值)
② 改写污染:测试题被改写/翻译后进入训练集
检测:embedding 相似度 + LLM 判定同义
③ 间接污染:训练集含测试集的讨论、题解、排行榜分析
检测:最难。只能靠时间切分 + 域名黑名单
8.2 实用检测方法
python
def ngram_contamination(train_docs, bench_items, n=13, thresh=0.2):
"""
13-gram 污染检测(GPT-3/Llama 采用的标准做法)
返回每个评测样本被污染的程度
"""
def grams(text, n):
toks = text.lower().split()
return {" ".join(toks[i:i+n]) for i in range(max(len(toks)-n+1, 0))}
# 建训练集 n-gram 索引(生产环境用布隆过滤器,内存可控)
train_index = set()
for d in train_docs:
train_index |= grams(d, n)
report = []
for item in bench_items:
g = grams(item["question"] + " " + item.get("answer", ""), n)
if not g:
continue
hit = len(g & train_index) / len(g)
if hit > thresh:
report.append({"id": item["id"], "overlap": round(hit, 3)})
return report
更聪明的无需访问训练集的检测方法(面试加分项):
- 选项顺序扰动:把选择题的选项打乱,如果准确率大幅下降,说明模型是背了答案位置而非理解。
- N-gram 预测法:给模型评测样本的前半段,看它能否逐字续写出后半段。能续写说明见过。
- 时间切分对比:比较模型在训练截止日期前后发布的同类评测上的表现差距。这是 LiveBench、LiveCodeBench 的核心思想。
- Min-K% Prob:取样本中概率最低的 K% token,算平均对数概率。见过的样本,即使是最低概率的 token 也不会太低。
python
import numpy as np
def min_k_prob(logprobs, k=20):
"""
Min-K% Prob 成员推断:
对训练过的样本,即使最"意外"的那部分 token 也不会太意外。
logprobs: 该样本每个 token 的 log 概率列表
返回值越高,越可能是训练数据。
"""
arr = np.asarray(logprobs)
k_num = max(1, int(len(arr) * k / 100))
return float(np.mean(np.sort(arr)[:k_num]))
九、数据飞轮:从一次性到持续
9.1 飞轮结构
生产环境的数据飞轮
┌──────────────────────────────────────┐
│ │
▼ │
[线上流量] │
│ 采样 + 脱敏 │
▼ │
[真实 query 池] ──> 分布分析 ──> 发现能力缺口 │
│ │ │
▼ ▼ │
[用户反馈信号] [针对性数据生产] │
点赞/点踩/重问/复制/中断 合成 + 人工标注 │
│ │ │
└──────────┬───────────────────┘ │
▼ │
[训练数据增量] │
│ │
▼ │
[模型迭代 + 灰度] ─────────────────┘
关键:负反馈信号(重问、中断、复制后编辑)
比正反馈(点赞)密度高一个数量级,是主力信号。
"重问"是最被低估的信号。用户在 30 秒内换个说法重新问同一件事,几乎必然意味着上一次回答失败了。这个信号完全隐式、无需用户主动操作、且密度极高。类似的还有:复制回答后立即编辑(说明部分可用)、多轮后放弃(说明彻底失败)。
9.2 数据版本化与可追溯
生产级数据工程必须能回答:"这个模型的能力退化,是哪批数据引入的?"
python
# 数据集清单(manifest)设计:每批数据都要可追溯、可回滚
manifest = {
"dataset_id": "sft-v2026.08.10",
"parent": "sft-v2026.07.28",
"shards": [
{"name": "human-curated-zh", "n": 12480, "sha256": "a3f1...",
"source": "内部标注平台 batch#77", "license": "proprietary"},
{"name": "synthetic-math-verified", "n": 30000, "sha256": "9c02...",
"source": "gen_linear_system v1.3", "verifier": "sympy==1.13",
"reject_rate": 0.18},
{"name": "tool-call-traces", "n": 8600, "sha256": "77bd...",
"source": "生产环境回放脱敏", "pii_scan": "passed"},
],
"filters_applied": ["dedup-minhash-0.8", "pii-redact-v3",
"bench-decontam-13gram", "toxicity<0.2"],
"contamination_report": "reports/decontam-20260810.json",
"diff_from_parent": {"added": 30000, "removed": 1240, "modified": 0},
"eval_gate": {"mmlu": 68.2, "gsm8k": 81.4, "regression": "pass"},
}
这份 manifest 的价值在于可做数据级的 A/B 归因:当 v2026.08.10 的模型在某个能力上退化,可以直接对比 diff,把嫌疑锁定在新增的 3 万条合成数学数据上,然后单独剔除重训验证。没有这套机制,数据问题的定位基本靠猜。
十、面试速答
Q:预训练数据和 SFT 数据,哪个决定模型能力上限?
A:预训练决定上限,SFT 决定这个上限能释放多少。预训练建立的是世界知识和语言建模能力,SFT 只是教模型用什么格式把已有能力表达出来,这就是表层对齐假说。实证支持是 LIMA 用 1000 条精选样本就逼近了 RLHF 的效果,说明对齐阶段需要的信息量极小。反过来说,试图用 SFT 注入预训练里没有的新知识,模型只会学到"要自信地回答这类问题"的表层模式而缺乏内部表示支撑,结果是加剧幻觉。所以做垂直领域时,如果是知识缺失就得做继续预训练,如果只是格式或风格不对才用 SFT。
Q:为什么去重对模型质量影响这么大?
A:三个层面。一是训练效率,重复数据让模型在相同 token 预算下见到的有效信息更少,去重后能用更少步数达到同等 loss。二是记忆化风险,未去重的模型会逐字背诵训练文本,输出中约 1% 的 token 是训练集直接拷贝,这既是隐私风险也是版权风险,去重能降低一个数量级。三是评测可信度,训练集与测试集重叠会系统性高估能力。实现上分三层:精确哈希去重最便宜、MinHash 加 LSH 做模糊去重是主力、后缀数组做子串级去重效果最好但内存开销是语料的八到十倍,很多团队因此放弃第三层。
Q:MinHash 去重在分布式环境下最容易出什么错?
A:置换参数没有全局固定。MinHash 依赖一组随机哈希函数,如果各个 worker 各自用随机种子初始化,同一份文档在不同节点算出的签名完全不同,分桶就失效了,去重率会莫名其妙地接近零。必须用固定种子生成参数并分发到所有节点。第二个常见坑是超大桶,某些模板化页面会让一个 LSH 桶里堆进几十万文档,两两比对直接爆炸,需要设桶大小上限并对超限桶单独按模板处理。第三个是保留策略,不能随机留一份,应该按域名权威度、文本长度或抓取时间择优。
Q:合成数据一定会导致模型坍缩吗?
A:不一定,坍缩的前提是完全替换式的递归训练,即第 n 代只用第 n-1 代的输出训练。Nature 那篇论文的实验设置就是完全替换。后续研究表明,如果采用数据累积模式,即保留全部真实数据、合成数据只作为增量叠加,坍缩不会发生,测试误差有界。此外还有几个关键的规避手段:用外部验证器(编译器、单测、符号求解器)过滤合成数据,这引入了模型分布之外的真实信息;用多个不同的教师模型混合生成,避免单一分布;提高采样温度保留分布尾部;以及持续监控 n-gram 熵和 embedding 空间的平均两两距离,一旦多样性指标趋势性下降就告警。
Q:怎么设计一批高质量的合成数学数据?
A:核心是反向构造加独立验证。第一步反向构造,先随机采样一个答案,再围绕答案构造题面,这样保证解一定存在、且可以精确控制难度和解的形式。第二步用独立于生成器的验证器复核,比如用 sympy 重新求解并比对,不一致的直接丢弃,实践中拒绝率大概在 15% 到 25%。第三步是把中间推理过程也生成出来,只有题目和答案训不出推理能力,必须有消元、代入这些步骤的自然语言描述。第四步做多样性控制,题面模板要足够多,并监控生成结果的 n-gram 重复度。最后要做污染检测,确保生成的题目没有和评测集撞车。
Q:FineWeb-Edu 的过滤思路是什么?为什么有效?
A:用一个强模型给五十万个网页样本打"教育价值零到五分",然后把这些标注蒸馏成一个基于 BERT 的轻量回归模型,用它扫全量语料,保留分数三分及以上的部分。有效的原因有两点:一是它评的是语义层面的教育价值,而不是传统 fastText 分类器学到的"像不像维基百科"这种风格特征,避免了误杀口语化但信息密度高的内容;二是它把昂贵的 LLM 打分变成了一次性成本,推理成本转嫁给了廉价代理模型,使得全量扫描在经济上可行。结果是数据量降到原来的百分之八,但 MMLU 和 ARC 反而显著提升。要注意的坑是硬阈值截断会导致多样性坍缩,更稳的做法是按分数分桶做加权降采样而不是一刀切。
Q:什么是退火阶段?为什么有效?
A:退火是预训练末期的一个独立阶段,通常只占总 token 的百分之三到五,特点是把学习率快速降到接近零,同时把数据换成最高质量的子集,往往还会掺入少量指令格式数据。有效的直观解释是,训练主体阶段学习率较高,模型参数在空间中大幅移动,处于一种"高温"的探索状态;退火相当于降温过程,让参数收敛到高质量数据附近的局部最优。Llama 3 和 MiniCPM 都报告了显著的 benchmark 提升,而成本只占总训练的几个百分点,是投入产出比极高的操作。附带的好处是退火阶段掺入指令数据能让后续 SFT 更容易收敛。
Q:怎么检测评测集污染?如果拿不到训练数据呢?
A:能拿到训练数据时用 n-gram 匹配,业界标准是 13-gram,重叠比例超过阈值就判定污染,工程上用布隆过滤器控制内存。拿不到训练数据时有几种成员推断方法。一是选项顺序扰动,把选择题选项打乱后如果准确率大幅下降,说明模型记的是答案位置而非理解题意。二是让模型续写,给评测样本前半段看能否逐字续写后半段。三是 Min-K% Prob,取样本中概率最低的百分之二十 token 算平均对数概率,训练见过的样本即使最意外的 token 也不会太意外。四是时间切分,对比模型在训练截止日期前后发布的同类题目上的表现差距,这也是 LiveBench 这类动态评测集的设计思想。
Q:数据配比怎么确定?
A:不可能在目标规模上直接搜索,标准做法是小模型代理实验。先定义若干数据域,在 1B 参数、10B token 的规模上用拉丁超立方或 Sobol 采样跑二十到五十组不同配比,然后拟合一个从配比向量到各域 loss 的回归模型,这就是 DoReMi 和 RegMix 的思路,用回归模型外推解出最优配比,再在 7B 规模验证一次才上大规模。这里最重要的注意事项是配比与规模存在交互效应,小规模的最优配比不能无脑外推。已知的规律是规模越大越应该提高代码和数学的占比,因为这类数据的收益在小模型上被容量瓶颈掩盖了。
Q:为什么纯语言模型也要训练大量代码数据?
A:三个层面。形式层面,代码有严格的语法树结构和极长的依赖距离,函数定义和调用可能隔几百行,这逼迫模型学习精确的结构化建模和长程依赖能力。语义层面,代码是可执行的形式化逻辑,而注释与实现的配对天然构成"自然语言到形式化表达"的高质量对齐数据。实证层面,多个消融实验显示去掉代码数据后模型在 BBH、GSM8K 这类多步推理任务上大幅下降,即使这些任务本身和写代码无关。所以现在即使是通用模型,代码占比也普遍在百分之十五到二十。
十一、高频追问清单
- 为什么现在的模型普遍训到远超 Chinchilla 最优的 token 数?背后的优化目标变了什么?
- 子串级去重(后缀数组)的内存瓶颈怎么解决?有哪些近似方案?
- 质量过滤器本身有偏见,怎么量化并缓解?
- 多语言场景下,低资源语言应该过采样多少倍?依据是什么?
- 退火阶段掺入 SFT 数据,会不会污染后续的 SFT 评估?
- WRAP 这类改写式合成,为什么不引入新信息却能提升效果?
- 合成数据比例超过多少会开始有害?有没有可监控的早期指标?
- 用 GPT-4 蒸馏数据训练,法律上有什么风险?技术上如何规避身份泄露?
- LESS 那类基于梯度相似度的选样方法,计算成本怎么控制?
- 数据飞轮里的隐式负反馈信号,怎么和显式点踩做加权融合?
- 如果发现某个 benchmark 被污染了,模型已经训完了,怎么补救?
- 领域继续预训练(CPT)时,通用数据要回放多少比例才能不灾难性遗忘?
- PII 脱敏和数据可用性怎么平衡?替换成假名会不会让模型学到错误关联?
- 数据 manifest 里的 eval_gate 应该包含哪些指标才能有效拦截退化?
至此 A 系列的数据篇告一段落。数据工程的核心矛盾其实一直没变:信息密度与分布覆盖之间的取舍。过滤得越严,密度越高但覆盖越窄;合成得越多,规模越大但分布越窄。所有技巧本质上都在这条张力线上找平衡点。明天的 A24 会转向另一个基础但极易被问倒的方向:位置编码与长度外推------为什么 RoPE 能外推、NTK 和 YaRN 到底改了什么、以及为什么长上下文扩展要放在训练的最后阶段。今天 B 系列的两篇(B23 灰度发布与回滚、B24 GUI 智能体与 Computer Use)则继续沿着工程线往下走。