qinghuan
← 监督微调与偏好对齐 / 全部笔记

单元 36 / 监督微调与偏好对齐

P1 · 重点提升L3 · 深入

GRPO 与可验证奖励:从组内比较到奖励漏洞

比较同一组候选的奖励,计算组相对优势,并检查自动评分器会被怎样钻空子;分清论文中的 GRPO 目标与 CPU 局部实验。

本篇目录 · 按需要跳转

读完这篇,你能做什么

核对组均值、总体标准差和奖励全相同的边界;在 CPU 实验中更新正确候选的概率,并拦住利用格式或类型错误骗过评分的输入。

学习理由、准备要求与知识点

为什么学它

不用训练价值模型可以省下一部分成本,但仍要生成候选、检查奖励是否可靠,也要处理组内奖励完全相同、无法区分好坏的情况。

先读这些内容

知识点清单

  • 同提示分组采样
  • 组均值基线
  • 总体与样本标准差
  • 全同奖励零优势
  • 结果监督与过程监督
  • token与序列归约
  • 可验证奖励边界
  • 奖励漏洞和保留验证器

运行前先确认论文机制按文中版本说明;Java 17 完整算法,Python 3.14 与 PyTorch 2.9.1+cpu 的小型 CPU 实验。未执行 GPU、多机或大规模模型训练。

01先弄懂它是什么

同一个问题生成多份答案后,可以先在这一组里比较谁做得更好,GRPO 就使用这种相对信号。它针对同一提示采样 G 份回答;若奖励为 [1,0,0,0],第一份高于组内平均水平,若全是零,就没有哪份更好的组内证据。GRPO 不需要另训配套的价值函数,但仍要生成多个候选、计算奖励、保存所需概率,所以不能仅凭这一点说训练一定便宜或稳定。

具体计算时,r_i 是第 i 份回答的奖励,mu 是这个问题的组内平均奖励,sigma 是标准差。常见的结果监督优势为 A_i=(r_i-mu)/(sigma+epsilon),形状为 [G]。若一批问题排成 [B,G],应在每题自己的 G 个候选里归一化,不要把不同难度的题混成一组。奖励可以来自人工偏好模型,也可以来自答案检查或执行规则;可验证奖励只是其中一种来源。

RLVR强调通过可核验规则获得奖励,例如精确答案、程序测试或形式验证。验证器只能证明其检查的属性:几个测试通过不等于程序对所有输入正确,最终答案相同也不证明中间推导可信。研究中需要把可验证目标、格式约束和任务真实目标分开写清,防止模型利用解析器漏洞获得高分。

文中这几个词,先认一认

组相对优势
先在同一道题的候选答案里算平均奖励,再看每份答案比平均值高低多少。本篇还用这一组的标准差加上小常数 epsilon 作分母来缩放差值,不把不同题目的候选混在一起算。
可验证奖励
按明确的程序或证明规则算出的任务反馈,例如运行检查程序后给出的通过结果。
零方差组
一组候选得到完全相同的奖励,因此奖励本身无法告诉我们该更偏好哪一个。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

DeepSeekMath 的结果监督先给整份回答一个组内相对奖励,再把这个值作为该回答各 token 的优势。随后还要使用概率比裁剪目标和参考策略的 KL 正则项。先在回答内部平均、再在组内平均,也是目标的一部分。

长短回答怎样加权、标准差是否修正、epsilon 放在哪里,都会影响数值。本篇明确采用总体标准差 correction=0,并在分母加 epsilon,这是可复现的教学约定,不是所有实现必须采用的唯一规则。

在本篇只给 0 或 1 的奖励规则下,一组候选全对或全错时,组内相对优势都是零,因为它们的奖励完全相同。但原因可能不同:全对也许是题太简单,全错也许是没探索到有效解,或验证器出了错。因此要报告两类组各占多少,不能悄悄丢掉它们。零优势只表示这组奖励没有提供策略更新信号;如果目标里还有 KL 等其他项,整体梯度仍可能不为零。

CPU实验用四个固定JSON候选,严格解析唯一整数answer字段得到奖励,随后做一次真实PyTorch梯度更新。它检验组归一化、梯度方向和解析边界,未声称候选来自语言模型采样,也未复现论文训练规模。Java算法使用Welford统计均值与方差,避免平方和减均值平方造成不必要的消减误差,再对每份奖励输出优势。

