Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

推理能力与可验证奖励

上一章的偏好需要有人比较。换成一道算术题,有些反馈可以直接计算。

例如 (2+3)×4(2+3)\times4 的答案是 20。模型写出答案以后,程序就能检查最后的数值。但如果它写的是:

2+3=6
6×4=20
答案=20

最后一行碰巧正确,前面的两步却都错了。这个小例子提醒我们:检查器只会检查被写进规则的东西。希望得到正确答案,还是希望得到一份每步可信的推导,需要分别说清楚。

1. 中间步骤为什么有帮助,又为什么需要检查

把复杂问题拆成中间步骤,可以把某些信息留在文本中,让后续生成继续利用。例如先算出 5,再计算 5×45\times4,不用把全部关系都压在下一个词元的预测里。

这也是逐步解答示范和推理过程数据受到关注的原因。不过,多写几行不等于多做了几步正确推理。模型可能重复、绕远、漏掉条件,或者写出与最终答案不一致的解释。

公开的解题步骤也不能直接当作内部计算过程的完整记录。关于思维链不忠实性的研究 展示了模型解释可能遗漏实际影响回答的因素。检查文字中的推导是否成立,与断言模型内部“就是这样想的”,是不同的问题。

在能严格定义的任务中,我们可以先验证结果;若需要可靠的中间步骤,还要增加相应的过程检查或监督。Let’s Verify Step by Step 比较了数学推理中的过程监督与结果监督,为这种区分提供了研究例子。

2. RLVR:用可检查的结果给奖励

可验证奖励强化学习常写作 RLVR。其核心是把某种可计算的任务检查作为奖励来源,例如:

任务可以检查什么检查仍有哪些边界
形式化算术题数值、单位或规定表达式解析规则和等价形式需要正确处理
代码任务是否通过指定测试有限测试不能证明对所有输入都正确
形式证明是否通过证明检查器命题本身是否对应原意,仍需核对
结构化提取字段、格式及与来源的一致性来源可能缺失、冲突或有歧义

奖励可以是简单的 1/0,也可以包含多项。若同时奖励正确性和格式,需要明确两者权重;格式正确不能掩盖内容错误。

RLVR 不必先训练一个模仿人类偏好的奖励模型,但仍依赖检查器。程序里一个过宽的匹配规则、遗漏的测试或错误参考答案,都可能变成策略反复利用的捷径。

我们的小例子把最后一段限制为完整的 答案=整数,不会把“答案=20或9”当作一次明确的正确回答。即便如此,它仍然不检查前面的等式——这是结果检查的范围,不是已经完成了过程验证。

3. GRPO:同一道题的几份尝试,互相作参照

上一章用价值函数估计“从这里出发通常有多好”。GRPO 提供另一种思路:对同一道题采样一组回答,用这组回答的奖励作参照。

原始 DeepSeekMath 研究 提出了 Group Relative Policy Optimization,通常简称 GRPO。它用组内相对奖励估计优势,省去单独训练价值模型这一部分,但仍需要策略、参考分布、奖励计算和多份候选生成。

假设一组有 GG 个回答,奖励为 r1,…,rGr_1,\ldots,r_G,平均奖励是 rˉ\bar r,组内标准差是 srs_r。一种归一化优势写法为:

A^i=ri−rˉsr+ε \hat A_i=\frac{r_i-\bar r}{s_r+\varepsilon}

这里的小常数 ε\varepsilon 防止除以零,不是上一章 PPO 的裁剪宽度。实现时两者应使用不同名称,避免混淆。

若奖励为 [1,0,1,0][1,0,1,0],平均为 0.5;采用总体标准差时,标准差也是 0.5,于是优势大约是 [1,−1,1,−1][1,-1,1,-1]。

若奖励全为 1 或全为 0,减去均值以后都是 0。这一组无法提供奖励高低的区分信号。如果目标还包含 KL 正则,KL 部分仍可能产生梯度,不能因此说整个训练一定毫无更新。

