☰
从SFT到强化学习微调:PPO、奖励模型与GRPO工程实践
2026/10/27 12:13:44 网站建设 项目流程

做过几轮大模型微调的人大概都遇到过同一种落差感:SFT(监督微调)跑完,loss曲线漂亮得像教科书,离线评测指标也稳步上涨,可模型一进真实业务对话就开始露馅——回答又臭又长、动不动先来一句"这是个很好的问题"、遇到需要明确取舍的指令就开始和稀泥。问题不在于模型没学会说话,而在于SFT的每条样本都是"标准答案",模型学到的是"在这个位置该接哪个token",它从来没被明确告知过"在两个都说得通的回答里,为什么这个更好、那个更差"。强化学习与finetuning结合的这一整套方法,补的正是这个缺口:把"什么算好"变成一个可以量化的打分信号,再让模型自己在采样、试错、比较中把策略往高分方向推。这套东西听起来门槛很高,拆开看其实就三块——一个能打分的裁判、一个更新稳定的算法、一套扛得住显存和吞吐的工程实现。下面我按自己实际跑过的链路,从动机、原理、参数到踩坑,一条条摊开讲。

1. SFT的天花板在哪:为什么"模仿"永远学不会"取舍"

1.1 最大似然估计的隐含假设,在对话任务里并不成立

SFT的优化目标是最小化交叉熵,也就是让模型在给定上文时,把人类标注的那个回答的每个token概率往上抬。这个目标背后藏着一个很强的假设:标注数据里的每个回答都是唯一正确解,且模型应当在这个位置上给出尽可能高的置信度。问题在于,对话任务里"正确"通常是分布式的——同一个问题,简洁版回答和详细版回答都可能被用户接受,甚至同一个用户在不同场景下偏好还不一样。最大似然把这几十种合理回答压成一个点估计,模型自然学不到分布的形状,只学会了分布的重心。

更麻烦的是曝光偏差(exposure bias)。训练时模型看到的每个前缀都来自人类标注的真实文本,推理时前缀来自它自己。一旦它在第20个token处犯了个小错,后面就要在一个自己从没见过的状态分布上继续生成。这个误差会沿着序列滚雪球,表现出来就是"开头还行,越说越偏"。

1.2 从一个具体案例看:为什么"更好"和"正确"不是同一件事

我之前做过一个指令改写的小任务,要求模型把口语化请求转写成结构化指令。SFT数据里有这样一组对比样本:用户说"帮我把这段话改短一点但别丢了关键信息",标准答案是"请在保持语义完整的前提下压缩以下文本"。训练完的模型确实能生成这个句式,但它在遇到更模糊的输入时会开始堆砌修饰词,因为它从没见过"过度冗长"被扣分的样子。也就是说,它的行为边界完全由数据覆盖度决定,而人类标注不可能覆盖所有长尾场景。

这就是强化学习介入的动机:它不要求你告诉模型"正确输出是什么",只需要你告诉它"这两个输出里哪个更好"。偏好信号比标准答案廉价得多,也更容易规模化。你用不着请专家逐字写完美回答,只要找人做A/B比较,或者在业务系统里埋一个用户反馈埋点,就能源源不断地产生训练信号。这个成本结构的差异,是RL微调能在一线真正跑起来的根本原因。

1.3 RL在微调链路上的位置,以及它到底改变了什么

常规链路是预训练 → SFT → RL微调。这个顺序不是随便排的:预训练给模型语言能力和世界知识,SFT教它"指令—响应"的格式和基本任务能力,RL则是在这个已经能跑通的策略上做分布层面的重新加权——把高概率但低质量的输出压下去,把低概率但高质量的输出抬上来。注意这里的关键词是"已经能跑通":如果SFT阶段模型连格式都对不齐,RL会在一个全是垃圾的采样空间里做梯度估计,奖励信号噪声大得根本训不动,这也是很多人抱怨"RL越训越差"的第一大原因。

提示:判断要不要上RL,先看SFT模型的采样里是否存在好答案。如果同一问题采样16条,一条能看的都没有,那问题在SFT数据覆盖度,加RL只会放大噪声。

2. PPO在大模型上的完整数据流:四个模型、两次前向、一次截断

2.1 四个模型的角色分工,别搞混

经典PPO(近端策略优化)在语言模型上要同时驻留四个模型实例,这是显存消耗的最大来源,也是新手最容易算错的地方:

