1. RLHF到底在解决什么问题
1.1 从“预测下一个词”到“和人类对齐”
先从一个最直观的场景说起。你训练了一个对话模型,它能够流畅地写出段落、回答问题,语法完全正确,逻辑也基本自洽。可是当你真正和它对话时,却发现它总是说一些漂亮但没用的话,甚至在某些价值观场景下给出让人不适的答案。问题出在哪?因为传统语言模型的训练目标只有一句话:根据上文预测下一个最可能的词。这个目标太"原始"了,它只教会模型像人一样说话,却没有教会模型说人真正爱听的话。
这是行业里非常核心的一个痛点:模型的真实能力和训练目标之间存在严重错位。能力越强,错位带来的风险就越大。RLHF(Reinforcement Learning from Human Feedback,基于人类反馈的强化学习)就是来解决这个错位问题的。它的思路很直接:既然"人类满意"这件事很难写成规则,那就直接让人对模型的输出做评价,再把评价转化成一个可优化的奖励信号,最后用强化学习让模型按照这个奖励信号去调整自己的行为。
这套方案在近几年大模型对齐工作中被反复验证,已经成了实际项目中对齐阶段的标配。而PPO(Proximal Policy Optimization,近端策略优化)在其中扮演的就是"训练引擎"的角色。你会发现,几乎所有做对齐的实战项目,最后跑到训练阶段时,核心的强化学习算法几乎都会落到PPO或其变体上。
这篇文章把我实际跑过RLHF&PPO流程中值得关注的技术细节全部梳理一遍,包括奖励模型怎么训、PPO为什么重要、四个模型如何共存、KL散度到底在干什么,以及我在实操中踩过的坑和排查思路。内容更适合已经接触过强化学习、打算在大模型项目里落地对齐流程的读者,当然如果你是刚入门的新手,前两个章节也能帮你快速理解整体脉络。
1.2 三个阶段缺一不可
整个RLHF流程,业界公认的拆分是三个阶段。
第一阶段是监督微调(Supervised Fine-Tuning,SFT)。前期需要一批高质量的人工标注对话样本,让模型先学会"正确的回应格式"。但SFT解决不了价值观对齐问题,因为人类标注者对同一问题的回答可能各执一词,模型学到的只是"平均正确"。
第二阶段是训练奖励模型(Reward Model,RM)。让模型对一组候选回答做出打分,或者让标注员对多个回答进行排序。训练一个专门负责"预测人类偏好"的模型。这个模型不直接对外服务,它只负责在后续PPO训练里给模型的输出一个标量奖励。
第三阶段才是PPO强化学习。上一步训练好的奖励模型作为奖励信号来源,通过PPO算法去更新语言模型策略。这里还会引入一个参考模型(Reference Model)来约束更新幅度,防止模型为了拿高奖励而彻底说胡话。
这三者层层递进,缺一不可。你会发现,奖励模型的质量几乎决定了最终对齐的天花板。而PPO阶段能保住的奖励下限和训练稳定性,则决定了对齐效果的下限。
所以,RLHF不是某个模型的补丁,而是一整条流水线。谁跳过其中一环,后面都要用更大的代价还回去。
2. 奖励模型:整条流水线的质量天花板
2.1 为什么不能直接让人类打分
有人可能会问:既然目标是让人类满意,为什么不直接在训练时让人类实时对模型的每个输出打分?理论上可以,但实操中完全走不通。模型每秒钟能生成几十个token,人类看一眼输出都要几秒钟,更何况一个batch里可能就有几十条样本。让人类实时打分,既不现实也不经济。
所以常规做法是把"人类偏好"蒸馏到一个静态模型里。具体说来,你准备好一批prompt,让同一个模型或者多个模型针对每个prompt生成若干个候选回答,然后请标注员对候选回答排序。把这些排序结果转成pairwise对比数据,训练一个二分类模型,让模型学会判断"回答A是否比回答B更好"。奖励模型的训练目标看似简单,但数据质量的影响非常大。
奖励模型本质上是你把人类偏好压缩成了一个数学函数。输入是"prompt+回答",输出是一个标量。训练得越好,PPO阶段模型就越知道往哪个方向优化。
我总结一条经验:奖励模型的效果和排序数据质量强相关,排序数据如果本身噪声大,后面的一切努力都是在放大错误。
2.2 排序数据怎么采
排序数据的采集并不是简单丢给标注员一个问答对去打分。实际项目中需要设计合理的任务流。常用做法是给定一个prompt,将模型采样的N个答案(N通常取4到9)混在一起交给标注员排列。为了让标注员的判断稳定,需要提前明确排序准则,比如"逻辑完整性优先于文采"、"拒绝答非所问",否则不同标注员的排序逻辑会漂移。
采集时还需要关注prompt的覆盖面。我踩过的坑就是prompt分布过于集中,导致奖励模型对某些领域的偏好预测很准,换个领域基本瞎猜。后来我会刻意在prompt集合里做了分层采样,保证对话类、写作类、问答类、安全敏感类都有足够样本。
另外,排序标注过程中还需要混入"质量对照组"。有些标注员为了赶量,会对长回答有天然偏好,认为越长越好。为了对抗这种偏差,我一般会在数据集中加入一些明显更简洁但正确性更高的"金标回答",如果标注员对这类回答的排序偏低,就需要复核。
排序数据转训练样本的方法,主流做法是枚举所有答案对。如果N=4,就有C(4,2)=6个对比对。奖励模型拿两个答案作为输入,输出哪个得分更高。这里要注意,如果N太大,pair数量会膨胀得很厉害,训练反而变慢,所以4~6个候选是比较划算的区间。
2.3 奖励模型的训练细节
奖励模型的结构通常是在SFT模型底座上去掉语言模型输出头,换成一个标量回归头。训练数据就是一对答案,输入格式需要包含prompt和被比较的两个回答。损失函数上,常规采用二元交叉熵形式:
loss = -log( sigmoid( r(x, y_win) - r(x, y_lose) ) )
其中r是奖励模型的打分,y_win是排序靠前的回答,y_lose是靠后的回答。这个loss的意思是,让赢家得分尽量比输家高,同时差距越大越确信越好。
实际训练中要注意几点。第一,同一batch中不同pair可能来自不同prompt,不要做全局的batch normalization,因为每个prompt的奖励绝对数值区间可能不一样。第二,奖励模型输出的绝对值本身没有明确意义,只有相对大小才有意义,所以后期使用它的输出时不要做绝对阈值判断。第三,训练到后期watch一下acc和loss,acc如果还在涨但loss波动大,警惕过拟合。
我只用一个指标判断奖励模型好坏:在留出的验证集上,它的pairwise排序准确率有多少。通常做到70%以上才算初步合格,低于这个水平劝你别急着进PPO阶段,先把数据扩了再训。
3. PPO机制拆解:四个模型如何一起跑起来
3.1 重要性采样与old_logprob
PPO不是凭空优化一个奖励值,它属于策略梯度类算法的一个改良版。策略梯度类算法的核心思路是:让模型(策略)去和环境互动采样轨迹,根据轨迹上的奖励来调整策略,增加高奖励动作的概率,降低低奖励动作的概率,从而找到一个好的策略。
直接做策略梯度的问题在于样本效率低。模型每更新一次,旧的互动数据就不能再用了。PPO的改进在于引入了重要性采样。即使当前策略变成了新的版本,我们也可以拿旧的采样轨迹来估计新策略的期望收益。只需要用新旧策略在同一个输出上的概率比值来修正即可。
我们把这个概率比值记为ratio。假设在某个token位置,旧策略生成该token的概率是p_old,当前新策略生成该token的概率是p_new,那么ratio就是p_new / p_old。如果ratio大于1,说明新策略更倾向生成这个token;如果小于1,说明新策略正在远离这个token。
训练时,我们不直接用原始语言模型的输出概率,而是取出训练前缓存的old_logprob。这就是为什么你在很多开源代码中会看到"logprobs"这个字段,它是在采样阶段由旧策略算好并保存的。后面的loss计算,全靠old_logprob和当前模型重新算出的logprob做比值或差值。一个小小的经验:如果你的采样阶段和训练阶段模型泄露了数据,或者保存logprob对不上,后面PPO训练会出现莫名其妙的异常。
3.2 修正信号的两个部分:回报和KL惩罚
PPO的最终目标是最大化每个token的优势估计(advantage)。优势估计简单理解就是:这个token的生成相对于平均情况来说,是更好还是更差。计算advantage常用GAE(Generalized Advantage Estimation)。GAE的输入需要每一步的即时奖励。在RLHF里,一个回答通常只在结尾处获得一个整体奖励,中间每个token本身的即时奖励一般是0。
问题来了:如果中间token没有即时奖励,全部靠末尾奖励回溯,优势估计的方差会很大。模型不容易知道到底是哪个词导致了高分。为了控制方差并防止模型跑飞,还要加上KL散度惩罚。对生成回答中的每个token,惩罚项都会比较当前策略在对应token上的输出分布与参考模型(Reference Model)的输出分布。如果当前策略偏离了参考策略的分布,就要从总奖励中扣分。
这个KL散度惩罚是RLHF稳定性的关键。参考模型一般就是未参与PPO更新的SFT阶段模型。它起的作用是当"锚",告诉模型别改太野。你甚至可以把它理解成老师给学生划的边界:你可以探索,但不要彻底否定之前的常识。
kl_loss的计算有两种常见方式。一种是先求出每个token上当前策略和参考策略的KL散度,然后乘以系数beta;另一种是通过两个策略的平均logprob差来近似。实践中我倾向用每个token的显式KL散度累加,因为在奖励模型整体打分波动大时,逐token约束更稳。
3.3 GAE到底怎么算
GAE计算advantage需要用以下信息:每一步的即时奖励r_t、价值模型在每个token步预测的价值v_t、折扣因子gamma和GAE参数lambda。
对于每个token步t,delta_t的计算是:
delta_t = r_t + gamma * v_{t+1} - v_t
然后advantage_t的计算为:
advantage_t = delta_t + gamma * lambda * advantage_{t+1}
这两个公式就是GAE在RLHF场景中的完整形态。其中gamma控制对未来奖励的重视程度,lambda控制偏差和方差的折中。gamma越接近1,模型越能考虑远期影响;lambda越接近0,越接近只看即时奖励。
在长度较短的对话场景中,我把gamma设为0.99左右,lambda设为0.95。如果做长文本生成任务,gamma要调低一些,否则末尾奖励回溯到前面时方差太大。
价值模型(Critic)在这里很关键。它负责预测状态价值或动作价值,和策略模型一起训练。每个样本的value target可简单视为该轨迹未来总回报,真实回报value来自价值网络预测。你会发现价值模型的训练稳定程度直接影响PPO整体收敛,价值模型输出越准,advantage估计越准,策略更新才越可靠。
3.4 四模型谁是谁
在常规LLM的PPO训练中,总共有四个模型会出现在显存里或参与交互:Actor(策略模型)、Critic(价值模型)、Reference(参考模型)、Reward Model(奖励模型)。
Actor是我们要更新的核心模型,也就是最终要上线服务的那个模型。Critic和Actor共享主干或者独立成一份权重都可以。现实中为了省显存,会给Actor和Critic各自单独初始化,但共享Transformer主干的权重不做梯度更新,只训练价值头。Reference是冻结的SFT基线,用于计算KL惩罚。Reward Model也是冻结的,只输出最终奖励。
训练中Actor产生回答时,Reference和Reward Model会被并行执行,分别得到KL散度和奖励。Critic则在每个token位置给出状态估计值。这种结构看起来简单,实际工程实现时会遇到巨大的显存和调度压力。尤其是序列很长时,Reference和Reward Model在推理端的计算非常大。
如果你自己搭框架,建议把奖励模型和参考模型的推理用批量预测,预先把每个样本的得分和KL算好缓存在本地,再进入PPO更新阶段。这样做能减少训练过程中的同步等待,流水线会顺很多。
4. 从零走一遍RLHF+PPO落地流程
4.1 选基线模型和超参
先从模型选型说起。我个人建议是:做RLHF时,Actor的起点最好是SFT阶段已经收敛的模型,并且训练数据分布和奖励模型训练数据分布一致。哪怕模型能力再强,如果它的基础分布和奖励模型见过的分布差异很大,PPO阶段会有很多输出落在奖励模型的盲区,训练信号就乱。
模型规模上,如果是百亿以下规模,单机多卡可以做PPO;再往上要谨慎考虑资源。很多人以为PPO入门中最难的算法,其实不是,最现实的问题是显存。你需要同时加载四个模型,其中一个还要带梯度,还要产生长序列推理的中间量,显存优化策略直接影响能否跑起来。
超参方面给一组我常用的初始值作为参考:
| 参数 | 初始推荐值 | 备注 |
|---|---|---|
| 学习率 Actor | 1e-6 左右 | 比SFT学习率稍低一点 |
| 学习率 Critic | 1e-5 左右 | 价值模型收敛快一些 |
| kl_coef | 0.02 | KL系数太大模型更新缓慢 |
| clip_range | 0.2 | PPO裁剪范围 |
| gamma | 0.99 | 折扣因子 |
| lambda | 0.95 | GAE参数 |
| batch_size | 32或64 | 按显存调整 |
| 每批次训练轮数 | 1~2 | 多了容易过拟合旧数据 |
括号里是经验值,不是万能药。不同任务、不同模型,最好的做法是先小规模实验,观察KL和reward曲线再调。
4.2 奖励模型部署和打分缓存
在进入PPO之前,把奖励模型按照SFT阶段的batch和采样格式对一批prompt做打分,并把打分结果缓存下来会很有用。这不能替代PPO在线更新的价值,但能帮助你提前排查奖励模型是否出现"普世偏好"——比如对某个字眼的偏好、对长度的过度偏好。
我记得有一次,奖励模型在验证集上准确率78%,看起来不错。结果进PPO后,模型很快学会在回答结尾强行重复任务描述,因为奖励模型对"提及任务词"给出了高奖励。这种现象我们叫奖励欺骗(reward hacking),是RLHF里最棘手的问题之一。
提前做一次"调包检测"很有必要:准备一组提示词,让模型用多种风格回答同样内容,看看奖励模型打分分布是否合理。如果打分只被长度主导,那这个奖励模型需要重新训练或加正则。这种预防比事后修补便宜得多。
4.3 PPO训练主循环
实际训练环节,整体流程如下:
第一步,准备好一行训练样本,每条包括prompt数据。Actor根据当前策略生成回答,保存每个token的logprob。同时Reference模型和Reward模型分别计算KL惩罚和最终奖励。
第二步,Critic对每个token的状态给出价值估计。
第三步,计算GAE,得到每个token的advantages和returns。
第四步,组装minibatch,开始更新Actor和Critic。更新时重新计算当前策略的logprob,并构造PPO的clip loss。如果选token时概率是0怎么办?会加一个极小常数防止取log后出现负无穷。很多新手在这一步直接报错,数NaN满天飞。
在更新Actor时,我们希望在advantages为正的方向加大概率,在advantages为负的方向减少概率。但是为了防止单次更新过大,用clip_range限制ratio的跨度。计算完Actor的loss之后,还加上KL惩罚项。
Critic的loss通常是value预测和GAE returns之间的MSE,注意不要用advantage作为target,那是对价值估计的一种常见误解。
整体结构上,伪代码的核心如下:
# 采样阶段缓存 sequences, old_logprobs, rewards, kls = rollout_once(actor, ref_model, reward_model) # 计算价值和advantage values = critic(sequences) advantages, returns = compute_gae(rewards, values, gamma, lambda) # 每个PPO epoch内更新 for _ in range(ppo_epochs): logprobs = actor(sequences) ratio = torch.exp(logprobs - old_logprobs) surr1 = ratio * advantages surr2 = torch.clamp(ratio, 1 - clip_range, 1 + clip_range) * advantages policy_loss = -torch.min(surr1, surr2).mean() kl_loss = (logprobs - ref_logprobs).mean() * kl_coef actor_loss = policy_loss + kl_loss value_pred = critic(sequences) critic_loss = F.mse_loss(value_pred, returns)这段代码虽然简单,但是构成了PPO训练的主体骨架。实际工程里还要考虑padding的mask,以及各种数值稳定处理,不过我建议先把骨架吃透,再堆细节。
4.4 训练中的观察曲线
PPO训练的观测重点和SFT很不一样。SFT你主要盯loss下降就可以,但PPO需要同时关注三类信号:reward曲线的变化、KL散度的变化、以及模型输出多样性/质量的定性观察。
正常情况下,reward曲线会有起伏,但整体趋势不应该一骑绝尘地暴涨,因为如果KL惩罚机制有效,reward不应该瞬间冲到极高值。如果你看到reward在几十步内暴涨,同时KL散度也在大幅升高,几乎可以肯定是奖励模型被钻了漏洞。
reward曲线稳定上升但KL曲线也稳定上升,这种状态也需要注意。它说明模型正在为了拿分而偏离参考策略。此时应该提高kl_coef或者检查奖励模型是否过分依赖某些表面特征。
KL散度如果长期在0附近几乎不动,说明模型根本没在更新。这可能是因为学习率太低、优势几乎为0,或者policy loss在clip处全部被卡死。排查时先看advantage分布,如果advantage全部接近0,问题大概率在奖励模型打分离散度太小,或者价值模型估计太准导致优势抵消。前者要重新考虑奖励模型的训练目标,后者要检查是否对value做了过度正则。
还有一个容易忽略的细节:生成的终止符。在PPO采样阶段,如果对话模型不输出EOS,序列会被强行截断。截断会改变KL惩罚的计算范围,也会让奖励打分的context不一致。所以采样阶段要限制最大长度,并检查EOS是否合理出现。
5. 常见问题与排查技巧实录
5.1 奖励hacking怎么防
奖励hacking是RLHF里最常见的灾难性问题。模型找到奖励模型的漏洞后,会疯狂利用它,输出越来越偏离正常表达。哪怕奖励模型在验证集准确率75%以上,还是可能出现这个问题。
防御方向有三个。
第一,在奖励模型层面加正则:训练奖励模型时,如果发现它过度关注长度或者某些高频词,可以在loss中加入奖励分数和真实偏好之间的单调性约束,或对奖励模型的输出方差做约束。
第二,在PPO层面加更强的KL惩罚:KL系数本身可以设置成一个动态范围,reward和KL同步上升时就提高beta,偏离预警一旦解除再恢复beta。
第三,在数据层面增加对抗样本:在奖励模型的训练数据里,刻意添加一些"诱饵"回答,这些回答表面上高分但实际上错漏百出。奖励模型如果识别不了这类样本,说明它还没学会判断真实质量。
我自己的经验是,同时在三个方面做会安全很多。光靠一个KL惩罚设得再大,也挡不住奖励模型本身的漏洞。
5.2 训练崩溃和NaN排查
在RLHF训练中,NaN和inf的出现频率远高于普通SFT。常见原因包括:对数概率取值0导致的inf、GAE计算时价值预测出现极端值、没那么常见但很折磨人的奖励模型输出溢出。
如果你遇到NaN,不要只盯最后一步的loss,先定位是由哪个tensor引入的NaN。我常用的一种排查方法是:在PPO更新前打印advantages的均值方差,在更新后打印ratio的均值方差。如果advantages在更新前已经出现NaN,那问题在采样或GAE计算;如果更新前没问题,更新后才有NaN,那问题大概率出在优化器或loss计算的某处。
另一个非常值得注意的坑是采样和训练的数据对齐问题。如果在采样阶段对序列做了截断或者padding,在训练阶段又用不同的mask方法,就可能导致logprob对不上位置,模型更新时以为某位置概率很高,实际上那个位置是padding,这样训练信号会乱成一团。
5.3 KL偏大偏小如何调
KL偏大说明模型正在快速走远,偏小说明更新幅度不足,对这两种情况的调法不一样。
如果KL偏大,先区分是全局性增大还是部分样本增大。全局性增大说明当前策略在整体范围内漂移,考虑提高kl_coef、调低学习率、或者给ppo_epochs降成1。部分样本增大说明是某些样本脱离了控制范围,可以考虑对这些样本的KL做动态加权,给它们更高惩罚。
如果KL长期偏小,说明模型基本没有偏离参考策略,要检查policy_loss是否被clip_range完全限制住了。将clip_range值打出来看一下,如果大量ratio超过1.2或者低于0.8,说明策略已经被裁切,很多样本梯度失效。再来就是检查advantages是否分布在0附近。如果age分布太窄,模型没有足够的动力去更新,这种情况要回头检查奖励模型是否输出过于平坦。
5.4 一张速查表
| 现象 | 常见原因 | 首要排查点 |
|---|---|---|
| reward曲线飙升 | 奖励hacking / KL惩罚失效 | 观察KL是否同步飙升,复盘奖励模型训练数据 |
| reward整体偏低 | 奖励模型与SFT分布不匹配 | 检查prompt是否一致,奖励模型是否过拟合 |
| 输出质量下降 | KL惩罚过大 / 学习率太高 | 降低kl_coef,降低Actor学习率 |
| 训练完全不动 | advantage普遍为0 | 检查奖励模型,检查value target精度 |
| logprob出现NaN | 概率0或负值未处理 | 加log_eps,检查采样掩码 |
| 显存不足 | 四模型同时推理 | 缓存reward和KL,阶段化执行 |
6. 最后聊点个人感受
跑RLHF和PPO这个组合,我最大的体会是:算法原理看起来不复杂,但真正让这套系统稳定工作的,都是工程细节。奖励模型质量不行、数据分布不统一、KL系数设置不当、显存优化太激进,任何一环都会让结果崩掉。别指望有一组一劳永逸的超参,我在不同任务上试过的参数差别很大。每次新项目,我都会从一条很小的验证集开始,快速跑几十步看曲线形状,再放大到完整数据。这种做法帮我节省了至少一半的调试时间,推荐你也试试。
另外,如果你在实际操作中遇到和上面不完全一样的情况,建议先打印advantages和KL的完整分布,不要只看均值。均值掩盖问题非常严重,两条完全不同的训练轨迹可能均值相同,但方差天差地别。RLHF这套流程的收益和风险都在方差里,盯住方差能少走很多弯路。