2-SFT 监督微调
1. SFT 的本质:从”会续写”到”会说话”
预训练模型的核心能力是续写:给一段文字,它学会预测下一个 token。输入是 “用户说了一段话”,输出是 “模型继续往下写”——模型并不理解什么是”回答”,什么是”指令”,什么是”角色”。
监督微调(Supervised Fine-Tuning, SFT)的目标,就是把这种续写能力重新定向为对话能力。具体来说,让模型学会:
- 把用户的指令理解为”需要回答”,而不是”需要续写”
- 按照指定格式输出回复(而不是续写用户的输入)
- 遵循不同的任务类型(问答、摘要、写作、代码等)
从训练角度,SFT 和预训练本质上都在做语言建模。但关键区别在于:训练数据的构造方式完全不同。预训练是让模型从海量互联网文本中自监督学习语言规律,SFT 则是用人工标注或模型生成的问答对,让模型学习”在给定指令条件下输出特定回答”。
经过 SFT 的模型,通常还需要经过 RLHF(DPO/PPO)阶段来进一步对齐人类偏好,但 SFT 是构建基础对话能力的必经步骤。
2. 数学形式:条件语言建模
2.1 目标函数
SFT 的本质仍是条件语言建模(Conditional Language Modeling)。给定一段上下文
展开来说,对于一条训练样本,我们将其拼接为如下序列:
[instruction] + [input] + [output]
模型对这个完整序列做自回归预测,loss 只在 [output] 部分(即 assistant 的回复)计算。
2.2 Token-level Cross Entropy
具体到每个 token,loss 的计算方式为:
这与预训练的语言模型 loss 形式完全一致。区别在于:
| 预训练 | SFT | |
|---|---|---|
| 训练文本来源 | 互联网文档(自动采集) | 人工标注/模型生成的问答数据 |
| 监督信号位置 | 所有 token | 仅 assistant 回复部分 |
| 数据格式 | 连续文本流 | 结构化 instruction-output 对 |
| 典型数据量 | 数千亿~万亿 tokens | 数十万~数百万条样本 |
2.3 Loss Mask 的作用
为什么只对 assistant 部分计算 loss?
SFT 训练中,输入部分(system prompt、user instruction、input)都是模型不该”学习回答”的内容。如果把这些位置也计入 loss,模型会学到”在 instruction 后面生成指定 output”——但我们真正想让模型学会的是在给定 instruction 后生成 output,而不是”在 instruction 之后生成特定文字”。
更具体地,假设一条训练数据是:
<|system|>: 你是一个有帮助的助手。
<|user|>: 请问中国的首都是哪里?
<|assistant|>: 中国的首都是北京。
在计算 loss 时:
<|system|>和<|user|>部分的 token:mask 掉,不计算梯度<|assistant|>部分的 token:计算交叉熵 loss
这确保了模型学到的是”根据 instruction 和 input 生成 output”,而不是”在输入文本后面原样复现 output”。
Loss Mask 的实现:在实践中,通常用一个 binary mask 数组标记哪些位置需要计算 loss:
loss_mask = [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, ...]
final_loss = (loss_mask * token_log_probs).sum() / loss_mask.sum()3. 数据形式与构造规范
3.1 基础数据格式
SFT 训练数据的核心字段:
| 字段 | 含义 | 示例 |
|---|---|---|
instruction | 任务指令 | ”请把以下英文翻译成中文” |
input | 输入内容(可为空) | “Hello, world!” |
output | 期望输出 | ”你好,世界!” |
对于纯开放式问答,instruction 可以直接是问题本身,input 为空。
3.2 Chat Template
在实际训练中,数据需要转换为模型的输入格式。不同的模型家族使用不同的 chat template:
LLaMA / Qwen 格式示例:
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
你是我的助手。<|eot_id|><|start_header_id|>user<|end_header_id|>
今天天气怎么样?<|eot_id|><|start_header_id|>assistant<|end_header_id|>
今天天气晴朗,适合出行。<|eot_id|>
ChatML 格式(Qwen2 等):
<|im_start|>system
你是我的助手<|im_end|>
<|im_start|>user
今天天气怎么样?<|im_end|>
<|im_start|>assistant
今天天气晴朗,适合出行。<|im_end|>统一要求:
- 模板一致性:同一训练任务中,所有数据必须使用完全相同的 template。不同 template 混用会导致模型学到”在不同格式下输出不同内容”,严重影响训练稳定性。
- 特殊 token 清晰:system/user/assistant 的 role token 必须和模型预训练时一致,否则模型会对奇怪的角色标记感到困惑。
3.3 多轮对话
多轮对话数据的构造相对复杂。关键原则是:将完整的多轮对话历史作为上下文,模型只预测下一个 assistant 回复。
示例(3 轮对话):
<|im_start|>system
你是一个有帮助的助手。<|im_end|>
<|im_start|>user
你好,请介绍一下北京。<|im_end|>
<|im_start|>assistant
北京是中国的首都,位于华北平原北部,是一个历史悠久的城市。<|im_end|>
<|im_start|>user
那里有什么好吃的?<|im_end|>
<|im_start|>assistant
北京有很多特色美食,比如北京烤鸭、炸酱面、豆汁儿等。<|im_end|>
在训练时,loss 只在最后一个 <|im_start|>assistant 之后的 token 上计算,前面的所有 user/assistant 轮次都作为 context。
多轮对话的训练策略:
- 方案一:每个 turn 单独作为一条样本,上下文截断到固定长度
- 方案二:将完整多轮对话作为一个样本训练,loss 只在最后 assistant 回合计算
- 方案三:随机选择一个 turn 作为监督目标,前面的所有内容作为 context(类似 DPO 的做法)
4. SFT 与相关概念的关系
4.1 SFT 与 Instruction Tuning
Instruction Tuning 是一个更宽泛的概念,指的是让模型学会遵循指令的能力。它的训练方式可以是 SFT(用指令-响应对数据),也可以是 RLHF(通过奖励模型训练)。
通常情况下,人们说”做 Instruction Tuning”时,指的就是”做 SFT”。但严格来说:
- Instruction Tuning 是目标:让模型学会遵循指令
- SFT 是方法:用监督学习(交叉熵)来实现这个目标
两者常被混用,不必过于纠结概念区分。
4.2 SFT 与 Pretraining 的核心差异
| 维度 | Pretraining | SFT |
|---|---|---|
| 数据来源 | 互联网海量文本 | 人工标注/高质量问答 |
| 数据量级 | 万亿 tokens | 数十万~数百万条 |
| 监督方式 | 自监督(next token prediction) | 有监督(指令-响应对) |
| Loss 覆盖 | 所有 token | 通常只覆盖 output 部分 |
| 训练目标 | 学习语言知识、常识、世界知识 | 学习指令跟随、任务完成 |
| 典型过拟合现象 | 较少 | 常见(见第 7 节) |
SFT 不是预训练的替代,而是预训练的后续微调阶段。预训练赋予模型语言能力和知识储备,SFT 则将这些能力引导到”按照指令完成任务”的轨道上。
5. 工程流程
5.1 数据预处理流水线
Step 1:数据清洗
原始数据 → 格式校验 → 质量过滤 → 去重 → 长度截断 → 格式转换 → 保存
- 格式校验:检查必填字段是否存在,role token 是否完整,特殊字符是否转义
- 质量过滤:移除有毒内容(色情、暴力、政治敏感)、过长/过短样本、重复内容过多的样本
- 去重:使用 MinHash 或 SimHash 对文本做近似去重,去除完全重复或高度相似的样本对
Step 2:样本构造
- 格式化:根据选定 chat template,将每条数据转换为模型输入格式
- 长度分析:统计样本长度分布,确定合理的 max_length(通常 2048~8192,根据模型和硬件决定)
- 长度截断策略:
- 方案 A:截断 input 保留 output(output 往往更重要)
- 方案 B:截断中间部分保留首尾(head + tail truncation)
- 方案 C:直接丢弃超过 max_length 的样本
Step 3:数据分区
建议的划分比例(可根据数据量调整):
- 训练集:95%
- 验证集:3%
- 测试集:2%
注意:验证集和训练集必须来自同分布,不能用 Pretraining 数据混入验证集。
5.2 Chat Template 设计规范
设计一个 SFT 可用的 chat template,需要满足以下要求:
- 覆盖所有角色:system、user、assistant 是必须的,有些场景还需要 tool、observation 等
- 区分明显:不同 role 的 token 在语义上不能有歧义
- 兼容预训练:template 中的 special token 应该在模型词表中存在
- 支持多轮:模板必须能处理任意轮次的对话
不推荐的做法:
- 在模板中使用模型词表中不存在的 token
- 不同数据使用不同的模板
- 在 assistant 回复中混入其他 role 的 token
5.3 多轮对话的监督信号构造
多轮对话训练时,一个常见问题是如何决定哪些 token 要计算 loss:
推荐策略:
- 最后一个回合监督:只对最后一轮 assistant 的回复计算 loss。优点是简单、训练稳定。
- 所有回合监督(不推荐):对所有 assistant 回合计算 loss。但容易导致模型过度关注中间轮次,忽略最后一轮的质量。
- 随机回合监督:每个样本随机选择一个回合的 assistant 作为监督目标。折中方案,训练效果通常优于仅最后一轮监督。
注意:user 的回复永远不计算 loss。
5.4 训练超参数设置
以下是 SFT 中相对稳定的超参数配置(以 7B~70B 模型为例):
| 参数 | 推荐范围 | 说明 |
|---|---|---|
| 学习率 | 1e-5 ~ 5e-5 | 大模型用更小的学习率,LLaMA3 8B 建议 2e-5 |
| Batch Size | 4~16(per device) | 配合 gradient accumulation 达到 global batch 32~128 |
| Epoch | 1~3 | SFT 数据量有限,过拟合风险高,通常不超过 3 epoch |
| Warmup | 3~10% 总步数 | 学习率从 10% 线性上升到峰值 |
| Weight Decay | 0.1 ~ 0.01 | 大模型通常用 0.1 |
| Max Length | 2048 / 4096 / 8192 | 根据显存和典型样本长度选择 |
| LR Schedule | cosine / constant | SFT 训练步数有限,cosine 衰减效果不明显 |
学习率选择建议:
- 数据量 < 100K 条:学习率 1e-5 ~ 2e-5
- 数据量 > 100K 条:学习率 2e-5 ~ 5e-5
- 二次预训练(续训):学习率 5e-6 ~ 1e-5
6. 训练稳定性与常见问题
6.1 训练崩溃与梯度问题
梯度爆炸:SFT 训练的后期,loss 可能突然上升,导致模型崩溃。
- 解决思路:使用 gradient clipping(通常 1.0),监控梯度的 norm 变化
- 检查学习率是否过高,SFT 后期建议使用更小的学习率
Loss 爆发(Loss Spike):训练到某一时刻 loss 突然升高。
- 原因:可能是学习率太高,或某些异常样本导致
- 解决:降低学习率,使用 early stopping 保存最佳 checkpoint
6.2 过拟合
SFT 数据量相对预训练非常小,模型很容易过拟合。表现为:
- 在训练集上 loss 持续下降,但验证集 loss 反而上升
- 模型开始逐字记忆训练数据中的回复
缓解方法:
- 增加数据量和数据多样性
- 使用 dropout(如果框架支持)
- 降低 epoch 数,保存验证集 loss 最低的 checkpoint
- 使用 larger batch size(等价于更多的 step)
- 添加适当的 weight decay
6.3 模板化回答(回复缺乏多样性)
模型在某些指令上反复给出格式化的回答,例如:
- “作为一个 AI 模型,我…”
- “根据我的分析,…”
- “以下是几个要点:1. … 2. … 3. …”
原因:训练数据中类似回答出现频率过高。
缓解方法:
- 在数据构造阶段,控制模板化回复的比例
- 增加数据的多样性,让模型学到更多样的表达方式
- 使用 RLHF/DPO 阶段进一步提升回复多样性
6.4 拒答倾向
模型在面对某些类型的问题时,倾向于说”我不知道”或给出安全但无用的回答。
原因:训练数据中包含大量拒答样本,或 RLHF 阶段过度强调安全性。
缓解方法:
- 在 SFT 数据中减少拒答样本的比例
- 在 RLHF 阶段调整 reward model 的权重
- 使用 DPO 训练,增加正负样本的对比
6.5 胡编乱造(Hallucination)
模型在给出事实性陈述时编造假信息。
原因:训练数据中的事实信息不准确,或模型缺乏相关领域的知识。
缓解方法:
- 在 SFT 数据中确保事实性输出的准确性
- 在预训练阶段增加相关领域的数据
- 使用 RAG(检索增强)架构在推理阶段引入外部知识
7. 中间评测与样本抽检
SFT 是一个数据驱动的过程,数据质量直接决定模型上限。因此,训练过程中的评测至关重要。
7.1 评测指标设计
| 指标类型 | 具体指标 | 评测方式 |
|---|---|---|
| 语言能力 | PPL / token accuracy | 在验证集上计算 |
| 任务能力 | Pass@K (code) / ROUGE (summarization) | 任务特定评测 |
| 对话能力 | 人工评估 / LLM 评估 | 用 GPT-4 评估模型回复质量 |
| 安全性 | 拒答率 / 毒性率 | 自动 + 人工 |
7.2 中间评测策略
- 每 N 步评测一次:建议每 500~1000 步在验证集上跑一次完整评测
- 保存多个 checkpoint:不要只保存最后一个 checkpoint,SFT 容易过拟合,训练过程中会存在一个最优 checkpoint
- Early Stopping:监控验证集 loss,当连续 3 次评测验证集 loss 上升时,停止训练
7.3 样本抽检
自动化指标只能反映整体趋势,具体问题需要通过人工抽检来发现。
抽检流程:
- 从当前 checkpoint 生成的样本中随机抽取 20~50 条
- 逐条检查:是否遵循指令、是否事实正确、是否通顺自然
- 记录问题类型:格式错误、事实错误、表达不当、创意不足等
- 根据问题分布,决定是否需要补充/清洗数据
抽检频率:建议每 epoch 结束进行一次。
8. SFT 数据构造模板
# 数据字段规范
fields:
- instruction: str # 任务指令,必填
- input: str # 输入内容,可为空字符串
- output: str # 期望输出,必填
- system: str # 系统提示,可选,默认"你是一个有帮助的助手"
- category: str # 任务类别,用于分层采样
- source: str # 数据来源,便于追溯
# Chat Template(以 Qwen 格式为例)
chat_template: |
{% for message in messages %}
{% if loop.first and messages[0]['role'] != 'system' %}
<|im_start|>system
你是一个有帮助的助手。<|im_end|>
{% endif %}
<|im_start|>{{ message['role'] }}
{{ message['content'] }}<|im_end|>
{% endfor %}
{% if add_generation_prompt %}
<|im_start|>assistant
{% endif %}
# 数据质量过滤规则
quality_filters:
- 长度约束:
instruction_min: 5
instruction_max: 512
output_min: 10
output_max: 4096
total_max_length: 8192
- 内容过滤:
exclude_patterns:
- "xxx"
- "测试"
min_instruction_complexity: 0.3
- 去重规则:
method: simhash
threshold: 0.99. SFT 训练排错清单
训练不收敛
| 检查项 | 解决方案 |
|---|---|
| 学习率是否过低 | 尝试 1e-5 ~ 5e-5 |
| 数据格式是否与 chat template 一致 | 检查 special token 是否匹配 |
| 数据是否存在大量重复 | 执行去重步骤 |
| Batch size 是否太小 | 增加 per_device_batch_size |
Loss 爆炸
| 检查项 | 解决方案 |
|---|---|
| 学习率是否过高 | 降低到 1e-5 |
| 是否使用 gradient clipping | 设置 max_grad_norm = 1.0 |
| 是否存在异常样本 | 检查数据中是否有超长/损坏的样本 |
过拟合
| 检查项 | 解决方案 |
|---|---|
| Epoch 是否过多 | 减少到 1~2 epoch |
| 数据量是否不足 | 增加数据多样性或数据量 |
| 学习率是否过高 | 降低学习率 |
| 是否保存了最佳 checkpoint | 使用验证集 loss 最低的 checkpoint |
回复质量差(模板化、拒答、胡编)
| 检查项 | 解决方案 |
|---|---|
| 数据质量是否不足 | 重新清洗数据,确保高质量 |
| 数据多样性是否不足 | 增加不同来源的样本 |
| 模板化数据是否过多 | 减少格式化回复的比例 |
| 拒答样本是否过多 | 减少”我不知道”类样本 |
| 事实性错误是否多 | 核实数据中的事实信息 |
评测指标与实际效果不一致
| 检查项 | 解决方案 |
|---|---|
| 评测集与训练集分布是否一致 | 确保同分布 |
| 自动化指标是否与人类偏好一致 | 引入人工抽检 |
| 是否存在数据泄露 | 检查评测集是否混入训练集 |
10. 总结
SFT 是将预训练模型转化为可用对话模型的关键步骤。其核心是:
- 数据为王:SFT 的效果上限由数据质量决定。数据格式统一、模板一致、去重清洗、事实准确是基本要求。
- Loss Mask 是关键:只对 assistant 回复计算 loss,避免模型学习”复述输入”。
- 多轮对话构造:将完整对话历史作为上下文,随机或固定选择监督回合。
- 训练稳定性:学习率不宜过高,梯度裁剪必须开启,checkpoint 得多保存。
- 持续评测:自动化指标 + 人工抽检双轨并行,及时发现数据问题和模型问题。
SFT 不是终点,而是起点。经过 SFT 的模型,通常还需要经过 RLHF(DPO/PPO)阶段来进一步对齐人类偏好,才能成为真正可用的大语言模型。