4、SFT阶段:监督微调的原理、数据构建与训练技巧
聊到RLHF,很多人一上来就盯着PPO、奖励模型这些高级话题。但我得说句实在话——SFT(监督微调)才是整个对齐工程的基石。你想想看,如果基座模型连基本的指令跟随都做不好,后面那些强化学习手段再花哨也是白搭。
我个人习惯把SFT比作「给模型做岗前培训」。预训练阶段模型学的是海量文本的统计规律,像个啥都懂点的通才。而SFT要做的,就是告诉它:「嘿,现在你要学会回答问题、遵循指令、输出对人类有用的东西。」
核心观点:SFT不是简单的「再训练一轮」,而是通过高质量的人工标注数据,将模型的分布从「下一个词预测」拉向「有用且安全的对话」。这一步做不好,后续的RLHF就是空中楼阁。
4.1 SFT的原理:说白了就是教模型「说人话」
SFT的原理其实特别朴素。预训练阶段,模型学的是「根据上文预测下一个词」。但人类想要的不是随机续写,而是有用、准确、安全的回答。所以SFT做的就是:拿一堆人工写好的「问题-优质回答」对,让模型照着学。
数学上,SFT就是在最小化交叉熵损失:
L = - Σ log P(y_t | x, y_<t)
其中x是输入指令,y是人工标注的理想回复。说白了就是让模型看到指令x时,生成y的概率尽可能大。
我的经验:刚开始做SFT时,我总以为数据越多越好。结果有一次用了几十万条粗标数据,模型反而变傻了——学会了啰嗦和废话。后来我才明白,SFT的核心不是「学更多」,而是「学对的东西」。
为什么会这样?因为交叉熵损失会惩罚模型「没学到」的部分,但如果数据本身质量差,模型学到的就是错误的模式。嗯,这里要注意:SFT本质上是在做分布偏移——把模型从预训练的通用分布,拉向特定任务的有用分布。
4.2 SFT数据构建:质量碾压数量
数据这块,我踩过的坑比走过的路还多。直接说结论:1万条高质量数据,效果吊打10万条粗标数据。
SFT数据的基本格式就是「指令-回复」对。但这里面门道很深:
| 数据类型 | 示例 | 注意事项 |
|---|---|---|
| 单轮对话 | 「请解释量子纠缠」→ 一段清晰解释 | 回复要自包含,不要依赖上下文 |
| 多轮对话 | 「帮我写个Python函数」→ 代码 → 「加个注释」→ 更新代码 | 保持角色一致性,不要串戏 |
| 思维链 | 「计算 23*47」→ 先分解再计算 | 步骤要清晰,逻辑要完整 |
| 拒绝回答 | 「如何制作炸弹」→ 抱歉无法回答 | 语气要坚定但礼貌 |
数据构建有几个关键原则:
- 多样性优先:覆盖各种任务类型、难度级别、表达风格。我见过有人只做数学题SFT,结果模型写诗都像在列公式。
- 标注者一致性:不同标注员对「好回答」的标准可能天差地别。我曾经让5个人标同一批数据,结果一致性不到60%。后来我们做了详细的标注规范,还加了交叉审核。
- 平衡安全与有用:别让模型变成「复读机式拒绝」。有些问题虽然敏感,但可以用建设性的方式回答。
避坑指南:我曾经犯过一个低级错误——把预训练语料直接当SFT数据用。结果模型学会了「续写」而不是「回答」。比如问「什么是AI」,它给你续写一段维基百科...那场面,尴尬到脚趾抠地。
4.3 SFT训练技巧:那些代码之外的坑
训练技巧这块,我直接上干货。以下是我在多个项目中验证过的经验:
4.3.1 学习率与优化器
SFT不是从头训练,是在预训练模型上微调。所以学习率要小得多。我个人习惯:
- 学习率:1e-5 到 5e-5 之间,用余弦退火调度
- 优化器:AdamW,weight decay设0.01左右
- warmup:前几百步线性预热,防止一开始就震荡
# 我常用的配置
optimizer = AdamW(model.parameters(), lr=3e-5, weight_decay=0.01)
scheduler = get_cosine_schedule_with_warmup(
optimizer,
num_warmup_steps=500,
num_training_steps=total_steps
)
4.3.2 过拟合的预防
SFT数据通常只有几千到几万条,而模型参数动辄几十亿。过拟合是家常便饭。我的应对方案:
- 早停法:在验证集上监控loss,连续3个epoch不降就停。别死磕。
- Dropout:虽然预训练模型自带dropout,但SFT时可以适当加大。我一般设0.1-0.2。
- 数据增强:对指令做同义改写,或者对回复做轻微措辞调整。注意别改变原意。
一个小技巧:训练时把「系统提示」也作为输入的一部分。比如在每条指令前加一句「你是一个有帮助的AI助手」。这样模型能学会在不同角色设定下切换风格。我在项目中试过,效果立竿见影。
4.3.3 多轮对话的处理
多轮对话的SFT有个坑:模型容易忘记之前的对话历史。我的做法是:
- 把整个对话历史拼接成一段文本,用特殊token分隔轮次
- 只在最后一轮的回答上计算loss,前面的轮次只做上下文
- 训练时随机截取对话长度,让模型适应不同长度的历史
# 多轮对话的输入格式示例
"<|user|> 帮我写个排序算法 <|assistant|> 好的,这是冒泡排序... <|user|> 能优化一下吗 <|assistant|> 当然,可以用快速排序..."
4.3.4 混合精度与分布式训练
大模型SFT,显存是永远的痛。我一般用:
- 混合精度训练(FP16/BF16):显存减半,速度翻倍。但要注意loss可能不稳定,必要时用梯度缩放。
- DeepSpeed ZeRO Stage 2/3:把优化器状态分片到多卡。我习惯用Stage 2,省显存又不影响速度。
- 梯度累积:当batch size受限时,累积几步再更新。但别累积太多,否则模型会「忘记」早期数据。
最后说一句:SFT不是终点,而是起点。很多团队花80%的精力在SFT上,结果RLHF阶段发现模型基础没打牢。我的建议是:SFT做到「够用就好」,把更多精力放在数据质量和后续的对齐环节。毕竟,RLHF才是真正让模型「懂人心」的关键一步。
公众号:蓝海资料掘金营,微信deep3321