ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RLHF与PPO实战:从奖励模型到策略优化的完整指南

RLHF与PPO实战:从奖励模型到策略优化的完整指南

1. 为什么需要RLHF:大模型训练的“最后一公里”

做过大模型训练的朋友应该都有这种感觉:预训练阶段像是在教一个孩子“识字、造句”,SFT阶段是在教他“把句子说完整、按格式回答”,但真正到了部署上线、面对真实用户的时候,你会明显发现——模型虽然在语法上无懈可击,却经常说一些“政治不正确”的话、给出有害建议、或者长篇大论讲了一堆根本不是用户想问的东西。这就是业内常说的“对齐”问题:模型能力很强,但它不一定会按照人类认可的价值观和行为方式来使用这些能力。

RLHF(Reinforcement Learning from Human Feedback,基于人类反馈的强化学习)就是专门解决这个问题的。它的核心思路非常直接:既然很难用规则写清楚“什么算好的回答、什么算坏的回答”,那就直接让真人来打分,再把人类偏好训练成一个自动化的“裁判”(奖励模型),最后用强化学习把模型往人类偏好的方向持续推。而PPO(Proximal Policy Optimization,近端策略优化)则是这个过程中最常用的强化学习优化算法,负责在每一轮迭代中安全、稳定地更新策略模型——你可以把它理解成驾驶时的“平稳加速”,既想让车跑得更快,又不想让方向盘乱抖、把乘客甩晕。

这篇文章适合两类人:一类是已经跑通SFT微调、准备尝试对齐训练的大模型工程师,另一类是刚研究强化学习、想知道RLHF和PPO到底怎么落到代码和训练框架上的人。我会尽量避免过于抽象的公式,尽量用实际训练过程中会遇到的场景、参数、坑来拆解整个框架。

先说一个我自己的整体感受:很多新手第一次接触RLHF,会被它的三段式流程吓到,觉得又要训奖励模型又要跑强化学习,复杂度太高。但实际上拆开看,每一步都是有迹可循的。难点不在“单个组件怎么搭”,而在“三个组件怎么配合”——尤其是PPO阶段策略模型和参考模型之间的KL散度控制、奖励模型和策略模型之间的“猫鼠游戏”,这两块搞明白,RLHF就算掌握了八成。

2. 核心机制拆解:RLHF的三段式架构

2.1 第一阶段:从SFT到人类偏好数据采集

RLHF的完整流程一般分成三个阶段,第一个阶段是SFT(Supervised Fine-Tuning)。这个阶段的目标比较朴素的:在一个高质量的人工标注数据集上,让预训练模型学会“有问有答”的交互格式。

值得提醒的是,不少团队在SFT阶段就埋了坑。我见过有人在SFT阶段用大量“标准答案”把模型喂得太过死板,结果到了RLHF阶段,模型的熵(entropy)已经低得不行了,PPO再怎么探索,输出的多样性都起不来。所以做RLHF之前,SFT数据集里最好保留一部分开放式问答、多轮对话、甚至带有一定分歧和噪声的样本,让模型保持一个适度的输出空间。如果SFT后模型已经像是“背稿子”一样只会一套模板,PPO很难救回来。

接着是关键的人类偏好数据采集。具体操作方式一般是这样:给定同一个提示词(prompt),让SFT模型生成多个回答(通常是2到4个),然后交给标注人员去排序,或者两两对比选优。为什么不用直接打分?因为人在打分时会存在明显的尺度漂移——同一个回答,上午可能给4分,下午就给3分。但“A好于B”这样的相对比较,一致性要高得多。这也是后来很多偏好数据集都采用pairwise或listwise排序的根本原因。

数据规模方面,早期经典的做法是用几万条人类偏好数据训奖励模型(像某公司早期对话模型的公开报告中提过的数万量级),后来很多开源项目把这个规模扩大到了几十万甚至上百万条。对于一般团队,我建议起步阶段不用贪多——3到5万条高质量的、覆盖各业务场景的偏好对比数据,已经足够训练出一个能用的奖励模型。关键不是总条数,而是提示词的多样性。如果5000条数据全都集中在“翻译任务”上,你的奖励模型就只会在翻译维度上做判断,面对“总结摘要”“情感分析”时就成了瞎猜。