图 1:本例使用总体标准差,分母加一个很小的数。组内相对比较需要差异;一组全成功,也可能暂时没有奖励区分信号。

优势怎样进入更新

原始方法对生成的词元使用新旧策略概率比,并结合裁剪代理目标与参考约束。对有终局奖励的回答,组内相对优势可以分配给该回答的多个生成位置;如何按回答和词元长度加权,也是目标的一部分。

为把计算看清楚,本章将一份完整候选视为一个动作,使用:

ρi=πθ(yi∣x)πold(yi∣x) \rho_i=\frac{\pi_\theta(y_i\mid x)} {\pi_{\mathrm{old}}(y_i\mid x)}

gi=min⁡(ρiA^i,clip⁡(ρi,0.8,1.2)A^i) \begin{aligned} g_i=\min\bigl(&\rho_i\hat A_i,\\ &\operatorname{clip}(\rho_i,0.8,1.2)\hat A_i\bigr) \end{aligned}

再提高组内平均 gig_i,并减去对固定参考分布的 KL 惩罚。候选只有四个,可以直接计算精确 KL。真实语言模型要处理逐词元概率、不同回答长度和相应的 KL 估计,不能把这张小表直接当作完整 GRPO 复现。

GRPO 的“组相对”也不意味着一组里最好的就必然正确。四个全错的答案,若奖励标准只偏爱流畅表达,组内比较照样可能推高一个流畅错误答案。

4. 动手:同一个策略,换两种检查器

这次给策略四个固定候选:

候选内容结果检查过程检查
02+3=5;5*4=20;答案=2011
12+3=5;5+4=9;答案=900
22+3=6;6*4=20;答案=2010
3答案=20或900

过程检查额外要求运算顺序、输入数字、中间值以及两个等式都符合这道题。它只认识本例这个封闭的算式格式,不是能评价任意自然语言证明的通用程序。

分别使用两种奖励训练。初始策略均匀,每轮抽 8 个候选,共 160 轮,每组重复优化 3 次。四个候选的奖励可以预先计算,之后查表;总共 1,280 次采样并不意味着出现了 1,280 份不同的新解答。

运行 verifiable_rewards.py,单独下载时还需要同目录下的 preference_optimization.py:

uv run --python 3.12 --with-requirements requirements.txt python src/examples/verifiable_rewards.py
候选 0:结果奖励=1,过程奖励=1;2+3=5;5*4=20;答案=20
候选 1:结果奖励=0,过程奖励=0;2+3=5;5+4=9;答案=9
候选 2:结果奖励=1,过程奖励=0;2+3=6;6*4=20;答案=20
候选 3:结果奖励=0,过程奖励=0;答案=20或9
结果奖励训练后的候选概率:0.573, 0.004, 0.421, 0.003
过程奖励训练后的候选概率:0.994, 0.002, 0.002, 0.002
160 轮中的同分组:结果=127;过程=129
同分组 [1,1,1,1] 的优势:0.0, 0.0, 0.0, 0.0
从结果奖励策略采样 32 份:结果通过=32,过程通过=18
只用过程通过样本训练学生后,正确候选概率=0.959
均匀采样 1 次:至少一份结果通过=0.500;至少一份过程通过=0.250
均匀采样 4 次:至少一份结果通过=0.938;至少一份过程通过=0.684
均匀采样 8 次:至少一份结果通过=0.996;至少一份过程通过=0.900

小数经过四舍五入,显示出的概率之和可能与 1 略有差异。

结果奖励把候选 0 和 2 都视为成功。训练后,它们合计占据绝大多数概率,其中候选 2 仍占约 42.1%。若目标只检查最后数值,这并不矛盾;若我们还希望推导可信,就暴露了奖励范围不足。

