推理能力与可验证奖励
上一章的偏好需要有人比较。换成一道算术题,有些反馈可以直接计算。
例如 的答案是 20。模型写出答案以后,程序就能检查最后的数值。但如果它写的是:
2+3=6
6×4=20
答案=20
最后一行碰巧正确,前面的两步却都错了。这个小例子提醒我们:检查器只会检查被写进规则的东西。希望得到正确答案,还是希望得到一份每步可信的推导,需要分别说清楚。
1. 中间步骤为什么有帮助,又为什么需要检查
把复杂问题拆成中间步骤,可以把某些信息留在文本中,让后续生成继续利用。例如先算出 5,再计算 ,不用把全部关系都压在下一个词元的预测里。
这也是逐步解答示范和推理过程数据受到关注的原因。不过,多写几行不等于多做了几步正确推理。模型可能重复、绕远、漏掉条件,或者写出与最终答案不一致的解释。
公开的解题步骤也不能直接当作内部计算过程的完整记录。关于思维链不忠实性的研究 展示了模型解释可能遗漏实际影响回答的因素。检查文字中的推导是否成立,与断言模型内部“就是这样想的”,是不同的问题。
在能严格定义的任务中,我们可以先验证结果;若需要可靠的中间步骤,还要增加相应的过程检查或监督。Let’s Verify Step by Step 比较了数学推理中的过程监督与结果监督,为这种区分提供了研究例子。
2. RLVR:用可检查的结果给奖励
可验证奖励强化学习常写作 RLVR。其核心是把某种可计算的任务检查作为奖励来源,例如:
| 任务 | 可以检查什么 | 检查仍有哪些边界 |
|---|---|---|
| 形式化算术题 | 数值、单位或规定表达式 | 解析规则和等价形式需要正确处理 |
| 代码任务 | 是否通过指定测试 | 有限测试不能证明对所有输入都正确 |
| 形式证明 | 是否通过证明检查器 | 命题本身是否对应原意,仍需核对 |
| 结构化提取 | 字段、格式及与来源的一致性 | 来源可能缺失、冲突或有歧义 |
奖励可以是简单的 1/0,也可以包含多项。若同时奖励正确性和格式,需要明确两者权重;格式正确不能掩盖内容错误。
RLVR 不必先训练一个模仿人类偏好的奖励模型,但仍依赖检查器。程序里一个过宽的匹配规则、遗漏的测试或错误参考答案,都可能变成策略反复利用的捷径。
我们的小例子把最后一段限制为完整的 答案=整数,不会把“答案=20或9”当作一次明确的正确回答。即便如此,它仍然不检查前面的等式——这是结果检查的范围,不是已经完成了过程验证。
3. GRPO:同一道题的几份尝试,互相作参照
上一章用价值函数估计“从这里出发通常有多好”。GRPO 提供另一种思路:对同一道题采样一组回答,用这组回答的奖励作参照。
原始 DeepSeekMath 研究 提出了 Group Relative Policy Optimization,通常简称 GRPO。它用组内相对奖励估计优势,省去单独训练价值模型这一部分,但仍需要策略、参考分布、奖励计算和多份候选生成。
假设一组有 个回答,奖励为 ,平均奖励是 ,组内标准差是 。一种归一化优势写法为:
这里的小常数 防止除以零,不是上一章 PPO 的裁剪宽度。实现时两者应使用不同名称,避免混淆。
若奖励为 ,平均为 0.5;采用总体标准差时,标准差也是 0.5,于是优势大约是 。
若奖励全为 1 或全为 0,减去均值以后都是 0。这一组无法提供奖励高低的区分信号。如果目标还包含 KL 正则,KL 部分仍可能产生梯度,不能因此说整个训练一定毫无更新。
图 1:本例使用总体标准差,分母加一个很小的数。组内相对比较需要差异;一组全成功,也可能暂时没有奖励区分信号。
优势怎样进入更新
原始方法对生成的词元使用新旧策略概率比,并结合裁剪代理目标与参考约束。对有终局奖励的回答,组内相对优势可以分配给该回答的多个生成位置;如何按回答和词元长度加权,也是目标的一部分。
为把计算看清楚,本章将一份完整候选视为一个动作,使用:
再提高组内平均 ,并减去对固定参考分布的 KL 惩罚。候选只有四个,可以直接计算精确 KL。真实语言模型要处理逐词元概率、不同回答长度和相应的 KL 估计,不能把这张小表直接当作完整 GRPO 复现。
GRPO 的“组相对”也不意味着一组里最好的就必然正确。四个全错的答案,若奖励标准只偏爱流畅表达,组内比较照样可能推高一个流畅错误答案。
4. 动手:同一个策略,换两种检查器
这次给策略四个固定候选:
| 候选 | 内容 | 结果检查 | 过程检查 |
|---|---|---|---|
| 0 | 2+3=5;5*4=20;答案=20 | 1 | 1 |
| 1 | 2+3=5;5+4=9;答案=9 | 0 | 0 |
| 2 | 2+3=6;6*4=20;答案=20 | 1 | 0 |
| 3 | 答案=20或9 | 0 | 0 |
过程检查额外要求运算顺序、输入数字、中间值以及两个等式都符合这道题。它只认识本例这个封闭的算式格式,不是能评价任意自然语言证明的通用程序。
分别使用两种奖励训练。初始策略均匀,每轮抽 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. 推理时多算一点:多试几次,机会怎样变化
如果一次采样通过检查的概率为 ,各次独立、允许重复采样,那么 次尝试里至少一次通过的概率是:
推导很简单:每次都不通过的概率是 ,连续 次都失败是 ,再用 1 减去它。
对于均匀的四候选策略,结果通过概率为 0.5,过程通过概率为 0.25。增加采样次数,两种“至少一次通过”的机会都提高:
图 3:理论计算使用固定的均匀策略,不使用训练后的概率。真实候选若相关性很强,不能直接套用独立采样公式。
不过,找到一份“结果通过”的候选,仍可能挑中候选 2。增加计算不会自动修复检查器。
实践中还可以让模型检查、修改自己的回答,或者用搜索和评分器在候选中选择。评估时需要同时报告生成数量、词元量、延迟、验证成本和最终任务成功率。“候选里至少存在一个正确答案”的指标,也不等于部署系统真的能把它选出来。
关于测试时计算分配的研究 探索了不同难度与计算策略之间的关系。它提醒我们:多算一些可以提供新的选择空间,怎样分配计算和怎样选答案同样重要。
7. 训练计算与推理计算,分别买到了什么
训练增加计算,是在修改参数,希望以后许多次使用都能受益。推理时增加计算,是对当前请求多生成、检查或搜索,通常不改变模型参数。
把教师的多次尝试筛选后蒸馏给学生,则把部分昂贵的生成和筛选成本提前到训练阶段。能否在便宜的学生推理中保留收益,需要测量,不能只看教师做得多好。
一份公平比较至少应说清:基础模型是什么,训练数据和训练计算增加了多少,每个测试问题又花了多少生成与验证成本,以及答案是怎样选择的。相同模型参数量不代表相同总成本,单个漂亮的成功案例也不能替代固定测试集。
8. 给第六篇实践再加一项检查
第二十六章已经完成基础模型、SFT 和旧文本混合的固定测试对照,第二十七章增加了 LoRA 与训练状态成本。你可以先把这些结果整理成一份小报告:
- 指令提取是否正确、是否按格式结束。
- 旧故事预测与完整续写是否退化。
- 可训练参数、处理位置与状态内存分别是多少。
- 哪些结论只适用于当前模板、单次随机设置和训练预算。
本章的组相对更新可作为进阶实验。若要把奖励学习接到真实语言模型上,先选一个能严格检查的小任务,固定解析与评分规则,再保留独立测试。不要只把奖励接进循环,看到分数上涨,就省掉对生成内容的检查。
9. 停下来试一试
- 候选 2 为什么通过结果检查,却不能作为可信推导?
- 为什么
答案=20或9不应被本例当作一次明确的正确答案? - 奖励 的组内平均、总体标准差和归一化优势分别是什么?
- 同分组的优势全零,是否意味着含 KL 正则的训练一定完全不更新?
- 过程检查器也可能出错吗?本例程序的适用范围是什么?
- ,独立采样 5 次,至少一次成功的概率是多少?
- “五个候选里有一个正确”与“系统最终给出正确答案”有什么差别?
- 教师生成的所有最终答案都正确,为什么仍要检查蒸馏用的中间步骤和题目覆盖?
参考答案与提示
- 最后数值为 20,但前面两条等式错误。检查的目标不同,得到的标签就不同。
- 它列出了多个候选,没有提交单一答案。仅搜索字符串里是否出现 20,会放过这种情况。
- 平均为 0.5,总体标准差为 0.5,优势约为 ;代码分母额外加入一个很小的数。
- 不一定。奖励比较项没有区分信号,KL 项仍可能把策略拉向参考分布。
- 可能。这里仅识别指定的两步算式格式,不能理解任意证明,更不能替代通用数学检查。
- 。
- 后者还需要可靠的选择机制,并计入选择所需成本;存在性并不保证能识别。
- 学生可能模仿错误过程或记住窄模板。结果正确不能独自证明过程正确,也不能证明能推广到新问题。
10. 继续阅读
- Lightman 等,Let’s Verify Step by Step:数学任务中的过程监督与结果监督。
- Shao 等,DeepSeekMath:GRPO 的原始方法与数学训练实验。
- DeepSeek-AI,DeepSeek-R1:区分预训练基础、强化学习阶段与蒸馏实验。
- Snell 等,Scaling LLM Test-Time Compute Optimally:研究推理计算怎样分配。
到这里,我们已经走过预训练、示范微调、偏好反馈和可验证奖励。下一篇把目光转向应用:怎样给模型提供合适的上下文、连接外部资料与工具,并检验整个系统能否可靠完成任务?