2.2 第二阶段:奖励模型的训练与关键技巧

奖励模型(Reward Model,RM)是整个RLHF的“裁判员”。它的结构不算复杂:在SFT模型的基础上,去掉最后用来预测下一个token的词头,换成一个回归头,输出一个标量得分。

训练方式是最经典的对排序学习:输入一个prompt和两个回答,模型要为每个回答各输出一个分数,因为回答的token序列长度不同,一般取最后一个token的score作为整段回答的得分。然后构造pairwise loss,让“更被人类偏好的回答”得分更高。

这里有一个很容易踩的坑:奖励模型不能只在“最终得分”上做文章,传统做法上会对每个token都输出一个score,只是取最后一位作为序列分。为什么需要这样?因为PPO阶段计算KL散度时,或者后续做token级密度估计时,有时会需要中间层的反馈信号。哪怕你目前用不到,保留每个token的score输出,代码结构上也更灵活。强行只用一个标量序列来表示整段回复,往往会在训练早期就损失很多梯度信息。

训练RM时还需要注意一个比较隐蔽的问题——奖励模型的“偏好耦合”。就是说,标注人员在对比两个回答时,很容易因为“A回答更长、更详细”而倾向选择A,但长并不等于好。这个问题没法靠单纯的loss修改彻底解决,业界常用的折中办法是:在准备数据时加入长度惩罚项,或者在标注规范里明确要求“不得因为长度差异而给高分”,再或者在RM训练时引入一个正则项,把回答长度的方差影响隔离掉。我在实际项目里试过直接在loss里加长度惩罚,效果一般,最有效的还是在数据采集环节把标注规范定清楚。

还有一个实操细节:奖励模型一般建议比策略模型小一个量级,或者至少不超过策略模型的规模。原因很简单,PPO更新策略时,奖励模型给出的score会直接作为梯度信号反向传播。如果RM也大到和策略模型一样、每步推理都慢吞吞的,整个PPO训练的吞吐量会下降得让人崩溃。常规做法是拿SFT模型的一个小checkpoint做RM底座,训练到收敛后冻结,专门用来打分布外样本的分数。

2.3 第三阶段:PPO驱动的策略优化前夜

第三阶段是RLHF真正“强化”的部分。在这个阶段,我们有三份模型同时参与训练:

第一份是策略模型(Policy Model),也就是我们要训练的目标模型。初始状态一般直接取SFT阶段的产物。

第二份是参考模型(Reference Model),它是策略模型的“锚点”。参考模型通常就是SFT阶段的模型冻结副本,不会更新。如果你训练过程中连参考模型也跟着动,KL散度计算就失去了基准,整个对齐方向会跑偏。

第三份是奖励模型(Reward Model),在PPO微调阶段它也是冻结不动的。

每次训练迭代的流程大致这样:给策略模型一个prompt(这个prompt来自特定分布,比如业务测试集、开源指令集等),策略模型生成一段回答。然后把同样的prompt喂给参考模型,生成一段参考输出。两段输出逐token计算KL散度,得到每一步的KL惩罚。同时,把prompt+生成的回答拼起来,送给奖励模型打分,得到reward。

最终的强化学习目标,就是把“奖励模型给的高分”和“与参考模型不要偏离太多”这两个目标做一个加权融合。这一步如果KL惩罚权重太高,模型几乎不会学到任何新东西,就在原地打转;如果太低,模型会立即“钻奖励模型的空子”,变成只会讨好RM的“马屁精”。

这些逻辑捋顺了之后,我们再细看PPO在里面到底扮演什么角色。

3. PPO算法解剖:稳定学习的艺术

3.1 为什么要用PPO而不是传统的策略梯度

强化学习中,策略梯度(Policy Gradient)家族的方法很多,最基本的是REINFORCE:根据一局完整交互(从生成第一个token到结束符)的总回报,按“好结果抬高动作概率、坏结果压低动作概率”来更新策略。

