11. 强化学习:用奖励改回答
监督微调教模型“模仿标准答案”。强化学习换一种方式:让模型自己生成多个回答,答案正确的得奖励,再用奖励调整模型。
nanochat 的强化学习只做一件事:在 GSM8K 数学题上提高答对率。它是可选阶段,不影响你理解主线。
为什么需要强化学习
SFT 的训练目标是最大化标准答案的概率。但同一个数学题有很多种正确写法,SFT 只能告诉模型“标准答案长这样”。
强化学习允许模型自己尝试。模型生成 16 个回答,其中一些正确、一些错误。正确的回答得到奖励,模型就更倾向于产生类似的回答;错误的回答得到较低奖励,模型就减少这种倾向。
这不需要为每个可能的正确回答准备标准答案。
训练前的准备
RL 从 SFT 模型开始:
chatsft_checkpoints/<model-tag>/如果 SFT 模型还不能稳定遵循格式、不能调用工具,直接做 RL 通常不会有效。RL 只是调整策略,不会补上基础对话能力。
读 RL 模型和任务加载scripts/chat_rl.py第 73–83 行↗训练数据是 GSM8K 训练集,评估数据是 GSM8K 测试集。每道题包含:
- 用户问题。
- 带解题步骤和最终数字的助手回答。
代码会去掉最后的助手回答,只保留到 <|assistant_start|> 为止,让模型自己续写。
一次 rollout 的流程
图 1:强化学习的一次 rollout。模型对同一道题生成多个回答,奖励函数只检查最终数字是否正确。
读 rollout 和优势计算scripts/chat_rl.py第 85–146 行↗关键参数:
| 参数 | 默认值 | 作用 |
|---|---|---|
--num-samples | 16 | 同一道题生成多少个回答 |
--temperature | 1.0 | 采样温度,保证回答有差异 |
--top-k | 50 | 只从概率最高的 50 个词元中采样 |
--max-new-tokens | 256 | 每个回答最多生成多少词元 |
--examples-per-step | 16 | 一次参数更新使用多少道题 |
如果所有回答都正确或都错误,奖励没有差异,模型就学不到“哪种回答更好”。因此温度不能太低,否则 16 个回答几乎一样。
奖励怎样计算
GSM8K 的标准答案用 #### 标记最终数字,例如:
... #### 42奖励函数从模型回答中提取最后一个 #### 后的数字,与标准答案比较:
- 数字相同:奖励 1。
- 数字不同或没有提取到:奖励 0。
tasks/gsm8k.py第 21–33 行↗读奖励函数tasks/gsm8k.py第 109–116 行↗这是一个非常稀疏的奖励:只看最终答案,不看中间步骤。它也意味着模型可能通过“猜数字”得到偶然奖励。奖励设计得越细,训练越稳定;nanochat 为了保持代码简单,只用了正确性这一项。
优势:比平均好多少
同一道题有 16 个回答,奖励可能是:
[1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]平均奖励是 3/16 = 0.1875。
对每个回答计算优势:
优势 = 奖励 - 平均奖励于是正确回答的优势为正,错误回答的优势为负。模型会提高正确回答的对数概率,降低错误回答的对数概率。
nanochat 没有除以标准差,也没有做标准化。源码注释把这种做法称为“比标准 GRPO 更简单,更接近 REINFORCE”。它也没有参考模型和 KL 正则,没有 PPO 的比例裁剪,因为采样来自当前模型,是 on-policy 的。
读优势和批次构造scripts/chat_rl.py第 128–146 行↗损失函数
模型对每个生成的词元给出对数概率。训练目标是:
最大化:优势 × 回答中每个词元的对数概率之和等价地,最小化:
损失 = -(优势 × 对数概率之和) / 有效词元数scripts/chat_rl.py第 245–274 行↗只有模型自己生成的助手词元参与计算。提示词、用户消息和工具输出都被掩码屏蔽。这样模型学习的是“怎样生成回答”,而不是“怎样复述题目”或“怎样预测计算器结果”。
工具调用在 RL 中的位置
数学题需要计算时,模型生成:
<|python_start|> 12*3 <|python_end|>推理引擎执行表达式,并强制注入:
<|output_start|> 36 <|output_end|>这些输出词元由引擎强制加入,掩码是 0。模型仍然会从“调用工具后得到 36”这个上下文中继续生成,但它不会因为工具输出本身而更新参数。
这一点很重要:模型要学的是“何时调用工具、调用什么表达式”,而不是记住计算器的输出。
评估指标:pass@k
RL 使用 pass@k 评估:同一道题生成 k 个回答,只要有一个正确就算通过。
读 pass@k 评估scripts/chat_rl.py第 150–191 行↗pass@1:一次尝试答对的概率
pass@16:十六次尝试中至少一次答对的概率2
pass@k 通常随着 k 增大而上升。它能反映模型的探索能力,但不能直接说明模型单次回答的准确率。
运行 RL
在 GPU 上运行:
torchrun --standalone --nproc_per_node=8 -m scripts.chat_rl -- \
--run=my-rl2
单卡:
python -m scripts.chat_rl --run=my-rlRL 需要不断生成多个回答,计算量比 SFT 大得多。CPU 上完整跑一轮 GSM8K 不现实。学习流程时可以先跳过,等你有了 GPU 再做。
常用参数:
python -m scripts.chat_rl \
--model-tag=d20 \
--num-samples=16 \
--examples-per-step=16 \
--max-new-tokens=256 \
--temperature=1.0 \
--top-k=502
3
4
5
6
7
当前实现的限制
- 只在 GSM8K 上训练,任务单一。
- 奖励只有 0 和 1,信号稀疏。
- 没有参考模型的 KL 约束,训练可能变得不稳定。
- 没有 PPO 的比例裁剪,算法更简单但也更粗糙。
- 保存检查点时不保存优化器状态,恢复训练的支持有限。
这不是一个通用的对齐系统,而是一个可以读懂、可以修改的强化学习示例。理解它之后,你可以尝试更细的奖励、更多样的问题或更稳定的约束。
本章要记住的三件事
- RL 让模型自己生成多个回答,再根据最终答案是否正确调整策略。
- 优势等于奖励减去同题平均奖励;只有模型生成的词元参与损失。
- 工具输出由推理引擎注入,不参与训练,模型学习的是工具调用动作。
下一章进入推理:推理引擎与键值缓存。