1. 从PPO到GRPO:为什么需要重新理解奖励与优势
如果你最近在折腾大模型的对齐训练,大概率绕不开GRPO这个词。我第一次接触它是在复现一个数学推理的强化学习流程时,当时脑子里全是PPO那一套:策略网络、价值网络、广义优势估计、裁剪目标函数。结果翻到GRPO的论文,第一反应是——价值网络去哪了?
这就是GRPO最核心的改动:它把价值模型(Critic)整个拿掉了。在传统的PPO流程里,我们需要一个和策略模型参数量相当的Critic来估计状态价值V(s),然后用它计算优势。但在大模型场景下,训练一个和策略同规模的价值网络,显存和算力开销几乎翻倍,而且价值估计本身在长序列、稀疏奖励的任务里极不稳定。GRPO的做法很直接:既然一个prompt会采样出一组回答,那我就用这组回答的奖励统计量来当基线,替代Critic的工作。
这个思路听起来简单,但它直接改变了奖励值和优势值的计算方式,也改变了整个训练流程的工程实现。很多人第一次看GRPO的公式会觉得"这不就是把PPO的GAE换成了组内归一化吗",但真正上手跑起来才会发现,奖励的设计、组大小的选择、优势归一化的细节,每一步都有坑。这篇内容我就围绕奖励值和优势值这两条主线,把GRPO从原理到实操拆开讲清楚,适合已经了解PPO基础、想搞明白GRPO到底怎么落地的人。
先说结论:GRPO里奖励值决定了"什么方向是对的",优势值决定了"每个token该往哪个方向更新多少"。前者是信号源,后者是放大器。搞混这两者的职责,训练大概率会崩。
2. GRPO的奖励值设计:从规则函数到奖励模型的取舍
2.1 奖励值在GRPO里到底扮演什么角色
在GRPO的框架里,奖励值不是逐token给出的,而是对整个生成序列给出一个标量分数。这一点和很多人的直觉不同。PPO在部分实现里会做token-level的奖励塑形,但GRPO的标准流程是:一个prompt采样G个回答,每个回答经过奖励函数得到一个分数,然后这G个分数在组内做归一化,得到每个回答的相对优势。
这意味着奖励函数的设计直接决定了训练信号的质量。如果奖励函数给出的分数区分度不够,组内归一化之后所有优势都接近零,梯度就消失了。我踩过的第一个坑就是:用了一个输出范围在0.9到1.0之间的奖励模型,结果组内归一化后优势值全在正负0.05之间晃,训练了三天loss几乎不动。
所以奖励值的设计要满足两个条件:一是区分度足够大,二是和任务目标真正对齐。区分度是工程问题,对齐是设计问题。
2.2 规则奖励与模型奖励的混合策略
GRPO在数学推理、代码生成这类任务上最常用的奖励是规则奖励。比如数学题,答案对给1分,错给0分,格式正确额外加0.1分。这种奖励的好处是绝对客观、零成本、不会reward hacking。但坏处是稀疏——如果模型一开始就全错,那所有回答都是0分,组内归一化后优势全是0,没有任何学习信号。
我的做法是混合奖励:规则奖励负责"对不对",模型奖励负责"好不好"。具体来说,最终奖励 = 0.8 × 规则奖励 + 0.2 × 格式奖励 + 0.1 × 长度惩罚。格式奖励可以用简单的规则判断,比如是否包含推理步骤标记、是否以特定格式结尾。长度惩罚是为了防止模型通过堆砌废话来碰运气。
这里有个细节值得展开:奖励的尺度要统一。如果你用了一个输出范围0到10的奖励模型,又用了一个0到1的规则奖励,直接相加会导致规则奖励被淹没。我的经验是先把所有奖励分量归一化到相近的量级,再按权重相加。下面这个表格是我在数学推理任务上实际用过的奖励配置:
| 奖励分量 | 计算方式 | 权重 | 取值范围 |
|---|---|---|---|
| 答案正确性 | 精确匹配最终答案 | 1.0 | 0 或 1 |
| 格式合规性 | 是否包含指定推理标记 | 0.2 | 0 或 1 |
| 长度惩罚 | 超出目标长度按比例扣分 | -0.1 | -1 到 0 |
| 重复惩罚 | 检测n-gram重复率 | -0.15 | -1 到 0 |
这套配置在我实测中比较稳,但要注意长度惩罚的系数不能太大,否则模型会学会输出极短的错误答案来规避惩罚。
2.3 奖励模型的训练与校准
如果你要用模型奖励,那奖励模型本身的训练质量就是天花板。我见过太多人直接拿一个开源的reward model就用,结果训练出来的策略模型学会了输出"看起来很好但实际很烂"的内容。奖励模型的校准比训练更重要。
校准的核心是消除位置偏差和长度偏差。位置偏差指的是奖励模型倾向于给某个位置的回答更高分,长度偏差指的是倾向于给更长或更短的回答更高分。我的做法是构造一批配对样本,同一问题的两个回答只有长度不同,然后检查奖励模型是否给出系统性偏差。如果存在,就用这批数据对奖励模型做几轮微调,把偏差压下去。
另一个实操技巧是奖励模型的输出要做温度缩放。很多奖励模型的输出logits范围很窄,直接当奖励用区分度不够。我会在验证集上拟合一个温度参数T,让奖励分布的标准差达到一个合理值(比如0.5左右),这样组内归一化后的优势才有足够的动态范围。
3. 优势值计算:组内归一化的数学细节与工程陷阱
3.1 从GAE到组内归一化的本质变化
PPO里的优势估计用的是GAE,它依赖价值网络对每个状态的价值估计,然后做时序差分。GRPO完全绕开了这条路,它的优势计算可以概括为:同一个prompt下的G个回答,每个回答的奖励减去组内均值,再除以组内标准差。
用公式表示就是:对于第i个回答,优势A_i = (r_i - mean(r)) / std(r)。这个值直接作为整个序列的优势,然后广播到每个token上,用于计算策略梯度。
这个变化带来的最大好处是不需要训练价值网络,省掉了大量显存和计算。但代价是,优势的估计完全依赖于组内样本的统计量。如果组大小G太小,均值和标准差的估计就不稳定,优势值会抖动得很厉害。我试过G=4的情况,训练曲线像心电图一样,根本没法收敛。后来调到G=8才勉强稳定,G=16就比较顺滑了。
3.2 组大小G的选择与显存权衡
G的选择是一个典型的工程权衡。G越大,优势估计越准,但每个prompt需要采样的回答数就越多,显存和计算开销线性增长。在单卡80G的配置下,如果模型是7B级别,序列长度2048,G=8大概能跑起来,G=16就需要梯度累积或者模型并行。
我的经验值是:G至少为8,推荐16。如果任务特别难,模型初始正确率极低,那G还要更大,因为你需要足够多的样本才能碰到一两个正奖励的回答,否则组内全是0分,优势全零。这种情况下可以考虑动态组大小:先采样一批,如果正奖励样本数少于2个,就继续采样直到满足条件或达到上限。
还有一个容易被忽略的点:组内归一化时标准差的计算方式。如果直接用有偏估计(除以G),当G较小时会低估标准差,导致优势值偏大。我一般用无偏估计(除以G-1),虽然差别不大,但在G=8这种量级下还是有影响的。另外,如果组内所有奖励相同,标准差为0,这时候要加一个极小值eps防止除零,同时把优势置零,因为这种情况下确实没有学习信号。
3.3 优势值的裁剪与白化
组内归一化之后,优势值的范围理论上在负无穷到正无穷之间,但实际中大部分值在-3到3之间。偶尔会出现极端值,比如某个回答奖励特别高,归一化后优势达到5以上。这种极端优势会导致策略更新步长过大,训练不稳定。
我的做法是对优势值做裁剪,把范围限制在[-3, 3]或者[-5, 5]。这个裁剪阈值不是拍脑袋定的,而是根据奖励分布来。如果奖励分布近似正态,那99.7%的值在3个标准差内,裁剪到3基本不影响正常样本,只挡住极端值。
另一个技巧是优势白化,也就是在batch维度上再做一次归一化,让整个batch的优势均值为0、方差为1。这能进一步稳定训练,尤其是在不同prompt的奖励尺度差异很大时。但要注意,白化是在组内归一化之后做的,顺序不能反。
4. 奖励与优势的联动:训练动态中的常见故障
4.1 奖励饱和与优势消失
训练进行到一定阶段后,模型对大部分prompt都能给出正确答案,这时候组内奖励全是1,标准差为0,优势全零,梯度消失。这不是bug,而是任务太简单了。解决办法是动态调整任务难度,比如从题库里采样时优先选模型当前正确率在30%到70%之间的题目。
我实现过一个简单的难度调度器:维护每个题目的历史正确率,每次采样时按正确率倒数的权重来选。这样模型始终在"跳一跳够得着"的区域训练,优势值始终有区分度。
4.2 奖励黑客与优势异常放大
奖励黑客是强化学习的老问题。在GRPO里,如果奖励函数有漏洞,模型会找到漏洞并放大。比如格式奖励如果只看是否包含某个标记,模型就会在所有回答里都塞这个标记,哪怕内容毫无意义。这时候优势值会异常放大,因为模型发现了一个"稳赚不赔"的策略。
我的排查方法是定期人工检查高优势样本。具体来说,每个训练周期结束后,把优势值最高的前20个回答打印出来看。如果发现明显的模式化作弊,就说明奖励函数需要修补。这个习惯帮我抓到了好几次奖励漏洞,比如模型学会了在答案里重复题目中的数字来碰运气。
4.3 组内奖励分布的可视化监控
训练过程中我必看的一个指标是组内奖励分布。具体来说,每个batch里,每个prompt的G个回答的奖励标准差,以及正奖励的比例。如果发现大量组的正奖励比例为0,说明任务太难;如果大量组的正奖励比例为1,说明任务太简单;如果标准差普遍低于0.1,说明奖励区分度不够。
这些指标比loss曲线更能反映训练健康度。我一般用TensorBoard或者WandB记录这些分布,每100步看一眼。下面是一个典型的健康训练曲线特征:
| 指标 | 健康范围 | 异常含义 |
|---|---|---|
| 组内正奖励比例 | 0.2 - 0.8 | 过低任务太难,过高任务太简单 |
| 组内奖励标准差 | 0.3 - 0.7 | 过低区分度不足,过高可能有不稳定样本 |
| 优势值绝对值均值 | 0.5 - 1.5 | 过低梯度消失,过高更新过猛 |
| 策略熵 | 缓慢下降 | 下降过快说明过早收敛 |
5. 实操中的参数配置与调试经验
5.1 学习率与KL系数的联合调参
GRPO通常会在策略梯度里加一个KL惩罚项,防止策略偏离参考模型太远。KL系数和学习率是一对需要联合调整的参数。我的经验是:学习率越小,KL系数可以越小。如果学习率是1e-6,KL系数0.01到0.04就够了;如果学习率是5e-6,KL系数要加到0.1左右。
调参的顺序是:先固定KL系数为0.04,调学习率,观察策略熵和奖励曲线。如果奖励上升但熵下降太快,说明学习率太大;如果奖励不动但熵稳定,说明学习率太小。找到合适的学习率后,再微调KL系数,让策略熵缓慢下降而不是骤降。
5.2 梯度累积与组大小的配合
当显存不够时,梯度累积是常用手段。但GRPO里有个坑:组内归一化必须在同一个batch内完成。如果你把一个prompt的G个回答拆到不同的梯度累积步里,那归一化就错了。所以梯度累积的维度只能是prompt维度,不能是组内维度。
我的做法是:一个梯度累积步处理一个prompt的全部G个回答,然后累积多个prompt的梯度再更新。这样组内归一化始终在完整组内进行,不会出错。
5.3 参考模型的更新策略
GRPO里的参考模型通常是SFT模型,用来计算KL惩罚。参考模型要不要更新?我的经验是不要频繁更新。如果参考模型更新太快,KL惩罚就失去了约束意义,策略会漂移。我一般每500到1000步才把参考模型同步一次,或者干脆不更新,只用初始SFT模型。
但有一种情况需要更新:如果SFT模型本身能力有限,策略很快就能超过它,那KL惩罚会一直很大,阻碍学习。这时候可以适当更新参考模型,但更新频率要低,且每次更新后要重新观察KL散度的变化。
6. 一个完整的奖励与优势计算流程复盘
6.1 从采样到优势的完整链路
我把整个流程串一遍,方便你对照自己的实现。假设一个batch有B个prompt,每个prompt采样G个回答:
- 对每个prompt,用当前策略模型采样G个回答,记录每个token的log概率。
- 对每个回答,用奖励函数计算标量奖励r。
- 对每个prompt的G个奖励,计算均值和标准差,做组内归一化得到优势A。
- 对优势做裁剪和白化。
- 用优势乘以每个token的log概率,计算策略梯度。
- 加上KL惩罚项,反向传播更新。
这个链路里最容易出错的是第3步和第4步。组内归一化时要注意标准差的计算方式,白化时要注意是在整个batch上做还是每个prompt单独做。我的做法是组内归一化后,再在整个batch上做一次白化,这样既保留了组内相对关系,又统一了不同prompt的尺度。
6.2 常见报错与排查清单
训练GRPO时我遇到过几类典型报错,列出来供你参考:
- 优势全零:检查奖励函数是否有区分度,检查组内是否所有奖励相同。
- loss突然爆炸:检查优势值是否有极端值,检查学习率是否过大,检查KL系数是否过小。
- 奖励上升但人工评估下降:典型的奖励黑客,检查奖励函数漏洞,检查高优势样本。
- 显存溢出:减小G或序列长度,开启梯度检查点,使用梯度累积。
- 训练速度极慢:检查是否在每个step都重新计算参考模型的log概率,这个可以缓存。
6.3 从零实现时的最小验证方案
如果你要自己实现GRPO,我建议先做一个最小验证:用一个极简的任务,比如让模型输出指定格式的字符串,奖励就是格式匹配度。G设为4,模型用一个小模型(比如0.5B),跑100步看奖励是否上升。这个最小方案能帮你快速验证奖励和优势的计算链路是否正确,避免在大模型上浪费时间调试。
验证通过后,再逐步增加任务难度和模型规模。我自己的经验是,最小验证能抓到80%的实现bug,剩下的20%通常和数值稳定性有关,需要在大模型上才能暴露。
7. 一些个人体会
GRPO最吸引我的地方是它把强化学习里最玄学的部分——价值估计——用组内统计量替代了。这让整个训练流程更透明,也更容易调试。但代价是,奖励函数的设计变得极其关键,因为优势值完全由奖励驱动,奖励设计不好,优势就是噪声。
我在实际项目里最大的体会是:花在奖励函数设计上的时间,应该和花在模型训练上的时间一样多。一个精心设计的奖励函数,能让GRPO在很少的步数内收敛;一个粗糙的奖励函数,跑再久也是白费。另外,组大小G不要抠门,显存不够就减模型规模或者序列长度,G太小带来的训练不稳定,后期调参的时间成本远高于省下的那点显存。
最后分享一个我常用的调试技巧:把每个batch里优势值最大的前5个回答和最小的后5个回答打印出来,人工读一遍。这比看任何曲线都直观,能快速发现奖励函数的问题和模型的异常行为。这个习惯我保持了很长时间,帮我省下了大量盲目调参的时间。