推理模型从零构建 第7章:改进 GRPO 训练

📅 2026-03-02📖 ~10 min readReasoningGRPOStability
本文是 Build a Reasoning Model (From Scratch)(Sebastian Raschka 著)第7章的学习笔记。上一章实现了基础版 GRPO,50 步就能从 15.2% 提升到 47.4%。但训练更久会出现不稳定和退化。本章逐步添加改进:训练指标追踪(发现问题)→ Advantage/Entropy 监控(诊断原因)→ Clipped Policy Ratio(限制更新幅度)→ KL 散度正则化(防止偏移)→ Format Reward(鼓励 <think> 格式)→ 前沿改进一览(DAPO、Dr. GRPO 等 15+ 技巧)。
← Ch6: GRPO强化学习Ch8: 推理蒸馏 →

1. 训练指标追踪

第6章只跑了 50 步训练,效果很好。但如果跑 500 步呢?我们需要追踪关键指标来了解训练动态。书中追踪了 4 个基础指标:

Basic GRPO training metrics

500 步 GRPO 训练的四个基础指标:loss、reward、response length、eval accuracy (图源: Reasoning from Scratch)

几个关键观察:

  • Average response length 应该先上升(模型学会写推理过程),但在 step 400 前出现了下降 — 这是不好的信号。
  • Reward 应该上升;如果达到 1.0 说明全部正确,训练信号消失,应该停止或换更难的题目。
  • Eval accuracy (MATH-500) 先上升后下降 — 出现了典型的 “训练过度” 退化现象。
  • Loss 在 RL 中不像预训练那样有明显含义,但后期的尖峰是不稳定的信号。

⚠️ 核心发现:基础版 GRPO 有快速的早期收益,但随后出现收益递减甚至退化。原因是算法不够稳定。这就是本章后续所有改进的出发点。


2. Advantage 与 Entropy 监控

2.1 Advantage 统计量

在第6章中我们已经计算了 advantage,但没有追踪它的统计特征。Advantage 的均值和标准差可以帮助我们诊断训练状态:

Python — 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))

Entropy calculation

Entropy = -∑ p_i × log(p_i),衡量模型对 next token 的不确定性 (图源: Reasoning from Scratch)
Python — sequence_logprob_and_entropy
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 更新的幅度

Clipped policy ratio in GRPO pipeline

Clipped ratio 对比更新前后的 log-probability,限制单步变化幅度 (图源: Reasoning from Scratch)

核心思想:比较更新前(old policy)和更新后(new policy)对相同 rollout 的 logprob 差异,用 ratio = exp(new_logp – old_logp) 衡量。如果 ratio 偏离 1.0 太远,就裁剪它:

Python — Clipped Policy Ratio Loss
# 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 太远

KL divergence in GRPO

KL 项:比较当前 policy 和冻结的 reference model 的 logprob 差异 (图源: Reasoning from Scratch)
Python — KL 正则化(核心修改)
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. GRPOOlmo 3DeepSeek 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(已原生支持):

Python — <think> token
# 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 函数

Format reward

Format reward:检查 <think> 和 </think> 是否按正确顺序出现 (图源: Reasoning from Scratch)
Python — 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+ 个改进方向:

GRPO improvements overview

GRPO 的 15+ 改进方向,来自 DAPO、Dr. GRPO、DeepSeek V3.2 等论文 (图源: Reasoning from Scratch)
# 改进 来源 核心思想
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) 的学习笔记。所有配图版权归原作者所有。代码基于原书示例,有简化和中文注释。

← Ch6: GRPO强化学习Ch8: 推理蒸馏 →

Related Posts