推理模型从零构建 第7章:改进 GRPO 训练
1. 训练指标追踪
第6章只跑了 50 步训练,效果很好。但如果跑 500 步呢?我们需要追踪关键指标来了解训练动态。书中追踪了 4 个基础指标:
几个关键观察:
- Average response length 应该先上升(模型学会写推理过程),但在 step 400 前出现了下降 — 这是不好的信号。
- Reward 应该上升;如果达到 1.0 说明全部正确,训练信号消失,应该停止或换更难的题目。
- Eval accuracy (MATH-500) 先上升后下降 — 出现了典型的 “训练过度” 退化现象。
- Loss 在 RL 中不像预训练那样有明显含义,但后期的尖峰是不稳定的信号。
⚠️ 核心发现:基础版 GRPO 有快速的早期收益,但随后出现收益递减甚至退化。原因是算法不够稳定。这就是本章后续所有改进的出发点。
2. Advantage 与 Entropy 监控
2.1 Advantage 统计量
在第6章中我们已经计算了 advantage,但没有追踪它的统计特征。Advantage 的均值和标准差可以帮助我们诊断训练状态:
def compute_advantage_stats(rewards_list):
rewards = torch.tensor(rewards_list)
advantages = (rewards - rewards.mean()) / (rewards.std() + 1e-4)
adv_avg = advantages.mean().item() # 应该永远是 0(GRPO 归一化的性质)
adv_std = advantages.std().item() # 信息量更大的指标
return advantages, adv_avg, adv_std
# 正常情况:有对有错 → std ≈ 1(健康的学习信号)
compute_advantage_stats([1., 1., 0., 0.])
# → mean=0.0000, std=0.9998
# 全对或全错 → std = 0(没有学习信号!)
compute_advantage_stats([0., 0., 0., 0.])
# → mean=0.0000, std=0.0000
Advantage 统计量的解读:
- adv_avg 永远是 0(GRPO 归一化的数学性质)— 这只是 sanity check。
- adv_std ≈ 1:梯度信号尺度合适,训练稳定。
- adv_std « 1:梯度信号消失,通常是 reward 坍缩的信号。
- adv_std » 1:梯度信号过大,可能导致不稳定。
2.2 Entropy 追踪
Entropy 衡量模型在生成下一个 token 时的不确定性。计算方式是 -sum(p * log(p)):
def sequence_logprob_and_entropy(model, token_ids, prompt_len):
logits = model(token_ids.unsqueeze(0)).squeeze(0).float()
logprobs = torch.log_softmax(logits, dim=-1)
# 选取实际生成的 token 的 logprob(和第6章相同)
targets = token_ids[1:]
selected = logprobs[:-1].gather(1, targets.unsqueeze(-1)).squeeze(-1)
logp_all_steps = torch.sum(selected[prompt_len - 1:])
# 新增:计算回答部分的平均 entropy
all_answer_logprobs = logprobs[:-1][prompt_len - 1:]
all_answer_probs = torch.exp(all_answer_logprobs) # logprob → prob
plogp = all_answer_probs * all_answer_logprobs # p * log(p)
step_entropy = -torch.sum(plogp, dim=-1) # 每个 token 的 entropy
entropy_all_steps = torch.mean(step_entropy) # 所有回答 token 的平均
return logp_all_steps, entropy_all_steps
Entropy 的解读规则:
- 极低 (≈ 0–0.5):一个 token 主导分布,模型过于确定,可能是 mode collapse(模式坍缩)的信号。
- 适中 (≈ 1–2):概率分布在几个 token 上,训练稳定。
- 极高 (接近 log(vocab_size)):概率接近均匀分布,模型几乎在随机生成。
ℹ️ 实际训练中的 entropy 变化:书中观察到 entropy 在 step 200 之后显著上升。单独看这似乎是坏信号,但结合 reward 和 adv_std 一起分析,发现模型仍在健康探索。不能孤立看单个指标,需要综合判断。
3. Clipped Policy Ratio
从这一节开始,我们正式改进 GRPO 算法。第一个改进来自 PPO:限制 policy 更新的幅度。
核心思想:比较更新前(old policy)和更新后(new policy)对相同 rollout 的 logprob 差异,用 ratio = exp(new_logp – old_logp) 衡量。如果 ratio 偏离 1.0 太远,就裁剪它:
# old_logps: 更新前模型对 rollout 的 logprob
# new_logps: 更新后模型对同样 rollout 的 logprob
log_ratio = new_logps - old_logps
ratio = torch.exp(log_ratio)
# DeepSeek-R1 用 clip_eps=10(非常宽松的裁剪)
clip_eps = 10.0
clipped_ratio = torch.clamp(ratio, 1.0 - clip_eps, 1.0 + clip_eps)
# PPO-style clipping:正 advantage 取 min,负 advantage 取 max
adv = advantages.detach()
unclipped = ratio * adv
clipped = clipped_ratio * adv
obj = torch.where(
adv >= 0,
torch.minimum(unclipped, clipped), # 限制正更新的上界
torch.maximum(unclipped, clipped), # 限制负更新的下界
)
clipped_pg_loss = -torch.mean(obj)
# 未裁剪: -2.5764
# 裁剪后: -2.3998 → 更新幅度略微减小,防止过激更新
ℹ️ 效果:加入 clipped policy ratio 后,500 步训练中 step 400 附近的退化消失了,训练变得更稳定。DeepSeek-R1 用 clip_eps=10(宽松),其他 RL 框架常用 0.1–0.2(严格)。宽松裁剪允许更大的更新步幅,但仍能防止极端情况。
4. KL 散度正则化
Clipped ratio 限制了单步更新幅度,但模型经过数百步后仍可能累积大量偏移。KL 散度正则化解决这个问题:显式惩罚模型偏离初始 policy 太远。
import copy
# 冻结 reference model(原始模型的副本,不参与训练)
ref_model = copy.deepcopy(model).to(device)
ref_model.eval()
for p in ref_model.parameters():
p.requires_grad = False
# 在 compute_grpo_loss 中新增 KL 计算:
for _ in range(num_rollouts):
token_ids, prompt_len, text = sample_response(model, ...)
# 当前模型的 logprob
logp = sequence_logprob(model, token_ids, prompt_len)
# reference model 的 logprob(不需要梯度)
with torch.no_grad():
ref_logp = sequence_logprob(ref_model, token_ids, prompt_len)
# ...
# Policy gradient loss + KL 正则化
pg_loss = -(advantages.detach() * logps).mean()
kl_loss = kl_coeff * torch.mean(logps - ref_logps) # 惩罚偏离 reference
loss = pg_loss + kl_loss # 总 loss
⚠️ KL 项的陷阱:书中实验发现,加入 KL 项后训练在 step 50 后快速崩溃,eval accuracy 降到 0%!原因有两个:(1) KL 是在 sum logprob 上计算的,长序列的 KL 量级更大,反而激励模型生成更长输出;(2) 当 reward 坍缩到全零时,advantage = 0,policy gradient 没有梯度,但 KL 项仍在推动模型走向高 entropy / 均匀分布。
这也是为什么多篇论文(Dr. GRPO、Olmo 3、DeepSeek V3.2)建议在数学推理训练中不使用 KL 项:
- KL 项增加了模型副本的内存开销
- sequence-level KL 对长度敏感,可能引入不良激励
- 简化算法(去掉 KL)在数学任务上效果更好
5. Format Reward 与 <think> 标签
DeepSeek-R1 使用 <think> </think> 标签来分隔推理过程和最终答案。要训练模型使用这种格式,需要添加 format reward。
5.1 添加 <think> 到 tokenizer
Base model 的 tokenizer 不认识 <think>(会拆成 <th ink > 三个 token)。可以手动添加 special token,或者直接使用 reasoning model 的 tokenizer(已原生支持):
# Base tokenizer 不认识
tokenizer_base.encode("") # → [13708, 766, 29] 拆成3个token
# 添加 special tokens 后
tokenizer_base._tok.add_special_tokens(
["", " ", "", " "]
)
tokenizer_base.encode("") # → [151667] 单个token
tokenizer_base.encode(" ") # → [151668] 单个token
# 或直接用 reasoning model 的 tokenizer(已原生支持)
tokenizer.encode("") # → [151667]
tokenizer.encode(" ") # → [151668]
5.2 Format Reward 函数
def reward_format(token_ids, prompt_len,
start_think_id=151667, end_think_id=151668):
"""检查生成部分是否包含正确顺序的 """
try:
gen = token_ids[prompt_len:].tolist()
return float(gen.index(start_think_id) < gen.index(end_think_id))
except ValueError:
return 0.0 # 缺少任一标签 → 0
# 在 GRPO loss 中组合两种 reward
rlvr_reward = reward_rlvr(text, example["answer"]) # 正确性
format_reward = reward_format(token_ids, prompt_len) # 格式
reward = rlvr_reward + format_reward_weight * format_reward # 总 reward
# 同时修改 prompt template 引导模型使用 格式
def render_prompt_with_think_tokens(prompt):
return (
"You are a helpful math assistant.\n"
"When solving the problem, first write your reasoning "
"inside and tags.\n"
"Then write the final result on a new line in the exact format:\n"
"\\boxed{ANSWER}\n\n"
f"Question:\n{prompt}\n\nAnswer:"
)
⚠️ Format reward 的注意事项:(1) Base model 没见过 <think> token,直接训练效果差 — 应该先做 instruction fine-tuning 或直接用 reasoning model 变体。(2) 实验中 format_reward_weight=1.0 导致模型过度追求格式而忽略正确性,eval accuracy 在 100 步后下降。可以降低权重到 0.1,或让 format reward 条件依赖于正确性:format_reward *= rlvr_reward(只有答对了才奖励格式)。
6. 前沿改进一览
DeepSeek-R1 之后,众多论文提出了 GRPO 改进。书中列出了 15+ 个改进方向:
| # | 改进 | 来源 | 核心思想 |
|---|---|---|---|
| 1 | Zero gradient signal filtering | DAPO | 过滤掉全对/全错的组,避免无效梯度 |
| 2 | Active sampling | DAPO | 主动选择模型能力边界的题目 |
| 3 | Token-level loss | DAPO | 在 token 层面计算 loss(而非 sequence 层面) |
| 4 | No KL loss | DAPO / Dr. GRPO | 去掉 KL 项,简化算法 |
| 5 | Clip higher | DAPO | 更宽松的 clip 范围以允许更大探索 |
| 6 | Truncated importance sampling | Yao et al. | 截断重要性采样以减少方差 |
| 7 | No std normalization | Dr. GRPO | 去掉 advantage 的标准差归一化 |
| 8 | Domain-specific KL strengths | DeepSeek V3.2 | 数学任务 KL=0,其他任务保留 KL |
| 9 | Reweighted KL | DeepSeek V3.2 | 用 importance weight 调整 KL 项 |
| 10 | Off-policy sequence masking | DeepSeek V3.2 | 屏蔽过时 rollout 的梯度 |
| 11 | Keep sampling mask | DeepSeek V3.2 | 保持 top-p/top-k 的采样 mask |
| 12 | Original advantage normalization | DeepSeek V3.2 | 保留原始 GRPO 的 advantage 归一化 |
| 13 | Per-reward group normalization | GDPO | 每种 reward 分别做组内归一化再聚合 |
| 14 | Sequence-level IS + clipping | GSPO | 序列级重要性采样 + 裁剪 |
| 15 | Clip IS weights (not token updates) | CISPO | 裁剪重要性采样权重而非 token 更新 |
ℹ️ 实践建议:不需要同时使用所有改进。根据书中的实验,一个简洁有效的配置是:基础 GRPO + Clipped Policy Ratio + 不使用 KL。这既保持了算法的简单性,又提供了足够的训练稳定性。Format reward 和其他改进可以根据具体场景选择性添加。
7. 关键收获
- 监控指标是 RL 训练的基础:不像预训练可以看 loss 曲线下降就行,GRPO 需要同时监控多个指标(loss、reward、response length、eval accuracy、adv_std、entropy)。单个指标可能有误导性,综合判断才能理解训练动态。
- Advantage std 是训练信号质量的 “体温计”:std ≈ 1 说明学习信号健康;std → 0 说明 reward 坍缩(全对或全错),没有学习信号。应结合 reward_avg 一起看 — 如果 reward_avg = 1.0 且 std = 0,说明模型已经全对,应该停止训练或换更难的题目。
- Clipped policy ratio 是最有效的稳定化手段:借鉴 PPO,限制单步更新幅度,消除了基础 GRPO 后期的退化问题。DeepSeek-R1 用 clip_eps=10(宽松),但即使是宽松裁剪也能有效防止极端更新。
- KL 散度在数学推理中弊大于利:增加内存开销(需要 reference model 副本),sequence-level KL 对长度敏感可能激励冗长输出,且当 reward 坍缩时 KL 项会主导梯度导致崩溃。多篇论文证实:数学任务上去掉 KL 效果更好。
- Format reward 需要谨慎调参:权重过大会导致模型追求格式而忽略正确性。建议:降低权重(0.1 而非 1.0),或使用条件 format reward(只有答对了才奖励格式)。
- GRPO 改进是一个活跃的研究前沿:DAPO、Dr. GRPO、DeepSeek V3.2、GDPO、GSPO、CISPO 等论文持续提出改进。核心趋势是:简化算法(去掉 KL、去掉 std 归一化)、提升探索(active sampling、更宽的 clip)、细化粒度(token-level loss 代替 sequence-level)。
ℹ️ 下一章预告:第6-7章用 RL 训练了推理模型,但 RL 训练成本高且不稳定。第8章将介绍推理蒸馏 (Reasoning Distillation) — 一种更简单的替代方案:用强大的 reasoning model 生成数据,然后通过 supervised fine-tuning 让小模型学会推理。这就是 “用 10 美元的训练成本复现 DeepSeek-R1” 的思路。
本文是 Build a Reasoning Model (From Scratch) (Sebastian Raschka) 的学习笔记。所有配图版权归原作者所有。代码基于原书示例,有简化和中文注释。