角色是否训练作用显存特征
Actor(策略模型)是生成回答,被优化参数+梯度+优化器状态,最大
Critic(价值模型)是估计状态价值,算优势同Actor,通常同规模
Reward Model否给完整回答打分只做前向,可通过分片/offload 压
Reference Model否算KL惩罚,防跑偏只做前向,权重冻结

一个7B规模的全参数PPO,四个模型用FP16存权重就是 4×14GB ≈ 56GB,加上Actor和Critic的Adam优化器状态(FP32下每个参数约12字节额外开销),光静态占用就能吃掉两张80G卡的绝大部分。这也是为什么实际工程里要么上ZeRO分片,要么干脆只训LoRA,把Actor/Critic的可训练部分压到1%以内。

2.2 一次更新到底发生了哪些计算

我把一个mini-batch的完整流程拆成六步,每一步都有踩坑点:

  1. Rollout采样:Actor用当前策略对一批prompt生成回答,这一步通常占总耗时的60%以上,因为自回归解码是串行的。
  2. 打分:Reward Model对每个(prompt, 完整回答)打一个标量分,Reference Model同步算出参考分布下的对数概率。
  3. 算KL:token级KL通常用近似式logp_actor - logp_ref逐token累加,再乘系数惩罚进奖励里。
  4. 算优势:Critic给出每个位置的value,配合GAE(广义优势估计)算出advantage。
  5. 策略更新:用PPO的截断目标做多轮epoch更新。
  6. 价值更新:Critic回归到实际的回报。

PPO的策略损失长这样,核心是那个clip,它限制了单次更新时新旧策略的比率偏离幅度,防止一步更新把策略推到采样分布之外:

# ratio = exp(logp_new - logp_old) ratio = torch.exp(logp_new - logp_old) surr1 = ratio * advantages surr2 = torch.clamp(ratio, 1.0 - clip_eps, 1.0 + clip_eps) * advantages policy_loss = -torch.min(surr1, surr2).mean() # 混合KL惩罚项,防止策略漂离参考模型太远 loss = policy_loss + kl_coef * kl_divergence

clip_eps常规取0.2,kl_coef初始常在0.01~0.05之间,很多实现用自适应版本:KL超过目标值的1.5倍就把系数调大,低于0.5倍就调小。这个自适应机制我强烈建议开,因为固定系数在训练后期几乎必然会失衡——早期策略离参考模型远,需要强约束;后期收敛了,同样的系数又会把模型捆死,学不动。

2.3 KL惩罚项的两种写法,选错了会白训

KL可以塞进奖励里(per-token reward shaping),也可以直接加到loss上。前者更常用,因为它让每个token的贡献可追溯,配合GAE能更细粒度地分配信用。但很多人忽略了参考分布的选择:Reference Model必须是SFT后的模型,不能是预训练模型。用预训练模型当参考,KL约束的方向就变成了"别离原始语言模型太远",等于把SFT学到的东西又往回拉,表现就是训练几十步后模型突然开始答非所问。

另一个细节是KL的估计方式。用logp_actor - logp_ref的一阶近似在token概率接近时足够准,但如果某处概率差得离谱(比如actor给了一个ref几乎不可能给的token),这个估计会严重低估真实KL,导致惩罚失效。稳妥做法是采样多个token再算k3估计,或者干脆把单token概率比裁到[-10, 10]区间内。

3. 奖励模型这条隐形地基:偏好数据怎么标、怎么训、怎么验

3.1 偏好数据的三种来源,成本和质量的权衡

奖励模型的质量直接决定RL的天花板,垃圾裁判带不出好选手。常见的偏好数据来源有三类:

  • 人工标注的成对比较:质量最高,但单条成本也最高,而且标注员之间的一致性往往没想象中好,两个标注员对同一组的判断一致率能到70%就已经不错了。
  • 从业务日志里挖的隐式反馈:比如用户点了"重新生成"、复制了某一版回答、或者在一轮对话里追问了三次,这些都是弱偏好信号。噪声大但量大,适合做冷启动。
  • 强模型蒸馏出来的偏好:用能力更强的模型对同一prompt采样多个回答并排序。成本可控,但会引入强模型自身的偏置,比如它可能系统性偏好结构化的长回答。

我自己的经验是把前两种混着用,比例大概3:7。纯人工数据训出来的奖励模型泛化性最好但覆盖窄,纯日志数据训出来的模型在分布内很准,一换场景就崩。