先跑通一个最小例子

在PyTorch 2.9.1+cpu运行experiment.py。四个固定JSON候选用于可重复机制检查,进行一次真实梯度更新;不下载权重,不训练推理语言模型。

展开收起代码 · experiment.pypython · 38 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

experiment.py · python
import json
import torch
torch.manual_seed(17)
torch.set_num_threads(1)
torch.set_default_dtype(torch.float64)

def verify(text, target):
    try:
        obj = json.loads(text)
    except (ValueError,TypeError):
        return 0.
    # 只接受唯一answer字段和真正整数;bool在Python里也是int的子类,需精确类型检查。
    if type(obj) is not dict or set(obj) != {"answer"} or type(obj["answer"]) is not int:
        return 0.
    return float(obj["answer"] == target)

assert verify('{"answer":4}',4)==1.
assert verify('{"answer":true}',1)==0.
assert verify('{"answer":4,"hint":"extra"}',4)==0.
assert verify('4 or 5',4)==0.
texts=['{"answer":4}','{"answer":5}','4','{"answer":false}']
rewards=torch.tensor([verify(t,4) for t in texts])
def normalize(r):
    return (r-r.mean())/(r.std(correction=0)+1e-8)  # 固定总体标准差口径。
advantages=normalize(rewards).detach()
assert abs(advantages.sum().item())<1e-12
assert torch.equal(normalize(torch.ones(4)),torch.zeros(4))
logits=torch.nn.Parameter(torch.zeros(4))
old=torch.log_softmax(logits,dim=0).detach()
optimizer=torch.optim.SGD([logits],lr=.2)
ratio=(torch.log_softmax(logits,dim=0)-old).exp()
surrogate=torch.minimum(ratio*advantages,ratio.clamp(.8,1.2)*advantages)
loss=-surrogate.mean()  # 固定候选组的一次调试更新,未做真实语言采样。
optimizer.zero_grad();loss.backward();optimizer.step()
assert torch.softmax(logits,dim=0)[0].item()>.25
assert torch.isfinite(logits).all()
print(f"rewards={rewards.tolist()} correct_candidate_probability={torch.softmax(logits,dim=0)[0].item():.6f}")
print("PASS 8 checks; group-relative CPU mechanism only")

核对运行结果奖励为[1,0,0,0],正确候选概率从0.25上升;输出PASS 8 checks。

正在加载流程图…

奖励必须在同一提示组内比较;全对、全错和验证器异常在更新前分别记录。
查看流程图源码(Mermaid)
flowchart TD
  N0["按同一提示收集候选组"]
  N1["验证器分别输出奖励与原因"]
  N2["检查组内方差及任务难度"]
  N3["计算优势和裁剪策略目标"]
  N4["用独立验证集审计奖励漏洞"]
  N0 --> N1
  N1 --> N2
  N2 --> N3
  N3 --> N4

按顺序看,每一步做了什么

  1. 按同一提示收集候选组
  2. 验证器分别输出奖励与原因
  3. 检查组内方差及任务难度
  4. 计算优势和裁剪策略目标
  5. 用独立验证集审计奖励漏洞

03对照实现代码

DeepSeekMath的GRPO组相对目标 (新标签页)↗

DeepSeekMath arXiv:2402.03300v2;PyTorch 2.9.1 CPU示例 · 第4.1.1节式3至4;第4.1.2节结果监督优势

依据固定论文公式原创的组归一化教学函数;标准差与epsilon约定明确,不冒称作者未公开的完整训练实现。

核心机制代码 · python
def relative_advantage(rewards, eps=1e-8):
    mean = rewards.mean(dim=-1, keepdim=True)  # [B,G]沿同提示的G维求均值。
    std = rewards.std(dim=-1, correction=0, keepdim=True)  # 明确总体标准差约定。
    return ((rewards-mean)/(std+eps)).detach()  # 奖励优势作为固定训练目标。

论文先给出PPO基于价值函数的优势,再引入同提示候选组以替代额外价值估计。式3包含裁剪代理目标、每回答token平均和参考KL项;本篇短函数只实现组优势,缺少其余部分,不能称为完整GRPO训练器或声称与所有后续实现逐行等价。

