logo

GRPO 入门:群体相对策略优化

Published on

GRPO 入门:群体相对策略优化

强化学习训练大模型,最容易把人吓住的不是公式,而是公式背后的工程账单。传统 PPO 往往需要策略模型、参考模型、奖励模型和价值模型一起工作。模型本来已经很大,再加一套价值模型,显存、通信和训练稳定性都会变成现实问题。

GRPO,也就是 Group Relative Policy Optimization,给出了一个很有启发性的思路:对于同一个问题,让当前策略一次生成一组答案,不急着问“这条答案的绝对价值是多少”,先在这一组答案里比较谁更好。答案比同伴好,就增加它出现的概率;比同伴差,就降低它出现的概率。

这个想法听起来像考试排名,但它确实抓住了推理训练的一个关键:很多任务有明确的对错验证器,却很难训练一个稳定的价值模型去预测每个中间状态到底值多少。只要能可靠地判断一组结果,组内相对优势就可以成为学习信号。

当然,GRPO 不是“多采样几次就自动变强”。采样质量、奖励函数、长度偏差、KL 约束、批次构造和验证器错误,任何一个环节出问题,模型都可能学会投机。理解它的价值,也要理解它的边界。

一、先回顾 PPO 在做什么

把语言模型看成一个策略:给定问题和已经生成的前缀,它决定下一个 Token 的概率。强化学习的目标,是让高奖励答案更容易被策略生成,同时不要让新策略偏离原策略太远。

PPO 通常会计算一个优势值:某个动作比当前状态下的平均预期好多少。优势为正,增加对应动作的概率;优势为负,降低对应动作的概率。为了防止一次更新把策略推得太远,PPO 还会使用裁剪目标或 KL 惩罚。

难点在于价值函数。它需要估计每个状态的未来回报,语言模型生成过程又很长,奖励可能只在最后才出现。价值估计有偏差时,优势也会跟着偏;价值模型还要占用额外参数和显存。

GRPO 的出发点是:同一个问题对应的一组回答,可以提供相对排序。先把这组回答的奖励做标准化,再把组内差异作为优势近似,便不必完全依赖单独的价值模型。

二、GRPO 的核心直觉

假设一个问题 q 采样出四个回答:

回答 A:奖励 1.0,答案正确,推导简洁
回答 B:奖励 0.0,答案错误
回答 C:奖励 1.0,答案正确,但过程很长
回答 D:奖励 0.0,格式不符合要求

组内平均奖励是 0.5。如果用标准差做归一化,A 和 C 会得到正优势,B 和 D 得到负优势。模型会学习提高能够产生 A、C 这类结果的 token 序列概率,同时降低 B、D 的概率。

同一个问题 q
    ├─ y1 → reward1
    ├─ y2 → reward2
    ├─ y3 → reward3
    └─ y4 → reward4
     组内平均值 / 标准差
       相对优势 A1...A4

常见的组内优势形式可以写成:

A_i = (r_i - mean(r_group)) / (std(r_group) + ε)

这里的 A_i 不是严格意义上对真实价值函数的完整估计,而是“这个回答在本组中相对表现如何”的信号。它的好处是简单、局部、容易利用可验证奖励;它的代价是依赖组内比较,奖励尺度和组构造会直接影响训练。

三、目标函数在优化什么

实现细节会因论文和代码版本不同而变化,但理解下面几部分就足够入门:

  1. 策略比率:比较当前策略与旧策略对已生成 Token 的概率。
  2. 优势:组内相对奖励,告诉优化方向。
  3. 稳定约束:通过裁剪或 KL 项,限制策略漂移。
  4. Mask:只对有效生成 Token 计算损失,忽略 Prompt 部分。

策略比率通常写成:

ρ_t(θ) = π_θ(y_t | q, y_<t) / π_old(y_t | q, y_<t)

直观理解就是:新模型现在有多愿意生成这个 Token,和采样时的旧模型相比变化了多少。优势为正时,希望比率往上;优势为负时,希望比率往下。但变化不能无限大,否则模型会为了眼前奖励迅速忘掉原来的语言能力。

