推理模型从零构建 第6章:GRPO 强化学习训练

📅 2026-03-02📖 ~10 min readReasoningGRPORL
本文是 Build a Reasoning Model (From Scratch)(Sebastian Raschka 著)第6章的学习笔记。这是全书的核心章节:从前几章的 inference-time 方法转向 train-time scaling,使用 GRPO (Group Relative Policy Optimization) 算法真正训练模型学会推理。从采样 rollout → 计算 reward → 求 advantage → 序列 log-prob → policy gradient loss → 更新权重,完整实现 RLVR 训练循环。仅 50 步训练,MATH-500 准确率从 15.2% → 47.4%
← Ch5: 自我优化Ch7: 改进GRPO →

1. 从 RLHF 到 RLVR

前几章的 inference-time scaling 不改变模型权重。本章进入 train-time scaling — 通过额外的训练阶段提升模型的推理能力。

RL in LLM training pipeline

RL 训练通常作为 post-training 阶段,在预训练或 SFT 之后进行 (图源: Reasoning from Scratch)

RL for LLMs 的演进路径:

  • RLHF (2022, InstructGPT):需要人类标注偏好数据 → 训练 reward model → 用 PPO 优化 policy。成本高,流程复杂。
  • RLVR (2025, DeepSeek-R1):用可验证的自动奖励替代人类标注。数学题判对错、代码能否运行 — 奖励是确定性的,不需要 reward model。
RLHF vs RLVR

RLHF 需要训练 reward model;RLVR 用规则验证器直接给出奖励 (图源: Reasoning from Scratch)

本章使用的算法是 GRPO — DeepSeek-R1 采用的 policy gradient 算法。相比 PPO,GRPO 不需要额外的 value network(critic),学习信号来自同一组 rollout 之间的相对比较,更简洁也更省资源。


2. GRPO 算法总览

GRPO 的完整流程可以分为 5 个阶段:

GRPO procedure overview

GRPO 五阶段:采样 rollout → 计算 reward → 求 advantage → 算 logprob → 更新 policy (图源: Reasoning from Scratch)

用厨师类比理解 GRPO:给同一道题生成多个回答(相当于厨师尝试多种做法),正确率高的获得正 advantage(鼓励),正确率低的获得负 advantage(抑制)。模型通过这种组内相对比较来学习。


3. 采样 Rollout

GRPO 的第一步是对每个训练问题生成多个回答(rollout)。这里使用之前章节的 temperature + top-p 采样:

Python — sample_response
@torch.no_grad()  # 不构建计算图(推理时不需要梯度)
def sample_response(model, tokenizer, prompt, device,
                     max_new_tokens=512, temperature=0.8, top_p=0.9):
    input_ids = torch.tensor(tokenizer.encode(prompt), device=device)

    cache = KVCache(n_layers=model.cfg["n_layers"])
    model.reset_kv_cache()
    logits = model(input_ids.unsqueeze(0), cache=cache)[:, -1]

    generated = []
    for _ in range(max_new_tokens):
        if temperature and temperature != 1.0:
            logits = logits / temperature
        probas = torch.softmax(logits, dim=-1)
        probas = top_p_filter(probas, top_p)
        next_token = torch.multinomial(probas.cpu(), num_samples=1).to(device)

        generated.append(next_token.item())
        if next_token.item() == tokenizer.eos_token_id:
            break
        logits = model(next_token, cache=cache)[:, -1]

    # 返回完整序列(prompt + 生成)、prompt 长度、解码后的文本
    full_ids = torch.cat([input_ids, torch.tensor(generated, device=device)])
    return full_ids, input_ids.numel(), tokenizer.decode(generated)

ℹ️ 为什么用 @torch.no_grad() 而不是 @torch.inference_mode()?虽然采样时不需要梯度,但生成的 tensor 后面会被 sequence_logprob 用到(那里需要梯度)。inference_mode 产生的 tensor 完全禁止参与梯度计算,会报错。no_grad 更温和,只是不追踪当前操作的梯度。


4. 计算 Reward 与 Advantage

4.1 RLVR Reward