但REINFORCE在实际代码里几乎跑不动,因为问答任务里每个token都是策略模型自己采样出来的,一轮生成可能几百个token,每个token的“好坏”评估严重依赖于蒙特卡洛采样,方差巨大。再加上奖励模型给的是一个稀疏的、序列级的分数,你根本不知道“是哪个token导致了低分”。这种情况下直接做策略梯度,训练曲线就像心电图一样,忽上忽下,动不动就梯度爆炸。

PPO的核心贡献,是在REINFORCE的基础上加了两个关键机制:重要性采样和裁剪目标。重要性采样让模型可以用“旧策略”采样的数据来更新“新策略”,这相当于一次性收集一批数据后能用好多轮更新,不至于每更新一步就重新采一轮样;裁剪(clip)则是限制每次更新幅度不要太大,不要因为某一条高奖励样本就把模型参数猛推一把,导致策略瞬间恶化。

日常和人聊的时候,我喜欢把PPO比作“带着安全带的油门”:传统策略梯度是光踩油门不系安全带,速度可能快,但撞了就是大事;PPO则是在踩油门的同时加了一个限位器,把单次更新的步子限制在一个“信任区间”里。

3.2 重要性采样、裁剪目标与GAE的配合

PPO的loss主要由三块构成:策略损失(policy loss)、价值损失(value loss)和熵奖励(entropy bonus)。分开说:

策略损失的最简形式是:

L = min(ratio * A, clip(ratio, 1-ε, 1+ε) * A)

其中ratio是“新策略在当前token上的概率 / 旧策略在当前token上的概率”,A是优势估计值。这个公式的意思是:如果优势A为正(这个动作比预期好),我们希望尽可能提高这个token的概率,但不能提高太多——ratio超过了1+ε就会被裁剪掉,不再贡献梯度。如果A为负,我们希望降低概率,但同样限制在单步不能降太多。

A的计算通常借助GAE(Generalized Advantage Estimation,广义优势估计)。GAE里有两个参数很关键:γ(折扣因子)和λ(GAE系数)。在LLM的RLHF场景里,因为奖励模型给出的是一个序列级的整体reward,GAE的物理意义没有传统强化学习里那么强,但依然能平滑每个token的优势估计。经验上,γ取0.99,λ取0.95,是个非常稳的起点。如果你的回答很长,可以适当把λ调低到0.9,因为太远的token对当前决策的影响确实弱。

价值损失就是PPO里价值网络(Value Network)的回归loss。价值网络用来预测每个token的期望回报,它在GAE估算中扮演着“基线”角色——优势 = 实际回报 - 预期回报。价值网络一般也用和策略模型同规模的小头,可以理解成一个“第二RM”,专门用来预测内在价值,但它不需要标注数据,只需要从实际采样中回归。

3.3 KL惩罚的加入与整体loss融合

RLHF里策略模型的最终优化目标,往往是:

Reinforce_Objective = Policy_Loss + vf_coef * Value_Loss + entropy_coef * Entropy - kl_coef * KL(Policy || Reference)

这里注意kl_coef前面是负号,也就是说我们要惩罚“偏离参考模型”的行为。为什么要保留参考模型做锚点?因为RM给偏好打分时并不总是完美的,如果把策略模型完全交给RM,模型会在RM的“盲区”里放飞自我,生成人类一眼就能识破的胡话。KL惩罚在这里像一个“下限保险”,保证即使RM有时看走眼了,模型的输出不至于变得语法都不通。

KL penalty有两种常见形式:adaptive KL(自适应KL)和fixed KL(固定KL)。业界常用的做法是adaptive:设定一个目标KL值,比如每批数据的平均KL希望维持在0.1~0.2之间。如果实际KL偏大太多,就自动提高kl_coef把模型拉回来;如果实际KL过小,说明更新力度不够,就降低kl_coef,给策略模型更大的探索空间。

这一节的结论是:PPO不是一套死板的“套路由”,而是一个由策略loss、价值loss、熵奖励、KL惩罚共同组成的“多目标平衡”系统。真正调参的经验,就是在四个系数之间找平衡的过程。

4. 实操过程:从数据到模型的全流程指南

4.1 核心参数设置与选择逻辑

我把自己跑过很多次RLHF后觉得最关键的一组参数整理如下,这不是唯一标准,但绝对是一个可以直接上手的基线。

