- 解释为什么对于答案可检查的领域,可验证奖励消除了奖励模型的攻击面。
- 推导 GRPO 目标:组相对优势、裁剪比率,以及相对参考策略的 k3 KL 惩罚。
- 在 NumPy 里对一个玩具可验证任务实现完整可跑的 GRPO 训练循环,看成功率爬升。
- 描述 DeepSeek-R1 pipeline 各阶段(冷启动 SFT、推理 RL、拒绝采样、全场景 RL)以及每个奖励项的来源。
- 实现 Kimi k1.5 长度控制奖励,并预测它对回答长度和准确率的影响。
- 识别两个经典失败模式:把验证器 reward hacking 掉,以及朴素格式奖励导致的长度塌缩。
问题
第 09 课搭出了 RLHF pipeline:先预训练,再用 PPO 对照一个学习出来的奖励模型优化策略。这条 pipeline 有两个昂贵的部件。
奖励模型是个负债。 它是一个模仿人类偏好的神经网络,而策略恰恰被优化去最大化它的打分。"打分高"和"真的对"之间的任何缝隙都是攻击面。训练规模一大,策略就会找到这些缝隙:冗长的回答、自信的含糊其辞、谄媚。对奖励模型的 red-teaming 永远做不完,因为策略永远在探测它。
critic 是死重。 PPO 学习价值函数 V_\psi(s) 只是为了给策略梯度算优势。对 LLM 负载来说,这是第二个全尺寸模型躺在显存里、接收梯度,而且更糟的是——对稀疏的二值奖励("答案对或不对"),它的价值估计几乎没用。当大多数 rollout 得分为 0 时,V_\psi(s) 学到的主要就是预测 0。
然而对一大类提示,这些机制都不需要。数学题有标准答案。编程题有单元测试。格式要求就是正则表达式。如果奖励可以被计算出来而不是被预测出来,奖励模型就消失了;如果奖励是单个终止标量,价值网络本来也是错误的工具。
这就是本课的设定:可验证奖励强化学习(RLVR)——奖励由程序发出——用组相对策略优化(GRPO)来优化——一个 PPO 变体,用同一提示采样出的一组样本的经验统计量取代 critic。
概念
RLVR:奖励是一个程序
验证器是从回答到奖励的函数:
其中 x 是提示,y 是回答。真实系统会叠加多个检查器:准确性奖励(对照标准答案精确匹配,或单元测试通过)、格式奖励(答案出现在指定标签内,CoT 与最终答案分离),以及可选的软性项(代码能否编译、kernel 的运行时间)。
验证器是精确的,在百万级 rollout 上跑起来很便宜,而且——因为它是一段代码——它无法像神经奖励模型那样被逐步漂移优化。它仍然可能在设计空间里被 hack:如果 check 只比对最终的数字,策略可能学会跳过推理直接猜。R1 的对策是要求推理过程以固定格式呈现,并把训练拉得足够长,让"猜"的胜率不如"推理"。验证器设计是一个工程决策,每个领域都要重新审视。
GRPO:组就是 baseline
PPO 的优势估计 A_t = Q(s_t, a_t) - V_\psi(s_t) 需要 critic。GRPO(Shao 等人 2024,随 DeepSeekMath 提出)改为从同一个提示用当前策略采样一组 G 个回答 \{y_1, \dots, y_G\},用验证器逐个打分,然后在组内归一化:
组均值就是"这个提示"这个状态的价值的蒙特卡洛估计——恰好是 critic 在近似的东西——而且是从你反正需要的 rollout 里免费算出来的。当全部 G 个回答拿到相同奖励(全过或全挂)时,每个优势都是 0,这个提示不贡献梯度。这是正确行为:一个被轻松解决或毫无希望解决的提示什么都教不了,GRPO 会自动把训练聚焦在有用的带宽内。
优化步是 PPO 的裁剪替代目标加上参考策略 KL,KL 用 k_3 估计量(无偏、恒非负):
用工程师的方式读这些部件:
- \rho_{i,t} 是采样策略与被更新策略之间的逐 token 重要性比率。有了 clip,对同一组数据做多个 epoch 仍近似保持同策略。
- 裁剪 \epsilon \approx 0.2 限制了任何单个 token 每次更新能把策略推多远。
- \beta \approx 0.001–0.04 把策略锚在参考策略(SFT checkpoint)上。太大,模型拒绝离开初始分布;太小,策略漂移成仍然满足验证器的退化文本。
R1 配方
DeepSeek-R1 报告(2025)是规模化 RLVR 的参考 pipeline:
- 冷启动 SFT — 几千条长思维链示例,让基座模型在 RL 开始前就能产出可解析的推理。从基座模型直接做纯 RL 会产出语言混杂的乱汤;格式必须先存在,才能被奖励。
- 面向推理的 RL — 在数学/代码提示上跑 GRPO,奖励 = 准确性 + 格式。"顿悟"行为(自我验证、反思、难题上更长的 CoT)就在这个阶段涌现。
- 拒绝采样 + SFT — 大量采样,保留验证器接受的那部分,在赢家上微调;然后再混入非推理数据(写作、问答),防止通用能力烂掉。
- 全场景 RL — 第二轮 GRPO,在可验证奖励之上叠加有用性/无害性奖励。
Kimi k1.5(2025)贡献了长度控制奖励。推理 RL 有一个已知病理:模型发现思考得更久会提高"蒙对"的几率,于是回答长度膨胀,推理成本爆炸。k1.5 重新塑形了奖励:
答对了,越短奖励越高;答错了,越长罚得越狠。同一套 RLVR 机器,更聪明的奖励形状。
构建它
本课交付一个完整的 NumPy GRPO 训练器:表格式 token 级策略、真实的验证器、组采样、带裁剪的多 epoch 更新、k_3 KL 惩罚,以及 k1.5 长度项。
玩具领域。 提示给出三个数字(2、3、5 的一个排列)和一个目标值。策略输出一个 5 token 的表达式——两个 +/* 运算符与三个数字交错,顺序自由。验证器按真实的运算符优先级求值并支付奖励:命中目标的准确性奖励、表达式合语法的格式奖励,以及 k1.5 长度项(这里长度固定,所以它表现为一个小常数——但这个函数被完整实现并测试,因为之后接入变长解码时不能改动训练器)。
有些目标值在 24 个可能表达式中只有少数几个能命中(17 = 2+3*5),所以组 baseline 有真正的活要干。
运行:
consolepython3 main.py你会看到每次迭代的统计(组奖励均值、格式违例、相对参考策略的 KL),以及最终评估——贪心策略能解出留出的提示。完整数学推导在 docs/en.md;训练器约 200 行,文档里的每个 GRPO 常数都对应代码里的一个具名常数。
验证它
code/tests/test_main.py 锁住了让 GRPO 成为 GRPO 的那些部件:
- 验证器尊重运算符优先级并拒绝不合语法的表达式;
- 组优势零均值,且全部奖励持平时严格为零;
- 对极端重要性比率,裁剪比率保持在 [1-\epsilon, 1+\epsilon] 的逻辑之内;
- k_3 估计量非负,且对相同策略为零;
- 长度奖励对"短而对"支付多于"长而对",对"长而错"罚得最狠;
- 固定种子的端到端训练把贪心成功率从冷启动拉升。
工程师为什么需要它
- 只要存在检查器,就选 RLVR。 数学答案、代码、结构化输出(JSON schema 校验!)、对照标注语料库的检索忠实性。把学习出来的奖励模型留给开放式质量,并做好永远 red-team 它的准备。
- 验证器是你攻击面的一部分。 在扩规模之前设计它:要求推理格式,在需要之前不要放部分得分进来,并审计"猜比推理更划算"的均衡。
- 组大小就是你的价值函数分辨率。 G=8 常见;难的提示分布需要更大的组或课程过滤(丢掉全零优势的提示——GRPO 隐式就在做这件事)。
- KL 锚是一个成本旋钮。 它是"RL 改进了模型"和"RL 产出了一个奖励最大化的退化方言"之间的区别。
- 这套循环正是 R1 级 pipeline 里跑的东西;LLM 更大,rollout 来自 vLLM,但目标函数就是你刚手工训练的这个。
提示
- 如果组内奖励全部相同,优势按定义为零——检查你的训练混合里是否有贴近模型能力边界的提示。
- 奖励均值为负且格式违例为零,通常说明策略找到了一个永远打不中目标的有效表达式族:调大 \beta 或换一个提示混合的种子。
- 长度塌缩的表现是成功率保持而每条回答的 token 数膨胀;k1.5 项正是为此而生。
关键术语
| 术语 | 人们说 | 实际含义 |
|---|---|---|
| RLVR | "带奖励的 RL" | 奖励由程序(验证器)计算,而不是由学习出来的奖励模型预测。 |
| GRPO | "无 critic 的 PPO" | 每个提示采一组 G 个样本;组统计量取代价值网络当 baseline。 |
| Verifier | "检查器" | 把(提示,回答)映射到奖励的代码:准确性、格式、测试、schema。 |
| k_3 估计量 | "KL 惩罚" | 非负、无偏的 KL 样本估计:\pi_{\text{ref}}/\pi_\theta - \log(\pi_{\text{ref}}/\pi_\theta) - 1。 |
| 冷启动 SFT | "阶段 0" | 一轮小规模 SFT,让推理在 RL 能奖励它之前就以可解析的格式存在。 |
| 长度控制 | "k1.5 技巧" | 奖励塑形:支付短而对的、惩罚长而错的,止住 CoT 膨胀。 |