PPO与RLHF:大模型对齐的起点
PPO是RLHF的工程底座,它用Clipping+KL+GAE三件套把REINFORCE的"随机游走"变成"稳态爬坡"——代价是同时维护4个模型,显存直接翻倍。
📍 本文适合已读完第1篇、理解REINFORCE的工程师阅读,预计阅读 20 分钟。
📅 信息截止:2025 年 5 月(本文中的数据和判断基于此时间点的信息)
---
1. 技术概述:从REINFORCE到RLHF
1.1 PPO一句话本质
PPO(Proximal Policy Optimization,近端策略优化)的本质是"小步快跑"——每次只更新一点点,用裁剪(Clip)和KL约束两把锁确保策略不会一夜之间面目全非。
在LLM场景中,PPO是RLHF(Reinforcement Learning from Human Feedback,基于人类反馈的强化学习)第三阶段的优化器。它承接了第1篇讲的REINFORCE骨架,但补上了三个关键稳定化机制,让RL训练从"运气成分很大"变成"可控的工业级方案"。
1.2 为什么REINFORCE不够用?——回顾第1篇的三大问题
第1篇结尾我们总结了REINFORCE的三个致命伤,这里快速回顾一下,因为PPO的三个核心组件就是一一对症下药的:
PPO = REINFORCE + Clipping + GAE + KL约束 ——记住这个等式就够了。接下来逐项拆解。
1.3 RLHF三阶段:全景概览
在深入PPO之前,先看清RLHF的整体地图。RLHF的训练流程是严格的三阶段串行:
阶段1:SFT(Supervised Fine-Tuning,监督微调)
给预训练模型喂一批人工标注的高质量"提示词→标准回复"对(InstructGPT用了约1.3万条),让模型学会基本的指令跟随格式。这一阶段的产物叫SFT模型——它已经会按格式回答问题了,但不知道"什么样的回答更好"。
阶段2:RM(Reward Model,奖励模型)
用SFT模型对每个提示词生成多个候选回答(通常是4~9个),让人类标注者排序"A比B好,B比C好",形成偏好对数据集(InstructGPT用了约3.3万条比较数据)。用这些偏好对训练一个奖励模型(RM)——输入"提示词+回答",输出一个标量分数,预测人类会打多少分。
RM本身也是一个语言模型(通常比待训练的模型小),只是把最后的分类头换成回归头,输出一个标量。
阶段3:PPO优化
用RM作为"裁判",用PPO作为"教练",把SFT模型训练成对齐后的最终模型。每次迭代:
💡 关键要点:RLHF = SFT学会格式 → RM学会打分 → PPO学会讨好人类。三阶段缺一不可。
---
2. 核心原理:PPO是怎么让训练变稳的?
2.1 PPO目标函数:ratio + clip + KL
PPO的核心是一个精心设计的目标函数。我们不直接最大化RM分数——那样会导致reward hacking(模型学会钻RM的空子)。而是在最大化RM分数的同时,用两把锁限制策略的更新幅度。
第一把锁:重要性采样比率 + Clipping
在REINFORCE中,每次采样后直接用 log_prob × reward 更新。PPO改进的第一步是引入重要性采样比率(ratio):
ratio的直觉:如果ratio=1.5,说明新模型比旧模型更"喜欢"这个token(概率增大了50%)。如果ratio=0.8,说明新模型没那么"喜欢"了。
为什么要用ratio而不是直接用新概率? 因为采样是用旧策略做的,轨迹是从旧策略分布里抽取的。如果直接用新策略的概率算梯度,会引入分布偏移偏差。ratio修正了这个偏差。
然后PPO的clip机制出场。clip的作用是:如果新旧策略对某个token的态度差异太大(ratio超出[1-ε, 1+ε]),就切断梯度,不更新这一步。典型的ε=0.2,即ratio超过1.2或低于0.8就不让更新。
用人话解释这个min操作:
一个直觉类比:考试提分
第1篇我们用了"教学生做题"的类比。现在升级一下:
第二把锁:KL散度约束
Clip只约束了相对变化幅度,但还有一个更根本的约束:模型不能偏离SFT模型太远。原因是:
KL项的实现方式有两种(InstructGPT和工程实践中两种都有用到):
方式一(在奖励里加KL惩罚):
在RM奖励中直接扣除当前策略与参考策略的KL散度。β是惩罚系数,通常取0.01~0.1。
方式二(PPO-ptx,混合预训练损失):
在PPO损失里加一份预训练的交叉熵损失,防止模型的语言能力退化。InstructGPT证明了这种方式可以减轻"对齐税(alignment tax)"——模型在对齐过程中在某些基准任务上的性能下降。
工业实践中通常两种方式混用:奖励里加KL惩罚 + PPO损失里加预训练损失。
完整PPO目标函数:
用代码表达PPO的核心逻辑比公式更直观:
def ppo_loss(actor, old_actor, ref_model, reward_model, prompt, beta=0.04, epsilon=0.2):
# 1. 用当前actor生成回答
response_tokens, log_probs_new = actor.generate(prompt, return_log_probs=True)
# 2. 取旧策略(采样时)的对数概率
log_probs_old = old_actor.get_log_probs(prompt, response_tokens)
# 3. 计算ratio
ratio = torch.exp(log_probs_new - log_probs_old) # π_new / π_old
# 4. 计算奖励
rm_score = reward_model(prompt, response_tokens) # RM给的偏好分
# KL惩罚:当前策略 vs 参考(SFT)策略
kl_div = (log_probs_new - ref_model.get_log_probs(prompt, response_tokens)).mean()
reward = rm_score - beta * kl_div
# 5. GAE优势估计(见2.2节)
advantages = compute_gae(rm_score, critic_values, gamma=0.95, lam=0.95)
# 6. PPO-Clip损失
surr1 = ratio * advantages
surr2 = torch.clamp(ratio, 1 - epsilon, 1 + epsilon) * advantages
policy_loss = -torch.min(surr1, surr2).mean()
return policy_loss💡 关键要点:PPO目标 = max(奖励) - β×KL(策略||参考),同时用clip(ratio, 1-ε, 1+ε)限制每次更新不超界。
2.2 GAE:把"全班平均分"变成"每道题单独打分"
第1篇举了GAE的例子——解方程"3x+5=20"的40个token分成4段,距离最终答案越近的token贡献越大。现在给出数学定义。
GAE(Generalized Advantage Estimation,广义优势估计) 的核心是给每个时间步的token算一个优势值,这个值不仅考虑即时回报,还回溯考虑了未来的奖励——折扣因子γ控制"多看重长期",λ控制"多相信当前估计vs远期信息"。
在LLM场景中,由于只有episode结束时才有奖励(稀疏奖励),需要Critic(价值网络)来估计每个中间状态的价值V(s_t)。GAE的魔力在于:它同时解决了第1篇的方差问题和信用分配问题:
这就是为什么PPO需要一个Critic:Critic学的是V(s_t)("在当前状态下,预计最终能得多少分"),GAE用它来算每个token的精确优势。第3节展开四模型架构时会详细讲Critic的生命周期。
💡 关键要点:GAE = 给每个token独立打分,距离奖励越近权重越高。需要Critic提供状态价值估计V(s)。
2.3 奖励模型是怎么"学会打分"的?
虽然RM不是PPO算法的一部分,但它是RLHF的枢纽——没有RM,PPO就没有优化信号。理解RM的数学直觉对理解整个RLHF至关重要。
RM的训练目标是:给定两个回答,预测哪一个人类更喜欢。
数学上,RM使用Bradley-Terry模型建模偏好概率。对于提示词x和两个回答y_w(win,被偏好的)和y_l(lose,被偏好的对手),人类选择y_w的概率为:
其中σ是sigmoid函数。要最大化这个概率,对应的损失函数是负对数似然:
这个损失的意思很直白:
RM的一个关键限制:RM学到的不是"绝对质量",而是"相对偏好"。这就意味着RM分数只有相对意义——分数从5涨到10不代表回答质量翻倍,只能说明"比之前更受人类偏好了"。这也解释了为什么RLHF训练到后期RM分数继续涨,但真实质量可能已经开始下降(reward hacking信号)。
💡 关键要点:RM用Bradley-Terry模型把人类偏好排序转化为标量分数,学习的本质是"相对偏好"而非"绝对质量"。
---
3. 深入理解:RLHF的"四模型"架构
3.1 四模型全景图
这是RLHF中最令人头疼的工程问题:PPO训练时需要同时维护4个模型:
四个模型各司其职:
3.2 每个模型的"来龙去脉"
Actor —— 你正在训练的模型
Reference Model —— SFT模型的冻结副本
KL(π_actor || π_ref),作为"不要跑太远"的参照物Critic —— 状态价值估计器
loss = (V_pred - R_actual)²。而且Critic也有自己的clip机制(PPO论文里叫value function clipping),防止V预测值跳得太猛。Reward Model —— 人类偏好的代理
显存压力到底有多大?
以训练一个7B的Actor为例(bf16精度):
这就是为什么RLHF吃显存——同样训一个7B模型,SFT只需要一张A100(80GB),PPO至少需要两张甚至更多。
💡 关键要点:四模型架构是RLHF最大的工程代价——Actor+Ref+Critic+RM=参数量×4,这也是为什么GRPO(去Critic)和DPO(去掉整个RL阶段)应运而生。
---
4. 总装:PPO-RLHF的完整Loss体系
前面的2.1、2.2、2.3节分别拆解了ratio+clip+KL、GAE、RM训练,3.1节拆解了四模型架构。现在把所有零件装回引擎盖下,看一眼完整的loss长什么样。
4.1 总Loss全景:两个优化器、四条信号链
PPO-RLHF训练中实际反向传播的loss并不是一个单一的数,而是两个独立优化器各自拉一条loss链——Actor和Critic各算各的,但共享同一套奖励信号和优势值。
上图看起来复杂,但拆成四条信号链就清晰了。训练时,前三条链在前向阶段产生数据、不产生梯度(torch.no_grad),第四条链才真正反向传播。
---
链一:总奖励 R_total —— "这个回答值多少分"
前向阶段用Reference Model和RM(都冻结)算出最终奖励,是后续所有计算的基础:
两项的含义:
KL出现在这里(而不是直接出现在Actor loss里),是因为它是奖励塑形(reward shaping) 的一部分——在RL的视角里,"偏离SFT太远"本身就是一种惩罚,被提前扣在奖励里。β控制扣多少,InstructGPT取0.02~0.04。
另一种等价做法是PPO-ptx:不在奖励里扣KL,而在Actor loss里直接加一份预训练loss。工程实践中两者经常混用,本文代码示例采用"奖励里扣KL"的方式(更主流,也是OpenRLHF/TRL的默认做法)。
# 链一:总奖励(采样阶段,不产生梯度)
kl_per_token = log_probs_old - ref_log_probs # shape: [batch, seq_len]
kl_sum = kl_per_token.sum(dim=-1) # shape: [batch]
R_total = rm_scores - beta * kl_sum # shape: [batch]---
链二:GAE优势 —— "每个token贡献了多少"
拿到R_total后,用Critic的V估计和GAE公式把"整体奖励"拆分为"逐token的优势值":
LLM场景的特殊性:奖励是稀疏的——episode中间R_total[t] = 0,只有最后一步(EOS token位置)R_total[t] = 总奖励值。所以δ_t主要由 决定,Critic的质量直接决定GAE的准确性。
# 链二:GAE(采样阶段,从后往前递推)
advantages = torch.zeros_like(R_total_vec) # 展开到序列长度
last_gae = 0
for t in reversed(range(seq_len)):
r_t = R_total if t == last_token_idx else 0 # 稀疏奖励
delta = r_t + gamma * V_next - V_old[t]
last_gae = delta + gamma * lam * last_gae
advantages[t] = last_gae
# 标准化(训练稳定性的关键)
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)---
链三:Actor损失 —— "调整生成策略"
拿到每token的优势A_t后,Actor loss就是经典的PPO-Clip(2.1节详述):
四个关键点:
# 链三:Actor损失(需要重新前向,需要梯度)
log_probs_new = actor(prompts, responses) # 重新前向,产生梯度
ratio = torch.exp(log_probs_new - log_probs_old.detach()) # old来自采样阶段,截断梯度
surr1 = ratio * advantages
surr2 = torch.clamp(ratio, 1 - epsilon, 1 + epsilon) * advantages
L_actor = -torch.min(surr1, surr2).mean()---
链四:Critic损失 —— "学会预测未来"
Critic的目标是让V(s_t)尽可能准地预测最终回报。它的loss就是value prediction + value clipping(PPO原始论文的另一个clip):
注意这里外层取max(不是min!),因为Critic的loss本身就是MSE,值越小越好。max确保clip不会让loss虚低——如果clip让loss变小了,取max用回未clip的版本,保证优化方向不歪。
Critic也有自己的ε(通常和Actor相同,都是0.2):
# 链四:Critic损失
values_new = critic(prompts, responses)
R_target = advantages + values_old.detach()
# Critic clip
values_clipped = values_old.detach() + torch.clamp(
values_new - values_old.detach(), -epsilon, epsilon
)
v_loss1 = (values_new - R_target) ** 2
v_loss2 = (values_clipped - R_target) ** 2
L_critic = torch.max(v_loss1, v_loss2).mean()---
四条链的总装关系总结:
RM前向 ──┐
├──→ R_total ──→ GAE → A_t ──┬──→ L_actor(Actor更新)
Ref KL ───┘ │
Critic V_old ──────────→ GAE → A_t ─────┤
└──→ R_target ──→ L_critic(Critic更新)
Actor和Critic各自维护自己的优化器、各自有自己的loss,但共享R_total和A_t两根信号管。这两根信号管在采样阶段一次性算好(冻结),然后反复用于多步Actor和Critic的参数更新。这也是PPO的经典做法——一个采样周期内做多步policy update(通常4~16步),避免生成新样本的频率过高。
💡 关键要点:PPO-RLHF = R_total(RM−βKL)→ GAE(V做baseline)→ L_actor(clip ratio) + L_critic(clip V),四条链两个优化器。
4.2 简化实现:完整训练步长代码
将上面四条链的逻辑整合成一个可运行的函数(简化版,去除分布式/梯度累积等工程细节):
def ppo_training_step(actor, critic, ref_model, rm_model,
prompts, optimizer_actor, optimizer_critic,
clip_epsilon=0.2, kl_beta=0.04, gamma=0.95, lam=0.95):
"""
PPO一次训练迭代的简化实现。
actor/critic: 待训练
ref_model/rm_model: 冻结
"""
# === 1. 采样阶段:Actor生成回答 ===
with torch.no_grad():
responses, log_probs_old, values_old = actor.generate_with_values(
prompts, return_log_probs=True
)
ref_log_probs = ref_model.get_log_probs(prompts, responses)
rm_scores = rm_model(prompts, responses)
# === 2. 计算总奖励 ===
kl = log_probs_old - ref_log_probs # token级KL
rewards = rm_scores - kl_beta * kl.sum(dim=-1) # RM分 - KL惩罚
# === 3. GAE优势估计 ===
advantages = torch.zeros_like(rewards)
last_gae = 0
for t in reversed(range(len(rewards))):
# 最后一个时间步的delta(没有s_{t+1},V=0)
delta = rewards[t] - values_old[t]
last_gae = delta + gamma * lam * last_gae
advantages[t] = last_gae
# 标准化优势(稳定训练)
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
# === 4. PPO-Clip策略损失 ===
log_probs_new = actor.get_log_probs(prompts, responses)
ratio = torch.exp(log_probs_new - log_probs_old)
surr1 = ratio * advantages
surr2 = torch.clamp(ratio, 1 - clip_epsilon, 1 + clip_epsilon) * advantages
policy_loss = -torch.min(surr1, surr2).mean()
# === 5. Critic损失 ===
values_new = critic(prompts, responses)
# Critic也有clip
values_clipped = values_old + torch.clamp(
values_new - values_old, -clip_epsilon, clip_epsilon
)
v_loss1 = (values_new - (advantages + values_old)) ** 2
v_loss2 = (values_clipped - (advantages + values_old)) ** 2
critic_loss = torch.max(v_loss1, v_loss2).mean()
# === 6. 反向传播 ===
optimizer_actor.zero_grad()
policy_loss.backward()
optimizer_actor.step()
optimizer_critic.zero_grad()
critic_loss.backward()
optimizer_critic.step()
return policy_loss.item(), critic_loss.item(), rewards.mean().item()这里需要关注几个容易被忽略但非常关键的工程细节:
kl.sum(dim=-1)意味着每个token产生的KL散度都要累加起来再扣。因为长回答天然KL大,短回答KL小,不能只算均值。💡 关键要点:PPO实现的核心 = ratio_clip策略损失 + critic_clip价值损失 + GAE优势 + token级KL惩罚。标准化优势是稳训的关键技巧。
---
5. 常见误区
误区一:"clip(ratio, 0.8, 1.2)意味着参数最多更新20%"
不对。clip限制的是重要性采样比率(新旧策略的概率比),不是参数变化量。ratio=1.2意味着"这个token在新策略下概率涨了20%",不是"模型参数变了20%"。实际上,在深度网络中,ratio变化和参数变化之间没有这种简单的线性关系。clip的作用是防止单步更新后新策略对旧策略的概率评估面目全非。
误区二:"KL惩罚可有可无,主要是理论形式"
KL惩罚非常关键,不是摆设。2025年有多篇研究(如InfoRM等)专门分析RLHF训练中KL系数对reward hacking的影响——KL系数太小(<0.01),模型很快就学会说"高分废话";KL系数太大(>0.5),模型几乎不更新。调KL系数是RLHF工程中最耗时的超参搜索之一。
误区三:"RM分数一直涨就说明训练在变好"
RM分数是代理指标,不是真实质量。有一个经典现象:PPO训练到中后期,RM分数继续涨甚至加速涨,但用GPT-4或人类重新评估时质量反而下降——这就是reward overoptimization。实际工程中必须定期用独立的评估集检查真实质量,不能只看RM分数曲线。
误区四:"Critic必须和Actor一样大"
不必要。InstructGPT的实践证明Critic可以比Actor小不少。不过工业实践中为了简化代码和训练流程,通常让Critic和Actor共享大部分结构(从同一个SFT模型初始化不同头)。GRPO的天才之举就是发现了"组内归一化也能提供baseline",彻底省掉了Critic。
💡 关键要点:Clip限制的是ratio不是参数、KL不是可有可无、RM分数不等于真实质量、Critic不必须和Actor一样大。
---
6. 本篇总结
这一篇我们建立了PPO/RLHF的完整认知:
🔗 下一篇预告:PPO需要4个模型,太吃显存。有一个人想了个天才的简化——如果能把"人类偏好"直接编码进损失函数,省掉RM和Critic,一个公式就能做对齐。这就是DPO(Direct Preference Optimization)。下篇深入拆解:Bradley-Terry模型怎么被"反过来"用?DPO损失函数背后的数学直觉是什么?
---
参考资源
- 《Training language models to follow instructions with human feedback》(Ouyang et al., 2022) — InstructGPT原始论文,RLHF的开山之作
- 《Proximal Policy Optimization Algorithms》(Schulman et al., 2017) — PPO原始论文,定义了clip机制和value function clipping
- 《High-Dimensional Continuous Control Using Generalized Advantage Estimation》(Schulman et al., 2016) — GAE原始论文
- 《Deep Reinforcement Learning from Human Preferences》(Christiano et al., 2017) — RLHF的概念起源,人类偏好→RM→RL
- HuggingFace TRL PPOTrainer文档 — 开源PPO实现,包含完整的日志指标说明
- OpenRLHF (https://github.com/OpenRLHF/OpenRLHF) — 基于Ray/vLLM/DeepSpeed的高性能RLHF框架
- 《The N+ Implementation Details of RLHF with PPO》(Huang et al., 2024) — HuggingFace博客,PPO实现中容易踩的工程坑
- 《RLHF: Reinforcement Learning from Human Feedback》— Chip Huyen的博客,从工程视角看RLHF全流程
- 《Illustrating Reinforcement Learning from Human Feedback》— HuggingFace博客,含交互式可视化