人工智能实践(语言智能)
第5讲:数据工程

5.2 准备 SFT 数据

SFT 数据的结构:四种主流格式、chat template、masking loss 与数据去污染

从"一堆对话"到"可训练的 SFT 数据集"

你拿到数据之后(不管来源是开源数据集合成还是低资源合成),下一步都是把它做成可被训练器正确消费的格式。本节回答三个问题:

  1. 用什么格式存?(Alpaca / ShareGPT / OpenAI messages / ChatML)
  2. 怎么让训练器只在 assistant token 上算损失?(chat template + masking)
  3. 怎么避免训练集泄漏到测试集?(decontamination)

本节不讨论"去哪里找数据"——那在 5.1 常见数据来源;也不讨论"如何合成数据"——那在 5.3 合成数据5.4 低资源合成

一、四种主流格式规范

1. Alpaca 格式

最简单、单轮、字段固定:

{
  "instruction": "用三句话介绍量子计算",
  "input": "",
  "output": "量子计算是一种利用量子力学原理进行信息处理的新型计算范式……"
}

拼接规则:instruction + "\n" + input(若 input 非空)→ user;output → assistant。

2. ShareGPT 格式

天然支持多轮,conversations 数组按对话顺序排列:

{
  "conversations": [
    {"from": "human", "value": "什么是机器学习?"},
    {"from": "gpt",   "value": "机器学习是人工智能的一个分支……"},
    {"from": "human", "value": "它和深度学习有什么区别?"},
    {"from": "gpt",   "value": "深度学习是机器学习的子集……"}
  ]
}

3. OpenAI messages 格式

当前最标准的"通用载体",被 Qwen / Llama / DeepSeek 等的官方 chat template 原生支持:

{
  "messages": [
    {"role": "system", "content": "你是一个有帮助的助手。"},
    {"role": "user",   "content": "用三句话介绍量子计算"},
    {"role": "assistant", "content": "量子计算是……"}
  ]
}

同时支持 tool 角色用于工具调用:

{
  "messages": [
    {"role": "user", "content": "2024 年中国 GDP 是多少?"},
    {"role": "assistant", "content": null,
     "tool_calls": [{"name": "search", "arguments": {"q": "GDP 2024 中国"}}]},
    {"role": "tool", "content": "中国 2024 年 GDP 为 134 万亿元……"},
    {"role": "assistant", "content": "根据最新数据……"}
  ]
}

4. Qwen ChatML 格式

Qwen 系列底层采用的聊天模板格式:

<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
用三句话介绍量子计算<|im_end|>
<|im_start|>assistant
量子计算是……<|im_end|>

格式对比速查

格式多轮角色支持工具调用推荐用法
Alpacauser/assistant快速原型
ShareGPThuman/gpt纯对话数据
OpenAI messagessystem/user/assistant/tool首选
Qwen ChatMLsystem/user/assistant模型底层

实用建议永远以 OpenAI messages 格式存储数据,在训练时用 tokenizer.apply_chat_template() 自动转换成目标模型需要的底层格式。这样可以在不同模型(Qwen、Llama、Mistral)之间无缝迁移。

二、Chat Template 与 Masking Loss

2.1 不要手动拼接

每个模型系列都有自己微妙的 chat template(差几个空格、<|im_end|> vs </s>、BOS 处理方式等),手动拼接是 bug 温床。正确做法:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-1.7B")

messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user",   "content": "用三句话介绍量子计算"},
    {"role": "assistant", "content": "量子计算是……"},
]

formatted = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=False,  # 训练时不加生成提示
)

2.2 Masking Loss 的核心公式

SFT 的训练目标是仅在 assistant token 上计算交叉熵损失

LSFT=1At=1TmtlogPθ(xtx<t)\mathcal{L}_{\text{SFT}} = -\frac{1}{|\mathcal{A}|}\sum_{t=1}^{T} m_t \cdot \log P_\theta(x_t \mid x_{<t})

其中 mt{0,1}m_t \in \{0, 1\} 是二值掩码:assistant token 为 1,其他为 0A=tmt|\mathcal{A}| = \sum_t m_t 是 assistant token 总数。

为什么 user / system 的 token 不贡献梯度?训练"学会说话"而不是"学会重复用户输入"。如果把 user token 也算进 loss,模型会把大量能力分配给"复述指令"。

2.3 用 TRL 一键搞定

from trl import SFTTrainer, SFTConfig

cfg = SFTConfig(
    output_dir="./out",
    max_length=2048,
    packing=False,  # 或 True 提升吞吐
)

trainer = SFTTrainer(
    model=model,
    args=cfg,
    train_dataset=dataset,  # 含 "messages" 字段即可
    processing_class=tokenizer,
)

TRL 会自动识别 assistant 片段并生成掩码,无需手动切分。

2.4 常见 Bug 速查

症状常见根因
训练 loss 异常低(<0.1)掩码把 user token 也算进去了(训练变成"复述" task)
推理时停不下来eos_token 没正确加入或未被作为终止符
模型"开头说话很奇怪"BOS 处理不一致(训练时加 BOS,推理时没加)
多轮对话不自然多轮之间用了错误的分隔符(如手动拼接了 \n 而非模板 turn 分隔)

三、数据去污染(Data Decontamination)

这是在"你交付模型之前"必须做的最后一步:检查 SFT 数据是否泄漏了常用评测集。如果泄漏,模型在基准上的得分会虚高、实际能力被严重误判。

3.1 常见污染源

  • Alpaca / FLAN:与 MMLU、BBH 部分重叠
  • UltraChat:偶尔复述 MT-Bench 题目
  • 自采网页数据:C-Eval、CMMLU 原始题目已广泛被爬

3.2 检测方法

  1. n-gram 匹配:对评测集每道题抽取 13-gram,检查 SFT 数据中是否命中
  2. embedding 相似度:对每条 SFT 样本与评测题计算余弦相似度,阈值通常 0.85+ 视为可疑
  3. 精确子串:对较短评测题(如 GSM8K)直接做 substring 查询
# 伪代码:13-gram 污染扫描
from collections import defaultdict

def ngrams(text, n=13):
    toks = text.split()
    return {" ".join(toks[i:i+n]) for i in range(len(toks)-n+1)}

eval_ngrams = set()
for item in eval_set:
    eval_ngrams |= ngrams(item["question"])

contaminated = []
for idx, sample in enumerate(sft_data):
    if ngrams(sample["user_msg"]) & eval_ngrams:
        contaminated.append(idx)

print(f"潜在污染: {len(contaminated)} / {len(sft_data)}")

发现污染不等于"这条样本不能用",但必须记录、报告、并在发布模型时注明评测集的去污染状态。这是学术伦理,也是工程信誉。

本节小结

维度要点
首选格式OpenAI messages,训练时用 apply_chat_template 转换
掩码损失仅对 assistant token 计算交叉熵
去污染训练前必须对评测集做 13-gram 扫描
代码原则永远不要手动拼接聊天模板