Reward 计算非常简洁:用第3章的验证器检查答案是否正确,正确 = 1.0,错误 = 0.0。额外要求答案必须在 \boxed{} 中(隐式的格式奖励):

Python — reward_rlvr
def reward_rlvr(answer_text, ground_truth):
    extracted = extract_final_candidate(answer_text, fallback=None)  # 必须有 \boxed{}
    if not extracted:
        return 0.0
    correct = grade_answer(extracted, ground_truth)
    return float(correct)  # 1.0 或 0.0

# 示例:4 个 rollout
# "\boxed{83}"                    → 1.0(正确 + 格式对)
# "The correct answer is \boxed{83}" → 1.0(正确 + 格式对)
# "The final answer is 83"        → 0.0(没有 \boxed{}!)
# "We get \boxed{38}"             → 0.0(格式对但答案错)

4.2 Group Relative Advantage

GRPO 的 “GR”(Group Relative)体现在 advantage 计算上:不看绝对 reward,看组内相对好坏

Python — Advantage 计算
# advantage_i = (r_i - mean(r)) / (std(r) + epsilon)
rewards = torch.tensor([1.0, 1.0, 0.0, 0.0])
advantages = (rewards - rewards.mean()) / (rewards.std() + 1e-4)
# → [0.866, 0.866, -0.866, -0.866]
# 正确回答获得正 advantage(鼓励),错误回答获得负 advantage(抑制)

⚠️ 重要特性:如果一组 rollout 全对或全错,r_i – mean = 0,advantage = 0,模型不更新。GRPO 只从”有对有错”的组中学习。这意味着太简单(全对)或太难(全错)的题目都不贡献梯度,训练自然聚焦在模型 “边界能力” 的题目上。


5. 序列 Log-Probability

GRPO 需要计算每个 rollout 的序列级 log-probability,用于衡量模型对这个回答的 “偏好程度”:

Python — sequence_logprob
def sequence_logprob(model, token_ids, prompt_len):
    """计算回答部分的序列级 log-probability(所有 token logprob 之和)"""
    logits = model(token_ids.unsqueeze(0)).squeeze(0).float()
    logprobs = torch.log_softmax(logits, dim=-1)

    # 用 gather 选取每个位置实际生成的 token 的 logprob
    selected = logprobs[:-1].gather(1, token_ids[1:].unsqueeze(-1)).squeeze(-1)

    # 只取回答部分(跳过 prompt),求和(不是平均!)
    return torch.sum(selected[prompt_len - 1:])

为什么用求和而不是平均?在第5章的评分场景中,我们用平均来公平比较不同长度的回答。但在 GRPO 训练中,我们需要序列的完整对数似然来正确缩放梯度。如果用平均,长回答的梯度会被隐式缩小,扭曲 policy update。求和还有一个额外好处:它自然偏好简洁的回答(短回答的 sum 更大,即更不负)。


6. Policy Gradient Loss

把所有组件组合起来,GRPO 的 loss 就是 advantage 加权的 log-probability:

Python — compute_grpo_loss(核心)
def compute_grpo_loss(model, tokenizer, example, device,
                       num_rollouts=4, max_new_tokens=256,
                       temperature=0.8, top_p=0.9):
    prompt = render_prompt(example["problem"])
    roll_logps, roll_rewards = [], []

    model.eval()
    for _ in range(num_rollouts):
        # Stage 1: 采样 rollout
        token_ids, prompt_len, text = sample_response(
            model, tokenizer, prompt, device,
            max_new_tokens, temperature, top_p)

        # Stage 2: 计算 reward
        reward = reward_rlvr(text, example["answer"])

        # Stage 4: 计算 sequence logprob
        logp = sequence_logprob(model, token_ids, prompt_len)

        roll_logps.append(logp)
        roll_rewards.append(reward)

    model.train()

    # Stage 3: 计算 advantage
    rewards = torch.tensor(roll_rewards, device=device)
    advantages = (rewards - rewards.mean()) / (rewards.std() + 1e-4)

    # Stage 5: Policy gradient loss
    logps = torch.stack(roll_logps)
    pg_loss = -(advantages.detach() * logps).mean()

    return pg_loss  # .detach() 确保只对 logps 反向传播