论文结果监督段落用组均值与标准差归一化回答奖励,过程监督则涉及多个推理步骤奖励及其后续累计。两者学习信号的粒度不同,不能把每步局部得分随意平均后仍称原来的过程优势。实验选择结果监督,使每个候选只有一个可检查标量。

算标准差时,PyTorch 的默认修正与总体标准差不是同一口径;本片段明确设置 correction=0,组很小时尤其要看清这个差别。keepdim 让组均值或标准差保留 [B,1] 形状,才能与 [B,G] 的候选逐组对应。若把这个维度去掉,某些 B、G 组合会沿错误方向广播。应直接检查分组形状和提示 ID,确认同组确实是同一道题的候选。

DeepSeek-R1论文讨论规则奖励的应用与局限,但公开论文不是对本机训练结果的背书。这里的JSON验证器由本文原创,只验证受控算术答案,不实现论文中的数学评分或代码沙箱。扩展成代码执行验证时,需要独立隔离、资源限制和可信测试,不能让候选程序读取或修改评分规则。

代码先调用谁,再调用谁

  1. 旧策略对同提示生成G份输出
  2. 奖励器逐份评分
  3. 按组形成结果或过程优势
  4. 结合旧概率和参考KL形成目标
  5. 更新策略并重新采样

04动手写一个例子

稳定计算单组相对优势

输入至少两项奖励、绝对值不超过一百万及正epsilon,按总体标准差输出优势;这是有界奖励的CPU统计契约。

输入 1
奖励[0,1],epsilon很小
输出 1
约[-1,1]
输入 2
奖励[1,1]
输出 2
[0,0]

计算均值与方差时,直接把所有平方相加再减均值平方,遇到数值很近的奖励可能损失有效位。Welford逐项维护样本数、均值和离均差平方和,每次只利用新旧均值间的关系,通常更适合稳定统计。本文还限制奖励范围,避免任意巨大实数造成溢出。

第一遍得到组均值及M2,使用M2/G定义总体方差。第二遍计算每个中心化奖励除以标准差加epsilon。这样全相同奖励中心化后就是零,不会产生NaN。epsilon不是人为奖励差:它只稳定分母,不能给完全没有差异的组创造学习方向。

处理完前k个输入后,mean为这些奖励的均值,M2为相对该均值的平方偏差和;更新公式保持这两个关系。最终优势之和理论为零,浮点下按容差检查。加入epsilon后方差通常略小于一,不能错误地把严格单位方差写进不变量或测试。

为什么这样写是对的第一遍mean与M2描述已读前缀;第二遍每个输出使用同一完整组统计量,理论中心化和为零。

时间 O(G)额外空间 O(G)输出,O(1)额外统计状态

两次线性扫描,不排序、不构建成对比较矩阵。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 40 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    static void near(double expected,double actual) {
        if (!Double.isFinite(actual) || Math.abs(expected-actual)>1e-9*Math.max(1,Math.abs(expected)))
            throw new AssertionError(expected+" != "+actual);
    }
    static void check(boolean ok) { if(!ok) throw new AssertionError(); }
    static void bad(Runnable r) {
        try { r.run(); } catch(IllegalArgumentException e) { return; }
        throw new AssertionError("invalid input accepted");
    }
static double[] relative(double[] rewards,double epsilon) {
        if(rewards==null||rewards.length<2||!Double.isFinite(epsilon)||epsilon<=0)
            throw new IllegalArgumentException("group or epsilon");
        double mean=0,m2=0; int n=0;
        for(double x:rewards) {
            if(!Double.isFinite(x)||Math.abs(x)>1e6) throw new IllegalArgumentException("reward range");
            n++; double delta=x-mean; mean+=delta/n;
            m2+=delta*(x-mean); // Welford更新,避免直接用平方和相减。
        }
        double sd=Math.sqrt(Math.max(0,m2/n)); // 本实验明确使用总体方差除以G。
        double[] result=new double[n];
        for(int i=0;i<n;i++) result[i]=(rewards[i]-mean)/(sd+epsilon);
        // 相同奖励的组自然得到零优势,不编造组内偏好。
        return result;
    }
    public static void main(String[] args) {
double eps=1e-8;
        double[] a=relative(new double[]{0,1},eps);
        near(-.5/(.5+eps),a[0]);near(.5/(.5+eps),a[1]);
        check(Arrays.equals(new double[]{0,0},relative(new double[]{1,1},eps)));
        check(Arrays.equals(new double[]{0,0},relative(new double[]{0,0},eps)));
        double[] b=relative(new double[]{2,3},eps);near(a[0],b[0]);near(a[1],b[1]);
        double[] c=relative(new double[]{1,0,0,0},eps);near(0,Arrays.stream(c).sum());check(c[0]>0&&c[1]<0);
        bad(()->relative(new double[]{1},eps));
        bad(()->relative(new double[]{0,Double.NaN},eps));
        bad(()->relative(new double[]{0,1},0));
        System.out.println("PASS 8 cases");
    }
}

