问一个刚预训练完的小模型”猫喜欢做什么?“,它可能接一句”早上喝一杯水对身体好。“它没有错:它学的就是”给一段文字,续写下一段”,而网上一个问句后面跟什么都有可能。GPT-3 也是这样,要靠精心设计的 few-shot 示例去哄。后训练要解决的问题是:怎么用很少的额外算力,把这台续写机变成一个能读懂指令、一次答对、知道什么时候停下来的助手。
前置:Transformer 架构、预训练数据、从线性回归到神经网络(交叉熵与梯度下降)
第一步:给它看示范(SFT)
最朴素的想法是:想要什么行为,就收集这种行为的数据,接着训。这就是 SFT(Supervised Fine-Tuning)。训练方法和预训练一模一样,还是 next-token 预测、还是交叉熵,变的只有数据和一处细节。
数据先要拼成模型能读的一串 token。对话被包进一个 chat template,Qwen 系的格式是每条消息以 <|im_start|>角色 开头、<|im_end|> 结尾:
<|im_start|>user
猫喜欢做什么?<|im_end|>
<|im_start|>assistant
猫喜欢晒太阳和睡觉。<|im_end|>推理时模板只拼到 <|im_start|>assistant 加换行,后面留给模型写。那处细节是:用户的问题不需要模型学着说,要学的只有回答。所以 labels 先全部填 -100(PyTorch 交叉熵默认忽略的值),只把 assistant 那段打开:
import torch, torch.nn.functional as F
torch.manual_seed(0)
prompt = [11, 12, 13, 14]; answer = [21, 22, 23]; EOS = 2
ids = torch.tensor(prompt + answer + [EOS])
labels = ids.clone(); labels[:len(prompt)] = -100
print("ids ", ids.tolist()); print("labels", labels.tolist())
logits = torch.randn(len(ids), 32)
# 位置 t 预测 t+1,所以 logits 去掉最后一位、labels 去掉第一位
loss = F.cross_entropy(logits[:-1], labels[1:], ignore_index=-100)
print("参与 loss 的 token 数:", (labels[1:] != -100).sum().item(), " loss:", round(loss.item(), 4))ids [11, 12, 13, 14, 21, 22, 23, 2]
labels [-100, -100, -100, -100, 21, 22, 23, 2]
参与 loss 的 token 数: 4 loss: 3.452注意结束符 2 也参与了 loss。如果把它也 mask 掉,模型学会了怎么回答,却学不会什么时候闭嘴,会一直生成下去。SFT 一般从预训练权重开始、用小得多的学习率(MiniMind 用 1e-5,是预训练 5e-4 的 1/50),免得把预训练学到的东西冲掉。
用 4 组问答把一个小模型训 100 步,loss 从 2.8 降到 0.004,再问”猫喜欢做什么?“就能答对并停下来。但换成没见过的”狗喜欢做什么?“,它仍然回答”猫喜欢晒太阳和睡觉。“格式学会了,内容是背的。这暴露了 SFT 的本质:它最擅长把预训练里已有的能力引出来,不擅长教新东西。模型够大时,少量高质量示范就够,泛化主要来自预训练。
做 SFT 时把回答末尾 <|im_end|> 的 label 也设成 -100,只让回答正文算 loss,最可能出现什么问题?
结束符也是一个要学的 token,模型只有在训练里反复看到"答完接 <|im_end|>",推理时才会主动输出它,推理框架才知道该停。
loss 照样会下降,只是少学了一个 token;复述问题是 prompt 没 mask 时才有的倾向;推理框架靠模型生成结束符来停,不会替模型加。
示范数据教会的不只是你想教的
SFT 数据的演变很有代表性。早期的 FLAN 把现成 NLP 数据集改写成指令任务,规模大但格式生硬,平均回答只有 31 个 token;Alpaca 用 GPT-3.5 生成 5.2 万条,回答 65 token;Open Assistant 是志愿者手写的,212 token;ShareGPT 是用户分享的 ChatGPT 对话,358 token。
在 LLaMA 13B 上分别微调后,Open Assistant 让 MMLU 几乎不动(42.3 → 43.3),对 Davinci-003 的偏好胜率却从几乎为零涨到 58.1。**风格能大幅改变用户偏好,能力却不一定变了。**人和 GPT-4 做两两比较时,都明显偏爱带列表的、更长的回答。只看点赞数,很容易误以为数据变好了。
更隐蔽的问题是幻觉。Open Assistant 里有一条讲 monopsony 的高质量回答,结尾引用了一篇 2013 年的论文。这条样本同时教了两件事:这篇论文的内容,以及”被要求时要在结尾给出引用”。如果模型本来不知道这篇论文,第一件学不会,第二件却会泛化成”不管知不知道,都给个引用”,于是开始编造文献。John Schulman 的说法很精确:标注员写了模型不知道的正确答案,等于在教模型猜;标注员不知道而模型知道,等于在教模型藏。理想的训练目标取决于模型自己知道什么,而外部标注员看不到这一点。
安全数据则相反,少量就很管用:几百条拒绝恶意请求的示例就能让有害输出大幅下降。难点在平衡违规率和误拒率,“怎么 kill 一个卡死的 Python 进程”被拒绝就是误拒,所以要专门收集这类边界 prompt。
最后一个现实:现在所谓的 base model 往往不”纯”。很多实验室把指令数据混进预训练末尾的学习率衰减阶段(mid-training),因为这一段离部署最近、学习率最低,最高质量的数据应该放在这里。预训练和后训练的边界已经模糊。
从模仿到优化:RLHF
SFT 有一个天花板:它只能模仿人写的东西。而人写出来的东西,不一定是人最喜欢的。有研究请自由撰稿人写新闻摘要,再让标注员比较撰稿人摘要和 InstructGPT 的摘要,结果几乎五五开,几位撰稿人自己也更喜欢模型写的。生成比验证难:很多人写不出好答案,但看两个答案能分出好坏。
这引出目标的切换:
SFT 是拟合一个分布,需要参考答案的样本;RL 把模型当成 policy,只需要一个能给输出打分的函数 R。人的偏好不能直接当函数调用,于是 InstructGPT 的三步流程是:
- SFT:标注员写示范,训一个 SFT 模型。
- 奖励模型(RM):SFT 模型对同一 prompt 生成多个回答,标注员排序,训一个给回答打分的模型。
- RL:用 PPO 最大化 RM 分数。
奖励模型用的是 Bradley–Terry 成对损失:对标注员偏好的 和不偏好的 ,
它不要求分数有绝对含义,只要求赢的比输的高。
为什么不能直接最大化 RM 分数?因为 RM 是学出来的,它只在见过的分布附近可靠。policy 一旦钻进 RM 的漏洞(比如无限拉长、堆砌讨好的话),RM 分数一路涨,真实质量反而下降。这叫过度优化:AlpacaFarm 的实验里,best-of-n、expert iteration、PPO 三种方法的真实胜率都随代理奖励先升后降。所以 RL 目标里要加一项对 SFT 模型 的 KL 惩罚:
KL 项让 policy 别离 SFT 模型太远,留在 RM 熟悉的区域里,同时起到熵奖励的作用,防止塌缩到一种回答。
PPO 是用来优化这个目标的算法。最原始的 policy gradient 形式上就是”按奖励加权的 SFT”,但方差大,而且每更新一步都要重新采样,采样就是慢的推理。PPO 用重要性权重复用旧样本走几步,再把概率比 ratio = π_new / π_old 裁剪在 [1-ε, 1+ε] 里,走太远就不再给奖励:
这里的优势 A 要靠另一个 value model 估计。于是 PPO 训练时同时要放 policy、reference、reward model、value model 四个模型,还对实现细节极其敏感,是出了名的难调。
RLHF 的目标里加了对 SFT 模型的 KL 惩罚 β·KL(π || π_ref),它起什么作用?
奖励模型是学出来的代理,离开它见过的分布就不可靠,KL 项把 policy 拴在 SFT 模型附近,同时抑制塌缩。
KL 不更新奖励模型;它约束的对象是 SFT 模型而不是预训练模型,InstructGPT 里修复 benchmark 掉分靠的是另外混入预训练梯度那一项。
跳过奖励模型:DPO
PPO 这么难,能不能不做 RL?DPO(Direct Preference Optimization)的出发点是一个数学观察:上面带 KL 的目标,如果允许 π 是任意分布,有闭式最优解
直觉很清楚:在参考模型的基础上,按奖励的指数重新加权,β 越小重新加权越狠。这个解没法直接用,因为 要对所有可能的回答求和。但把它倒过来,奖励可以用 policy 表示:
把这个”隐含奖励”代入 Bradley–Terry 损失。同一个 x 的两个回答相减, 正好抵消,难算的部分消失了:
没有奖励模型,没有采样,没有 value model,只需要偏好对数据、一个冻结的参考模型和普通的反向传播。它的梯度是
方括号里是”推高好回答、压低坏回答”,前面的 σ 是权重:隐含奖励判断错得越离谱,这一步走得越大。下面的 log π 是整条回答所有 token 对数概率之和(和 SFT 一样只算回答部分),数字是构造的:
def dpo_loss(pc, pr, rc, rr, beta=0.1):
# pc/pr: policy 对 chosen/rejected 的 log 概率;rc/rr: 参考模型的
margin = beta * ((pc - rc) - (pr - rr))
return -F.logsigmoid(margin), torch.sigmoid(-margin)
for name, pc, pr in [("刚初始化", -50., -48.), ("偏向 chosen", -48., -51.), ("偏向 rejected", -52., -47.)]:
l, w = dpo_loss(torch.tensor(pc), torch.tensor(pr), torch.tensor(-50.), torch.tensor(-48.))
print(f"{name:12s} loss {l.item():.3f} 梯度权重 {w.item():.3f}")刚初始化 loss 0.693 梯度权重 0.500
偏向 chosen loss 0.474 梯度权重 0.378
偏向 rejected loss 0.854 梯度权重 0.574第一行是训练第一步:policy 就是参考模型,margin 为 0,loss 恰好是 log 2 ≈ 0.693。注意参考模型本身更偏爱 rejected(-48 比 -50 高)并不影响:DPO 看的是相对参考模型的变化,不是绝对概率。这也意味着一个常见现象:训练中 chosen 和 rejected 的绝对概率可能一起下降,只要 rejected 降得更多,loss 就在变小。
DPO 和 PPO 谁更好,结论依赖设置。AI2 的一篇论文里 PPO 略胜,另一篇(Tulu 3)里长度归一化的 DPO 最好。课上的判断是:方向都是”好的往上推、坏的往下压,步长设对”,不在前沿规模时差别可能不大。Llama 的做法是外面套一层 expert iteration:每轮让模型对每个 prompt 生成 K 个回答,用奖励模型做拒绝采样得到新 SFT 数据,再做 DPO,最好的模型进入下一轮。变体 SimPO 去掉了参考模型并按长度归一化,目的是防止模型靠拉长回答刷分。
β = 0.5,训练一段时间后,y_w 和 y_l 的 log(πθ/π_ref) 都是 +1。这对样本的 DPO 损失和梯度权重是多少?
DPO 只看两者之差:0.5 × (1 − 1) = 0,−log σ(0) = log 2 ≈ 0.693,权重 σ(0) = 0.5,和刚初始化时一样。
两个都升高不代表学会了偏好,相对差距没拉开;0.474 是 margin 为 0.5 时的值,0.313 对应 margin 为 1,都需要两个对数比不相等。
奖励不用学:RLVR 与 GRPO
RLHF 的根本限制是那个学出来的奖励模型:优化得越狠,越会被钻空子,加多少 KL 最终都会碰壁。AlphaGo 没有这个问题,因为胜负没有歧义,优化的就是真实目标,所以可以一直加算力。RLVR(RL with Verifiable Rewards)的思路是找答案能自动检查的领域:数学题对照标准答案,代码跑单元测试。算法和 RLHF 差不多,差别在奖励从哪来。
有了可靠的 0/1 奖励,DPO 不太合适(它要成对偏好,对错题没有天然的配对结构),PPO 的 value model 又贵又难调。DeepSeekMath 提出的 GRPO(Group Relative Policy Optimization)只改了一处:对同一道题采样 G 个回答,用组内统计量代替 value model 当 baseline:
以前要训一个网络预测”这题我平均能拿几分”,现在直接对同一题多做几次,比平均好的就鼓励,比平均差的就压低。减去组内均值是合法的 baseline:policy gradient 里减去一个只和 prompt 有关、不随回答变化的量,梯度期望不变,只降低方差。
def grpo_adv(r, G, std=True, eps=1e-4):
r = r.view(-1, G); a = r - r.mean(1, keepdim=True) # 每行是同一道题的 G 个回答
if std:
# unbiased=False 是除以 G 的总体标准差;torch 默认除以 G-1,结果会不同
a = a / (r.std(1, keepdim=True, unbiased=False) + eps)
return a.view(-1)
r = torch.tensor([1., 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1])
print("GRPO ", [round(x, 2) for x in grpo_adv(r, 4).tolist()])
print("Dr.GRPO", [round(x, 2) for x in grpo_adv(r, 4, std=False).tolist()])GRPO [1.73, -0.58, -0.58, -0.58, 1.0, 1.0, -1.0, -1.0, 0.0, 0.0, 0.0, 0.0]
Dr.GRPO [0.75, -0.25, -0.25, -0.25, 0.5, 0.5, -0.5, -0.5, 0.0, 0.0, 0.0, 0.0]三组分别是难题(4 个里对 1 个)、中等题、全对的简单题。看第三组:全对或全错时所有 advantage 都是 0,这道题花了 4 次 rollout 的算力,却不产生任何梯度。所以 RL 的数据要按难度筛,只留模型”有时对、有时错”的题,Kimi K1.5 就把模型多次采样都能答对的题去掉了。
再看第一组:除以 std 后难题被放大了约 2.31 倍,中等题只放大 2 倍。除以 std 不在”合法 baseline”的推导里,它让太难和太简单(std 小)的题权重变大,而我们真正想让模型学的是能力边界附近的题。另一处偏差藏在 loss 聚合里:原版 GRPO 先在每条回答内部按长度 |o_i| 平均,于是每个 token 的权重是 A_i/|o_i|。错误回答 A 为负,写得越长每个 token 被罚得越少;极端情况下,模型知道答不对,就一直写下去,惩罚趋近 0。Dr. GRPO 去掉了 std 和 1/|o_i| 两项,实验里 reward 曲线几乎一样,但 GRPO 的输出长度一路涨到 1000 token 以上,Dr. GRPO 稳定在 500 左右,多出来的长度主要来自错误回答。
还有一个容易被公式吓到的事实:如果每批 rollout 只更新一次(完全 on-policy),θ 就等于 θ_old,ratio 恒为 1,clip 永远不触发,GRPO 退化成”组内归一化奖励的 policy gradient 加一个 KL 惩罚”。
一组 4 个 rollout 的 rewards 是 [1, 1, 1, 0]。按 GRPO 的组内 z-score(总体标准差,忽略 eps),advantage 约是多少?
均值 0.75,标准差 sqrt((3 × 0.25² + 0.75²) / 4) ≈ 0.433,所以对的回答是 0.25 / 0.433 ≈ 0.58,错的是 −0.75 / 0.433 ≈ −1.73,正好是难题那组的镜像。
[0.25, …, −0.75] 只减了均值没除 std,是 Dr. GRPO 的结果;只有全对或全错时才没有信号,这里还有一个错的。
一个完整的推理模型配方
DeepSeek-R1 是把这些拼起来的公开配方。它先做了一个干净的实验 R1-Zero:基础模型 DeepSeek-V3 不做 SFT,直接 GRPO,奖励只有两项,答案对不对和格式对不对(推理过程包在 think 标签里)。结果在 AIME 2024 上 pass@1 达到 71.0,接近 o1 的 74.4。这说明只用结果奖励就能把推理能力练出来,不需要过程奖励模型(PRM)或蒙特卡洛树搜索,R1 论文专门记录了这两条路的失败:中间步骤的对错很难定义和标注,还容易被 reward hacking;token 级的搜索空间比棋盘大太多。R1-Zero 里”推理越来越长""出现 aha moment”的现象被大量宣传,但前者至少一部分可以用上面的长度偏差解释。
生产版 R1 的流程是:
DeepSeek-V3 → 少量长 CoT 做 SFT 冷启动 → 推理 RL(GRPO)→ 大规模 SFT + RLHF冷启动 SFT 让推理过程更可读;RL 阶段加了一项”语言一致性”奖励,因为纯 RL 下推理过程常常中英混杂,加了之后指标略降但更好读;最后一阶段混入非推理数据和不可验证任务的 RLHF,让它也是一个好用的聊天助手。最终 AIME 2024 达到 79.8,MATH-500 达到 97.3。
两个副产品同样重要。一是蒸馏:用 R1 生成 80 万条推理数据去 SFT 小模型,R1-Distill-Qwen-32B 的 AIME 2024 pass@1 达到 72.6。二是少量数据就够:s1 只用 1000 道题配长推理过程做 SFT,MATH-500 就到 93.0。对小模型来说,从强模型蒸馏往往比自己做 RL 更划算,Qwen3 的小模型就是这么来的。
Kimi K1.5 从另一条推导(DPO 式的闭式解加平方损失)也得到了”组内均值做 baseline 的 policy gradient”,而且它不除以 std、不按长度归一化。两条独立路线收敛到同一个 baseline,说明这一部分是本质的。Kimi 还显式加了长度奖励:组内越短的正确回答越好,因为推理成本是真金白银。
Qwen3 又往前走了一步:用 /think 和 /no_think 标签把思考和不思考两种模式训进同一个模型,思考到用户给定的预算就插入一句”时间有限,直接给答案”并收尾。从推理 RL 到通用 RL,Arena-Hard 和工具调用一路涨,AIME 却略降。后训练的每一步都在不同能力之间做取舍。
关于 RLVR 和推理模型的训练,下面哪些说法正确?
规则检查器给的是真实目标,没有可钻的代理模型;组内奖励相同时 advantage 全为 0;R1-Zero 的奖励只有答案正确性和格式。
只有减去均值才是合法 baseline,除以 std 会让 std 小的题被放大,梯度不再无偏;长度增长可能来自长度归一化对错误回答的偏差,Dr. GRPO 去掉它后长度稳定,分数几乎不变。
RLHF 的代价
RL 优化的是期望奖励,不是一个分布,所以它允许每个 prompt 的输出塌缩到少数几种回答上。代价有几处。一是多样性下降:RLHF 后模型回答的熵大量集中在接近 0 的位置,人类回答的熵分布宽得多。二是校准变差:GPT-4 预训练模型在 MMLU 上的校准误差 ECE 是 0.007,PPO 之后变成 0.074,模型说”我有 90% 把握”不再意味着 90% 正确。三是迎合:偏好标注员(以及做评审的 LLM)偏爱长的、自信的、带列表的回答,模型就学会了这些,众包标注员对自信语气下的事实错误低估最多达 22%。
还有标注员本身的影响:谁来标注,模型就像谁。用民意调查题测模型观点,基础模型和指令微调后的模型给出的观点分布明显不同,而标注员群体的构成正是一个可能的来源。现在开源社区的偏好数据大多来自 AI 反馈(UltraFeedback、Tulu 3 用 GPT-4 系打分),GPT-4 做成对评判和人类在系统级排名上的 Spearman 相关达到 0.98,成本低一个数量级;但要推进前沿或需要律师、医生级别的知识时,仍然离不开专家标注。这些”评判者本身有偏差”的问题,在 评估 里还会再遇到。
记住这三件事
- SFT 和预训练是同一个损失,只让回答部分(含结束符)算 loss;它擅长引出已有能力,在模型不知道的事实上微调反而教会它编造。
- RLHF 优化学出来的奖励,必须用 KL 拴住 policy;DPO 利用闭式解把 RL 变成偏好对上的分类损失,
log Z在相减时抵消。 - 能自动判对错的领域用 RLVR:GRPO 用组内均值代替 value model,全对全错的题没有信号,除以 std 和长度归一化会引入偏差。
延伸阅读
- Stanford CS336 Lecture 15–16(SFT、RLHF、RLVR)
- Ouyang et al. 2022, Training language models to follow instructions with human feedback(InstructGPT)
- Rafailov et al. 2023, Direct Preference Optimization
- Shao et al. 2024, DeepSeekMath(GRPO);Liu et al. 2025, Understanding R1-Zero-Like Training(Dr. GRPO)
- DeepSeek-AI 2025, DeepSeek-R1
- MiniMind(从零实现 SFT / DPO / GRPO 的小模型仓库)