图 2:这是固定候选空间中的策略学习,不是模型发现了新的解题方法。过程奖励能在本例区分两个最终答案相同、步骤不同的候选。

随着策略集中到高分候选,很多组里所有奖励相同,所以同分组数量很高。这是当前采样与任务下的现象;在更难的问题上,也可能大量出现全零组。只看平均奖励,不容易发现有效区分信号已经变少。

展开完整代码:验证器、组内优势、策略更新与过滤蒸馏
"""第 30 章:有限候选上的结果验证、过程验证与组相对更新;不训练语言模型。"""

import re
import torch
from torch import nn
from torch.nn import functional as F
from preference_optimization import clipped_gain

TRACES = [
    "2+3=5;5*4=20;答案=20",
    "2+3=5;5+4=9;答案=9",
    "2+3=6;6*4=20;答案=20",
    "答案=20或9",
]


def outcome_reward(text):
    # 只读取严格格式的最后一段,不评判之前的推导。
    final = text.split(";")[-1]
    match = re.fullmatch(r"答案=(-?\d+)", final)
    return int(match is not None and int(match.group(1)) == 20)


def process_reward(text):
    parts = text.split(";")
    if len(parts) != 3 or not outcome_reward(text):
        return 0
    parsed = [re.fullmatch(r"(-?\d+)([+*])(-?\d+)=(-?\d+)", part) for part in parts[:2]]
    if not all(parsed):
        return 0
    first, second = [match.groups() for match in parsed]
    a, op1, b, middle = int(first[0]), first[1], int(first[2]), int(first[3])
    left, op2, c, result = int(second[0]), second[1], int(second[2]), int(second[3])
    return int((a, b, c, op1, op2) == (2, 3, 4, "+", "*")
               and middle == a+b and left == middle and result == left*c)


def advantages(rewards):
    # 使用总体标准差;同分组的奖励项梯度为零。
    return (rewards-rewards.mean())/(rewards.std(unbiased=False)+1e-8)


def train(verifier, rounds=160, group_size=8, beta=.05):
    logits = nn.Parameter(torch.zeros(4, dtype=torch.float64))
    optimizer = torch.optim.SGD([logits], lr=.15)
    generator = torch.Generator().manual_seed(2026)
    reference = torch.full((4,), .25, dtype=torch.float64)
    all_rewards = torch.tensor([verifier(trace) for trace in TRACES], dtype=torch.float64)
    history, equal_groups = [], 0
    for step in range(1, rounds+1):
        old = logits.detach().softmax(-1)
        chosen = torch.multinomial(old, group_size, replacement=True, generator=generator)
        sampled_rewards = all_rewards[chosen]
        equal_groups += bool((sampled_rewards == sampled_rewards[0]).all())
        relative = advantages(sampled_rewards).detach()
        old_log = old[chosen].log()
        for _ in range(3):
            log_probs = logits.log_softmax(-1)
            ratio = (log_probs[chosen]-old_log).exp()
            surrogate = clipped_gain(ratio, relative).mean()
            kl = (log_probs.exp()*(log_probs-reference.log())).sum()
            loss = -surrogate+beta*kl
            optimizer.zero_grad(set_to_none=True)
            loss.backward()
            optimizer.step()
        if step % 20 == 0:
            probabilities = logits.detach().softmax(-1)
            history.append((step, float(probabilities @ all_rewards), float(probabilities[0])))
    return logits.detach().softmax(-1), history, equal_groups


def distill_filtered(teacher):
    generator = torch.Generator().manual_seed(2027)
    candidates = torch.multinomial(teacher, 32, replacement=True, generator=generator)
    outcome_ids = [i for i in candidates.tolist() if outcome_reward(TRACES[i])]
    process_ids = [i for i in candidates.tolist() if process_reward(TRACES[i])]
    if not process_ids:
        raise RuntimeError("没有通过过程检查的样本,不能开始本例蒸馏")
    student = nn.Parameter(torch.zeros(4, dtype=torch.float64))
    optimizer = torch.optim.SGD([student], lr=.2)
    targets = torch.tensor(process_ids)
    for _ in range(100):
        loss = F.cross_entropy(student.expand(len(targets), -1), targets)
        optimizer.zero_grad(set_to_none=True)
        loss.backward()
        optimizer.step()
    return len(outcome_ids), len(process_ids), student.detach().softmax(-1)


