
最近被朋友拉去讨论一个很现实的问题很多人在 LLM 的强化学习代码里看到 GRPO第一反应是“这不就是 PPO 换了个名字”等真打开 loss 函数又开始怀疑人生因为 GRPO 里根本找不到 critic 网络连 advantage 都不是用价值函数算的。其实这两个算法每天都在被大量调用但真正能一句话说清“它到底在更新什么”的人远比想象中少。我先给一个最简答案PPO 和 GRPO 更新的都是策略网络本身的参数也就是你那个模型里所有可训练权重更新方向由“当前策略的输出概率和旧策略的输出概率的比值”以及“这条轨迹或这组输出的优势估计”共同决定。别小看这句话整篇文章的所有细节都是围绕这句话展开的。1. 先看明白这两套算法更新的一直是策略网络1.1 你的模型在训练里唯一变动的对象强化学习里有个容易混淆的点训练过程中同时存在好几个“网络”到底哪个在动PPO 传统的实现里通常有策略网络 actor、价值网络 critic有些框架还会给 reward model 单独存一份权重。很多人一看要训练三个模块就默认“PPO 在更新所有网络”。实际情况是PPO 和 GRPO 的优化目标只针对策略网络。critic 只是个辅助模块它存在的意义是估算状态的价值用来降低 advantage 估计的方差但最终推动策略参数更新的梯度不来自 critic 自身的损失。critic 也会更新不过那是用价值回归的 MSE 算出来的属于另一条独立的优化线。GRPO 更彻底连 critic 都没有直接把一个 batch 里多条样本的 reward 做归一化用组内相对位置当 advantage。所以你观察训练日志的时候会看到一个关键现象策略网络的 loss 在下降critic 的 loss 也在下降但它们各自对应不同的训练目标。真正改变“模型怎么回答问题”的只有策略网络那份梯度critic 某种程度上是“为了让策略更新更稳”而存在的陪练。1.2 什么是“更新策略”从概率分布的角度理解一个语言模型或一个连续动作控制器本质上都是在给定输入时输出一个概率分布。LLM 是词表上的分类分布连续控制环境通常是高斯分布。策略更新就是调整这个分布的参数让“能带来更高奖励的动作/回答”在下一步采样时有更大的概率被选中。这句话听起来简单但真正难的在于“能带来更高奖励”这个信号并不等于环境直接给你的那个数字。环境给的是奖励策略梯度需要的是某个动作相对平均水平好多少。同样的 0.8 分如果整组样本都是 0.9 以上那这个 0.8 反而是差动作如果整组样本都在 0.3 以下0.8 就是优秀动作。所以算法要花的力气主要在“怎么把原始奖励转换成让策略知道该往哪边走的梯度方向”。PPO 用价值函数 V(s) 来当基线算 A r γV(s) - V(s)。GRPO 用同一组 prompt 生成的所有回答的平均分当基线A_i (r_i - mean) / std。基线不同但最终目标一致告诉策略网络这批样本里哪个动作/回答是值得提升概率的。理解到这一层你再看那些公式就不会晕了。2. PPO 的更新目标裁剪比率的来龙去脉2.1 从策略梯度的脆弱说起先回到最原始的策略梯度。假设我们用当前策略 πθ 采样了一批轨迹想通过梯度上升让策略变好。最朴素的写法是∇θ J(θ) E[ ∇θ log πθ(a|s) * A ]意思是如果优势 A 为正就增加这个动作的对数概率如果 A 为负就减少它。方向没问题但直接用当前策略刚采的数据做一次大步幅更新下一轮新旧策略差异一大梯度估计就会失真甚至出现越更新越差的情况。核心理由是一条轨迹是在旧策略 πθ_old 下采出来的如果 πθ 已经变了很多那这批样本对当前策略的“代表性”就下降了。你拿着过期的数据去判断新策略好不好评估本身就不可信。所以 PPO 把更新拆成了多次小的迭代每次从环境中采一批数据固定为旧策略然后用同一个 batch 反复做若干轮梯度更新。为了保证多轮更新后策略不会一下子飞出旧策略的信任范围它引入了一个比率ratio πθ(a|s) / πθ_old(a|s)这个比率表示新策略下这个动作的概率相对旧策略放大了多少。如果 ratio 是 1.2说明策略更新后这个动作出现的概率提升了 20%这是好事还是坏事要看优势 A 的正负。2.2 新旧策略的比值与 clip 区间PPO 的完整目标通常写成LCLIP E[ min(ratio * A, clip(ratio, 1-ε, 1ε) * A) ]拆开来看如果 A 0说明这个动作比平均情况好我们希望 ratio 越大越好但为了防止一次更新太猛clip 把它限制在 1ε 以内。min 操作保证实际用的增长上限就是 1ε。如果 A 0说明这个动作不好我们想让 ratio 变小但 clip 同样限制它不低于 1-ε。此时 min(ratio*A, clip(ratio)A) 要特别注意因为 A 是负数ratioA 是负得更多的那个min 会选中 clip 后的版本也就是说即使 ratio 已经从 0.8 掉到 0.2loss 的梯度也会在边界处截断不再继续惩罚。ε 通常取 0.2代表允许新旧策略在单个更新步里最多偏离约 20%。这也是为什么 PPO 训练看起来比原始策略梯度“稳”它用显式的区间控制而不是靠调小学习率来间接控制。2.3 ratio 之外的三个损失项实际工程里的 PPO loss 不是只有 clip 那一项。以常见实现为例完整损失长这样L LCLIP - c1 * LVF c2 * entropy_bonusLVF 是 critic 的价值回归损失训练的是价值网络不直接更新策略。它用 MSE 让 V(s) 逼近真实回报目的就是让 advantage 估计更准。entropy_bonus 是策略分布的熵鼓励动作分布别太早坍缩。语言模型场景里熵会随 KL 惩罚一起控制连续控制场景里这个正则项尤其重要。这里还容易漏掉一个隐式项老练的框架会在更新时给 ratio 增加一个 stop_gradient 的旧 log prob避免反向传播把梯度传到旧策略分支上。所以我一直跟人强调PPO 代码里那些看似“花哨”的项最终都在为同一个目标服务让策略参数沿着可信的方向更新同时保留足够的探索性。3. GRPO 到底改了什么去掉 Critic用组内均值当基线3.1 为什么 LLM 场景的 PPO 会先“重”后慢把 PPO 直接搬到语言模型生成任务上会遇到两个很实际的问题。第一critic 网络本身要跟着语言模型一起训练。语言模型动辄几十亿参数value head 即使只是一个很小的 MLP也要接收整个隐层输出做前向计算这一下就把显存和算力成本抬高了一截。更麻烦的是价值函数在语言生成的任务里并不好学因为很多环境的 reward 是稀疏的、规则化的状态价值很难被准确估计。第二生成任务的“状态”是一整段 token 序列critic 要为每个位置的隐状态都输出一个标量价值这个监督信号本身噪声很大。训练不稳定的时候你不知道是策略坏了还是 critic 坏了。GRPO 的思路很直接既然 critic 又贵又不容易训好那就不训了。用一个 prompt 采样出 G 个回答把这组回答的平均奖励当基线用回答之间的相对水平来构造 advantage。3.2 组内相对优势的计算逻辑对一个训练 batch 里的每个问题 q先由旧策略采样出 G 个输出。假设奖励模型或规则函数给每个输出一个分数 r_iGRPO 的做法是A_i (r_i - mean(r_1, ..., r_G)) / std(r_1, ..., r_G)如果只对一个回答做这种归一化就叫“归一化奖励”但 GRPO 是在组内做的所以叫“组相对”。这里的关键是baseline 不来自价值网络而是来自同一组样本的均值。这样做有两个直觉上的好处组内比较能抹掉不同问题之间的难度差异。问题 A 大家都拿 2 分问题 B 大家都拿 9 分直接用绝对分数会让模型只顾着冲高分题而组内归一化之后每个问题内部都有正有负策略在每个问题上都能获得有效的“做得更好/更差”信号。方差估计是通过同一 batch 内样本的离散程度算的不需要额外网络也就不存在 critic 不收敛导致 advantage 常年不准的问题。在 DeepSeek 系列的论文和很多开源代码里优势还会进一步除以组内标准差这是为了让梯度尺度更稳定。如果你只在代码里看到一句advantages (rewards - rewards.mean(dim0)) / (rewards.std(dim0) eps)那就是 GRPO advantage 的核心实现简单得几乎不像强化学习。3.3 GRPO 的裁剪目标和 KL 约束GRPO 的策略更新目标和 PPO 非常像也是裁剪过的比率项LGRPO E[ 1/G * Σ 1/|o_i| * Σ_t min(ratio_i,t * A_i, clip(ratio_i,t, 1-ε, 1ε) * A_i) - β * KL[πθ || πref] ]注意两个细节对每个 token 位置 t 都计算 ratio 和梯度最后除以输出长度 |o_i| 做平均避免长回答天然获得更多梯度权重。后面多了一项 KL 惩罚通常是让模型不要偏离参考策略 πref 太远。参考策略一般是 SFT 之后的模型这一项的作用是保留基础语言能力。在 PPO 的经典实现里KL 惩罚有时以奖励塑造的形式加在 reward 上有时作为单独项。GRPO 更常见的是显式写进 loss。用 β 控制强度0.01 甚至更低一旦设置太大模型会为了“像参考策略”而不敢优化生成质量上不去。从这个角度看GRPO 并没有发明一套全新的优化思想它是在保留 PPO“比率裁剪控制更新幅度”这个核心的前提下把价值基线替换成了组内均值基线。这也是为什么标题问“到底在更新什么”答案从 PPO 到 GRPO 一直没变——更新的就是策略网络变的只是优势怎么算。4. PPO 连续动作控制一个足够简单又足够真实的代码骨架4.1 高斯策略输出的前向与 log_prob连续动作场景最典型的实现是高斯策略网络输出动作均值 μlog_std 可以是一个独立的可学习参数也可以由网络输出。采样时动作 a ~ N(μ, σ)计算 log_prob 时把多维高斯分布的密度函数写出来。随手写一个极简 PyTorch 片段import torch import torch.nn as nn import torch.distributions as D class GaussianPolicy(nn.Module): def __init__(self, state_dim, action_dim, hidden_dim64): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.Tanh(), nn.Linear(hidden_dim, hidden_dim), nn.Tanh(), nn.Linear(hidden_dim, action_dim), ) self.log_std nn.Parameter(torch.zeros(action_dim)) def forward(self, obs): mean self.net(obs) std torch.exp(self.log_std) dist D.Independent(D.Normal(mean, std), 1) return dist这里用Independent(..., 1)是为了让多维动作的 log_prob 按行求和得到整个动作向量出现的对数概率。很多新手会漏掉这一层导致 log_prob 维度变成 [batch, action_dim]后续算 ratio 时怎么减都减不对。4.2 采样一个 batch 后怎么构造 loss采集一批轨迹后你要保存的不仅仅是奖励还有“当前旧策略给出这批动作的概率”。用当前策略重新算一遍 log_prob和保存的旧 log_prob 做差就得到 ratio 的对数形式ratio torch.exp(log_prob_new - log_prob_old)优势估计最简单的写法是用 GAE也可以先用 r γ * next_value - value 过渡。假设你已经有了 advantagesPPO 的 loss 核心就是eps 0.2 surr1 ratio * advantages surr2 torch.clamp(ratio, 1 - eps, 1 eps) * advantages policy_loss -torch.min(surr1, surr2).mean()这里取负号是因为 PyTorch 做梯度下降而强化学习目标是梯度上升。CLIP 的 min 操作在前面加负号等效于在正的优化目标里做 max。如果你的动作空间很大或者你想要更保守的更新可以把 entropy 奖励加上entropy dist.entropy().mean() loss policy_loss - 0.01 * entropy这个 entropy 项的系数很敏感。我自己的经验是从 0.01 开始试如果训练前期动作熵掉得太快就调大一点如果策略一直不收敛反而要减小甚至关掉。4.3 dual-clip PPO 是什么时候用dual-clip这个热词提得越来越多主要是处理 PPO 在 off-policy 或重复利用数据时出现的极端情况。普通单边 clip 只在 ratio 超过 [1-ε, 1ε] 时截断但如果 advantage 为负而且 ratio 极其小原始公式里会选出一个很负的损失导致一次更新产生很大的破坏性梯度。dual-clip 会额外加一个下限dual_loss torch.max( torch.min(surr1, surr2), (1 - eps) * advantages )意思是当 A 0 时即便 ratio 已经掉到接近 0损失也被限制在一个更温和的范围内当 A 0 时这个 max 通常不会生效。简单说dual-clip 是为了让负优势样本的更新更“钝”适合你反复使用一个数据 buffer 做多次更新的场景。在标准 on-policy PPO 里这个技巧没有那么关键但在 LLM 的 GRPO 实现里一组输出会被重复用十几轮梯度更新这时如果你的 ratio 因为 KL 失控变得非常大或非常小dual-clip 就是一个便宜的保险丝。5. 实操中最容易误会的几个“更新”细节5.1 更新的是策略不是奖励模型我最常被问的问题之一是“GRPO 会不会把 reward model 一起训练了”。答案是不会。reward model 在用 RL 做对齐之前就已经训练完毕在整个 PPO/GRPO 训练阶段权重是冻结的甚至推理完奖励之后连梯度都不需要经过它。代码里你会看到奖励模型的requires_grad_(False)就是为了省显存因为它只负责前向打分。训练日志里如果出现了 reward model 的 loss 在变化那多半是日志写得不清楚把验证集上的 reward 曲线也叫成了 loss。 reward 曲线只能反映策略生成的回答越来越受奖励模型认可它不代表奖励模型本身被更新了。策略和奖励模型是“运动员”和“裁判”的关系裁判只打分不上场跑步。5.2 Critic 在 GRPO 里消失后方差怎么办GRPO 不用 critic很多人天然会担心 advantage 估计方差变大。实际上组内均值基线已经承担了大部分去均值的功能但没有 critic 也意味着它无法估计“从某个状态开始未来还有多少回报”。对句子生成这种任务状态空间几乎是无限且连续critic 学到的状态价值误差很大一开始就等于往 advantage 里注入了系统性偏差。组内奖励归一化则回避了这个难题直接用多条样本的相对排名代替了绝对价值。所以 GRPO 看似方差更大但因为偏差更小反而在语言模型场景里经常表现得更稳。需要付出的代价是采样开销变大。GRPO 要同一个 prompt 采 G 条回答G 通常在 8 到 64 之间才能让组内均值有意义。如果 G 太小随便一个极端奖励样本就能把整组的均值拉偏。5.3 熵奖励和 KL 惩罚的用途不同这两者经常被混为一谈但更新对象完全不一样。熵奖励直接作用在当前策略的分布上熵大意味着概率分布更均匀模型还愿意探索更多答案熵小说明分布集中基本在复读一种写法。增加熵奖励的副作用是生成多样性变好但平均奖励可能下降。KL 惩罚约束的是当前策略和参考策略的距离参考策略通常是不变的。KL 过大说明模型已经开始在语言风格、格式上与初始模型严重脱离哪怕奖励分数还在涨生成内容也可能已经变得晦涩甚至崩坏。实操上如果你发现模型输出开始出现重复套话或者乱用格式先查 KL 值是不是很高而不是急着降熵奖励。反过来如果模型输出千篇一律、reward 虽然高但多样性极差那再考虑调熵系数。6. 扩展开来损失数值、经验回放与训练稳定性的经验笔记6.1 观察 loss 曲线时看什么很多人盯着 policy loss 的绝对值不放总觉得它应该一直下降。实际不是。policy loss 更合理的状态是在一个范围内波动因为每次用新采样的数据更新时优势估计的正负样本比例会变化。真正值得盯的是平均 reward 是否在爬升KL 是否保持在设置的目标区间response 文本质量是否对齐entropy 是否过早坍缩。val loss 或 MSE 的下降在强化学习里不是首要目标策略的“行为表现”才是。我自己做 GRPO 调参时最顺手的做法是把每个 batch 的样本按组拆开打印每组内优势值的分布。如果一组里所有优势都集中在零点附近说明该组的奖励互相之间差别太小训练信号很弱这时该检查奖励设计而不是加大学习率。这个问题在 reward model 打分时特别常见打完分大家都相差 0.1 以内归一化后优势被压缩到接近零策略根本学不进去。6.2 我踩过的一些坑第一个坑是忘记把旧 log_prob 冻结。更新时如果重新前向得到的 log_prob 反向传播到了旧策略分布上ratio 里就会混入旧策略自身的梯度导致 loss 曲线出现一种“看似在拟合实际梯度里有两份重复路径”的怪象。正确做法是我前面写的旧 log_prob 要.detach()或用 stop_gradient。第二个坑是把 GRPO 的组归一化轴写错。同一个 prompt 采样的 G 条回答应该共享一个均值/标准差而不是把整个 batch 混在一起归一化。后者相当于把不同难度的任务拉到同一个尺度下比较会让简单问题的正样本带偏模型。第三个坑是 KL 惩罚加在 reward 上还是 loss 上。很多框架两种都叫 KL但行为不一样。如果加在 reward 上等于每一步生成的 KL 都会进入 advantage 的计算这种方式对单步 KL 更敏感如果是 loss 里的显式项它只在更新时生效不影响采样阶段。GRPO 常见的做法是 loss 项定期用一个小模型计算 KL 再惩罚好处是采样不会因 KL 波动影响奖励排序。6.3 一个比较实用的调参顺序如果模型从头开始训 GRPO我不会一上来就狂调 clip 和 β。先固定一个小的 β比如 0.01用很小的学习率跑 500 步然后看平均 KL 和平均 reward 的关系。如果 KL 很小但 reward 不涨说明更新幅度太小我会先调高学习率再看 clip 范围是否需要放宽如果 reward 涨但 KL 涨得也厉害就增大 β再考虑把 clip 从 0.2 降到 0.1。这套顺序比一次性把所有参数同时调要容易定位问题得多。PPO 的连续控制也是一样我习惯先固定 entropy 系数调 GAE 里的 λ。λ 接近 1优势估计会更偏长远回报方差大λ 接近 0偏向短视方差小但容易错过长期收益。有一回我只把 λ 从 0.95 调成 0.99一个连续控制任务的平均回报就涨了接近 20%效果比调学习率还明显。这些细节本质上都在回答同一个问题算法更新的是策略但如何让这个更新过程既快又稳靠的是优势估计、比率裁剪、KL 约束和采样设计这一整套系统。把这套系统想透了PPO 和 GRPO 在你眼里就不再是两个黑盒子而是同一套“让策略往好方向挪一步”的思想只是挪动的方式和量尺不同罢了。