推理模型从零构建 第6章:GRPO 强化学习训练
1. 从 RLHF 到 RLVR
前几章的 inference-time scaling 不改变模型权重。本章进入 train-time scaling — 通过额外的训练阶段提升模型的推理能力。
RL for LLMs 的演进路径:
- RLHF (2022, InstructGPT):需要人类标注偏好数据 → 训练 reward model → 用 PPO 优化 policy。成本高,流程复杂。
- RLVR (2025, DeepSeek-R1):用可验证的自动奖励替代人类标注。数学题判对错、代码能否运行 — 奖励是确定性的,不需要 reward model。
本章使用的算法是 GRPO — DeepSeek-R1 采用的 policy gradient 算法。相比 PPO,GRPO 不需要额外的 value network(critic),学习信号来自同一组 rollout 之间的相对比较,更简洁也更省资源。
2. GRPO 算法总览
GRPO 的完整流程可以分为 5 个阶段:
用厨师类比理解 GRPO:给同一道题生成多个回答(相当于厨师尝试多种做法),正确率高的获得正 advantage(鼓励),正确率低的获得负 advantage(抑制)。模型通过这种组内相对比较来学习。
3. 采样 Rollout
GRPO 的第一步是对每个训练问题生成多个回答(rollout)。这里使用之前章节的 temperature + top-p 采样:
@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{} 中(隐式的格式奖励):
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,看组内相对好坏:
# 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,用于衡量模型对这个回答的 “偏好程度”:
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:
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 训练循环
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) 的学习笔记。所有配图版权归原作者所有。代码基于原书示例,有简化和中文注释。