这些特殊情况也要试一试

  • 全对与全错组都输出零优势
  • 少于两候选拒绝
  • 总体标准差与样本标准差必须区分
  • epsilon加入后不要求严格单位方差
进阶选读 · 验证器的对抗审计与奖励归因

场景模型越来越擅长输出评分器偏好的格式,却没有提高真实解题率。

做法保留隐藏测试和独立解析器对照;逐类审计错误接受、错误拒绝、答案泄漏和重复候选,记录每个奖励分量与改版影响。

复杂度G份候选的统计O(G),验证器执行成本按测试数和程序预算独立计量。

好处与代价严格格式降低歧义却可能错误拒绝等价答案;宽松解析提高覆盖也扩大漏洞面。

05项目里怎么用

每次训练日志同时记录通过率、全零组比例、全一组比例、输出长度和验证器异常率。平均奖励不变可能来自不同的错误分布,标准化优势看起来正常也不证明标签可靠。将解析失败与答案错误分开计数,有助于判断应该改模型、数据还是验证契约。

每题多生成几个候选,比较机会更多,生成和验证费用也会增加。在总 token 预算相同的条件下,一道题用得更多,就可能少覆盖一些题。因此比较 GRPO 变体时,应固定提示集合、生成预算、求和或平均方式,以及验证器版本,并列明无法固定的条件;否则成功率提升可能只是因为多花了采样成本。

可执行奖励环境必须保存不可被候选修改的测试与评判逻辑,并限制CPU、内存、时间和文件权限。人工抽查仍用于发现规则没有覆盖的任务含义。本文没有运行候选代码、GPU训练或多机采样;JSON检查和小张量更新只为机制提供局部证据。

容易踩的坑

  • 把不同提示混成一组归一化
  • 忘记std默认修正影响小组
  • 把零优势说成所有梯度为零
  • 只检查答案子串导致伪通过
  • 比较方法时不对齐采样预算

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
结果奖励验收清楚、标注成本较低长链路信用分配稀疏答案或执行结果可核验
过程奖励提供更细粒度反馈过程标签和评分器难维护步骤语义可验证的任务
更大候选组增加相对比较机会生成与验证成本上升奖励有差异且预算允许

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01GRPO一定使用规则奖励吗?

展开参考答案与推导

不一定,组内奖励可来自奖励模型或规则;RLVR描述奖励来源。

02全对与全错为何都零优势?

展开参考答案与推导

组内没有相对差异,但任务难度和业务含义不同,需要分别监控。

03epsilon能解决全错探索吗?

展开参考答案与推导

不能,它只稳定除法,无法创造正确候选或奖励信息。

换个情况,还会处理吗

01通过率升高但答案错误增多怎么办?

展开参考答案与推导

审计验证器假阳性、解析捷径和测试泄漏,用独立标准复评。

02G从2改8后效果提高如何报告?

展开参考答案与推导

同时报告生成token、验证成本、题目覆盖及全同奖励比例。

03全部奖励相同但参数仍变化是不是bug?

展开参考答案与推导

检查KL、熵或其他正则项,它们仍可能产生梯度。

写代码试一试

01计算组相对优势。

输入 / 条件
[0,1]与小epsilon
输出 / 预期
约[-1,1]

提示 总体方差除G

展开参考答案与推导

使用两遍统计或Welford后再中心化。

02拒绝布尔值冒充整数。

输入 / 条件
JSON {"answer":true},目标1
输出 / 预期
奖励0

提示 Python bool属于int子类

展开参考答案与推导

精确检查type(value) is int。

03构造零方差测试。