3.2 Bradley-Terry假设和它的失效场景

标准奖励模型建立在Bradley-Terry模型上:给定一对回答,好回答的得分减去差回答的得分,经过sigmoid后应该逼近1。损失函数就是一个二分类交叉熵:

# chosen/rejected 分别为被选中和被拒绝的回答的序列级奖励 logits = reward_chosen - reward_rejected loss = -torch.log(torch.sigmoid(logits) + 1e-8).mean()

这个假设的隐含前提是偏好是全局一致的、可传递的。但真实标注里经常出现:A比B好、B比C好,但C又比A好。这种循环偏好在标注一致率不高时特别常见,直接喂进BT模型会让三个样本的损失互相打架,训出来的模型对三个都是"差不多"。处理办法是在数据清洗阶段做传递性检查,把明显矛盾的三角关系剔除,或者引入带噪声的偏好模型(比如把标注员的一致性作为权重乘进loss)。

3.3 奖励模型的验收标准,不能只看pairwise准确率

大部分人验收奖励模型只看验证集上的pairwise accuracy,这个指标有个大坑:它对长度极度敏感。如果你的标注数据里好回答平均比差回答长20个token,那模型很快会学到一个捷径——谁长给谁高分,准确率照样能到70%以上,因为它压根没学内容。验收时我一般会额外跑三组检查:

  1. 长度相关性:算奖励分数和回答长度的Spearman相关系数,超过0.3就要警惕。
  2. 对抗样本:人工构造一批"内容明显更差但更长"的样本,看模型是否误判。
  3. 分布外泛化:拿一个完全没出现在训练集里的任务类型做测试,准确率掉超过15个点说明过拟合严重。

注意:奖励模型训过头(比如训到验证准确率95%)往往不是好事,它会在训练分布上变得极端自信,输出的分数分布被拉得很开,PPO拿到这种奖励很容易被少数几个高分样本带偏。我一般会让它早停,保持在80%~85%的准确率区间。

4. 显存账本与吞吐取舍:单机多卡跑RL微调的现实配置

4.1 全参数PPO的显存拆解,以及为什么大多数人应该先试LoRA

拿一个7B模型举例,FP16权重14GB,Adam的FP32动量+方差再加约56GB,梯度14GB,粗算一下Actor一个就要80GB以上,还没算激活值。四个模型叠一起,8张80G卡打底才比较从容。这还是batch size压到很小的情况,而RL对batch size相当敏感——采样数量太少,优势估计的方差会大到让训练曲线抖成心电图。

所以我的建议很直接:第一次跑通链路,一定用LoRA。只训Actor和Critic的低秩适配器,把可训练参数压到几十M,优化器状态几乎可以忽略,Reference Model和Reward Model做4-bit量化后各自只占4~5GB,整条链路4张80G卡甚至2张就能跑起来。等链路验证通了,再考虑要不要放开全参数——而且很多时候LoRA的效果已经够用,全参数RL的收益并没有账面上那么大。

配置方案显存需求(7B)训练速度效果上限适用场景
全参数PPO8×80G慢高有充足算力、追求极致
LoRA PPO + 量化裁判2~4×80G中中高大多数团队的首选
LoRA + 去掉Critic(GRPO)2×80G较快中高显存紧张、想快速迭代
纯DPO(无rollout)1~2×80G快中偏好数据现成、预算有限

4.2 Rollout加速:整条链路的实际瓶颈不在反向传播

很多人优化RL训练时盯的是反向传播,实际上时间大头在生成。一个batch 512条、每条平均512 token的采样,串行解码能吃掉十几分钟。工程上的标准解法是训练和推理分离部署:用一套专门的高吞吐推理引擎负责rollout,训完一步就把权重同步过去。麻烦点在于权重同步的频率和方式,同步太频繁通信开销吃掉收益,同步太慢策略陈旧(off-policy程度加深)会让重要性采样比率方差爆炸。

我的经验值是每训4~8个mini-batch同步一次,并且在重要性采样比率上做累积截断——如果某条样本的比率超过5,说明它来自一个太旧的策略,直接丢弃比留着更划算。另外rollout的采样温度也有讲究,训练早期温度可以高一点(0.9~1.0)保证探索,后期降到0.7左右让输出更集中,这部分和课程学习的思路一致。

4.3 一个容易被忽略的坑:padding与变长序列的对齐