def main():
    torch.set_num_threads(1)
    for i, trace in enumerate(TRACES):
        print(f"候选 {i}:结果奖励={outcome_reward(trace)},过程奖励={process_reward(trace)};{trace}")
    outcome, _, equal_outcome = train(outcome_reward)
    process, _, equal_process = train(process_reward)
    print("结果奖励训练后的候选概率:" + ", ".join(f"{p:.3f}" for p in outcome))
    print("过程奖励训练后的候选概率:" + ", ".join(f"{p:.3f}" for p in process))
    print(f"160 轮中的同分组:结果={equal_outcome};过程={equal_process}")
    print("同分组 [1,1,1,1] 的优势:" +
          ", ".join(f"{a:.1f}" for a in advantages(torch.ones(4, dtype=torch.float64))))
    count_outcome, count_process, student = distill_filtered(outcome)
    print(f"从结果奖励策略采样 32 份:结果通过={count_outcome},过程通过={count_process}")
    print(f"只用过程通过样本训练学生后,正确候选概率={student[0]:.3f}")
    for n in [1, 4, 8]:
        print(f"均匀采样 {n} 次:至少一份结果通过={1-.5**n:.3f};"
              f"至少一份过程通过={1-.75**n:.3f}")


if __name__ == "__main__":
    main()

5. 推理过程蒸馏:把筛选后的尝试变成示范

另一条路线不在学生上直接运行同样的强化学习。先让教师生成候选解答,筛选出适合学习的记录,再用“题目—解答过程—答案”进行监督训练。这通常被称为推理过程蒸馏的一种形式。

我们的代码从结果奖励训练后的策略采样 32 份候选:32 份结果都通过,其中只有 18 份通过过程检查。用这 18 份作为监督目标,再训练一个新的四候选分布,正确候选概率达到约 0.959。

这个学生只学会了在同一道题的固定候选里更常选择候选 0,不构成通用推理能力迁移的证据。它检验的是一个小流程:教师产生数据,过滤决定保留什么,学生模仿保留下来的目标。

真实语言模型的蒸馏会使用许多题目和解答序列。可以沿用第二十六章的监督微调思路,对助手回答中的推导与答案计算损失;还需要评估新问题、不同难度以及没有教师轨迹时的表现。若教师数据包含错误步骤,学生也可能学会流畅地重演错误。

DeepSeek-R1 的研究 展示了强化学习、数据构造与小模型蒸馏等路线。阅读时要分清不同实验:R1-Zero 从已有预训练模型出发进行强化学习,不等于从随机权重只靠奖励学会语言;R1 则使用了冷启动数据和多个训练阶段。论文中的大模型结果,也不能由本章四候选实验推出。

6. 推理时多算一点:多试几次,机会怎样变化

如果一次采样通过检查的概率为 pp,各次独立、允许重复采样,那么 nn 次尝试里至少一次通过的概率是:

P(至少一次通过)=1−(1−p)n P(\text{至少一次通过})=1-(1-p)^n

推导很简单:每次都不通过的概率是 1−p1-p,连续 nn 次都失败是 (1−p)n(1-p)^n,再用 1 减去它。

对于均匀的四候选策略,结果通过概率为 0.5,过程通过概率为 0.25。增加采样次数,两种“至少一次通过”的机会都提高:

图 3:理论计算使用固定的均匀策略,不使用训练后的概率。真实候选若相关性很强,不能直接套用独立采样公式。

