☰
GRPO样本增强实战:组内比较让强化学习真正生效
2026/10/22 4:19:51 网站建设 项目流程

有一次我把GRPO的训练脚本迁移到一个新任务上,跑了一个epoch,loss曲线非常漂亮地往下走,可下游测试集纹丝不动。后来我把训练日志里的组内reward标准差打出来看了一眼,差点没背过气:标准差基本为0。也就是说,同一个prompt采样出来的G条输出,答案几乎一样,组内标准化之后优势全部被压成了零,策略根本学不到东西。

这件事让我重新理解了GRPO这个算法。它最大的特点不是去掉critic省显存,而是把“样本”的地位抬到了前所未有的高度——在GRPO里,优势估计完全来自同一组样本之间的相对比较。样本长什么样、组内有没有区分度、reward信号干不干净,直接决定训练有没有效果。这篇文章就围绕“样本增强”展开,聊一聊GRPO从PPO演进过程中,样本环节发生了什么变化,以及我自己在工程项目里验证过的增强手段和踩过的坑。适合已经开始用GRPO、并且发现训练不太稳定的读者,也适合刚接触RLHF、想搞清楚GRPO和PPO差异的人。

1. GRPO的演进逻辑:从“值函数估计”到“组内样本比较”

1.1 PPO的critic为何在大模型RLHF里显得笨重

PPO是Actor-Critic框架。actor负责生成策略,critic负责估计每个状态的价值。训练时,critic算TD误差,通过GAE做多步优势估计。对大模型而言,critic是一个和生成模型差不多大的transformer,前向传播一次的成本几乎等同actor,这让训练内存占用和计算开销直接翻倍。

这还不是最致命的。大模型RLHF里reward往往极度稀疏,一个数学题要么全对要么全错,critic在这种信号上很难学到稳的value估计。我见过不少PPO跑LLM的项目,loss在降,但实际生成的样本质量并没有提升,问题多半出在critic给出的advantage噪声太大,policy gradient被噪声带着跑。为了压噪声,很多人把GAE里的lambda调得很高,但这样又引入了偏差。总而言之,critic在LLM场景里是一个费力不讨好的组件。

1.2 GRPO是怎么用“组内对比”替代critic的

GRPO换了一套思路:不再试图学习一个值函数,而是对同一个prompt做G次采样,得到G条输出,分别打分,然后在组内做标准化。

优势估计就变成了:

A_i = (r_i - mean_r) / (std_r + epsilon)

这个公式看着简单,背后的意义不小。第一,它完全不依赖额外网络,省掉了critic的显存和计算量。第二,组内标准化把reward的绝对尺度抹掉了,即使全局reward均值在训练中漂移,组间漂移不会影响梯度方向。第三,它天然支持稀疏reward,因为比较是在同一问题下进行的,模型只要学会“这组的哪些样本比其他样本好”就够了。

1.3 关键转变:训练样本从“流动轨迹”变成了“结构化组”

PPO里训练的基本单位是transition (state, action, reward),一条条独立样本。GRPO里基本单位是“一个prompt和它的G条输出”这一整个组。这个转变会影响很多东西。

首先,组内样本的质量分布比单条样本的绝对质量更重要。G条样本全差没关系,只要有一条相对好,模型也能学到“往好的方向走”。G条样本全是同一个答案,那就是灾难。其次,组内的标准化让reward的绝对值不那么重要,但组内排名的稳定性变重要了——reward函数如果对同一质量样本给出忽高忽低的分数,排名就会乱,training signal就成了噪声。所以GRPO落地之后,大家最需要操心的不是模型层,而是“样本层”:怎么采样、怎么清洗、怎么构造对比组。这其实就是样本增强这一概念的由来。

2. 样本增强的第一层:让“组”里有差异

2.1 组内差异化生成:温度、top-p与显式扰动

组内标准化的前提是组内必须有方差。如果采样温度设成0,G条输出完全相同,std=0,优势全为0。但即使温度不是0,很多模型在确定性高的题目上也会给出几乎一样的答案,这需要刻意干预。

我常用的做法是给每个组内的G次rollout分配不同的采样温度,比如温度列表[0.7, 1.0, 1.2]轮流用。温度高的样本负责探索长推导、非常规解法,温度低的样本负责稳定地给出常规正确路径。这样组内既有保守答案,也有激进答案,归一化之后的优势排序才有信息量。

另一个有效的手段是“分解扰动”:让模型用不同策略求解。例如在prompt末尾追加“请你用方程的方法求解”,另一条是“请用算术方法求解”,迫使组内输出在方法空间里分化。这对数学推理任务特别好用,因为两个方法都对时,模型能学到“有更巧妙的解法”。

2.2 Prompt侧增强:变体、难度分层和课程式覆盖