RL里一个batch内的回答长度差异巨大,短的几十token,长的接近上限。如果用固定长度padding,那些padding位置的logprob会被算进KL和优势里,造成系统性偏差。更隐蔽的问题是长度归一化:算序列级奖励时是求和还是求平均,直接改变了模型的优化方向。求和会让模型倾向于生成更长的回答(因为每个token都可能贡献正奖励),求平均则会让模型倾向于变短。这两种做法在不同任务上各有适用场景,但必须和奖励模型的训练方式保持一致——奖励模型是在完整序列上训的,那RL阶段用求和更自然;如果你在奖励里显式加了长度惩罚,那就得把归一化方式一起调。

5. 不训奖励模型的替代路线:DPO、KTO、GRPO的边界与选型

5.1 DPO:把RL问题解析地折叠进监督损失

DPO(直接偏好优化)的巧妙之处在于,它证明了在KL约束下的奖励最大化问题存在闭式解,于是可以把"训奖励模型+跑PPO"这两步合并成一个直接在偏好对上优化的分类损失:

# pi_logp: 当前策略对chosen/rejected的序列对数概率 # ref_logp: 参考模型的对数概率 chosen_logratio = pi_logp_chosen - ref_logp_chosen rejected_logratio = pi_logp_rejected - ref_logp_rejected loss = -F.logsigmoid(beta * (chosen_logratio - rejected_logratio)).mean()

那个beta是温度系数,控制策略允许偏离参考模型的程度,常用0.1。它的实际作用和PPO里的KL系数是同一个东西,只是换了个名字。DPO的好处是不需要在线采样,一个epoch就是一次普通的前向反向,工程复杂度低了一个数量级,单卡就能跑。坏处也很明确:它是off-policy的,训练数据固定,模型没法在训练过程中探索到自己没想到过的回答。如果你的偏好数据覆盖面足够广,DPO性价比极高;如果数据窄,DPO很容易过拟合到那批样本的措辞上。

5.2 KTO:当你的数据只有"好"和"坏",没有配对

现实里更常见的情况是:你有一批用户点赞的回答,和一批被举报的回答,但它们之间没有配对关系。KTO(Kahneman-Tversky优化)就是为这个场景设计的,它借用了前景理论的思路——人对损失的敏感度高于收益,所以模型对差样本的惩罚权重应该大于对好样本的奖励权重。它不需要成对数据,只需要每条样本打上"好/坏"的二元标签,这让数据收集成本大幅下降。代价是信号强度弱于成对比较,通常需要更多数据量才能达到同等效果。

5.3 GRPO:砍掉Critic,用组内相对优势代替价值估计

GRPO的核心改动是去掉价值模型。对同一个prompt采样一组(通常8~16条)回答,用组内奖励的均值和标准差做归一化,直接得到每个回答的优势:

# rewards shape: [group_size] group_mean = rewards.mean() group_std = rewards.std() + 1e-4 advantages = (rewards - group_mean) / group_std # 后续直接乘到token级logprob上,无需critic的逐位置value

这一刀砍下去,省掉的是整个Critic模型的显存和计算,同时避免了价值估计不准带来的偏差——要知道在长序列任务上,Critic的value估计误差本来就很大,GAE的偏差修正效果有限。GRPO的适用前提是同一prompt的组内奖励要有区分度,如果所有采样得分都差不多(比如任务太简单或太难),归一化后的优势全是噪声,训练会原地打转。

方法需要奖励模型需要在线采样需要Critic数据形态典型适用场景
PPO是是是prompt集合追求上限、算力充足
DPO否否否成对偏好偏好数据现成、快速迭代
KTO否否否单条好坏标签只有点赞/举报日志
GRPO是是否prompt集合显存受限、数学/代码类可验证任务

选型的判断逻辑其实很简单:先看你的数据长什么样。有成对偏好就直接DPO起步,跑完看效果;只有单标签就用KTO;如果任务本身有可验证的正确答案(比如代码能不能跑通、数学题答案对不对),那根本不需要奖励模型,直接规则打分上GRPO,这是最省事也最稳的一条路。

6. 曲线异常排查链路:奖励暴涨、长度漂移与输出坍塌

6.1 奖励一路涨但人工评测在跌,这是reward hacking的典型信号