不过,找到一份“结果通过”的候选,仍可能挑中候选 2。增加计算不会自动修复检查器。

实践中还可以让模型检查、修改自己的回答,或者用搜索和评分器在候选中选择。评估时需要同时报告生成数量、词元量、延迟、验证成本和最终任务成功率。“候选里至少存在一个正确答案”的指标,也不等于部署系统真的能把它选出来。

关于测试时计算分配的研究 探索了不同难度与计算策略之间的关系。它提醒我们:多算一些可以提供新的选择空间,怎样分配计算和怎样选答案同样重要。

7. 训练计算与推理计算,分别买到了什么

训练增加计算,是在修改参数,希望以后许多次使用都能受益。推理时增加计算,是对当前请求多生成、检查或搜索,通常不改变模型参数。

把教师的多次尝试筛选后蒸馏给学生,则把部分昂贵的生成和筛选成本提前到训练阶段。能否在便宜的学生推理中保留收益,需要测量,不能只看教师做得多好。

一份公平比较至少应说清:基础模型是什么,训练数据和训练计算增加了多少,每个测试问题又花了多少生成与验证成本,以及答案是怎样选择的。相同模型参数量不代表相同总成本,单个漂亮的成功案例也不能替代固定测试集。

8. 给第六篇实践再加一项检查

第二十六章已经完成基础模型、SFT 和旧文本混合的固定测试对照,第二十七章增加了 LoRA 与训练状态成本。你可以先把这些结果整理成一份小报告:

  1. 指令提取是否正确、是否按格式结束。
  2. 旧故事预测与完整续写是否退化。
  3. 可训练参数、处理位置与状态内存分别是多少。
  4. 哪些结论只适用于当前模板、单次随机设置和训练预算。

本章的组相对更新可作为进阶实验。若要把奖励学习接到真实语言模型上,先选一个能严格检查的小任务,固定解析与评分规则,再保留独立测试。不要只把奖励接进循环,看到分数上涨,就省掉对生成内容的检查。

9. 停下来试一试

  1. 候选 2 为什么通过结果检查,却不能作为可信推导?
  2. 为什么 答案=20或9 不应被本例当作一次明确的正确答案?
  3. 奖励 [1,0,1,0][1,0,1,0] 的组内平均、总体标准差和归一化优势分别是什么?
  4. 同分组的优势全零,是否意味着含 KL 正则的训练一定完全不更新?
  5. 过程检查器也可能出错吗?本例程序的适用范围是什么?
  6. p=0.2p=0.2,独立采样 5 次,至少一次成功的概率是多少?
  7. “五个候选里有一个正确”与“系统最终给出正确答案”有什么差别?
  8. 教师生成的所有最终答案都正确,为什么仍要检查蒸馏用的中间步骤和题目覆盖?
参考答案与提示
  1. 最后数值为 20,但前面两条等式错误。检查的目标不同,得到的标签就不同。
  2. 它列出了多个候选,没有提交单一答案。仅搜索字符串里是否出现 20,会放过这种情况。
  3. 平均为 0.5,总体标准差为 0.5,优势约为 [1,−1,1,−1][1,-1,1,-1];代码分母额外加入一个很小的数。
  4. 不一定。奖励比较项没有区分信号,KL 项仍可能把策略拉向参考分布。
  5. 可能。这里仅识别指定的两步算式格式,不能理解任意证明,更不能替代通用数学检查。
  6. 1−0.85=0.672321-0.8^5=0.67232。
  7. 后者还需要可靠的选择机制,并计入选择所需成本;存在性并不保证能识别。
  8. 学生可能模仿错误过程或记住窄模板。结果正确不能独自证明过程正确,也不能证明能推广到新问题。

10. 继续阅读

到这里,我们已经走过预训练、示范微调、偏好反馈和可验证奖励。下一篇把目光转向应用:怎样给模型提供合适的上下文、连接外部资料与工具,并检验整个系统能否可靠完成任务?