参数推荐值说明
γ (gamma)0.99回报折扣因子,长回答任务可降到0.95
λ (GAE lambda)0.95优势平滑系数
clip ε0.2PPO裁剪范围,0.1更稳,0.3更激进
kl_coef初始值0.1启动时给一个相对保守的值,配合自适应调节
lr(策略模型)1e-6到2e-6RL阶段学习率要比SFT低一个量级
lr(价值模型)1e-5(可相对更高)价值网络收敛更快,可单独设置
batch size(PPO步数)128~256个prompt每轮PPO采样的prompt数量
mini-batch size4~8PPO内每次更新的样本量
entropy coef0.01太大模型输出会飘,太小容易模式坍缩
max response length256~512取决于任务,太长影响训练吞吐
训练轮数(epochs)3~5PPO每个样本复用的更新次数

很多新手最容易低估的是学习率。我见过有人直接拿SFT阶段的1e-5甚至5e-5来跑PPO,结果模型输出很快退化到“反复重复高奖励词”。原因很简单:RM给的是一个带噪声的估计,如果学习率太大,模型会过度拟合这个噪声。RLHF阶段的策略模型学习率降到1e-6量级是我做项目的铁律,哪怕训练慢一点,也要保证不出各种各样的“奖励失真”问题。

4.2 奖励归一化与分布处理

在PPO训练中,reward的绝对数值并不重要,重要的是相对分布。原因在于PPO的裁剪机制是“相对优势”驱动的,而不是“绝对分数”驱动的。所以我们会把采样得到的一批reward做标准化:计算这批数据的均值和方差,把reward除以标准差(或者做z-score归一化),让优势估计在一个更稳定的尺度上。

需要特别注意的是,这种标准化是“batch级”的,不是“全局”的。如果你的batch里恰好都是困难样本,RM给出的分都普遍偏低,那么这批数据中相对最高的sample会获得正优势。这是合理的,因为PPO所关心的永远是“在同一批比较下谁更优”,而不是“绝对飞到天上”。

奖励分布还有一个常见问题——极端离群值。当某个样本获得特别高的reward或者特别低的reward时,GAE计算容易溢出,优势估计异常大。最简单有效的处理方式是做winsorize:把reward中前1%和后1%的值裁剪到与第1和第99百分位相同。这个操作能明显提升训练的稳定性。

4.3 完整训练循环的每一步

用伪代码展示一整个PPO训练step的结构(这里不限定具体框架,因为各家的实现细节会有差别,但核心模块是共通的):

# 伪代码:RLHF / PPO 单batch训练循环 for batch_prompts in dataloader: # 1. 采样阶段 rollouts = policy_model.generate(batch_prompts, temperature=0.7) # 2. 奖励打分 rewards = reward_model.score(batch_prompts, rollouts) rewards = normalize_rewards(rewards) # 归一化 # 3. 参考模型KL计算 ref_logprobs = ref_model.log_probs(batch_prompts, rollouts) policy_logprobs = old_policy_model.log_probs(batch_prompts, rollouts) kl = policy_logprobs - ref_logprobs # 4. 优势估计(GAE) values = value_model.forward(batch_prompts, rollouts) advantages = compute_gae(rewards, values, gamma=0.99, lam=0.95) # 5. PPO内循环更新 for _ in range(ppo_epochs): for mini_batch in split(rollouts): curr_logprobs = policy_model.log_probs(mini_batch) ratio = exp(curr_logprobs - old_logprobs) # 裁剪目标 pg_loss = -min(ratio * advantages, clip(ratio, 1-eps, 1+eps) * advantages) # 价值损失 vf_loss = mse_loss(value_model(mini_batch), returns) # 熵奖励 entropy = policy_model.entropy(mini_batch) # KL惩罚 kl_penalty = kl_coef * (curr_logprobs - ref_logprobs) loss = pg_loss.mean() + vf_coef * vf_loss.mean() \ - entropy_coef * entropy.mean() + kl_penalty.mean() optimizer.zero_grad() loss.backward() grad_clip(max_norm=1.0) optimizer.step()