KL 约束可以让新策略靠近参考策略:

loss = policy_loss + β × KL(new_policy || reference_policy)

β 太小,模型可能为了奖励钻漏洞,语言质量和泛化能力下降;β 太大,模型几乎不敢改变,训练收益很弱。它不是一个永远正确的常数,需要结合奖励曲线、验证集和生成样本调节。

四、奖励函数决定模型究竟学会什么

推理训练里,奖励常常由多个部分组成:答案正确性、格式合法性、过程约束、长度惩罚和安全规则。最理想的是可验证奖励,例如数学题重新计算结果、代码运行测试、SQL 在隔离数据库执行后比较结果。

type RewardInput = {
  question: string
  answer: string
  expected: unknown
}

type RewardBreakdown = {
  correctness: number
  format: number
  safety: number
  lengthPenalty: number
  total: number
}

function scoreAnswer(input: RewardInput): RewardBreakdown {
  const correctness = verifyAnswer(input) ? 1 : 0
  const format = hasRequiredSections(input.answer) ? 0.2 : 0
  const safety = violatesPolicy(input.answer) ? -1 : 0
  const lengthPenalty = Math.max(input.answer.length - 4000, 0) * -0.0001

  return {
    correctness,
    format,
    safety,
    lengthPenalty,
    total: correctness + format + safety + lengthPenalty,
  }
}

上面只是示意,真正危险的是奖励函数与目标之间的错位。你奖励“答案很长”,模型就会学会拖长;你奖励出现某些关键词,它就可能机械堆词;你只奖励最终数字,不检查单位和约束,它可能通过投机得到一个碰巧正确的结果。

奖励设计前要问:这个分数是否真的代表我们想要的行为?有没有容易钻的空子?错误奖励会让模型在训练中非常努力地变坏,而且它的坏法通常还会越来越稳定。

五、组怎么采样,比想象中更重要

每个问题采样多少条回答,直接影响组内信号。组太小,奖励差异可能只是随机噪声;组太大,显存、推理时间和训练成本都会上升。不同难度的问题也不应该机械使用同样的组大小。

还要处理“整组奖励相同”的情况。如果四个回答全部正确,标准差接近零,组内就没有方向信号;如果全部错误,也很难知道该向哪里改。可采取的办法包括:提高采样难度、混合不同温度、重新分配题目,或者保留其他形式的监督信号。

type SampleGroup = {
  questionId: string
  prompt: string
  candidates: Array<{
    text: string
    reward: number
    advantage: number
  }>
}

function normalizeRewards(rewards: number[]) {
  const mean = rewards.reduce((sum, value) => sum + value, 0) / rewards.length
  const variance =
    rewards.reduce((sum, value) => sum + (value - mean) ** 2, 0) /
    rewards.length
  const std = Math.sqrt(variance)

  return rewards.map((value) => (value - mean) / (std + 1e-6))
}

数据管道还要保证同一组回答确实对应同一个问题和同一套奖励规则。混入不同版本的验证器、不同答案标准或不同系统提示,会让相对优势失去意义。

六、长度偏差是一个隐蔽的坑

语言模型的回答由很多 Token 组成。如果一个回答更长,它会贡献更多个位置的梯度;在某些实现里,即使最终奖励相同,长回答也可能因为 token 级损失累积而获得不同的训练影响。模型于是可能学会“写得更多”来获得更大的优化机会。

这也是为什么推理模型常会出现冗长倾向:训练目标奖励了正确性,却没有认真约束无效重复;或者验证器只看最后答案,过程长度就成了可以自由膨胀的空间。

处理长度问题不能简单地“一律惩罚长答案”。复杂问题本来就需要更多步骤,过强的长度惩罚会逼模型跳过必要推导。更合理的方式是分析正确率与长度的关系,惩罚重复、空转和无贡献段落,并按任务类型设置合理上限。

七、一个简化训练流程

把 GRPO 训练过程拆成工程步骤,大致是:

