GRPO目标函数深度拆解:从PPO到无Critic强化学习实战

发布时间:2026/10/26 16:42:35
GRPO目标函数深度拆解:从PPO到无Critic强化学习实战 1. 从PPO到GRPO为什么我们需要一个新的目标函数如果你最近在折腾大模型的对齐训练尤其是数学推理和代码生成这两个方向大概率会频繁撞见GRPO这个词。我第一次在项目里认真对待它是因为用PPO做数学题强化时critic模型价值网络的显存占用和训练不稳定性实在让人头疼——一个7B的策略模型配一个同等量级的critic显存直接翻倍而且critic的估计误差会反过来污染策略更新。GRPOGroup Relative Policy Optimization的核心思路就是干脆不要critic了用一组采样样本的相对奖励来代替价值基线。这个改动听起来简单但它直接改变了目标函数的构造方式也带来了一系列需要重新理解的细节。这篇文章我打算把GRPO的目标函数从头拆一遍。不是那种贴个公式就完事的论文笔记而是把我自己在复现和调参过程中踩过的坑、想明白的道理、以及实际写代码时需要注意的数值细节都摊开讲。适合的读者是已经了解PPO基本流程、想搞清楚GRPO到底改了什么、准备自己动手实现或者微调开源GRPO代码的人。如果你还没接触过策略梯度建议先把REINFORCE和PPO的损失函数过一遍不然读起来会有点吃力。先给一个最直观的对比。PPO的目标函数长这样L_PPO E[min(r_t * A_t, clip(r_t, 1-ε, 1ε) * A_t)]其中r_t是重要性采样比A_t是优势函数通常用GAE广义优势估计算出来而GAE需要一个价值网络V(s)来提供基线。GRPO做的事情是把A_t的计算方式换掉不再用V(s)而是用同一组prompt下采样的多个response的奖励均值作为基线。这样一来优势就变成了这个回答比同组平均好多少。目标函数的主体结构还是PPO那套clip机制但优势的语义和数值分布完全变了。为什么这个改动值得单独写一篇文章因为优势函数的语义变化会直接影响几个关键超参的选择KL散度的系数怎么定、clip的ε取多少、组大小G设成几、奖励归一化的方式等等。这些参数在PPO里的经验值搬到GRPO上往往不work必须重新理解目标函数才能调对。2. GRPO目标函数的完整拆解2.1 目标函数的数学形式与每一项的含义GRPO的目标函数在论文里的标准写法是J_GRPO(θ) E[q~P(Q), {o_i}~π_old(O|q)] * (1/G) * Σ_i [ min( ratio_i * A_i, clip(ratio_i, 1-ε, 1ε) * A_i ) - β * D_KL(π_θ || π_ref) ]逐项拆开看q~P(Q)从问题分布里采样一个prompt比如一道数学题。{o_i}~π_old(O|q)用旧策略采样时的策略对同一个q生成G个回答这G个回答构成一个组。ratio_i π_θ(o_i|q) / π_old(o_i|q)第i个回答的重要性采样比衡量新旧策略对这个回答的概率变化。A_i第i个回答的优势这是GRPO最核心的改动下面单独讲。clip(ratio_i, 1-ε, 1ε)PPO的截断机制防止单次更新步子太大。β * D_KL(π_θ || π_ref)对参考模型的KL惩罚防止策略跑偏太远。注意这里的KL是直接加在目标函数里的不是像PPO那样可以自适应调整。有一个细节容易被忽略KL项的位置。在PPO里KL通常作为奖励的一部分reward shaping或者自适应系数而GRPO把它直接写进了损失函数每个token都算KL。这意味着β的选择非常敏感后面会专门讲。2.2 优势函数A_i的计算组内归一化是灵魂GRPO的优势计算是整个方法的核心创新点。具体步骤对同一个prompt q用旧策略采样G个回答{o_1, o_2, ..., o_G}。每个回答得到一个奖励r_i比如数学题的正确性0/1或者reward model的打分。计算组内均值和标准差mean_r (1/G) * Σ r_istd_r sqrt((1/G) * Σ (r_i - mean_r)^2)。优势A_i (r_i - mean_r) / (std_r ε_std)其中ε_std是一个很小的数防止除零。这个做法的直觉是我不需要知道这道题的绝对难度只需要知道这G个回答里哪个相对更好。如果一道题特别难G个回答全错那mean_r0所有A_i也都是0或者接近0这个样本对梯度几乎没有贡献。如果一道题简单G个回答全对同样A_i都接近0。只有那些有的对有的错的样本才会产生有效的梯度信号。这其实是一种隐式的课程学习训练初期模型能力弱大部分题全错梯度信号稀疏随着能力提升越来越多的题进入部分正确区间梯度信号变密。这个特性和PPO用value network做基线的方式有本质区别——PPO的基线是跨样本学习的GRPO的基线是组内相对的。注意组大小G的选择直接影响优势的方差。G太小比如2均值和标准差的估计噪声很大优势信号不稳定G太大比如64采样成本线性增长而且组内可能全是相同答案优势仍然为0。实践中G8到16是比较常见的起点。2.3 KL散度项的处理为什么它比PPO更关键GRPO的KL惩罚是逐token计算的D_KL(π_θ || π_ref) π_ref(o_i|q) / π_θ(o_i|q) - log(π_ref(o_i|q) / π_θ(o_i|q)) - 1这个形式是KL散度的一种无偏估计k3估计器比直接算log概率差的期望更稳定。实际实现中通常用每个token的log概率差来近似kl_per_token log_prob_ref - log_prob_theta然后取平均或者求和。这里有个工程上的坑KL的归约方式sum还是mean会显著影响β的有效值。如果序列长度是512sum和mean之间差了512倍β需要相应调整。我看过不少开源实现在这点上不一致导致复现结果对不上。β的作用是拉住策略不要偏离参考模型太远。在GRPO里这个约束比PPO更重要因为没有critic来平滑更新。β太大策略几乎不更新训练停滞β太小策略会迅速过拟合到reward的漏洞上reward hacking。我的经验是β从0.01到0.1之间扫具体取决于reward的尺度和任务难度。3. 实操从零实现GRPO目标函数的关键步骤3.1 采样阶段的数据组织实现GRPO的第一步是把数据组织成组的结构。假设一个batch有B个prompt每个prompt采样G个回答那实际的前向传播是B*G条序列。这里有个显存和计算的权衡如果B*G太大一次前向放不下需要梯度累积或者拆分。但拆分的时候要注意同一组的G个回答必须在同一次更新里算优势不能跨batch。我通常的做法是设B8, G16这样一次处理128条序列。对于7B模型、序列长度1024的情况用bf16加梯度检查点单卡80G可以跑起来。如果资源紧张B4, G8也能work只是梯度噪声会大一些。数据组织的伪代码# 采样阶段 prompts sample_batch(B) # B个问题 all_responses [] all_log_probs_old [] all_rewards [] for prompt in prompts: responses policy_old.generate(prompt, num_return_sequencesG) log_probs compute_log_probs(policy_old, prompt, responses) rewards reward_fn(prompt, responses) # 长度G的向量 all_responses.append(responses) all_log_probs_old.append(log_probs) all_rewards.append(rewards) # 优势计算 advantages [] for rewards in all_rewards: mean_r rewards.mean() std_r rewards.std() 1e-8 adv (rewards - mean_r) / std_r advantages.append(adv)3.2 损失函数的逐项实现与数值稳定训练阶段的核心是计算GRPO损失。这里我把每个环节的数值处理都写清楚def grpo_loss(policy_theta, policy_ref, prompts, responses, log_probs_old, advantages, beta0.04, epsilon0.2): # 1. 计算当前策略的log概率 log_probs_theta compute_log_probs(policy_theta, prompts, responses) # 2. 重要性采样比逐token ratio torch.exp(log_probs_theta - log_probs_old) # 3. PPO clip项 surr1 ratio * advantages surr2 torch.clamp(ratio, 1-epsilon, 1epsilon) * advantages policy_loss -torch.min(surr1, surr2).mean() # 4. KL惩罚k3估计器 log_probs_ref compute_log_probs(policy_ref, prompts, responses) log_ratio_ref log_probs_ref - log_probs_theta kl torch.exp(log_ratio_ref) - log_ratio_ref - 1 kl_loss kl.mean() # 5. 总损失 total_loss policy_loss beta * kl_loss return total_loss几个数值稳定的关键点ratio的计算用exp(log_theta - log_old)而不是直接除概率避免下溢。log概率差通常很小新旧策略不会差太多exp之后在1附近。advantages的广播优势是序列级别的一个回答一个标量但ratio是token级别的。需要把优势广播到每个token上通常是对序列内所有token用同一个优势值。KL的归约上面用的是mean如果改成sumβ要除以序列长度。我建议统一用meanβ的含义更直观。mask处理padding token不参与损失计算需要在算mean的时候用mask加权。实操心得训练初期log_probs_old和log_probs_theta几乎一样ratio接近1clip项不起作用梯度主要来自优势的符号。随着训练推进ratio会偏离1clip开始生效。如果你发现训练中期ratio的分布集中在clip边界附近说明学习率太大或者ε太小。3.3 参考模型的更新策略GRPO需要一个参考模型π_ref来算KL。这个参考模型通常是SFT之后的模型在整个RL训练过程中冻结不动。但这里有个选择是每个step都算一遍ref的log概率还是缓存起来如果prompt和response在训练中不变on-policy采样一个batch只更新一次那ref的log概率可以在采样阶段就算好缓存训练时直接读。这样能省一次前向传播。但如果做multiple epochs同一个batch更新多次response不变ref的log概率也不变仍然可以缓存。只有policy在变。我实测下来缓存ref log概率能省大约30%的训练时间显存换时间的选择。代价是需要额外的存储空间B*G条序列的log概率序列长度1024B8, G16大概128M个floatbf16下256MB可以接受。4. 调参实战那些论文里不会写的细节4.1 β和ε的联合调节βKL系数和εclip范围是GRPO里最需要联合调节的两个参数。它们的作用有重叠都在限制策略的更新幅度。区别在于β是软约束通过损失惩罚ε是硬约束直接截断梯度。我的调节顺序是先固定ε0.2PPO的默认值扫β。从0.001开始每次乘3看训练曲线。找到那个策略在更新但KL不爆炸的点。然后微调ε。如果β已经调好但训练仍然不稳定reward震荡把ε降到0.1试试。如果训练太慢reward几乎不涨先检查优势是不是大部分为0组内全对或全错如果是说明任务难度和模型能力不匹配需要换数据或者调G。一个常见的误区是β设得太大。我见过有人直接用PPO的KL系数0.2结果策略几乎不动。GRPO的KL是逐token算的累积起来比PPO的序列级KL大很多β要相应缩小一个量级。4.2 组大小G与batch size的权衡G的选择影响优势估计的质量batch sizeprompt数量B影响梯度的方差。在总采样预算固定的情况下B和G怎么分配G大B小优势估计准但梯度方差大因为prompt少样本多样性低。G小B大梯度方差小但优势估计噪声大。我的经验是G至少8B至少4。如果预算有限优先保G因为优势估计的偏差比梯度方差更难纠正。具体来说B4, G16通常比B16, G4效果好。还有一个细节如果reward是稀疏的0/1数学题对错G8时组内全对或全错的概率不低。假设模型正确率p0.3G8时全错的概率是0.7^8≈0.06全对是0.3^8≈0.00007。全错的样本会被浪费掉。如果p0.1全错概率0.43浪费严重。这时候要么增大G要么用reward shaping让奖励更稠密。4.3 奖励归一化的陷阱优势计算里的std归一化除以组内标准差看起来无害但有个陷阱当组内奖励全部相同的时候std0除以ε_std会得到一个巨大的数或者NaN。虽然加了ε_std1e-8但如果奖励的尺度很小比如都在0.001附近std可能也是1e-3量级归一化后优势会被放大1000倍。我的处理方式是如果std小于某个阈值比如1e-3直接把优势置零跳过这个组。代码std_r rewards.std() if std_r 1e-3: adv torch.zeros_like(rewards) else: adv (rewards - rewards.mean()) / (std_r 1e-8)另外如果reward的分布有极端值比如大部分0偶尔100归一化后优势的分布会非常偏。可以考虑先对reward做clip或者用rank-based的归一化。5. 常见问题与排查速查5.1 训练不收敛的排查路径现象可能原因排查方法解决reward不涨优势全为0打印每组的reward分布换数据/增大G/改rewardreward震荡β太小或学习率太大看KL曲线的波动增大β/降lrKL爆炸β太小监控KL值增大β/加KL clip策略输出重复reward hacking看生成样本加多样性惩罚/调reward显存OOMB*G太大算显存占用减B或G/梯度累积5.2 优势计算的三个隐蔽bug第一个bug是组内顺序错乱。采样的时候G个回答是按顺序存的但经过dataloader shuffle之后可能打乱。优势计算必须保证同一组的G个回答在一起。我的做法是在数据里加一个group_id算优势的时候用groupby。第二个bug是log概率的mask不一致。算old log prob和theta log prob的时候如果mask的padding位置不一致ratio会算错。确保两次前向用的是同一个attention mask。第三个bug是reward的广播维度。优势是[B, G]的形状但log_probs是[B*G, seq_len]。reshape的时候容易搞反。建议统一用[B, G, seq_len]的形状最后再展平。5.3 与PPO的实测对比我在一个数学推理任务上做了对比实验7B模型同样的数据和学习率指标PPOGRPO显存占用2x模型1x模型ref训练速度1.0x1.4x最终准确率62.3%64.1%训练稳定性需要调critic更稳GRPO省掉了critic显存和速度都有优势。准确率略高可能是因为优势估计更直接。但GRPO对G的依赖更强如果G太小效果会明显下降。6. 一些扩展想法GRPO的目标函数还有几个可以玩的方向。一个是自适应β根据当前KL值动态调整βKL大就增大βKL小就减小。这比固定β更鲁棒但实现上要注意不要引入震荡。另一个是分层优势如果reward有多个维度正确性、长度、格式可以对每个维度分别算组内优势再加权。还有就是跨组基线用多个组的奖励均值作为基线而不是单组这样在G小的时候更稳。我在实际项目里的体会是GRPO的目标函数看起来简单但每个细节都有坑。最重要的还是把优势计算的逻辑理清楚然后耐心调β和G。这两个参数调好了训练基本就稳了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询