27届大模型面试准备(二十三):数据工程与合成数据——配方、去重、质量过滤与数据飞轮

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%(上升中)

  注意:代码占比远超其直觉重要性。
  即使目标不是代码模型,代码数据也被证明能显著提升
  逻辑推理、结构化输出和长程依赖能力。

"为什么纯语言模型也要喂大量代码"是高频面试题。三个层面的答案:

  1. 形式层:代码有严格的语法树结构和长程依赖(函数定义与调用可能隔几百行),逼迫模型学习精确的结构化建模能力。
  2. 语义层:代码是"可执行的逻辑",注释与实现的配对天然是"自然语言到形式化"的对齐数据。
  3. 实证层:多篇消融显示,去掉代码数据后,模型在 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. 去重后模型困惑度更低,且用更少的训练步数达到同等效果;
  2. 未去重的模型会逐字背诵训练数据,输出中约 1% 的 token 是训练集的直接拷贝,去重后降低 10 倍;
  3. 训练集与测试集的重叠会严重高估评测结果。

第二点直接关系到隐私和版权风险,是产品上线前的合规红线。

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

三个容易被追问的实现坑

  1. 置换参数必须全局固定。分布式跑的时候如果各 worker 各自初始化随机数,签名就不可比,去重完全失效。这是实际项目里最常见的低级错误。
  2. 超大桶要特判。某些模板页会让一个 band 桶里塞进几十万文档,两两比较直接爆炸。做法是设上限,超过就整桶按"疑似模板"降权或抽样保留。
  3. 保留哪一个副本有讲究。不能随机留一个。合理策略是留 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 为什么必须用合成数据

两个现实压力:

  1. 高质量自然数据快见底了。Epoch AI 的估算是高质量英文文本存量约 10T~20T token,而单个前沿模型的训练量已经到 15T~36T。继续 scaling 只能靠合成。
  2. 自然数据的分布不匹配需求。互联网上几乎没有"完整的多步推理过程",人们只写结论不写思考链;也没有"函数调用轨迹"这种 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)

这段代码体现的三个原则,是面试里讲"如何保证合成数据质量"的标准答案:

  1. 生成时反向构造:先定答案再造题目,保证解一定存在且是整数(可控难度)。
  2. 生成后独立验证:用 sympy 重新求解并比对,不一致就丢。验证器必须独立于生成器。
  3. 过程也要合成:只有 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

更聪明的无需访问训练集的检测方法(面试加分项):

  1. 选项顺序扰动:把选择题的选项打乱,如果准确率大幅下降,说明模型是背了答案位置而非理解。
  2. N-gram 预测法:给模型评测样本的前半段,看它能否逐字续写出后半段。能续写说明见过。
  3. 时间切分对比:比较模型在训练截止日期前后发布的同类评测上的表现差距。这是 LiveBench、LiveCodeBench 的核心思想。
  4. 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 这类多步推理任务上大幅下降,即使这些任务本身和写代码无关。所以现在即使是通用模型,代码占比也普遍在百分之十五到二十。


十一、高频追问清单

  1. 为什么现在的模型普遍训到远超 Chinchilla 最优的 token 数?背后的优化目标变了什么?
  2. 子串级去重(后缀数组)的内存瓶颈怎么解决?有哪些近似方案?
  3. 质量过滤器本身有偏见,怎么量化并缓解?
  4. 多语言场景下,低资源语言应该过采样多少倍?依据是什么?
  5. 退火阶段掺入 SFT 数据,会不会污染后续的 SFT 评估?
  6. WRAP 这类改写式合成,为什么不引入新信息却能提升效果?
  7. 合成数据比例超过多少会开始有害?有没有可监控的早期指标?
  8. 用 GPT-4 蒸馏数据训练,法律上有什么风险?技术上如何规避身份泄露?
  9. LESS 那类基于梯度相似度的选样方法,计算成本怎么控制?
  10. 数据飞轮里的隐式负反馈信号,怎么和显式点踩做加权融合?
  11. 如果发现某个 benchmark 被污染了,模型已经训完了,怎么补救?
  12. 领域继续预训练(CPT)时,通用数据要回放多少比例才能不灾难性遗忘?
  13. PII 脱敏和数据可用性怎么平衡?替换成假名会不会让模型学到错误关联?
  14. 数据 manifest 里的 eval_gate 应该包含哪些指标才能有效拦截退化?

至此 A 系列的数据篇告一段落。数据工程的核心矛盾其实一直没变:信息密度与分布覆盖之间的取舍。过滤得越严,密度越高但覆盖越窄;合成得越多,规模越大但分布越窄。所有技巧本质上都在这条张力线上找平衡点。明天的 A24 会转向另一个基础但极易被问倒的方向:位置编码与长度外推------为什么 RoPE 能外推、NTK 和 YaRN 到底改了什么、以及为什么长上下文扩展要放在训练的最后阶段。今天 B 系列的两篇(B23 灰度发布与回滚、B24 GUI 智能体与 Computer Use)则继续沿着工程线往下走。

相关推荐
寻道码路2 小时前
大模型工程化实战(一):概率坍塌的救赎 - 给LLM输出加锁
大模型·agent·langgraph·ai工程化·llm确定性
m0_547486664 小时前
《人工智能导论:深度学习大模型基础》全套PPT课件2026
人工智能·深度学习·大模型
@Mr_LiuYang6 小时前
大模型提示注入攻防实验--《深入理解 AI Agent:设计原理与工程实践 》实验2-5
人工智能·大模型·提示词注入·攻防实验
七牛云行业应用7 小时前
ComfyUI + MiniMax H3 实战教程:全模态视频生成,API 和本地两种玩法
人工智能·大模型·音视频
带娃的IT创业者1 天前
Kimi-K3 开源背后:2.8 万亿参数的“暴力美学”与智能体的新拐点
人工智能·开源·大模型·智能体·开源模型·kimi-k3·moonshot ai
VIP_CQCRE1 天前
用 Ace Data Cloud 快速接入 OpenAI Chat Completions API:兼容官方格式,更适合开发者落地
ai·大模型·openai·api·ace data cloud
卷心菜的学习路1 天前
基于多模态向量检索的数学相似题推荐系统:完整设计、算法与实验
java·python·算法·大模型·推荐算法·数学相似题
thesky1234561 天前
27届大模型面试准备(二十一):MoE 架构全攻略——稀疏激活、路由、负载均衡与专家并行
大模型·负载均衡·moe·deepseek·混合专家·专家并行·稀疏激活
tachibana21 天前
知识库文档上传接口
数据库·人工智能·大模型·llm