读取问题批次
使用旧策略为每个问题采样 G 个回答
运行验证器,得到每个回答的奖励
在组内计算平均值、标准差和相对优势
计算策略比率、裁剪损失与 KL 约束
反向传播,更新策略模型
在独立验证集生成样本并检查退化

伪代码可以这样表达:

for batch in dataloader:
    groups = sample_group(policy_old, batch, group_size=G)
    rewards = [verify(item) for item in groups]
    advantages = group_normalize(rewards)

    logprob_new = policy.logprob(groups)
    logprob_old = policy_old.logprob(groups).detach()
    ratio = (logprob_new - logprob_old).exp()

    clipped = ratio.clamp(1 - eps, 1 + eps)
    policy_loss = -torch.minimum(ratio * advantages, clipped * advantages)
    kl_loss = beta * approximate_kl(policy, reference_policy, groups)
    loss = masked_mean(policy_loss) + kl_loss

    optimizer.zero_grad()
    loss.backward()
    clip_grad_norm_(policy.parameters(), max_norm=1.0)
    optimizer.step()

真实训练还要处理 padding、序列 mask、混合精度、梯度累积、并行采样、检查点和断点恢复。不要把这段伪代码直接当成可用于生产训练的实现;它的作用是帮助我们看清数据从哪里来、奖励怎样变成梯度方向。

八、训练中应该盯哪些指标

只看训练 loss 很危险。GRPO 训练至少要同步观察:验证集正确率、奖励各分项、组内奖励方差、平均输出长度、截断比例、KL 距离、梯度范数、重复率和无效格式率。

如果总奖励上升但正确率不变,可能是格式奖励或长度奖励被模型钻了空子;如果正确率上升但 KL 快速变大,可能出现策略漂移;如果奖励方差长期接近零,当前题目或采样配置没有提供有效比较;如果输出越来越长却没有更多正确答案,长度约束需要重做。

每隔一段时间固定抽样,人工阅读一小批训练前后的答案。数字是仪表盘,样本才是路况。只相信曲线,往往会在模型已经学会投机之后才发现问题。

九、GRPO 和 SFT、PPO 如何配合

GRPO 不一定替代所有训练阶段。通常需要先通过预训练获得语言能力,再用 SFT 让模型学会基本的任务格式和回答习惯,随后用强化学习优化可验证的目标。SFT 提供稳定的起点,GRPO 提供根据奖励调整行为的能力。

PPO 仍然有价值,尤其是在奖励复杂、需要价值估计或已有成熟基础设施的场景。GRPO 的优势是减少对独立价值模型的依赖,但它并不免费:同一问题要采样多个回答,在线验证和组内计算也要付出成本。

选择哪种方法,不应该只看论文标题或某个模型的成功经验。要看任务能否可靠验证、奖励是否有区分度、采样成本是否可接受、团队是否能监控训练退化。没有可靠奖励时,换一个更漂亮的优化名字不会改变问题。

十、我的总结:比较可以替代一部分“绝对判断”

GRPO 最值得学习的地方,不只是一个强化学习算法,而是一种工程思路:当我们很难准确说出“这个状态值多少分”时,可以先构造同条件下的一组候选,比较它们谁更符合目标。

这对大模型推理尤其有用。数学、代码和结构化任务都有机会建立验证器,模型可以通过不断比较正确和错误的尝试,逐渐把概率质量推向更好的路径。但比较必须建立在同一问题、同一规则和可靠奖励之上;否则组内排名只是在放大噪声。

我研究这类训练方法时最警惕的,始终是奖励函数。模型不会按照我们的愿望学习,只会按照真正进入目标函数的信号学习。你奖励什么,它就会优化什么;你漏掉什么,它就会把什么当成无关紧要。

所以学习 GRPO,不要只记住“采样一组答案、计算相对优势”这句口诀。还要记住后半句:验证器决定奖励质量,KL 约束决定漂移速度,监控指标决定你能否及时发现投机。把这几件事一起做好,群体相对优化才可能把“多试几次”变成真正的推理能力,而不是更昂贵的随机生成。

🤪 您也可以编辑此页: