📖 教程进阶⏱️ 22 分钟

PPO与RLHF:大模型对齐的奠基之作

📅 2026/6/16✍️ 管理员💬 0 条评论

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的解法-----------------------方差大同一道题答两次,因为奖励噪声差10分,更新幅度差10个单位优势函数 + GAE信用分配100个token全按同一个总分更新,80个无关token搭了20个好token的便车GAE(按距离加权分配贡献)步子太大一次更新策略面目全非,原来会说的话突然不会说了Clipping + KL约束

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模型训练成对齐后的最终模型。每次迭代:

  • Actor(待训练的SFT模型)生成回答
  • RM给这个回答打分
  • PPO根据分数和约束计算梯度,更新Actor参数
  • 循环

  • 💡 关键要点: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)


    rt(θ)=πθ(atst)πold(atst)r_t(\theta) = \frac{\pi_\theta(a_t \mid s_t)}{\pi_{\text{old}}(a_t \mid s_t)}

  • πθ\pi_\theta:当前策略(新模型)给这个token的概率
  • πold\pi_{\text{old}}:旧策略(采样时的模型)给这个token的概率

  • 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就不让更新。


    LCLIP=min(rtAt, clip(rt, 1ε, 1+ε)At)L^{\text{CLIP}} = \min\left(r_t \cdot A_t,\ \text{clip}(r_t,\ 1-\varepsilon,\ 1+\varepsilon) \cdot A_t\right)

    用人话解释这个min操作:

  • 如果优势A_t是正的(这个token干得好),我们想增大r_t。但如果r_t已经涨到1.2以上——"行了你已经够喜欢这个token了,别再加了"——clip把你摁住。
  • 如果优势A_t是负的(这个token干得差),我们想减小r_t。但如果r_t已经跌到0.8以下——"已经降得够狠了,适可而止"——clip把你拉住。

  • 一个直觉类比:考试提分


    第1篇我们用了"教学生做题"的类比。现在升级一下:


  • REINFORCE:考了90分,"所有答题习惯一律强化"(包括蒙对的题)。考了30分,"所有习惯一律打压"。
  • PPO:考了90分,"把得分高的题型习惯强化一下,但别太激进——万一只是运气呢?一次最多改20%"(clip)。考了30分,"把明显答错的改掉,但别把本来会的也给改了"。同时还有个"底线检查"——"你写字的风格不能突然从楷书变成草书"(KL约束)。

  • 第二把锁:KL散度约束


    Clip只约束了相对变化幅度,但还有一个更根本的约束:模型不能偏离SFT模型太远。原因是:


  • 防止语言模型退化:如果完全放开让RM打分,模型可能学会输出"高分的胡言乱语"——句子不通顺但RM给高分。KL约束强制输出保持在"正常的语言分布"内。
  • 防止reward hacking:RM毕竟只是人类偏好的近似,有漏洞可钻。KL约束是最后的保险——"就算你发现了RM的漏洞,也不能偏离太远"。

  • KL项的实现方式有两种(InstructGPT和工程实践中两种都有用到):


    方式一(在奖励里加KL惩罚)


    Rtotal=RRMβKL(πθπref)R_{\text{total}} = R_{\text{RM}} - \beta \cdot \text{KL}(\pi_\theta \parallel \pi_{\text{ref}})

    在RM奖励中直接扣除当前策略与参考策略的KL散度。β是惩罚系数,通常取0.01~0.1。


    方式二(PPO-ptx,混合预训练损失)


    Ltotal=LPPO+γLpretrainL_{\text{total}} = L_{\text{PPO}} + \gamma \cdot L_{\text{pretrain}}

    在PPO损失里加一份预训练的交叉熵损失,防止模型的语言能力退化。InstructGPT证明了这种方式可以减轻"对齐税(alignment tax)"——模型在对齐过程中在某些基准任务上的性能下降。


    工业实践中通常两种方式混用:奖励里加KL惩罚 + PPO损失里加预训练损失。


    完整PPO目标函数


    用代码表达PPO的核心逻辑比公式更直观:


    python
    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远期信息"。


    AtGAE=δt+(γλ)δt+1+(γλ)2δt+2+A_t^{\text{GAE}} = \delta_t + (\gamma\lambda)\cdot\delta_{t+1} + (\gamma\lambda)^2\cdot\delta_{t+2} + \cdots

    其中 δt=rt+γV(st+1)V(st)\text{其中}\ \delta_t = r_t + \gamma \cdot V(s_{t+1}) - V(s_t)

    在LLM场景中,由于只有episode结束时才有奖励(稀疏奖励),需要Critic(价值网络)来估计每个中间状态的价值V(s_t)。GAE的魔力在于:它同时解决了第1篇的方差问题和信用分配问题:


  • 降低方差:GAE中用V(s_t)做baseline,减去期望水平,只看"相对优势"
  • 信用分配(γλ)k(\gamma\lambda)^k的指数衰减意味着,距离奖励越远的token,权重越低。λ=0.95时,第1个token的权重是0.95,第2个0.90,第20个只有0.36——天然地把功劳向末尾集中。

  • 这就是为什么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的概率为:


    P(yw>ylx)=σ(R(x,yw)R(x,yl))P(y_w > y_l \mid x) = \sigma\left(R(x, y_w) - R(x, y_l)\right)

    其中σ是sigmoid函数。要最大化这个概率,对应的损失函数是负对数似然:


    LRM=logσ(R(x,yw)R(x,yl))=log11+exp((RwRl))L_{\text{RM}} = -\log\sigma\left(R(x, y_w) - R(x, y_l)\right) = -\log\frac{1}{1 + \exp\left(-(R_w - R_l)\right)}

    这个损失的意思很直白:

  • 如果RwRlR_w - R_l很大(高分回答远好于低分回答) → σ很大 → log(大数)-\log(\text{大数})很小 → 损失小,符合预期
  • 如果RwRl0R_w - R_l \approx 0(两者差不多) → σ≈0.5 → log(0.5)0.69-\log(0.5) \approx 0.69 → 损失不大不小
  • 如果RwRl<0R_w - R_l < 0(预测反了,认为差的更好) → σ很小 → log(小数)-\log(\text{小数})很大 → 大损失,狠罚

  • RM的一个关键限制:RM学到的不是"绝对质量",而是"相对偏好"。这就意味着RM分数只有相对意义——分数从5涨到10不代表回答质量翻倍,只能说明"比之前更受人类偏好了"。这也解释了为什么RLHF训练到后期RM分数继续涨,但真实质量可能已经开始下降(reward hacking信号)。


    💡 关键要点:RM用Bradley-Terry模型把人类偏好排序转化为标量分数,学习的本质是"相对偏好"而非"绝对质量"。

    ---


    3. 深入理解:RLHF的"四模型"架构


    3.1 四模型全景图


    这是RLHF中最令人头疼的工程问题:PPO训练时需要同时维护4个模型


    加载图表...

    四个模型各司其职


    模型全称是否训练显存占比(估)作用-------------------------------------------Actor策略模型✅ 是~35%待优化的LLM,生成回答Reference参考模型❌ 冻结~30%SFT模型副本,计算KL惩罚的锚点Critic价值模型✅ 是~30%估计每个状态的V值,供GAE算优势Reward Model奖励模型❌ 冻结~5%给回答打分,PPO的优化信号源

    3.2 每个模型的"来龙去脉"


    Actor —— 你正在训练的模型


  • 初始化:从SFT模型的权重加载
  • 训练中:每次迭代采样生成回答,根据PPO目标函数更新参数
  • 生命周期:整个PPO阶段持续更新

  • Reference Model —— SFT模型的冻结副本


  • 初始化:SFT模型加载后立即冻结,参数永不更新
  • 作用:计算 KL(π_actor || π_ref),作为"不要跑太远"的参照物
  • 为什么需要它?因为Actor在不断变化,需要一个固定的锚点来判断它偏离了多少。如果直接用Actor的上一个版本做参照,参照物也在动,就会漂移。

  • Critic —— 状态价值估计器


  • 初始化:通常从RM的权重加载(因为RM也习惯于评估质量),把头换成输出1个标量
  • 训练中:和Actor交替更新,学习预测每个token在未来能得多少奖励
  • 为什么需要它?GAE需要V(s_t)来算优势。RM只给最后一步的奖励,中间每一步的先见之明全靠Critic。
  • 工程细节:Critic的loss通常用MSE loss = (V_pred - R_actual)²。而且Critic也有自己的clip机制(PPO论文里叫value function clipping),防止V预测值跳得太猛。

  • Reward Model —— 人类偏好的代理


  • 初始化:从阶段2训练好的RM权重加载,冻结
  • 大小:通常比Actor小很多(InstructGPT的RM是6B,对1.3B的Actor来说够用;但训练更大模型时常需要34B甚至更大的RM)
  • 为什么需要它?没有人类实时盯着打分,RM是唯一的自动打分来源。

  • 显存压力到底有多大?


    以训练一个7B的Actor为例(bf16精度):


    组件参数量显存(bf16)---------------------------Actor7B14 GBReference7B14 GBCritic7B14 GBRM~3B6 GB模型权重总计~48 GB优化器状态(Adam,Actor+Critic)~28 GB激活值 + 梯度~20 GB总计(估算)~96 GB

    这就是为什么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(都冻结)算出最终奖励,是后续所有计算的基础:


    Rtotal(x,y)=RRM(x,y)βtKL(πθ(atst)πref(atst))R_{\text{total}}(x, y) = R_{\text{RM}}(x, y) - \beta \cdot \sum_t \text{KL}\left(\pi_\theta(a_t \mid s_t) \parallel \pi_{\text{ref}}(a_t \mid s_t)\right)

    两项的含义:


    项来源作用----------------RRM(x,y)R_{\text{RM}}(x, y)RM前向打分"人类觉得这个回答有多好"KL(πθπref)\text{KL}(\pi_\theta \parallel \pi_{\text{ref}})Actor vs Reference的逐token KL"当前策略偏离SFT有多远"

    KL出现在这里(而不是直接出现在Actor loss里),是因为它是奖励塑形(reward shaping) 的一部分——在RL的视角里,"偏离SFT太远"本身就是一种惩罚,被提前扣在奖励里。β控制扣多少,InstructGPT取0.02~0.04。


    另一种等价做法是PPO-ptx:不在奖励里扣KL,而在Actor loss里直接加一份预训练loss。工程实践中两者经常混用,本文代码示例采用"奖励里扣KL"的方式(更主流,也是OpenRLHF/TRL的默认做法)。


    python
    # 链一:总奖励(采样阶段,不产生梯度)
    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的优势值":


    AtGAE=δt+(γλ)δt+1+(γλ)2δt+2+A_t^{\text{GAE}} = \delta_t + (\gamma\lambda)\cdot\delta_{t+1} + (\gamma\lambda)^2\cdot\delta_{t+2} + \cdots

    其中 δt=Rtotal[t]+γV(st+1)V(st)\text{其中}\ \delta_t = R_{\text{total}}[t] + \gamma \cdot V(s_{t+1}) - V(s_t)

    LLM场景的特殊性:奖励是稀疏的——episode中间R_total[t] = 0,只有最后一步(EOS token位置)R_total[t] = 总奖励值。所以δ_t主要由 V(st+1)V(st)V(s_{t+1}) - V(s_t) 决定,Critic的质量直接决定GAE的准确性。


    python
    # 链二: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节详述):


    Lactor(θ)=Et[min(rt(θ)At, clip(rt(θ), 1ε, 1+ε)At)]L_{\text{actor}}(\theta) = -\mathbb{E}_t\left[\min\left(r_t(\theta) \cdot A_t,\ \text{clip}(r_t(\theta),\ 1-\varepsilon,\ 1+\varepsilon) \cdot A_t\right)\right]

    四个关键点:

  • rt(θ)=πθ(atst)/πold(atst)r_t(\theta) = \pi_\theta(a_t \mid s_t) / \pi_{\text{old}}(a_t \mid s_t) —— 重要性采样比率
  • 外层取min —— 当ratio变动方向对Actor有利时做clip,防止步子太大
  • 外层取负号 —— 因为要最大化优势,但优化器做的是最小化loss
  • ε=0.2 —— 一次更新最多让ratio变20%,是业界通用默认值

  • python
    # 链三: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):


    Lcritic(ψ)=Et[max((Vnew(t)Rtarget(t))2, (Vclipped(t)Rtarget(t))2)]L_{\text{critic}}(\psi) = \mathbb{E}_t\left[\max\left((V_{\text{new}}(t) - R_{\text{target}}(t))^2,\ (V_{\text{clipped}}(t) - R_{\text{target}}(t))^2\right)\right]

    其中 Rtarget(t)=At+Vold(t) —— "真实的累计回报"\text{其中}\ R_{\text{target}}(t) = A_t + V_{\text{old}}(t) \text{ —— "真实的累计回报"}

    注意这里外层取max(不是min!),因为Critic的loss本身就是MSE,值越小越好。max确保clip不会让loss虚低——如果clip让loss变小了,取max用回未clip的版本,保证优化方向不歪。


    Critic也有自己的ε(通常和Actor相同,都是0.2):


    python
    # 链四: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 简化实现:完整训练步长代码


    将上面四条链的逻辑整合成一个可运行的函数(简化版,去除分布式/梯度累积等工程细节):


    python
    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()

    这里需要关注几个容易被忽略但非常关键的工程细节:


  • 优势标准化(Advantage Normalization):在batch内把优势值标准化到均值0、标准差1。这能让他训得明显更稳——想象一个batch里有10条轨迹,有的优势+50有的-3,标准化后都在相似的尺度上,梯度不会忽大忽小。

  • Critic也有clip:PPO论文里明确提到了value function clipping——Critic预测的V值变化太大了也要裁。因为Critic如果一步预测跳得太厉害,会污染GAE的优势估计。

  • KL惩罚是token级别的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的完整认知:


  • RLHF三阶段:SFT学会格式 → RM学会评分 → PPO学会对齐
  • PPO = REINFORCE + Clipping + GAE + KL:四个组件各司其职,把不稳定的策略梯度升级为工业级方案
  • 四模型架构是核心代价:Actor+Ref+RM+Critic占用约4倍参数量,这是推动DPO和GRPO出现的直接动力
  • PPO的目标函数逻辑:在最大化RM分数的同时,clip限制单步变化幅度,KL防止整体偏离SFT太远

  • 🔗 下一篇预告: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博客,含交互式可视化


    📚 继续学习:大模型强化学习与强化微调:从策略梯度到前沿算法

    这篇是「大模型强化学习与强化微调:从策略梯度到前沿算法」学习路径的第 2 篇,共 3

    回到学习路径 →

    评论 (0)

    请先登录后发表评论

    暂无评论,来发表第一条评论吧