样本增强不能只在解码参数上做文章,prompt本身的多样性也要动。同一个原始问题,做数值替换、角色反转、约束条件变化,生成一批语义等价但表面不同的变体,这样的话组内输出即使语义接近,具体形式上也有差异。比如“3个苹果分给4个人”可以改成“5个橙子分给6个人”,但要控制计算复杂度不变,避免变体本身变成难度陷阱。

我建议把prompt池按难度分层:简单题负责让模型稳定拿到基础奖励,中等题负责提升推导质量,困难题负责激发长链思考。训练进度前20%多采样简单题,后面逐步切到困难题。这种课程式覆盖能够降低GRPO初期的不稳定性。关键是:不要一个epoch内全部混着来,那样组内reward方差被难度差异主导,模型分不清进步来自能力提升还是题目变简单。

2.3 组间信息一致性:避免batch里的prompt互相污染

有个容易忽略的细节:组内标准化虽然只在同prompt内进行,但loss里梯度会被组间平均。如果同一个batch里prompt难度差异特别大,简单组的advantage普遍接近上限,困难组的advantage普遍接近下限,平均之后困难组的信号被稀释。

我的做法是给batch内prompt做难度分桶,保证每个batch内难度尽量一致。如果实在混着来,至少要在记录训练指标时分开统计简单/困难子集的收敛情况,否则很容易被平均loss欺骗。

下面这个表是我在项目里常用的组内差异来源清单,每条都对应一种增强方式:

增强维度实现方式对组内advantage的影响主要风险
解码温度组内轮换动态温度增大组内方差温度过高产生垃圾文本
解码top-p0.9~0.98随机扰动增加表达多样性语法连贯性下降
Prompt变体数值/语义/结构扰动防止组内同质变体难度失衡
采样长度对不同组设不同max_tokens拉高长链样本占比GPU显存波动
策略条件注入“用XX方法求解”等指令方法空间分化可能引入指令偏差

3. 样本增强的第二层:reward与mask的质量筛选

3.1 Reward信号的清洗:稀疏规则reward的解法

数学题最常用的奖励是“答案是否正确”,这是个0/1信号。0/1信号放进GRPO里有个明显问题:一个组里如果全部答对或全部答错,归一化之后优势全是0,等于这个组的样本直接被浪费。而且二进制奖励的排名是离散的,同分样本很多,策略梯度缺乏精细方向。

所以我对规则reward做了软化处理:对结果正确但过程有瑕疵的样本给0.8,正确且简洁给1.0,过程部分正确给0.3,完全错误给0。更激进一点的做法是用编辑距离或者和标准答案之间的过程相似度构造连续reward。注意这里不是要取代规则reward,而是在规则基础上做梯度化的平滑,让组内样本的排序不再那么粗糙。

3.2 长度归一化与mask:样本增强里的平权问题

GRPO的loss中通常会对每条输出除以它的长度|o_i|,这样长输出不会因为token多而主导梯度。这一点在样本分布不均衡时非常重要:如果一个组里有一条2000字的rollout,其余几条100字,那2000字那条即使在每个token上优势相同,累计梯度也会是其他样本的20倍。

更精细的做法是引入sample mask。比如把生成结果里的思考过程模板、无意义重复片段标记为ignore,不参与loss计算;对超过max_len截断后的padding位置掩码。mask不只是为了对齐batch,更是为了控制“哪些token值得学习”。

3.3 KL正则与参考模型:样本增强里的稳定器

GRPO里通常带一个对参考模型的KL惩罚项,系数beta控制策略不要偏离初始模型太远。beta太小,策略会迅速奔向reward高分区域,输出多样性急剧下降;beta太大,样本多样性保住了,但reward提升缓慢。实操里beta一般设0.01~0.1,不同模型的敏感度差异很大。

一个实用的增强技巧是:不把所有token都纳入KL惩罚,只在关键位置(比如推理步骤结尾、结论句)惩罚高。但这么做需要额外的启发式规则,工程成本不低。我自己的经验是,前期先用统一的KL惩罚,等训练中段策略稳定后再放宽,效果往往比一开始就精细化处理要好。

4. 两代公开实践里的GRPO样本处理演进

4.1 早期数学推理场景:样本结构极简但有效

GRPO第一次引起大家注意是在数学推理强化学习的研究里。那是这类方法最朴素的形态:对每个问题采样64条回答,用规则判断对错,整组归一化后直接更新,并通过KL正则限制策略偏移。样本处理几乎不需要考虑长度、格式、语言混杂这些扰动,因为数学题的输入输出都比较干净,reward信号天然可区分。

