Re0-05 : TRL GRPOTrainer(实战篇)
博客列表 主页

第五章:使用 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.6
    • advantages_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.65
    • advantages_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_penalty
  • total_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}\]
  • 优势函数是零均值的
  • 减少方差,提高训练稳定性

为什么有效?

  1. 自动归一化:不同 prompt 的奖励尺度不同,相对优势消除了这个问题
  2. 减少方差:相对比较比绝对评估更稳定
  3. 无需价值网络:大幅简化实现
  4. 适应性强:自动适应不同难度的任务

1.1.3 GRPOTrainer vs PPOTrainer 对比

维度 PPOTrainer GRPOTrainer
价值网络 需要 不需要
优势估计 GAE(Generalized Advantage Estimation) 群组相对优势
实现复杂度 [高] 很复杂 [中] 适中
训练稳定性 [中] 需要调参 [较高] 较稳定
显存占用 [高] 很大(策略模型+价值网络) [较高] 较大(策略模型)
计算成本 高(需要两个网络) 中等(只需策略网络)

1.1.4 参考模型的作用

参考模型(Reference Model)

  • 通常是训练前的 SFT 模型的副本
  • 参数被冻结,不参与训练
  • 作用:计算 KL 散度,防止模型偏离太远

为什么需要参考模型?

问题:过度优化(Over-optimization) 如果只优化奖励,模型可能会:

  1. 生成奇怪的、不自然的文本
  2. 利用奖励函数的漏洞(reward hacking)
  3. 丧失预训练时学到的知识
  4. 和DPO中的参考模型完全一样的作用,相关参数也一样

β (kl_coef) 控制平衡

  • β 大 → 模型更保守,接近参考模型
  • β 小 → 模型更激进,追求高奖励

实际实现: GRPOTrainer 会自动创建参考模型:

  1. 复制当前模型的参数
  2. 冻结参数(requires_grad=False)
  3. 仅用于计算 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_coef
  • loss 不收敛 → 减小学习率

2.3 常见问题与解决方案

问题 1:平均奖励不上升

可能原因

  1. 奖励函数设计不当(所有响应奖励相同)
  2. 学习率过小或过大
  3. num_generations 太小(优势估计不准)
  4. 温度设置不当

解决方案

  • 检查奖励函数:确保有区分度
  • 调整学习率:尝试 5e-72e-6
  • 增加 num_generations 到 8
  • 调整 temperature 到 0.8-1.0

问题 2:KL 散度过大

现象objective/kl > 20

原因:模型偏离参考模型太远

解决方案

  • 增加 kl_coef(如从 0.050.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 慢的原因

  1. 需要在线生成响应
  2. 需要计算奖励函数
  3. 生成多个响应增加计算量

加速方案

  • 启用 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) 人工评估生成质量

参考资料