数学公式:

L_PG = -(1/N) ∑ A_i ∑ log p(y_t | y_<t, x)

其中 A_i 是 advantage(固定的学习信号),内层 sum 是 sequence logprob(含梯度)。负号是因为 PyTorch optimizer 做最小化,而我们要最大化好回答的概率。


7. GRPO 训练循环

GRPO training loop

训练循环:遍历数据 → 每题算 GRPO loss → 反向传播 → 更新权重 → 保存 checkpoint (图源: Reasoning from Scratch)
Python — 训练循环(简化)
def train_rlvr_grpo(model, tokenizer, math_data, device,
                     steps=50, num_rollouts=4, max_new_tokens=512,
                     lr=1e-5, checkpoint_every=50):

    optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
    model.train()

    for step in range(steps):
        optimizer.zero_grad()
        example = math_data[step % len(math_data)]

        # 计算 GRPO loss(包含 rollout 采样 + reward + advantage + logprob)
        stats = compute_grpo_loss(
            model, tokenizer, example, device,
            num_rollouts=num_rollouts,
            max_new_tokens=max_new_tokens)

        # 反向传播 + 梯度裁剪 + 权重更新
        stats["loss_tensor"].backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        optimizer.step()

        # 定期保存 checkpoint
        if (step + 1) % checkpoint_every == 0:
            torch.save(model.state_dict(), f"grpo-step{step+1:05d}.pth")

训练 50 步的结果:

模型 MATH-500 准确率 平均回答长度
Qwen3 0.6B Base 15.2% 79 tokens
Qwen3 0.6B Reasoning (官方) 48.2% 1370 tokens
GRPO 训练 50 步 47.4% 586 tokens

ℹ️ 惊人的效率:仅用 50 个训练步骤(每步 8 个 rollout × 512 tokens),就从 15.2% 提升到 47.4%,几乎追平官方 reasoning model 的 48.2%。而且平均回答长度只有官方 reasoning model 的一半 — 说明模型学会了更简洁的推理方式。但注意:训练更久不一定更好,GRPO 可能变得不稳定,这就是第7章要解决的问题。


8. 关键收获

  • GRPO 是当前最实用的 LLM 推理训练算法:DeepSeek-R1 用它训练出了与 o1 竞争的推理模型。相比 PPO,GRPO 省去了 value network,更简单也更省资源。
  • RLVR 的核心创新是自动化奖励:不需要人类标注,不需要 reward model,只要有一个可以自动验证答案的函数(数学判对错、代码能否运行),就能训练推理模型。
  • Group Relative Advantage 是 GRPO 的精髓:只学习 “有对有错” 的题目,自动聚焦于模型能力边界。太简单(全对)和太难(全错)的题目自动被过滤。
  • Sequence logprob(求和)与 avg logprob(平均)用途不同:评分用平均(公平比较不同长度),训练用求和(保留梯度尺度,偏好简洁回答)。
  • 50 步就能追平官方 reasoning model:说明 base model 的预训练知识已经很丰富,RL 训练的作用主要是教模型如何组织推理过程(写出步骤、使用 \boxed{} 格式),而不是教它新的数学知识。
  • 但基础 GRPO 存在稳定性问题:训练更久可能退化。缺少 KL 正则化约束,模型可能偏离初始 policy 太远,产生 “reward hacking” 等问题。

ℹ️ 下一章预告:第7章将引入多项 GRPO 改进:KL 散度正则化(防止 policy 偏移太远)、clipped policy ratio(限制单步更新幅度)、entropy 追踪(防止模式坍缩)、format reward(奖励 <think> 格式),以及来自 DAPO、Dr. GRPO 等论文的前沿改进。

本文是 Build a Reasoning Model (From Scratch) (Sebastian Raschka) 的学习笔记。所有配图版权归原作者所有。代码基于原书示例,有简化和中文注释。

← Ch5: 自我优化Ch7: 改进GRPO →

Related Posts