这一阶段验证了一个关键结论:只要组内样本有差异,去critic的GRPO能跑赢用了critic的PPO,尤其是在reward稀疏的数学题上。原因是相对比较对reward尺度不敏感,且避免了critic的噪声。

4.2 通用推理任务时代:样本工程的厚度呈指数增长

当GRPO被迁移到通用推理任务时,情况完全不同了。模型生成的文本可能很长、语言混杂、格式混乱,甚至出现不肯停止的现象。这时候公开讨论里出现了大量“输出格式约束”“语言一致性奖励”“质量问题过滤”这类组件。这些本质都是在做样本增强:在它们成为训练信号之前,先把明显不合适的样本拦下来。

以前跑GRPO只需要关心“答对没有”,后来需要关心rollout的长度分布、格式合规率、重复片段比例。这让我意识到,GRPO这个算法的重心已经从“设计目标函数”转向了“设计样本流水线”。

4.3 演进趋势隐含的结论:样本质量等于优势质量

把两代实践放在一起看,结论很清晰:模型规模越大、任务越开放,组内样本的同质化问题越严重,而GRPO的优势完全依赖组内样本的相对比较。样本越强,advantage才越有信息量。

这个结论影响着资源配置。我现在启动一个新GRPO任务时,花在采样策略、reward整形、数据清洗上的时间是花在loss实验上的三倍以上。相反,如果只盯着GRPO源码调参数,上游样本烂掉,下游一切白搭。

5. 一个可直接复用的GRPO样本增强实现

5.1 核心损失代码

下面这个PyTorch实现不是完整训练管线,而是把前文提到的关键点都集中在一起:组内标准化、优势截断、长度归一化、k3形式的KL散度。代码里注释的部分就是样本增强的重点位置。

import torch import torch.nn.functional as F def grpo_loss(logps, ref_logps, old_logps, rewards, mask, beta=0.04, clip_eps=0.2, adv_clip=10.0): """ logps: [B, G, T] 当前策略的token级log概率 ref_logps: [B, G, T] 参考模型的token级log概率 old_logps: [B, G, T] 采样rollout时旧策略的log概率 rewards: [B, G] 每条样本的reward mask: [B, G, T] 1表示参与训练,0表示忽略 """ B, G, T = logps.shape # 1) 组内标准化,得到优势 mean_r = rewards.mean(dim=1, keepdim=True) # [B, 1] std_r = rewards.std(dim=1, keepdim=True) + 1e-6 # [B, 1] adv = (rewards - mean_r) / std_r # 样本增强:对异常大优势做截断,防止单条样本主导 adv = adv.clamp(-adv_clip, adv_clip).unsqueeze(-1) # [B, G, 1] # 2) 重要性采样比率 ratio = torch.exp(logps - old_logps) # 3) 带clip的policy loss pg_loss = torch.min( ratio * adv, torch.clamp(ratio, 1.0 - clip_eps, 1.0 + clip_eps) * adv ) # 4) k3估计的KL散度 kl = torch.exp(ref_logps - logps) - (ref_logps - logps) - 1.0 # 5) 每条样本先token平均(长度归一化),再组平均 loss_per_sample = ((pg_loss - beta * kl) * mask).sum(dim=-1) / mask.sum(dim=-1) # [B, G] loss = -loss_per_sample.mean() return loss

几个实现细节值得说清楚。

组内标准化里加了1e-6,是为了防止std=0时除零。如果你前面已经做了组内去重,仍然可能出现整组只有一条有效样本的情况,这时候分母尤其要小心。

adv_clip我一般设10到20。别小看这个裁剪,当一组样本里出现一个离群的高分样本,不裁剪的话它的advantage可能膨胀到几十倍,整个组的梯度都被它一个人带走了。裁剪之后,该样本仍然排第一,但不会用离谱的倍数去压过其他样本。

长度归一化用的是mask.sum(dim=-1),而不是直接除以T。因为padding位置本身不参与loss,分母应该只统计真实参与训练的token数。如果你的实现里没有mask,那至少在截断长度上要保证一致,否则长短样本的梯度占比会失衡。

k3形式的KL散度exp(ref_logps - logps) - (ref_logps - logps) - 1好处是不需要额外一次前向传播来估计KL,而且方差比单纯用对数差更小。代价是在采样分布和当前分布差得远的时候,指数项容易溢出,所以训练过程中如果看到NaN,先检查这个位置。

5.2 一个带增强策略的Rollout采样示例

损失函数是下游,样本是从上游来的。我一般把采样过程封装成一个group_sample函数,里面动态控制温度和去重逻辑:

def group_sample(model, tokenizer, prompt, group_size=16, temperatures=[0.7, 1.0, 1.2, 1.5]): outputs = [] for i in range(group_size): temp = temperatures[i % len(temperatures)] out = model.generate( tokenizer(prompt, return_tensors="pt").to(model.device), do_sample=True, temperature=temp, top_p=0.95, max_new_tokens=1024, ) text = tokenizer.decode(out[0], skip_special_tokens=True) outputs.append(text) # 简单N-gram去重,返回去重后的样本 outputs = dedup_by_ngram(outputs, n=8) return outputs

这里温度列表不是固定一套,我通常会根据任务调整。如果发现组内std普遍低于0.3,就把高温档从1.2提到1.5,甚至加一档1.7。如果发现std过大、训练波动剧烈,就减少高温档。本质上是拿解码参数去控制“样本对比强度”。

N-gram去重是防止生成式模型在几个高概率答案之间循环。同一个组里出现8个n-gram完全相同的输出时,基本可以断定它们是同一段话的微扰版本,留着只会稀释组内信号。

需要提醒的是,GRPO原始设定里样本是on-policy的。如果你做了去重,剩余样本仍然是本次采样内产生的,直接参与训练没问题。但如果想用上一轮rollout缓存下来的旧样本凑数,就要注意logps和old_logps的对应关系,不要拿旧样本配新策略的logp算比率,否则重要性权重会偏掉。

5.3 训练指标里应该盯的四个量

很多人跑GRPO只看loss曲线,但我建议额外记录下面四个量,它们比loss更能反映样本状态:

  • 组内reward的mean和std:std长期小于0.3,意味着组内区分度不足;std超过2,说明有异常值。
  • mask后的有效token占比:如果大量token被mask,说明生成了很多无意义内容,模型在刷长度。
  • KL散度均值:KL突然飙升说明策略正在快速偏离参考模型,reward再高也不能放任。
  • 每条样本的平均长度:长度分布如果逐渐两极分化,长文本会开始主导梯度。

这四个量放在训练日志里,每100步打一次,基本能覆盖样本侧的大部分问题。

6. 我踩过的样本相关性坑:方差消失、异常值与同质化

6.1 坑一:temperature被悄悄改成0后,loss冻住

最开始遇到GRPO不收敛时,我第一反应是调学习率,结果越调越怪。后来翻采样代码,发现某个同学为了“复现实验”把temperature写死成了0。这个配置在PPO里问题不大,因为还有critic给advantage;但在GRPO里等于自杀——所有样本一样,组内std为0,loss变成纯KL项的更新,策略既不前进也不后退。

从那以后我把组内reward std作为核心监控指标,低于阈值立刻告警。只要std不归零,GRPO的训练信号就在。

6.2 坑二:组内outlier拖垮整个组的优势

另一个常见情况是组内出现一条特别极端的样本,比如一个超长推导突然拿了高分。它的reward高出组内均值好几个标准差,标准化后advantage变成十几,整个batch的梯度几乎全被它贡献。表面上看训练在推进,实际其他样本的优势被压缩到接近0,组内有效对比变成了“只有这一条说了算”。

处理办法是在reward进入标准化之前先做分位数裁剪,也就是winsorize:把reward序列里1%和99%分位数以外的值拉到边界。这样保留排序关系,但不会让极端值独占梯度。

6.3 坑三:重复文本让组内有效样本归零

生成式模型在部分简单题上会陷入反复绕圈,同一个答案生成很多遍,只是措辞微调。组内样本数量是16,但去重后有效样本只剩3条,组内比较基本失效。我是在group_sample里加了一级N-gram去重,并规定去重后不足4条的组直接丢弃。丢组比硬凑重要,因为强行把不足2条的组拿来算标准化,std会被人为拉大,得到虚假优势。

6.4 坑四:简单题与难题混训,模型被平均掉

还有一次效果不如预期,原因是训练集里简单题和难题混杂,一个batch里既有小学运算又有高难度证明。简单题组reward普遍高,难题组普遍低,组内标准化虽然各自独立,但组间的梯度平均之后,难题组的更新幅度被简单题稀释。后来按难度分层构造batch,问题就消失了。

这里我的习惯是,每500步分别统计简单/中等/困难子集的reward均值,三个曲线分开看,而不是看一条平均线。平均线往往掩盖了“简单题已经饱和、难题还在原地踏步”的真相。

这些坑踩过之后,我总结了一句话:GRPO的样本增强不是一个独立的工程模块,而是优势估计这个核心机制的一部分。每次训练开始前我只检查三件事——组内reward的std有没有信息量、长度分布是否健康、reward在质量上有没有区分度。这三件事比loss曲线诚实得多。

如果以后继续往这个方向做,我大概率会尝试引入一个轻量级的“样本筛选器”或者反思式的生成策略,在采样的源头就把组内差异做大。GRPO的样本潜力还远没挖干净,这篇就当是阶段性记录,等踩出新坑再回来补。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询