第五章:使用 GRPO 进行可验证奖励训练(实战篇)
配套代码:
grpotrainer.py
本篇聚焦 GRPO 的实战:如何用 TRL 的 GRPOTrainer 跑通训练、监控与调试,以及一个完整的数学任务示例。原理分析见上一篇(原理篇)。
1. GRPOTrainer 深度解析
1.1 GRPOTrainer 的工作原理
GRPOTrainer 是整个 GRPO 算法的实现,理解它的工作机制对于调试和优化训练至关重要。
1.1.1 完整训练循环
GRPOTrainer 单步训练流程(详细版)
输入:1 个 batch 的 prompts (例如 batch_size=2)
步骤 1:在线采样(Online Sampling)
- 对每个 prompt:生成 k=4 个不同响应(使用当前策略模型 π_θ)
- 结果:2 prompts × 4 responses = 8 个响应
示例:
- Prompt 1:”计算 5+3”
- Response 1.1: “5+3=8”
- Response 1.2: “首先5加3等于8,答案是8”
- Response 1.3: “计算得出8”
- Response 1.4: “结果为8”
- Prompt 2:”计算 7-2”
- Response 2.1: “7-2=5”
- Response 2.2: “7减去2等于5”
- Response 2.3: “答案是5”
- Response 2.4: “计算结果5”
步骤 2:奖励评估(Reward Evaluation)
- 调用
reward_function(prompts, responses)
示例奖励:
- Prompt 1 的奖励:[0.5, 0.9, 0.6, 0.4]
- Prompt 2 的奖励:[0.7, 0.8, 0.5, 0.6]
步骤 3:计算群组相对优势(Group Relative Advantage)
对每个 prompt 的响应组:
- Prompt 1:
mean_reward_1 = (0.5+0.9+0.6+0.4)/4 = 0.6advantages_1 = [0.5-0.6, 0.9-0.6, 0.6-0.6, 0.4-0.6] = [-0.1, +0.3, 0.0, -0.2]
- Prompt 2:
mean_reward_2 = (0.7+0.8+0.5+0.6)/4 = 0.65advantages_2 = [+0.05, +0.15, -0.15, -0.05]
步骤 4:计算策略损失(Policy Loss)
- 对每个 (response, advantage):
log_prob = log π_θ(response | prompt)policy_loss += -advantage * log_prob
理解:
advantage > 0→ 增加生成该响应的概率advantage < 0→ 减少生成该响应的概率
步骤 5:KL 正则化(KL Regularization)
- 计算当前模型与参考模型的 KL 散度:
kl_penalty = β * KL(π_θ || π_ref) - 作用:防止模型偏离参考模型太远
步骤 6:总损失与反向传播
total_loss = policy_loss + kl_penaltytotal_loss.backward()optimizer.step()
步骤 7:更新参考模型(可选)
- 某些实现会定期更新参考模型为当前模型的 EMA
1.1.2 群组相对优势的数学原理
传统 PPO 的问题:
- 需要价值网络估计状态价值
- 需要额外训练价值网络(计算成本高)
- 价值估计可能不准确(影响训练稳定性)
- 实现复杂
GRPO 的创新: 不需要价值网络!使用群组相对优势
对每个 prompt 生成 k 个响应:{r_1, r_2, ..., r_k}
优势估计:
对于每个 prompt 生成 $k$ 个响应 ${a_1, a_2, …, a_k}$,优势函数定义为:
\[A(s, a_i) = R(s, a_i) - \text{mean}(R(s, \cdot))\]其中:
- $R(s, a_i)$:响应 $i$ 的奖励
- $\text{mean}(R(s, \cdot))$:同组所有响应的平均奖励
直觉理解:
- “这个响应比同组其他响应好多少?”
- 不需要知道”绝对好”的标准
- 只需要相对排名
- 使用组内均值作为 baseline
数学证明(简化):
\[\begin{aligned} \mathbb{E}[A(s,a)] &= \mathbb{E}[R(s,a) - \text{mean}(R(s,\cdot))] \\ &= \mathbb{E}[R(s,a)] - \mathbb{E}[\text{mean}(R(s,\cdot))] \\ &= \mathbb{E}[R(s,a)] - \mathbb{E}[R(s,a)] \\ &= 0 \end{aligned}\]- 优势函数是零均值的
- 减少方差,提高训练稳定性
为什么有效?
- 自动归一化:不同 prompt 的奖励尺度不同,相对优势消除了这个问题
- 减少方差:相对比较比绝对评估更稳定
- 无需价值网络:大幅简化实现
- 适应性强:自动适应不同难度的任务
1.1.3 GRPOTrainer vs PPOTrainer 对比
| 维度 | PPOTrainer | GRPOTrainer |
|---|---|---|
| 价值网络 | 需要 | 不需要 |
| 优势估计 | GAE(Generalized Advantage Estimation) | 群组相对优势 |
| 实现复杂度 | [高] 很复杂 | [中] 适中 |
| 训练稳定性 | [中] 需要调参 | [较高] 较稳定 |
| 显存占用 | [高] 很大(策略模型+价值网络) | [较高] 较大(策略模型) |
| 计算成本 | 高(需要两个网络) | 中等(只需策略网络) |
1.1.4 参考模型的作用
参考模型(Reference Model):
- 通常是训练前的 SFT 模型的副本
- 参数被冻结,不参与训练
- 作用:计算 KL 散度,防止模型偏离太远
为什么需要参考模型?
问题:过度优化(Over-optimization) 如果只优化奖励,模型可能会:
- 生成奇怪的、不自然的文本
- 利用奖励函数的漏洞(reward hacking)
- 丧失预训练时学到的知识
- 和DPO中的参考模型完全一样的作用,相关参数也一样
β (kl_coef) 控制平衡:
- β 大 → 模型更保守,接近参考模型
- β 小 → 模型更激进,追求高奖励
实际实现: GRPOTrainer 会自动创建参考模型:
- 复制当前模型的参数
- 冻结参数(requires_grad=False)
- 仅用于计算 KL 散度
监控指标:
objective/kl:KL 散度值,理想范围为 0.1~5.0- 过小(<0.1):模型几乎没有更新
- 过大(>10):模型偏离太远,可能不稳定
1.2 基本配置
from trl import GRPOConfig, GRPOTrainer
# GRPO 配置
training_args = GRPOConfig(
output_dir="./grpo-output",
# GRPO 核心参数
num_generations=4, # [关键] 每个 prompt 生成几个响应
temperature=0.7, # [关键] 采样温度
kl_coef=0.05, # [关键] KL 正则化系数
# 生成参数
max_new_tokens=256, # 生成的最大 token 数
max_prompt_length=512, # prompt 最大长度
# 训练参数
num_train_epochs=3, # GRPO 可能需要更多 epochs
per_device_train_batch_size=1, # 通常较小
gradient_accumulation_steps=8, # 通过累积增加有效 batch
learning_rate=1e-6, # GRPO 用很小的学习率
# 其他
gradient_checkpointing=True,
bf16=True,
)
1.3 关键参数详解
参数 1:num_generations
num_generations:每个 prompt 生成的响应数量
影响:
- 训练信号稳定性(越大越稳定)
- 计算成本(越大越贵)
- 优势估计质量(越大越准确)
推荐值:
num_generations = 4-8(标准)num_generations = 2-4(快速实验)num_generations = 8-16(高质量训练)
注意:
- 实际
batch_size = per_device_batch_size * num_generations - 需要考虑显存限制
参数 2:temperature
temperature:采样温度,控制生成的多样性
影响:
- temperature 高(如 1.0):生成更多样,探索更多可能性,但可能质量下降
- temperature 低(如 0.5):生成更确定,利用已知好的模式,但可能缺乏探索
推荐值:
temperature = 0.7-0.9(标准)temperature = 0.5-0.7(保守,质量优先)temperature = 0.9-1.2(激进,探索优先)
调整策略:
- 训练初期:较高温度(探索)
- 训练后期:较低温度(利用)
参数 3:kl_coef
kl_coef:KL 正则化系数,类似 DPO 的 beta
作用:
- 防止模型偏离参考模型太远
KL(π_θ || π_ref)← 惩罚项
影响:
- kl_coef 大(如 0.1):模型更保守,改进速度慢但稳定
- kl_coef 小(如 0.01):模型更激进,改进速度快但可能不稳定
推荐值:
kl_coef = 0.05(标准)kl_coef = 0.01-0.03(激进)kl_coef = 0.1-0.2(保守)
监控:
objective/kl应该保持在 < 10- 如果 KL 过大,增加 kl_coef
1.4 数据集准备
# GRPO 只需要 prompt,不需要 response
# 因为响应会在训练时动态生成
from datasets import Dataset
# 方式 1: 从现有数据集加载
dataset = load_dataset("gsm8k", "main")
train_dataset = dataset["train"]
# 转换为 GRPO 格式
def format_for_grpo(sample):
return {
"query": format_prompt(sample["question"]),
"ground_truth": sample["answer"] # 用于奖励计算
}
train_dataset = train_dataset.map(format_for_grpo)
# 方式 2: 自定义数据集
data = {
"query": [
"Question: What is 2+2? Answer:",
"Question: What is 3+3? Answer:",
],
"ground_truth": [4, 6]
}
train_dataset = Dataset.from_dict(data)
1.5 创建 GRPOTrainer
from trl import GRPOTrainer
# 定义奖励函数
def reward_function(prompts, responses):
"""
计算响应的奖励
Args:
prompts: List[str] - prompt 列表
responses: List[str] - 响应列表
Returns:
List[float] - 奖励列表
"""
rewards = []
for prompt, response in zip(prompts, responses):
reward = compute_reward(prompt, response)
rewards.append(reward)
return rewards
# 创建 trainer
trainer = GRPOTrainer(
model=model,
args=training_args,
train_dataset=train_dataset,
processing_class=tokenizer,
reward_function=reward_function, # [关键] 自定义奖励函数
peft_config=lora_config,
)
# 开始训练
trainer.train()
2. 训练监控与调试
2.1 关键指标
| 指标 | 含义 | 期望趋势 |
|---|---|---|
rewards/mean |
平均奖励 | ↑ 上升 |
rewards/best |
最佳奖励 | ↑ 上升 |
rewards/worst |
最差奖励 | ↑ 上升(但可以较慢) |
objective/kl |
KL 散度 | → 保持稳定(< 10) |
objective/entropy |
策略熵 | → 保持适中 |
loss |
训练损失 | ↓ 下降 |
2.2 训练阶段分析
典型训练曲线:
阶段 1:快速改进期(Step 0-500)
rewards/mean:0.2 → 0.5 → 0.7- 现象:模型快速学习基本模式
阶段 2:平稳提升期(Step 500 至 2000)
rewards/mean:0.7 → 0.8 → 0.85- 现象:模型优化细节
阶段 3:收敛期(Step 2000+)
rewards/mean:0.85 → 0.87 → 0.88- 现象:改进变慢,接近上限
警告信号:
rewards/mean不上升 → 检查奖励函数或学习率objective/kl过大(>20)→ 增加kl_coefloss不收敛 → 减小学习率
2.3 常见问题与解决方案
问题 1:平均奖励不上升
可能原因:
- 奖励函数设计不当(所有响应奖励相同)
- 学习率过小或过大
num_generations太小(优势估计不准)- 温度设置不当
解决方案:
- 检查奖励函数:确保有区分度
- 调整学习率:尝试
5e-7或2e-6 - 增加
num_generations到 8 - 调整
temperature到 0.8-1.0
问题 2:KL 散度过大
现象:objective/kl > 20
原因:模型偏离参考模型太远
解决方案:
- 增加
kl_coef(如从0.05→0.1) - 减小学习率
- 减少训练步数
问题 3:显存不足
GRPO 显存占用 ≈ base_model + num_generations × batch_size
解决方案:
- 减小
num_generations(8 → 4) - 减小
per_device_train_batch_size(2 → 1) - 启用
gradient_checkpointing - 使用 4-bit 量化
- 减小
max_new_tokens
问题 4:训练速度慢
GRPO 比 SFT/DPO 慢的原因:
- 需要在线生成响应
- 需要计算奖励函数
- 生成多个响应增加计算量
加速方案:
- 启用 Flash Attention
- 使用
bf16混合精度 - 优化奖励函数计算
- 使用更少的
num_generations
3. 完整示例:数学问题求解
3.1 数据准备
from datasets import load_dataset
# 加载 GSM8K 数据集
dataset = load_dataset("gsm8k", "main")
# 格式化为 GRPO 格式
def format_math_problem(sample):
prompt = f"""Question: {sample['question']}
Please solve this step by step and provide your final answer after ####.
Answer:"""
# 提取正确答案
answer = extract_number(sample['answer'])
return {
"query": prompt,
"ground_truth": answer
}
train_dataset = dataset["train"].map(format_math_problem)
3.2 奖励函数
import re
def math_reward_function(prompts, responses):
"""数学问题奖励函数"""
rewards = []
for prompt, response in zip(prompts, responses):
reward = 0.0
# 检查格式:是否包含 ####
if "####" in response:
reward += 0.2
# 检查推理:是否包含数学运算
if any(op in response for op in ['+', '-', '*', '/', '=']):
reward += 0.2
# 检查答案(如果有 ground_truth)
predicted = extract_answer(response)
if predicted is not None:
reward += 0.2
# 这里需要从 prompt 中恢复 ground_truth
# 实际应用中需要维护映射
# if predicted == ground_truth:
# reward += 0.4
rewards.append(reward)
return rewards
3.3 训练配置
from trl import GRPOConfig, GRPOTrainer
# 配置
config = GRPOConfig(
output_dir="./grpo-math-output",
num_generations=6, # 每题生成 6 个解答
temperature=0.8, # 适中的温度
kl_coef=0.05, # 标准 KL 系数
max_new_tokens=300, # 数学题需要较长推理
learning_rate=5e-7, # 较小学习率
num_train_epochs=3,
per_device_train_batch_size=1,
gradient_accumulation_steps=16,
gradient_checkpointing=True,
bf16=True,
logging_steps=10,
)
# 创建 trainer
trainer = GRPOTrainer(
model=model,
args=config,
train_dataset=train_dataset,
processing_class=tokenizer,
reward_function=math_reward_function,
peft_config=lora_config,
)
# 训练
trainer.train()
4. 运行代码
4.1 环境准备
# 安装依赖
pip install transformers datasets peft bitsandbytes accelerate trl
# 可选:安装 Flash Attention 以加速训练
pip install flash-attn --no-build-isolation
4.2 快速开始
# 运行训练
python grpotrainer.py
# 监控训练(如果使用 tensorboard)
tensorboard --logdir ./grpo-training-output/logs
4.3 自定义配置
在 grpotrainer.py 中修改:
# 模型
MODEL_NAME = "Qwen/Qwen3-8B"
# 数据集
DATASET_NAME = "gsm8k" # 或其他数据集
# GRPO 参数
NUM_GENERATIONS = 4 # 每个 prompt 生成数
KL_COEF = 0.05 # KL 正则化系数
TEMPERATURE = 0.7 # 采样温度
# 修改奖励函数(在代码中)
def reward_function(prompts, responses):
# 你的自定义奖励逻辑
...
5. 本章小结
| 概念 | 说明 |
|---|---|
| GRPO | 群组相对策略优化,支持自定义奖励 |
| 可验证奖励 | 基于明确标准的奖励(如正确性) |
| 群组相对优势 | 使用同组响应的相对排名估计优势 |
| 在线采样 | 训练时动态生成响应,而非使用离线数据 |
| num_generations | 关键参数,控制每个 prompt 的采样数 |
小结
这五篇 Blog 的撰写过程是在一起完成的,它们都使用 HF 生态的 TRL 实现不同问题的 Finetune。基本上从最简单的基础开始,逐步详细介绍了 HF 最核心的几个库的使用,并且提供了详细的代码作为参考,
LLM Finetune 还有很多值得研究的工具,无论是侧重单卡性能优化的Unsloth等工具,还是侧重使用多卡分布式训练的 DeepSpeed , FSDP , Accelerate等框架,以及推理工具vllm 和配套Online RL 工具 Verl。 至于会不会有更多的Blog 来讨论他们,留给后来的我再决定吧。
Ps. 我们还没有实现一个完全自定义的外部奖励API,并通过这个任意程度自定义的API去实现GRPO或者类似的RL算法,所以大概率会有下一章,应该会使用Verl?
回顾
| 章节 | 主要内容 | 核心技能 |
|---|---|---|
| 第一章: Trainer | Loss Masking、量化、LoRA | [已完成] 手动 SFT 实现 |
| 第二章: SFTTrainer | TRL 库、自动化 SFT | [已完成] 高效 SFT 训练 |
| 第三章: DPO | 偏好对齐、离线 RL | [已完成] 人类偏好学习 |
| 第四章: GRPO(原理篇) | 在线采样、群组相对优势、奖励系统 | [已完成] 方法论与直觉 |
| 第五章: GRPO(实战篇) | GRPOTrainer、监控调试、完整示例 | [已完成] 跑通训练与调参 |
常见问题
Q: GRPO 训练速度很慢怎么办?
A: 1) 减小 num_generations 2) 启用 Flash Attention 3) 优化奖励函数计算 4) 使用更小的数据集快速迭代Q: 平均奖励不上升?
A: 1) 检查奖励函数是否有区分度 2) 增加 num_generations 3) 调整学习率 4) 检查温度设置Q: GRPO 和 DPO 能否结合使用?
A: 可以!推荐流程:SFT → DPO → GRPO,这样模型既有通用能力,又能针对特定任务优化Q: 如何评估 GRPO 训练效果?
A: 1) 监控 rewards/mean 上升趋势 2) 在测试集上评估任务指标(如数学题准确率) 3) 人工评估生成质量