1. 从PPO到GRPO:为什么需要一个新的强化学习算法
如果你最近在关注大模型强化学习方向,大概率会频繁刷到GRPO这个词。它全称是Group Relative Policy Optimization,中文一般叫“组相对策略优化”。我第一次看到这个名字的时候,第一反应是:又是一个PPO的变体?但真正把论文和开源实现翻了一遍之后,我发现它解决的问题非常具体,而且解决思路相当巧妙。
先说清楚它到底解决什么问题。在GRPO出现之前,大模型做强化学习微调,主流方案是PPO。PPO本身没问题,稳定、成熟、生态好,但它有一个绕不开的成本:需要一个和策略模型规模相当的Critic网络(价值网络)来估计基线。这个Critic网络要训练、要占显存、要调参,在大模型场景下,这部分开销几乎和策略模型本身一样大。你想想,一个7B的模型做RLHF,PPO要再挂一个7B的Critic,显存直接翻倍,训练成本也跟着翻倍。
GRPO的核心洞察是:基线的估计不一定非要靠一个学出来的价值网络。既然我们做的是“组”采样——同一个问题采样多个回答——那完全可以用这一组回答的奖励统计量(均值和标准差)来充当基线。这样一来,Critic网络直接省掉了,显存和计算量大幅下降,同时还能保持甚至提升训练稳定性。
这个思路听起来简单,但背后有扎实的数学支撑。它把“优势估计”从“依赖价值函数”变成了“依赖组内相对比较”,本质上是一种无Critic的优势估计方法。这也是为什么它和“深度强化学习算法”“强化学习算法”这些热搜词高度相关——它属于策略梯度方法的一个新分支。
这篇文章我会从算法原理、组采样机制、损失函数设计、实操配置、常见坑几个角度,把GRPO讲透。适合已经了解PPO基础、想搞清楚GRPO到底怎么落地的人,也适合刚接触强化学习、想理解“为什么基线估计可以不用Critic”的读者。我会尽量用生活化的类比把数学讲清楚,同时给出可以直接参考的配置和排查思路。
2. GRPO的核心机制:用组内相对值替代价值网络
2.1 优势函数到底在估计什么
要理解GRPO,得先理解优势函数(Advantage)在策略梯度里的作用。简单说,策略梯度的更新方向是“让高回报的动作更可能被选中,让低回报的动作更不可能被选中”。但直接用回报R做权重会有问题:如果所有动作的回报都是正的,那所有动作都会被强化,只是幅度不同;如果都是负的,所有动作都会被抑制。这会导致方差很大,训练不稳定。
优势函数A(s,a) = Q(s,a) - V(s) 的作用就是减去一个基线,让“比平均好”的动作得到正的优势,“比平均差”的动作得到负的优势。这样更新方向就清晰了:好的往上推,差的往下压。
在PPO里,V(s)是由Critic网络估计的。Critic网络通过回归实际回报来学习,训练过程中它自己也在变,所以基线是“动态估计”的。这带来两个问题:一是Critic本身可能估计不准,二是Critic的训练需要额外的数据和计算。
2.2 组采样:把“基线”变成“组内均值”
GRPO的做法很直接:对于同一个问题(prompt),我不只采样一个回答,而是采样一组(比如8个、16个)回答。然后对每个回答算奖励,用这一组奖励的均值作为基线,用标准差做归一化。
用公式表达就是:对于第i个回答,它的优势估计是
A_i = (r_i - mean(r)) / std(r)
其中r_i是第i个回答的奖励,mean(r)是组内奖励均值,std(r)是组内奖励标准差。
这个形式你可能会觉得眼熟——它和“标准化”“归一化”很像。没错,GRPO本质上就是把组内奖励做了一次标准化,然后用标准化后的值作为优势。这样做的好处是:
- 不需要Critic网络:基线直接来自组内统计,省掉了一个大模型。
- 方差更小:组内比较天然地控制了方差,因为同一组回答面对的是同一个问题,难度一致。
- 实现简单:不需要维护价值网络的状态,不需要处理Critic和策略的同步问题。
我打个比方。PPO像是老师给每个学生打分,然后还要另外请一个“基准评估员”来估计“这个难度的题目平均能考多少分”,再用学生分数减去这个估计值。GRPO则是:同一道题让一组学生做,直接看每个学生在这组里的相对排名和分差,不需要额外的评估员。哪个更省事,一目了然。
2.3 组大小怎么选:不是越大越好
组大小(group size)是GRPO最关键的参数之一。常见的选择是8、16、32、64。组越大,均值和标准差的估计越准,但采样成本也越高。
我实测下来的经验是:
| 组大小 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 4-8 | 快速实验、显存紧张 | 采样快、迭代快 | 统计量噪声大,训练可能不稳 |
| 16-32 | 常规训练 | 统计稳定,效果均衡 | 采样成本中等 |
| 64+ | 追求极致效果 | 基线估计最准 | 采样成本高,收益递减 |
注意:组大小不是越大越好。当组大小超过一定阈值后,均值和标准差的估计已经足够准,再增大只会增加采样成本,对效果提升有限。我一般从16开始试,如果训练曲线抖动明显再往上加。
还有一个细节:组内的回答必须是针对同一个prompt采样的。如果你把不同prompt的回答混在一组里算均值和标准差,那就失去了“同题比较”的意义,基线会变得没有意义。这一点在实现时特别容易搞错,尤其是做batch训练的时候,要确保每个prompt对应的那组回答是独立计算的。
2.4 和PPO的对比:省了什么,保留了什么
GRPO保留了PPO的核心结构:重要性采样比率、裁剪(clip)、多轮更新。它省掉的只是Critic网络和GAE(广义优势估计)那套东西。
具体来说,PPO的损失函数里有几块:策略损失(带clip)、价值损失、熵奖励。GRPO去掉了价值损失,把优势估计换成了组内标准化。策略损失和熵奖励基本保留。
这意味着什么?意味着GRPO的训练流程比PPO简单很多:不需要训练Critic,不需要计算GAE,不需要处理Critic和策略的更新频率问题。但代价是,你必须做组采样,采样成本比PPO高。所以GRPO适合的场景是:采样相对便宜,但显存和Critic训练成本是瓶颈的情况。在大模型RLHF里,这个条件通常成立,因为生成比训练便宜,而显存是硬约束。
3. 损失函数拆解:GRPO到底在优化什么
3.1 策略损失:裁剪的重要性采样
GRPO的策略损失和PPO几乎一样,核心是重要性采样比率和裁剪:
ratio = exp(log_prob_new - log_prob_old) loss_policy = -min(ratio * A, clip(ratio, 1-eps, 1+eps) * A)
这里的A就是前面算出来的组内标准化优势。eps是裁剪范围,通常取0.1或0.2。
为什么要裁剪?因为重要性采样比率可能变得很大或很小,导致更新步长失控。裁剪的作用是限制每次更新的幅度,让训练更稳定。这个机制在PPO里已经被验证过无数次,GRPO直接沿用。
但有一个细节需要注意:GRPO的优势A是组内标准化的,它的尺度比PPO的GAE优势要小(因为除以了标准差)。所以在设置学习率的时候,可能需要比PPO稍大一点,或者对优势做额外的缩放。我一般会先跑一个小规模实验,观察loss的量级和梯度范数,再决定学习率。
3.2 KL惩罚:别让策略跑太远
GRPO通常会加一个KL惩罚项,约束新策略不要偏离参考策略(通常是SFT模型)太远。这个KL不是加在奖励里,而是直接加在损失里:
loss_kl = beta * KL(policy || ref_policy)
beta是KL系数,控制约束强度。beta越大,策略越保守;beta越小,策略越激进。
这里有个实操经验:KL系数不要设太大。我见过有人把beta设到0.1甚至更高,结果策略几乎不更新,训练半天没效果。一般从0.01到0.04之间试,先看KL散度的实际值,如果KL一直很小(比如小于0.01),说明约束太强,可以调小beta;如果KL爆炸式增长,说明约束太弱,要调大beta。
提示:KL惩罚和裁剪是两道独立的保险。裁剪限制单步更新幅度,KL限制整体偏离程度。两者配合使用,训练稳定性会好很多。
3.3 熵奖励:探索与利用的平衡
熵奖励的作用是鼓励策略保持一定的随机性,避免过早收敛到局部最优。GRPO里通常也会加一个熵奖励项:
loss_entropy = -entropy_coef * entropy(policy)
entropy_coef一般设得很小,比如0.001到0.01。太大了会导致策略过于随机,学不到东西;太小了又起不到探索作用。
我的经验是:在训练初期可以稍微大一点,让策略多探索;训练后期可以调小,让策略收敛。有些实现会动态调整熵系数,但手动调也不麻烦。
3.4 总损失:三项加权求和
把上面三项加起来就是GRPO的总损失:
loss = loss_policy + beta * loss_kl - entropy_coef * entropy
注意符号:KL是惩罚,所以是加;熵是奖励(鼓励熵大),所以是减。
这个结构和PPO的总损失很像,只是少了价值损失。实现的时候,建议把每一项的值都打日志,方便排查。我遇到过loss_policy正常但KL爆炸的情况,也遇到过熵突然降到接近0导致策略完全确定的情况。分开监控每一项,问题定位会快很多。
4. 实操配置:从零跑通一个GRPO训练
4.1 环境准备与依赖
GRPO的实现目前主流是基于PyTorch和HuggingFace的transformers。如果你用的是TRL库,它已经内置了GRPOTrainer,可以直接调用。如果想自己实现,核心依赖包括:
- torch >= 2.0
- transformers >= 4.40
- accelerate(用于分布式训练)
- deepspeed(可选,用于显存优化)
我建议先用TRL的GRPOTrainer跑通一个最小示例,理解流程后再考虑自己改。自己实现的好处是灵活,可以定制奖励函数、组采样策略、损失权重等。
4.2 数据格式与采样流程
GRPO的训练数据通常是prompt集合,每个prompt不需要标注答案,只需要有奖励函数来打分。数据格式大致是:
dataset = [ {"prompt": "问题1"}, {"prompt": "问题2"}, ... ]训练时,对每个prompt采样group_size个回答,然后用奖励函数给每个回答打分。奖励函数可以是规则式的(比如答案是否正确、格式是否合规),也可以是模型式的(比如用奖励模型打分)。
采样流程的关键点:
- 每个prompt独立采样一组,组内回答不混。
- 采样时用当前策略,记录log_prob。
- 奖励函数对每个回答打分,得到r_i。
- 组内标准化得到优势A_i。
- 用A_i计算策略损失,反向传播。
注意:采样时的log_prob要保存好,后面计算重要性采样比率时要用。如果采样和训练用了不同的精度(比如采样用fp16,训练用fp32),要注意数值一致性。
4.3 关键超参数配置参考
下面是我常用的一套配置,供参考:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| group_size | 16 | 组大小,显存够可以加到32 |
| learning_rate | 1e-6 ~ 5e-6 | 比PPO稍大,因为优势尺度小 |
| kl_coef (beta) | 0.01 ~ 0.04 | KL惩罚系数 |
| clip_eps | 0.1 ~ 0.2 | 裁剪范围 |
| entropy_coef | 0.001 ~ 0.01 | 熵奖励系数 |
| max_new_tokens | 256 ~ 512 | 生成最大长度 |
| temperature | 0.7 ~ 1.0 | 采样温度 |
| batch_size | 根据显存调整 | 每个设备上的prompt数 |
这套配置不是金科玉律,但作为一个起点是靠谱的。我建议先跑100步左右,观察loss、KL、熵、奖励均值的变化趋势,再决定怎么调。
4.4 训练监控:看什么指标
GRPO训练要重点监控这几个指标:
- 奖励均值:应该整体上升,但会有波动。如果一直不涨,可能是奖励函数设计有问题,或者学习率太小。
- KL散度:应该保持在一个合理范围(比如0.01到0.5之间)。太小说明策略没怎么更新,太大说明跑偏了。
- 熵:应该缓慢下降,但不应该降到0。如果熵突然暴跌,说明策略过早确定,需要调大熵系数或调小学习率。
- 梯度范数:如果梯度范数爆炸,说明更新太猛,要调小学习率或调大裁剪范围。
- 组内奖励标准差:如果标准差接近0,说明组内回答几乎一样,优势估计没有区分度,可能是温度太低或组太小。
我一般会把这些指标画成曲线,每50步看一次。如果发现异常,先暂停训练,定位原因再继续。
5. 踩坑实录:GRPO训练中容易翻车的几个地方
5.1 组内回答全一样,优势全是零
这是新手最容易踩的坑。如果采样温度太低,或者模型对某个prompt太确定,组内所有回答可能完全一样,奖励也一样。这时候组内标准差为0,标准化的时候会除以0,优势变成NaN,训练直接崩。
解决办法有两个:一是提高采样温度,增加多样性;二是加一个极小值eps到标准差里,防止除零:
A_i = (r_i - mean(r)) / (std(r) + eps)
eps一般取1e-8。但要注意,如果标准差本来就很小,加eps后优势会被放大,可能导致更新过猛。所以更好的做法是同时监控组内标准差,如果长期接近0,说明采样策略需要调整。
5.2 KL系数设太大,策略几乎不更新
我见过不少人把KL系数设成0.1甚至0.5,结果训练了几千步,奖励几乎没变化。原因很简单:KL惩罚太强,策略不敢偏离参考模型,更新幅度被压得很小。
排查方法:打印KL散度的实际值。如果KL一直小于0.001,说明约束太强,策略根本没动。这时候应该调小beta,比如从0.1降到0.01,再观察。
反过来,如果KL散度爆炸式增长(比如超过10),说明约束太弱,策略跑太远,可能会输出乱码或重复。这时候要调大beta,或者检查奖励函数是不是有问题。
5.3 奖励函数设计不合理,模型学会“刷分”
GRPO依赖奖励函数来提供信号。如果奖励函数设计得不好,模型会学会钻空子。比如奖励函数只检查答案长度,模型就会输出超长回答;奖励函数只检查关键词,模型就会堆砌关键词。
我踩过的一个坑是:奖励函数对“格式正确”给高分,结果模型学会了输出格式正确但内容空洞的回答。后来改成“格式正确且答案正确”才给高分,问题才解决。
提示:奖励函数最好有多维度,并且各维度之间有制衡。单一维度的奖励很容易被“刷”。
5.4 显存不够,组采样跑不起来
GRPO的显存开销主要来自组采样:group_size个回答同时生成,KV cache会占用大量显存。如果group_size=32,max_new_tokens=512,显存需求会很大。
解决办法:
- 减小group_size(比如从32降到8)
- 减小max_new_tokens
- 用梯度检查点(gradient checkpointing)
- 用DeepSpeed ZeRO阶段2或3
- 分批采样,不要一次性生成所有回答
我一般会先估算显存:每个token的KV cache大小乘以group_size乘以max_new_tokens,再加上模型本身的显存。如果超了,就逐项往下调。
5.5 训练不稳定,loss忽高忽低
GRPO训练不稳定通常有几个原因:学习率太大、组太小、KL系数不合适、奖励函数噪声大。
排查顺序:
- 先看梯度范数,如果经常爆炸,调小学习率。
- 再看组内标准差,如果很小,增大group_size或提高温度。
- 再看KL散度,如果波动很大,调整beta。
- 最后看奖励函数,如果奖励本身噪声很大(比如用模型打分且模型不稳定),考虑平滑奖励或换更稳定的奖励函数。
我自己的经验是,GRPO比PPO对超参数更敏感一些,尤其是学习率和组大小。所以调参的时候要有耐心,一次只改一个参数,观察变化。
6. GRPO适合什么场景,不适合什么场景
6.1 适合的场景:显存受限、采样便宜
GRPO最大的优势是省掉了Critic网络,所以特别适合以下场景:
- 大模型RLHF:7B以上的模型,显存是瓶颈,GRPO能省掉一个和策略同规模的Critic。
- 采样成本低:如果生成比训练便宜(通常如此),组采样的额外成本可以接受。
- 奖励函数明确:有可靠的奖励信号,不需要Critic来估计基线。
在这些场景下,GRPO通常比PPO更划算:训练更快、显存更省、效果相当甚至更好。
6.2 不适合的场景:采样昂贵、奖励稀疏
GRPO也不是万能的。以下场景要慎重:
- 采样非常昂贵:比如需要调用外部API生成,或者生成一次要几秒钟。组采样会让成本成倍增加。
- 奖励极其稀疏:如果大部分回答奖励都是0,组内标准差可能很小,优势估计没有区分度。
- 需要精细的价值估计:有些任务需要准确的价值函数来做信用分配,GRPO的组内标准化可能不够精细。
我个人的判断标准是:如果组采样的成本低于训练一个Critic的成本,GRPO就值得试;否则还是PPO更合适。
6.3 和其他强化学习算法的关系
GRPO不是孤立的。它和以下算法有联系:
- PPO:GRPO是PPO的无Critic变体,保留了裁剪和重要性采样。
- RLOO:RLOO(REINFORCE Leave-One-Out)也用组采样,但基线是“留一法”均值,GRPO用的是均值和标准差。
- ReMax:ReMax用贪心解码的奖励作为基线,GRPO用组内均值。
- DPO:DPO是离线方法,不需要采样;GRPO是在线方法,需要采样。两者解决的问题不同。
理解这些关系有助于你根据具体场景选择算法。我的经验是:如果显存够、采样便宜,PPO和GRPO都可以;如果显存紧张,优先GRPO;如果完全不能在线采样,考虑DPO。
7. 一些实操心得和后续扩展方向
7.1 奖励函数是重中之重
跑了这么多实验,我最大的体会是:GRPO的效果上限由奖励函数决定。算法本身再优雅,奖励函数设计得不好,模型也学不到东西。所以与其花时间调算法超参数,不如先把奖励函数打磨好。
一个好的奖励函数应该:多维度、有制衡、噪声小、和最终目标对齐。如果可能,先用小规模实验验证奖励函数的区分度,再上大规模训练。
7.2 组大小和温度的联动
组大小和采样温度是联动的。温度高,组内多样性好,但可能生成质量下降;温度低,生成质量高,但组内可能全一样。我一般会先固定温度(比如0.8),调组大小;如果组内标准差还是太小,再适当提高温度。
7.3 后续可以尝试的方向
GRPO本身还有很多可以改进的地方。比如:
- 动态组大小:根据组内标准差动态调整组大小,标准差小的时候多采样几个。
- 自适应KL系数:根据KL散度的实际值动态调整beta,保持KL在目标范围内。
- 混合基线:组内均值加一个全局基线,兼顾组内比较和全局信息。
- 多轮GRPO:把GRPO扩展到多轮对话场景,每轮都做组采样和优势估计。
这些方向我还在实验中,有些有效果,有些还在验证。如果你也在做GRPO相关的工作,欢迎交流。
最后分享一个小技巧:训练初期先用小模型(比如1B以下)跑通流程,确认奖励函数和超参数没问题,再上大模型。这样能省很多时间和算力。我一开始直接上7B,结果调了两周才发现是奖励函数的问题,换成小模型后一天就定位到了。这个教训挺深刻的。