我见过最典型的一次故障:训练到200步左右,奖励模型的打分从0.3一路涨到0.85,loss也很健康,但抽样看输出,模型开始疯狂输出"当然可以!这是一个非常好的问题,让我从以下几个方面为您详细解答……"这种套话开头。这就是奖励劫持(reward hacking)——策略找到了奖励模型的漏洞,用谄媚式、模板化的表达骗高分,而不是真的提升回答质量。

排查路径我是这么走的:第一步,把高分样本按奖励分数排序,取top 20条人工看,如果它们有明显共性(同样的开头、同样的句式、同样的长度区间),基本可以确认是劫持。第二步,用这批高分样本反过来测奖励模型,如果它给出的分数显著高于人工判断,说明裁判本身有偏。第三步,检查训练数据的标注分布,很多时候问题出在标注员对"礼貌、结构清晰"的回答有系统性偏好,奖励模型忠实地学了这个偏好,策略就顺着往上爬。

修复手段有几种,我一般组合使用:一是在奖励里加入与参考模型的KL惩罚并调大系数,把策略拽回来;二是混入一部分SFT数据做辅助损失,这叫RLHF里的PTX技巧,能有效防止模型的语言能力退化;三是定期用新策略的采样重新标注一批数据,增量训练奖励模型,让裁判跟上选手的进化。

6.2 输出长度单调上涨,八成是求和式奖励在作祟

长度漂移是第二高频问题。表现是训练曲线一切正常,但平均输出长度从200 token慢慢爬到800 token,而且内容密度明显下降。根因通常在奖励聚合方式上:序列级奖励用求和,等于给每个token都发了一份奖励,模型很自然地学会"多说多得分"。即使在奖励里加了长度惩罚,如果惩罚力度不够(比如每token扣0.001),也压不住这个趋势。

处理方式我试过三种,效果从弱到强:第一种是在奖励里加硬性长度归一化,除以长度的某个幂次;第二种是在优势计算时对长序列做折扣;第三种最直接——在prompt层面做长度分层采样,强制batch里长短样本比例固定,让模型没法通过单方面变长来刷分。第三种改动最小,见效也最快,我一般先用它止血。

6.3 熵突然塌到接近零,模型开始复读同一句话

训练中期如果发现策略的熵(输出分布的混乱度)快速下降并稳定在极低值,同时采样出来的多条回答几乎一模一样,这是模式坍塌。原因通常是优势估计全为正或者全为负——当一组采样的奖励都很高时,归一化后的优势有正有负还好;但如果奖励模型对某个特定表达给了极高分数,其余都是中等,那这个表达的梯度会被反复强化,几轮之后策略就锁死在这个模式上。

缓解手段包括:提高采样温度、增大组内采样数量(GRPO里从8提到16)、在loss里加一个熵正则项(系数0.001~0.01),以及检查奖励模型的分数分布是不是太极端。我个人的经验是熵正则不要一开始就加,先观察前100步的熵曲线,如果它下降得比预期快(比如50步内掉了30%),再补上去。

7. 落地优先级:从一个能跑通的最小闭环开始

如果让我给一个从零开始的团队排优先级,顺序大概是这样:先把SFT做到"采样16条里至少有3条能看",这是所有后续工作的前提;然后用现成的偏好数据跑一遍DPO,它的工程成本最低,能让你在两三天内看到"偏好优化到底有没有用"的答案;如果DPO的效果天花板明显(比如在某些需要探索的任务上始终上不去),再考虑引入奖励模型和在线RL。

奖励模型这一步,别一上来就追求大而全。先用5000~10000条高质量成对数据训一个小的,验收时重看长度相关性和分布外泛化,确认它没有学歪,再往上堆数据量。在线RL阶段,第一次一定用LoRA + GRPO或者LoRA + PPO,把显存压下来,把整条链路的权重同步、rollout吞吐、KL自适应这些工程问题先趟平。等这一套跑稳了,你会发现真正决定效果的从来不是算法有多花哨,而是奖励信号是否诚实——裁判不靠谱,再精妙的优化算法也只是在更快地学错东西。

我在实际项目里最深的一个体会是:每次训练崩掉,回头看几乎都能在数据或者奖励模型上找到根因,而不是PPO的某个超参没调对。所以我现在养成了一个习惯,训练脚本跑起来之后,第一件事不是盯loss,而是每隔50步抽20条输出存下来,攒够一批就人工过一遍。这个动作看着笨,但它比任何监控指标都更早地告诉你模型在往哪个方向歪。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询