11.4 GRPO 训练

配套代码:code/chapter11

在完成 SFT 训练后,我们已经得到了一个能够生成结构化答案的模型。但是,SFT 模型只是学会了"模仿"训练数据中的推理过程,并没有真正学会"思考"。强化学习可以让模型通过试错来优化推理策略,从而超越训练数据的质量。

11.4.1 从 PPO 到 GRPO

在强化学习领域,PPO(Proximal Policy Optimization)[1]是最经典的算法之一。PPO 通过限制策略更新的幅度,保证训练的稳定性。但是,PPO 在 LLM 训练中存在一些问题:需要训练 Value Model(价值模型),增加了训练复杂度和显存占用;需要同时维护四个模型(Policy Model、Reference Model、Value Model、Reward Model),工程实现复杂;训练不稳定,容易出现奖励崩塌或策略退化。

GRPO(Group Relative Policy Optimization)[2]是一种简化的 PPO 变体,专门为 LLM 设计。GRPO 的核心思想是:不需要 Value Model,使用组内相对奖励代替绝对奖励;简化训练流程,只需要 Policy Model 和 Reference Model;提高训练稳定性,减少奖励崩塌的风险。

让我们通过数学公式来理解 GRPO 的原理。PPO 的目标函数为:

JPPO(θ)=Es,a∼πθ[min⁡(πθ(a∣s)πold(a∣s)A(s,a),clip(πθ(a∣s)πold(a∣s),1−ϵ,1+ϵ)A(s,a))]J_{\text{PPO}}(\theta) = \mathbb{E}_{s,a \sim \pi_\theta} \left[ \min\left( \frac{\pi_\theta(a|s)}{\pi_{\text{old}}(a|s)} A(s,a), \text{clip}\left(\frac{\pi_\theta(a|s)}{\pi_{\text{old}}(a|s)}, 1-\epsilon, 1+\epsilon\right) A(s,a) \right) \right]

其中 A(s,a)A(s,a) 是优势函数(Advantage),需要 Value Model 来估计:

A(s,a)=Q(s,a)−V(s)=r(s,a)+γV(s′)−V(s)A(s,a) = Q(s,a) - V(s) = r(s,a) + \gamma V(s') - V(s)

GRPO 的目标函数简化为:

JGRPO(θ)=Es,a∼πθ[πθ(a∣s)πref(a∣s)⋅(r(s,a)−rˉgroup)]−β⋅DKL(πθ∣∣πref)J_{\text{GRPO}}(\theta) = \mathbb{E}_{s,a \sim \pi_\theta} \left[ \frac{\pi_\theta(a|s)}{\pi_{\text{ref}}(a|s)} \cdot (r(s,a) - \bar{r}_{\text{group}}) \right] - \beta \cdot D_{KL}(\pi_\theta || \pi_{\text{ref}})

其中 rˉgroup\bar{r}_{\text{group}} 是组内平均奖励,β\beta 是 KL 散度惩罚系数。关键区别在于:GRPO 使用 r(s,a)−rˉgroupr(s,a) - \bar{r}_{\text{group}} 代替优势函数 A(s,a)A(s,a),不需要 Value Model;GRPO 使用组内相对奖励,减少奖励方差;GRPO 添加 KL 散度惩罚,防止策略偏离太远。

如图 11.7 所示,PPO 和 GRPO 的训练流程对比。

图 11.7 PPO vs GRPO 训练流程

可以看到,GRPO 省去了 Value Model 的训练,大大简化了流程。

如表 11.6 所示,PPO 和 GRPO 的详细对比。

表 11.6 PPO vs GRPO 对比

对于 LLM 训练,GRPO 是更好的选择,因为它更简单、更稳定、显存占用更低。

11.4.2 GRPO 训练实战

现在让我们使用 HelloAgents 进行 GRPO 训练。GRPO 训练的前提是已经完成 SFT 训练,因为 GRPO 需要一个合理的初始策略。

基础 GRPO 训练示例:

from hello_agents.tools import RLTrainingTool

# 创建训练工具
rl_tool = RLTrainingTool()

# GRPO训练
result = rl_tool.run({
    # 训练配置
    "action": "train",
    "algorithm": "grpo",
    
    # 模型配置
    "model_name": "./models/sft_full",  # 从SFT模型开始
    "output_dir": "./models/grpo_model",
    
    # 数据配置
    "max_samples": 100,     # 使用100个样本快速测试
    
    # 训练参数
    "num_epochs": 3,
    "batch_size": 4,
    "learning_rate": 1e-5,  # GRPO学习率通常比SFT小
    
    # GRPO特定参数
    "num_generations": 4,   # 每个问题生成4个答案
    "kl_coef": 0.05,        # KL散度惩罚系数
    
    # LoRA配置
    "use_lora": True,
    "lora_rank": 16,
    "lora_alpha": 32,
    
    # 奖励函数配置
    "reward_type": "accuracy",  # 使用准确率奖励
})

print(f"\n✓ 训练完成!")
print(f"  - 模型保存路径: {result['model_path']}")
print(f"  - 训练样本数: {result['num_samples']}")
print(f"  - 训练轮数: {result['num_epochs']}")
print(f"  - 平均奖励: {result['average_reward']:.4f}")

如果 GRPO 训练过程中平均奖励逐渐提升,KL 散度保持在合理范围内,说明训练正常进行。

GRPO 有一些特定的参数需要理解和调优。

生成参数:

  • num_generations: 每个问题生成多少个答案。越多越好,但计算成本也越高。典型值为 4-8。生成多个答案的目的是计算组内相对奖励,增加训练信号的多样性。
  • max_new_tokens: 每个答案最多生成多少个 token。太少可能截断答案,太多浪费计算。建议 256-512。
  • temperature: 生成温度,控制随机性。0 表示贪婪解码,1 表示标准采样。GRPO 建议 0.7-1.0,保持一定的探索性。

优化参数:

  • learning_rate: GRPO 的学习率通常比 SFT 小,因为我们不想偏离 SFT 模型太远。建议 1e-6 到 1e-5;小模型上学习率过大(如 5e-5)可能导致策略坍塌。
  • kl_coef: KL 散度惩罚系数,控制策略更新的幅度。太小(0.01)可能导致策略偏离太远,太大(0.5)可能限制学习。建议 0.05-0.1。
  • clip_range: 策略比率裁剪范围,类似 PPO 的 epsilon。建议 0.2。

奖励参数:

  • reward_type: 奖励函数类型,可以是"accuracy"、"length_penalty"、"step"或"combined"。
  • reward_config: 奖励函数的额外配置,如长度惩罚的目标长度、步骤奖励的系数等。

让我们进行一次完整的 GRPO 训练,使用全部数据和最佳实践:

from hello_agents.tools import RLTrainingTool

rl_tool = RLTrainingTool()

# 完整GRPO训练
result = rl_tool.run({
    "action": "train",
    "algorithm": "grpo",

    # 模型配置
    "model_name": "./models/sft_full",
    "output_dir": "./models/grpo_full",
    
    # 数据配置
    "max_samples": None,    # 使用全部数据
    
    # 训练参数
    "num_epochs": 3,
    "batch_size": 4,
    "learning_rate": 1e-5,
    "warmup_ratio": 0.1,
    
    # GRPO特定参数
    "num_generations": 4,
    "max_new_tokens": 512,
    "temperature": 0.8,
    "kl_coef": 0.05,
    "clip_range": 0.2,
    
    # LoRA配置
    "use_lora": True,
    "lora_rank": 16,
    "lora_alpha": 32,
    
    # 奖励函数配置
    "reward_type": "combined",
    "reward_config": {
        "components": [
            {"type": "accuracy", "weight": 1.0},
            {"type": "length_penalty", "weight": 0.5, "target_length": 200},
            {"type": "step", "weight": 0.3, "step_bonus": 0.1}
        ]
    },
    
    # 其他配置
    "save_steps": 500,
    "logging_steps": 100,
})

print(f"训练完成! 模型保存在: {result['model_path']}")

11.4.3 GRPO 训练过程解析

让我们深入理解 GRPO 的训练过程,看看每一步都发生了什么。

(1)训练循环

GRPO 的训练循环包括以下步骤:

  1. 采样阶段:对于每个问题,使用当前策略生成多个答案(num_generations个)。这些答案构成一个"组",用于计算相对奖励。

  2. 奖励计算:对每个生成的答案计算奖励 rir_i。奖励可以是准确率、长度惩罚、步骤奖励或它们的组合。

  3. 相对奖励:计算组内平均奖励 rˉ=1N∑i=1Nri\bar{r} = \frac{1}{N}\sum_{i=1}^{N} r_i,然后计算相对奖励 r^i=ri−rˉ\hat{r}_i = r_i - \bar{r}。这样做的好处是减少奖励方差,使训练更稳定。

  4. 策略更新:使用相对奖励更新策略,同时添加 KL 散度惩罚,防止策略偏离参考模型太远。

  5. 重复:重复上述步骤,直到完成所有训练轮次。

让我们通过一个具体例子来理解:

# 假设我们有一个问题
question = "What is 48 + 24?"

# 生成4个答案
answers = [
    "48 + 24 = 72. Final Answer: 72",      # 正确
    "48 + 24 = 72. Final Answer: 72",      # 正确
    "48 + 24 = 70. Final Answer: 70",      # 错误
    "Let me think... 72. Final Answer: 72" # 正确但冗长
]

# 计算奖励(假设使用准确率 + 长度惩罚)
rewards = [1.0, 1.0, 0.0, 0.8]  # 第4个答案因为冗长被惩罚

# 计算组内平均奖励
avg_reward = (1.0 + 1.0 + 0.0 + 0.8) / 4 = 0.7

# 计算相对奖励
relative_rewards = [
    1.0 - 0.7 = 0.3,   # 正确且简洁,相对奖励为正
    1.0 - 0.7 = 0.3,   # 正确且简洁,相对奖励为正
    0.0 - 0.7 = -0.7,  # 错误,相对奖励为负
    0.8 - 0.7 = 0.1    # 正确但冗长,相对奖励较小
]

# 策略更新:增加前两个答案的概率,减少第三个答案的概率

可以看到,相对奖励机制鼓励模型生成"比平均水平更好"的答案,而不是简单地追求高奖励。这样可以减少奖励方差,提高训练稳定性。

(2)KL 散度惩罚

KL 散度惩罚是 GRPO 的关键组成部分,它防止策略偏离参考模型太远。KL 散度定义为:

DKL(πθ∣∣πref)=Es,a∼πθ[log⁡πθ(a∣s)πref(a∣s)]D_{KL}(\pi_\theta || \pi_{\text{ref}}) = \mathbb{E}_{s,a \sim \pi_\theta} \left[ \log \frac{\pi_\theta(a|s)}{\pi_{\text{ref}}(a|s)} \right]

在实践中,我们计算每个 token 的 KL 散度,然后求和:

DKL=∑t=1Tlog⁡πθ(at∣s,a<t)πref(at∣s,a<t)D_{KL} = \sum_{t=1}^{T} \log \frac{\pi_\theta(a_t|s, a_{<t})}{\pi_{\text{ref}}(a_t|s, a_{<t})}

KL 散度越大,说明当前策略与参考模型差异越大。通过添加 KL 散度惩罚项 −β⋅DKL-\beta \cdot D_{KL},我们限制策略更新的幅度,避免"遗忘"SFT 阶段学到的知识。

kl_coef (β\beta) 的选择很重要:

  • 太小(0.01):策略可能偏离太远,导致输出格式混乱或质量下降
  • 太大(0.5):策略更新受限,学习缓慢,难以超越 SFT 模型
  • 建议(0.05-0.1):平衡探索和稳定性

(3)训练监控

在 GRPO 训练过程中,我们需要监控以下指标:

  • 平均奖励(Average Reward):应该逐渐上升。如果奖励不上升,可能是学习率太小、KL 惩罚太大、奖励函数设计不合理。如果奖励先升后降,可能是过拟合或奖励崩塌。

  • KL 散度(KL Divergence):应该保持在合理范围内(0.01-0.1)。如果 KL 散度过大(>0.5),说明策略偏离太远,需要增大 kl_coef 或降低学习率。如果 KL 散度过小(<0.001),说明策略几乎没有更新,需要减小 kl_coef 或增大学习率。

  • 准确率(Accuracy):应该逐渐提升。这是最直观的指标,反映模型的实际能力。

  • 生成质量(Generation Quality):需要人工检查生成的答案,确保格式正确、推理清晰。

HelloAgents 集成了两种主流的训练监控工具:Weights & Biases(wandb)和 TensorBoard。

方式 1:使用 Weights & Biases(推荐)

Weights & Biases 是目前最流行的机器学习实验跟踪平台,提供了强大的可视化和实验管理功能。

import os

# 1. 设置wandb(需要先注册账号: https://wandb.ai)
os.environ["WANDB_PROJECT"] = "hello-agents-grpo"  # 项目名称
os.environ["WANDB_LOG_MODEL"] = "false"            # 不上传模型文件

# 2. 在训练配置中启用wandb
result = rl_tool.run({
    "action": "train",
    "algorithm": "grpo",
    "model_name": "Qwen/Qwen3-0.6B",
    "output_dir": "./models/grpo_monitored",
    "num_epochs": 2,
    "batch_size": 2,
    "use_lora": True,
    # wandb会自动记录所有训练指标
})

# 训练完成后,访问 https://wandb.ai 查看训练曲线

wandb 会自动记录以下指标:

  • train/reward: 平均奖励
  • train/kl: KL 散度
  • train/loss: 训练损失
  • train/learning_rate: 学习率
  • train/epoch: 训练轮数

方式 2:使用 TensorBoard

TensorBoard 是 TensorFlow 提供的可视化工具,也支持 PyTorch 训练。

# 1. 训练时会自动在output_dir下创建tensorboard日志
result = rl_tool.run({
    "action": "train",
    "algorithm": "grpo",
    "model_name": "Qwen/Qwen3-0.6B",
    "output_dir": "./models/grpo_tb",
    "num_epochs": 2,
    "batch_size": 2,
    "use_lora": True,
})

# 2. 启动TensorBoard查看训练曲线
# 在命令行运行:
# tensorboard --logdir=./models/grpo_tb
# 然后访问 http://localhost:6006

方式 3:离线监控(无需外部工具)

如果不想使用 wandb 或 TensorBoard,也可以通过训练日志进行监控:

# 训练过程会打印详细日志
result = rl_tool.run({
    "action": "train",
    "algorithm": "grpo",
    "model_name": "Qwen/Qwen3-0.6B",
    "output_dir": "./models/grpo_simple",
    "num_epochs": 2,
    "batch_size": 2,
    "use_lora": True,
})

# 日志示例:
# Epoch 1/2 | Step 100/500 | Reward: 0.45 | KL: 0.023 | Loss: 1.234
# Epoch 1/2 | Step 200/500 | Reward: 0.52 | KL: 0.031 | Loss: 1.156
# ...

在 GRPO 训练中,可能会遇到一些问题。当奖励不上升时,可能是学习率太小或 KL 惩罚太大限制了策略更新,也可能是奖励函数设计不合理或 SFT 模型质量太差,此时可以增大学习率(从 1e-5 到 5e-5)、减小 kl_coef(从 0.1 到 0.05)、检查奖励函数或重新训练 SFT 模型。

当 KL 散度爆炸(超过 0.5 甚至 1.0)导致生成答案格式混乱时,通常是学习率太大或 KL 惩罚太小,或者奖励函数过于激进,可以降低学习率(从 5e-5 到 1e-5)、增大 kl_coef(从 0.05 到 0.1)、调整奖励函数或使用梯度裁剪。

当生成质量下降(准确率提升但格式混乱、推理不清晰)时,可能是奖励函数只关注准确率忽略了其他质量指标,或 KL 惩罚太小导致模型偏离 SFT 太远,或出现过拟合,此时应使用组合奖励函数同时优化多个指标、增大 kl_coef 保持一致性、减少训练轮数或增加训练数据。

GRPO 训练的显存占用比 SFT 高,因为需要同时生成多个答案并存储参考模型输出,容易出现 OOM。可以通过减小 num_generations(从 8 到 4)、batch_size(从 4 到 2)或 max_new_tokens(从 512 到 256),或使用梯度检查点和混合精度训练来缓解。