这里有一个我个人强烈建议的工程细节:在每个PPO step开始采样之前,先做一次“旧策略log概率”的预计算,也就是把当前策略模型在rollout上的logprob存下来。后续内循环更新时,ratio计算基于这份快照,而不是实时去算。否则同一个rollout在每次内循环更新时都会因为策略已经更新而无法准确计算ratio,梯度就会不稳。

温度参数也值得一聊。采样时设置temperature=0.7左右,既能保持多样性又不至于太乱。温度太高,产生大量低质量回答,RM会给一堆低分,优势估计缺乏区分度;温度太低(比如0.1),输出过于确定,策略基本没有探索能力,训练成果会很有限。

5. 常见问题与排查技巧实录

5.1 奖励黑客:模型找到了RM的“盲区”

这是RLHF项目最经典也最头疼的问题。特征是:KL散度很低——说明模型没有偏离参考模型太远,但reward却在不断上升,同时人工看生成结果却感觉质量在下降,甚至出现疯狂的重复句子、自我矛盾等。

这种现象的本质是:RM本身不是完美裁判,它可能存在“喜欢无脑重复某个句式”“偏好超长回答”等偏见。策略模型在RL更新时非常敏锐,会抓住RM的任何一个偏见迅速放大。我遇到过最夸张的一次,模型学到的“超能力”是在回答末尾疯狂加感叹号和表情符号,因为RM给这样的回答打了高分。

处理奖励黑客的手段主要有两个方向:一是“削弱模型的投机动力”,也就是提高KL惩罚权重,让模型不能离参考模型太远;二是“改进裁判”,重新检查RM在哪些维度上存在偏好偏差,甚至用打乱指标的方式训练一个更平衡的RM。如果模型已经开始出现明显的奖励黑客行为,我建议立即停止训练,先修RM再继续。硬着头皮往下跑,只会在一个错误的方向上越走越远。

5.2 KL散度失控:模型漂移问题

KL散度监控是判断RLHF是否健康的“体温计”。常规做法是每100到200个steps打印一次平均KL值。如果KL在训练开始后急速上升,可能有两类原因:一类是kl_coef设置得太低,策略模型在自由探索,飞得太远;另一类是数据分布有问题——比如prompt分布中突然混入了一大批和当前任务完全无关的样本。

我实践中还遇到过一种隐蔽的情况:KL值看上去很正常(保持在0.1附近),但模型的实际输出已经完全变了。原因是KL penalty计算时用的是“当前最新的参考模型”和策略模型之间的逐token KL,如果两边的tokenizer不一致,或者使用batch内padding过长导致大量pad token参与计算,KL会被“稀释”到一个很低的水平。检查方法很简单:把参考模型和策略模型的输出各打印几段,肉眼对比风格是否一致。如果肉眼已经看出明显差异但KL数值却低,那多半是计算逻辑里有bug。

5.3 训练直接崩掉:loss变成NaN或飞上天的排查

训练崩掉是跑RLHF的老家常便饭。最常见的元凶是“logprob计算错误”:比如生成时用半精度,计算logprob时却用fp32,导致精度对不上;或者padding mask没有正确设置,导致pad token也计入了logprob和loss。

一个高效的排查顺序我可以分享出来:

  1. 检查reward归一化是否做了,极端离群值是否被裁剪。
  2. 检查优势估计(advantage)的维度是否和序列长度一致。
  3. 检查value loss是否发散——单独打印value loss的数值,如果它比policy loss大好几个量级,说明价值网络拟合不好。
  4. 检查学习率是否过高,尤其是价值网络。
  5. 最后再怀疑模型结构本身。

排除速度最快的是找“复现点”:用小批量数据、关闭随机因素(固定seed),如果能复现崩溃,就逐步简化——先只跑1个step,关掉KL惩罚、关掉entropy,让loss退化为纯MLE+价值回归,看问题是否还在。如果在最小配置下依然崩溃,那几乎可以确定是底层实现bug;如果能跑通,再逐个恢复组件,就能快速定位到问题源头。