输入 / 条件
[0,0,0,0]
输出 / 预期
四个零且无NaN

提示 分母加epsilon

展开参考答案与推导

中心化后全零,再验证有限性。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

核对组均值、总体标准差和奖励全相同的边界;在 CPU 实验中更新正确候选的概率,并拦住利用格式或类型错误骗过评分的输入。

本篇重点加练

零方差组与变长归约决定有效学习信号,重点区分没有梯度和没有任务进步。

  • 全同奖励零优势
  • token与序列归约

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01代码推演

    同一提示下奖励为 [0,0,1,2],另一组全为 1。分别采用总体和样本标准差约定写优势计算表,明确退化组政策。

    提交内容
    两组计算及契约
    怎样算完成
    • 标准差约定显式
    • 退化组不被忽略

    练到的知识点同提示分组采样 · 组均值基线 · 总体与样本标准差 · 全同奖励零优势

  2. 作业 02原理问答

    比较最终答案判定与逐步过程判定的监督信息;为变长回答写清 token 归约和序列归约各对应的样本权重。

    提交内容
    监督与归约对照表
    怎样算完成
    • 奖励来源准确
    • 不同长度权重可追踪

    练到的知识点结果监督与过程监督 · token与序列归约 · 可验证奖励边界

  3. 作业 03原理问答

    两组分别全成功与全失败,但长度分布不同。分析只由组相对任务优势产生的训练信号,并列出仍可能改变参数的其他目标项。

    提交内容
    组统计与目标项清单
    怎样算完成
    • 任务优势与完整损失分开
    • 不能从零优势直接推断全部更新

    练到的知识点全同奖励零优势 · token与序列归约

综合应用

  1. 作业 04编程实践

    为整数算术题编写受约束验证器与批量分组记录器,测试缺失答案、格式伪装、额外文本和同组全失败输入。

    提交内容
    CPU 验证器与攻击样例
    怎样算完成
    • 解析规则明确
    • 失败类型不混成一类

    练到的知识点同提示分组采样 · 全同奖励零优势 · 可验证奖励边界

  2. 作业 05排错实验

    设计至少五个验证器漏洞样例,并保留一组不参与开发的攻击样例;记录修补前后的误接受与误拒绝。

    提交内容
    漏洞集与保留审计报告
    怎样算完成
    • 保留集真实隔离
    • 修补不只优化已知样例

    练到的知识点可验证奖励边界 · 奖励漏洞和保留验证器

  3. 作业 06面试场景

    组大小由 4 增到 8 后成功率提高。制定等总候选预算的对照,分别考察探索覆盖与优化更新次数。

    提交内容
    公平比较方案
    怎样算完成
    • 采样及训练成本同时记录
    • 归因范围明确

    练到的知识点同提示分组采样 · 组均值基线 · 全同奖励零优势

  4. 作业 07编程实践

    CPU 实现变长候选的组相对损失核对器,测试全同奖励及一个非退化组,比较 token 等权与序列等权两种明确目标。

    提交内容
    实现与逐候选贡献表
    怎样算完成
    • 组内统计和全批归约分开
    • 空序列有处理契约

    练到的知识点全同奖励零优势 · token与序列归约

  5. 作业 08设计评审

    一次采样因预算只保留短回答,之后仍将结果当原始候选组训练。审阅筛选如何改变组构成、目标权重与可报告的成功率。

    提交内容
    选择流程及偏差审查
    怎样算完成
    • 被丢候选有记录
    • 条件分布变化不被隐藏

    练到的知识点token与序列归约

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    在 CPU 可枚举的候选空间中比较稀疏结果奖励和人工构造的过程奖励,检查奖励提升是否对应真实任务成功。

    提交内容
    合成实验与奖励审计
    怎样算完成
    • 真实性判定独立
    • 过程奖励偏差被纳入检查

    练到的知识点结果监督与过程监督 · token与序列归约 · 奖励漏洞和保留验证器

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 条件变化

    若把候选组中每个回答复制两次,哪些组统计与损失应保持不变,哪些取决于归约约定?给出一般条件并用小输入核验。

    提交内容
    复制变换论证与检查程序
    怎样算完成
    • 复制不是独立新样本
    • 每种归约的条件显式

    练到的知识点全同奖励零优势 · token与序列归约

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