还有一个常见细节:梯度裁剪(gradient clipping)在PPO里非常重要。设置max_norm=1.0是我见过的大多数项目的做法,但如果你用较大的PPO batch或者较长的回答,有时候即使梯度裁剪了仍然不稳定。那就需要检查是否在“从fp16转fp32”的过程中引入了inf。很多包里对fp16的optimizer处理有坑,混合精度时best practice是在loss缩放后再梯度反传,不要手动在fp16下做梯度累计。

5.4 一些容易被忽视的工程坑

  • 数据并行时,reward归一化的统计口径最好是全局的,不要每个GPU单独算。如果4卡各自做归一化,各自的均值和方差可能差异很大,导致同样的答案在不同卡上获得不同的优势信号,策略更新方向就乱了。

  • prompt的采样分布要严格控制,不要和SFT训练集完全重叠。RLHF阶段的prompt最好有“一定难度”——如果prompt全是模型已经能应对得很好的简单问题,reward差异很小,梯度信号微弱,训练就是白跑。

  • 奖励模型和策略模型最好用同一个tokenizer版本。如果版本不对,生成的token_id对应关系一错,后面所有计算都白搭。这个坑非常隐蔽,因为两个模型单独用都没问题,放在一起跑了很久才可能发现。

  • 长期训练时建议定期保存中间checkpoint,并且至少保留最近3个KL表现最优的版本。RLHF模型存在“过训”风险,可能第1000步效果最好,到第3000步反而返回了。有多个checkpoint,就能做对比评估,而不是只能拿最后一版。

6. 常见问题速查表

为了方便大家在实际踩坑时快速定位,我把上面提到的常见问题整理成了一个速查表:

症状可能原因首选排查手段
reward升高但质量下降奖励黑客检查KL散度是否过低,提高kl_coef或修复RM
训练初期KL飞速上涨kl_coef太小/参考模型不匹配调大kl_coef,检查tokenizer是否一致
KL正常但输出风格剧变pad padding混入计算/逐token KL实现有误肉眼对比策略模型与参考模型的生成结果
loss出现NaNlogprob计算错误/浮点溢出最小复现法逐步禁用组件定位
value loss发散学习率过高/价值网络结构不当单独打印价值loss,降低价值网络lr
训练吞吐太低RM模型过大/生成长度过长减小RM规模,限制最大回答长度
同一输出多卡reward差异大归一化统计非全局改为全局同步归一化
后期模型输出过于单一熵奖励太低/模式坍缩调大entropy_coef,检查采样温度

7. 一些实在的建议

我个人在实际操作中的体会是:RLHF的效果上限,很大程度上在进入PPO之前就已经决定了。RM训得好不好、偏好数据有没有标注偏差、SFT模型有没有保留足够探索空间,这些因素加起来,比PPO算法本身的调参影响要大得多。很多人把精力全花在调clip值、调KL惩罚系数上,却忽视了一个事实——RM的偏见会被PPO放大得淋漓尽致。

所以我的建议是:跑PPO之前,先花至少两倍的时间审视RM的案例。拿一批从未见过的验证prompt,让RM打高分和低分各找出几十个case,人工看看这些分数是否抓到了人类关心的核心维度。如果RM对“事实性错误”不敏感,只对“语气友好”敏感,那么你的RLHF最终得到的将是一个“说话很好听但内容不可靠”的模型。这样你就理解为什么有些人做了RLHF反而效果更差了——穷根究底,RM从一开始就不靠谱。

回顾整个RLHF与PPO的框架,你会发现它本质上是一种“构建信任机制”的思路:预训练给能力,SFT给格式,RM给价值观,PPO给优化路径。无论你是用这个框架做垂直领域的专业助手,还是做某个产品的对话交互,核心的设计逻辑都是一样的。先保证数据靠谱,再用KL做保险,然后才轮到在上面雕花。下次如果你手头的模型“什么都懂但就是不听话”,不妨把这条链路从头到尾过一遍,大概率能找出症结所在。

纸上谈兵不如亲自跑一次小规模的实验:准备三千条偏好数据,用一个小规模模型做RM,再做五百步PPO,把每一步的KL、reward、生成样例都记录下来,你会对这套框架建立起比看任何文章都更扎实的直觉感。之后再做大规模的实验,效率会高出一大截。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进