强化学习中的GRPO算法讲解
强化学习中的GRPO算法讲解
GRPO (Group Relative Policy Optimization) 详解
GRPO是一种用于大语言模型对齐的强化学习算法,由DeepSeek团队提出。它是RLHF(Reinforcement Learning from Human Feedback)的一种高效变体。
核心思想
GRPO的关键创新是:不需要单独训练critic模型(价值函数网络),而是通过组内相对比较来估计优势函数(advantage)。
关键公式
1. 优势函数估计
对于同一个prompt生成的G个输出,GRPO使用组平均作为基线:
Ai=ri−1G∑j=1GrjA_i = r_i - \frac{1}{G}\sum_{j=1}^{G} r_jAi=ri−G1j=1∑Grj
Note:原始论文的公式为:

代码实现的时候,一般来说底下的std为了不为0,要加一个很小的数比如e的负4次方
其中:
- rir_iri 是第i个输出的奖励分数
- AiA_iAi 是第i个输出的优势值
- G 是组大小(通常为4-8)
2. 策略梯度目标
LGRPO=E[min(πθ(y∣x)πref(y∣x)⋅A,clip(πθ(y∣x)πref(y∣x),1−ϵ,1+ϵ)⋅A)]−β⋅KL(πθ∣∣πref)L_{GRPO} = \mathbb{E}\left[\min\left(\frac{\pi_\theta(y|x)}{\pi_{ref}(y|x)} \cdot A, \text{clip}\left(\frac{\pi_\theta(y|x)}{\pi_{ref}(y|x)}, 1-\epsilon, 1+\epsilon\right) \cdot A\right)\right] - \beta \cdot KL(\pi_\theta || \pi_{ref})LGRPO=E[min(πref(y∣x)πθ(y∣x)⋅A,clip(πref(y∣x)πθ(y∣x),1−ϵ,1+ϵ)⋅A)]−β⋅KL(πθ∣∣πref)
其中:
- πθ\pi_\thetaπθ 是当前策略
- πref\pi_{ref}πref 是参考策略(防止偏离太远)
- ϵ\epsilonϵ 是clip参数(通常0.2)
- β\betaβ 是KL惩罚系数
deepseek的公式:

数值示例
假设我们有一个问答任务:
输入(Prompt): “什么是光合作用?”
生成4个回答(G=4):
| 输出ID | 回答内容 | 奖励分数 rir_iri | 概率比 πθπref\frac{\pi_\theta}{\pi_{ref}}πrefπθ |
|---|---|---|---|
| 1 | “植物制造养分的过程” | 0.6 | 1.3 |
| 2 | “光合作用是植物利用光能、水和二氧化碳合成有机物并释放氧气的过程” | 0.9 | 1.1 |
| 3 | “不知道” | 0.1 | 0.8 |
| 4 | “植物吸收阳光” | 0.4 | 1.2 |
计算步骤
Step 1: 计算组平均奖励
rˉ=0.6+0.9+0.1+0.44=2.04=0.5\bar{r} = \frac{0.6 + 0.9 + 0.1 + 0.4}{4} = \frac{2.0}{4} = 0.5rˉ=40.6+0.9+0.1+0.4=42.0=0.5
Step 2: 计算每个输出的优势值
- A1=0.6−0.5=0.1A_1 = 0.6 - 0.5 = 0.1A1=0.6−0.5=0.1 (略好于平均)
- A2=0.9−0.5=0.4A_2 = 0.9 - 0.5 = 0.4A2=0.9−0.5=0.4 (明显好于平均)
- A3=0.1−0.5=−0.4A_3 = 0.1 - 0.5 = -0.4A3=0.1−0.5=−0.4 (明显差于平均)
- A4=0.4−0.5=−0.1A_4 = 0.4 - 0.5 = -0.1A4=0.4−0.5=−0.1 (略差于平均)
Step 3: 计算策略梯度(简化版,假设 ϵ=0.2\epsilon=0.2ϵ=0.2)
对于输出2(最好的回答):
- 概率比 = 1.1
- Clip范围 = [0.8, 1.2]
- 由于 1.1 在范围内,使用原值
- 贡献 = 1.1 × 0.4 = 0.44 (正梯度,鼓励这个输出)
对于输出3(最差的回答):
- 概率比 = 0.8
- Clip范围 = [0.8, 1.2]
- 由于 0.8 在范围内,使用原值
- 贡献 = 0.8 × (-0.4) = -0.32 (负梯度,抑制这个输出)
GRPO的优势
- 无需Critic模型 - 节省50%的显存和计算
- 稳定训练 - 组内比较减少方差
- 易于实现 - 相比PPO更简单
- 高效采样 - 一次生成多个样本用于训练
与PPO的对比
| 特性 | PPO | GRPO |
|---|---|---|
| Critic模型 | 需要 | 不需要 |
| 优势估计 | 通过Value函数 | 通过组平均 |
| 显存占用 | 高(2个模型) | 低(1个模型) |
| 训练稳定性 | 较好 | 更好 |
这就是GRPO的核心原理!它通过巧妙的组内比较机制,既保持了强化学习的优化效果,又大幅降低了计算成本。
用户:
1.关于新旧两种策略是怎么来的?是当前训练模型生成的吗?2.我希望你能够将上面例子中,最终的Loss计算出来
Claude:
好问题!让我详细解答这两个问题。
1. 新旧策略的来源
旧策略 πref\pi_{ref}πref (Reference Policy):
- 在训练开始前,保存当前模型的一个冻结副本
- 这个副本在整个GRPO训练过程中不更新参数
- 作用:防止新策略偏离太远,保持生成质量
新策略 πθ\pi_\thetaπθ (Current Policy):
- 就是正在训练的模型
- 参数会根据梯度不断更新
- 每次迭代都会变化
具体流程:
初始化: model = 预训练好的LLM
Step 1: π_ref = copy(model) # 冻结副本
freeze(π_ref) # 不更新参数
Step 2: π_θ = model # 当前模型(会更新)
训练循环:
- 用 π_θ 生成多个输出
- 用 π_ref 计算概率(作为基准)
- 计算loss并更新 π_θ
- π_ref 保持不变
2. 完整的Loss计算示例
让我用之前的例子完整计算Loss。
给定数据(扩展版)
假设我们需要计算输出2的Loss(最好的那个回答)
已知信息:
- r2=0.9r_2 = 0.9r2=0.9
- A2=0.4A_2 = 0.4A2=0.4
- πθ(y2∣x)πref(y2∣x)=1.1\frac{\pi_\theta(y_2|x)}{\pi_{ref}(y_2|x)} = 1.1πref(y2∣x)πθ(y2∣x)=1.1
- ϵ=0.2\epsilon = 0.2ϵ=0.2 (clip参数)
- β=0.01\beta = 0.01β=0.01 (KL惩罚系数)
输出2的token序列及概率(假设):
| Token | 新策略 πθ\pi_\thetaπθ | 旧策略 πref\pi_{ref}πref |
|---|---|---|
| 光合 | 0.8 | 0.7 |
| 作用 | 0.9 | 0.85 |
| 是 | 0.95 | 0.9 |
| 植物 | 0.85 | 0.8 |
| … | … | … |
Step-by-Step 计算
Step 1: 计算序列的概率比
假设完整序列的对数概率:
- logπθ(y2∣x)=−2.5\log \pi_\theta(y_2|x) = -2.5logπθ(y2∣x)=−2.5
- logπref(y2∣x)=−2.6\log \pi_{ref}(y_2|x) = -2.6logπref(y2∣x)=−2.6
概率比:
ratio=πθ(y2∣x)πref(y2∣x)=e−2.5−(−2.6)=e0.1≈1.105ratio = \frac{\pi_\theta(y_2|x)}{\pi_{ref}(y_2|x)} = e^{-2.5 - (-2.6)} = e^{0.1} \approx 1.105ratio=πref(y2∣x)πθ(y2∣x)=e−2.5−(−2.6)=e0.1≈1.105
我们之前说的1.1就是这个值。
Step 2: 计算Clipped比率
Clip范围:[1−ϵ,1+ϵ]=[0.8,1.2][1-\epsilon, 1+\epsilon] = [0.8, 1.2][1−ϵ,1+ϵ]=[0.8,1.2]
由于 1.105∈[0.8,1.2]1.105 \in [0.8, 1.2]1.105∈[0.8,1.2],所以:
ratioclipped=1.105ratio_{clipped} = 1.105ratioclipped=1.105
Step 3: 计算策略梯度项
LPG=min(ratio⋅A2, ratioclipped⋅A2)L_{PG} = \min(ratio \cdot A_2, \ ratio_{clipped} \cdot A_2)LPG=min(ratio⋅A2, ratioclipped⋅A2)
LPG=min(1.105×0.4, 1.105×0.4)L_{PG} = \min(1.105 \times 0.4, \ 1.105 \times 0.4)LPG=min(1.105×0.4, 1.105×0.4)
LPG=0.442L_{PG} = 0.442LPG=0.442
Step 4: 计算KL散度惩罚
KL散度(近似):
KL=logπref(y2∣x)−logπθ(y2∣x)=−2.6−(−2.5)=−0.1KL = \log \pi_{ref}(y_2|x) - \log \pi_\theta(y_2|x) = -2.6 - (-2.5) = -0.1KL=logπref(y2∣x)−logπθ(y2∣x)=−2.6−(−2.5)=−0.1
或者更精确的逐token计算:
KL≈∑tokensπθ⋅logπθπrefKL \approx \sum_{tokens} \pi_\theta \cdot \log \frac{\pi_\theta}{\pi_{ref}}KL≈tokens∑πθ⋅logπrefπθ
假设计算出 KL=0.05KL = 0.05KL=0.05
Step 5: 计算总Loss(针对输出2)
LGRPO(2)=LPG−β⋅KLL_{GRPO}^{(2)} = L_{PG} - \beta \cdot KLLGRPO(2)=LPG−β⋅KL
LGRPO(2)=0.442−0.01×0.05L_{GRPO}^{(2)} = 0.442 - 0.01 \times 0.05LGRPO(2)=0.442−0.01×0.05
LGRPO(2)=0.442−0.0005L_{GRPO}^{(2)} = 0.442 - 0.0005LGRPO(2)=0.442−0.0005
LGRPO(2)=0.4415L_{GRPO}^{(2)} = 0.4415LGRPO(2)=0.4415
完整的Batch Loss
对所有4个输出分别计算,然后取平均:
| 输出 | AiA_iAi | ratio | LPGL_{PG}LPG | KL | LGRPO(i)L_{GRPO}^{(i)}LGRPO(i) |
|---|---|---|---|---|---|
| 1 | 0.1 | 1.3 | 0.13 | 0.08 | 0.1292 |
| 2 | 0.4 | 1.1 | 0.44 | 0.05 | 0.4395 |
| 3 | -0.4 | 0.8 | -0.32 | -0.02 | -0.3198 |
| 4 | -0.1 | 1.2 | -0.12 | 0.06 | -0.1206 |
最终的Batch Loss:
Ltotal=14(0.1292+0.4395−0.3198−0.1206)L_{total} = \frac{1}{4}(0.1292 + 0.4395 - 0.3198 - 0.1206)Ltotal=41(0.1292+0.4395−0.3198−0.1206)
Ltotal=0.12834=0.0321L_{total} = \frac{0.1283}{4} = 0.0321Ltotal=40.1283=0.0321
梯度更新
由于这是最大化目标,实际优化时:
θ←θ+α⋅∇θLtotal\theta \leftarrow \theta + \alpha \cdot \nabla_\theta L_{total}θ←θ+α⋅∇θLtotal
其中 α\alphaα 是学习率(如 3×10−63 \times 10^{-6}3×10−6)
GRPO的额外补充

1.deepseek的原始公式如上,其中“old策略” 和 “ref策略” 指的是两个完全不同的模型。ref模型是最开始就冻结的要训练的模型。
2.π_old是主策略模型的一个“快照”:在每一轮训练迭代的开始,训练框架会通过 model_old.load_state_dict(model_train.state_dict())这样的操作,将当前主策略模型 π_θ的参数复制给一个用于充当 π_old的模型实例或直接保存为参数快照。
3.π_old的快照生成时会占用空间,和训练模型大小一样。π_old的快照默认是每次迭代训练都进行的。也可以两轮或三轮,但是不推荐。
其实π_old快照会导致有一个时间,显存容纳三个模型
GROP的demo代码
import torch
import numpy as np
"""
GRPO 算法核心逻辑演示
展示完整的数值计算流程
"""
def grpo_advantage_calculation():
"""
演示 GRPO 的优势函数计算
"""
print("=" * 60)
print("步骤 1: 优势函数计算(GRPO 核心)")
print("=" * 60)
# 配置
num_prompts = 2
num_generations = 4
# 原始奖励(来自奖励函数)
rewards = torch.tensor([
# Prompt 1 的 4 个生成
0.936, 0.920, 0.800, 0.804,
# Prompt 2 的 4 个生成
0.880, 0.856, 0.760, 0.896,
])
print(f"\n原始奖励:")
print(f"Prompt 1: {rewards[:4].tolist()}")
print(f"Prompt 2: {rewards[4:].tolist()}")
# 重塑为分组形式 [num_prompts, num_generations]
grouped_rewards = rewards.view(num_prompts, num_generations)
print(f"\n分组奖励 shape: {grouped_rewards.shape}")
print(grouped_rewards)
# 计算每组的均值和标准差
mean_grouped = grouped_rewards.mean(dim=1) # [num_prompts]
std_grouped = grouped_rewards.std(dim=1) # [num_prompts]
print(f"\n每组的均值: {mean_grouped.tolist()}")
print(f"每组的标准差: {std_grouped.tolist()}")
# 扩展维度以匹配原始形状
mean_expanded = mean_grouped.repeat_interleave(num_generations)
std_expanded = std_grouped.repeat_interleave(num_generations)
print(f"\n扩展后的均值: {mean_expanded.tolist()}")
print(f"扩展后的标准差: {std_expanded.tolist()}")
# 计算优势函数(归一化)
advantages = (rewards - mean_expanded) / (std_expanded + 1e-4)
print(f"\n优势函数(Advantages):")
print(f"Prompt 1: {advantages[:4].tolist()}")
print(f"Prompt 2: {advantages[4:].tolist()}")
print("\n解释:")
print("- 正值:比平均水平好,应该增强")
print("- 负值:比平均水平差,应该削弱")
print("- 零值:就是平均水平")
return advantages
def grpo_loss_calculation():
"""
演示 GRPO 的损失函数计算
"""
print("\n" + "=" * 60)
print("步骤 2: 损失函数计算")
print("=" * 60)
# 使用前面计算的优势函数
advantages = torch.tensor([0.97, 0.75, -0.89, -0.83, 0.52, 0.13, -1.44, 0.79])
# 假设每个 completion 有 5 个 tokens
seq_length = 5
batch_size = 8
# 当前策略的 log probabilities
per_token_logps = torch.randn(batch_size, seq_length) * 0.1 - 0.5
# 旧策略的 log probabilities(第一次迭代时相同)
old_per_token_logps = per_token_logps.clone()
# 参考模型的 log probabilities(通常更保守)
ref_per_token_logps = per_token_logps - 0.1
print(f"\nLog probabilities shape: {per_token_logps.shape}")
print(f"第一个样本的 log probs: {per_token_logps[0].tolist()}")
# 计算概率比率
log_ratio = per_token_logps - old_per_token_logps
ratio = torch.exp(log_ratio)
print(f"\n概率比率(ratio):")
print(f"第一个样本: {ratio[0].tolist()}")
print("(第一次迭代时都接近 1.0)")
# PPO-style 裁剪
epsilon = 0.2
ratio_clipped = torch.clamp(ratio, 1 - epsilon, 1 + epsilon)
print(f"\n裁剪后的比率(范围 [{1-epsilon}, {1+epsilon}]):")
print(f"第一个样本: {ratio_clipped[0].tolist()}")
# 计算每个 token 的损失
advantages_expanded = advantages.unsqueeze(1) # [batch_size, 1]
per_token_loss1 = ratio * advantages_expanded
per_token_loss2 = ratio_clipped * advantages_expanded
per_token_loss = -torch.min(per_token_loss1, per_token_loss2)
print(f"\n每个 token 的损失 shape: {per_token_loss.shape}")
print(f"第一个样本(advantage={advantages[0]:.2f}):")
print(f" 损失: {per_token_loss[0].tolist()}")
# KL 散度
beta = 0.1
kl_div = torch.exp(ref_per_token_logps - per_token_logps) - (ref_per_token_logps - per_token_logps) - 1
per_token_loss_with_kl = per_token_loss + beta * kl_div
print(f"\n加上 KL 惩罚后:")
print(f"第一个样本: {per_token_loss_with_kl[0].tolist()}")
# 序列级别的损失(GRPO 风格)
completion_mask = torch.ones(batch_size, seq_length) # 假设都是有效 token
loss_per_sequence = (per_token_loss_with_kl * completion_mask).sum(1) / completion_mask.sum(1)
print(f"\n每个序列的损失:")
for i in range(batch_size):
print(f" 样本 {i+1} (adv={advantages[i]:6.2f}): loss={loss_per_sequence[i]:.4f}")
# 最终损失
final_loss = loss_per_sequence.mean()
print(f"\n最终损失: {final_loss:.4f}")
return final_loss
def visualize_grpo_effect():
"""
可视化 GRPO 的学习效果
"""
print("\n" + "=" * 60)
print("步骤 3: GRPO 的学习效果可视化")
print("=" * 60)
# 假设一个 prompt 的 4 个生成
completions = [
"Completion 1 (最好)",
"Completion 2 (较好)",
"Completion 3 (较差)",
"Completion 4 (最差)"
]
# 奖励和优势
rewards = torch.tensor([0.9, 0.85, 0.7, 0.65])
mean_reward = rewards.mean()
std_reward = rewards.std()
advantages = (rewards - mean_reward) / std_reward
print(f"\n{'Completion':<20} {'奖励':>8} {'优势':>8} {'学习方向':>12}")
print("-" * 60)
for i, comp in enumerate(completions):
direction = "↑ 增强" if advantages[i] > 0 else "↓ 削弱"
print(f"{comp:<20} {rewards[i]:>8.2f} {advantages[i]:>8.2f} {direction:>12}")
print("\n关键洞察:")
print("1. GRPO 通过组内对比学习")
print("2. 不需要知道 0.9 是高分还是低分")
print("3. 只需要知道 0.9 > 0.65,就知道应该强化前者")
print("4. 标准化后的优势函数让训练更稳定")
def compare_with_without_clipping():
"""
对比有无裁剪的区别
"""
print("\n" + "=" * 60)
print("步骤 4: 裁剪机制的作用")
print("=" * 60)
# 模拟一个 token 的概率大幅变化
old_prob = 0.01
new_prob = 0.15
ratio = new_prob / old_prob
advantage = 0.5 # 这是一个好的输出
epsilon = 0.2
print(f"\n场景: 某个 token 的概率从 {old_prob} 增长到 {new_prob}")
print(f"概率比率: {ratio:.2f}")
print(f"优势函数: {advantage}")
# 不裁剪
loss_no_clip = -ratio * advantage
# 裁剪
ratio_clipped = min(ratio, 1 + epsilon)
loss_with_clip = -ratio_clipped * advantage
print(f"\n不裁剪的损失: {loss_no_clip:.4f}")
print(f"裁剪后的损失: {loss_with_clip:.4f}")
print(f"差异: {abs(loss_no_clip - loss_with_clip):.4f}")
print("\n为什么需要裁剪?")
print(f"- 不裁剪: ratio={ratio:.1f} 太大,导致梯度爆炸")
print(f"- 裁剪后: ratio={ratio_clipped:.1f} 被限制,更新更平稳")
print("- 结果: 训练更稳定,不会因为单个样本崩溃")
def simulate_multi_iteration():
"""
模拟多轮迭代的过程
"""
print("\n" + "=" * 60)
print("步骤 5: 多轮迭代(num_iterations > 1)")
print("=" * 60)
# 初始的 log probabilities
initial_logp = -0.5
old_logp = initial_logp # 保持不变
current_logp = initial_logp
advantage = 1.0 # 一个好的输出
learning_rate = 0.01
print(f"\n初始 log prob: {initial_logp}")
print(f"优势函数: {advantage}")
print(f"学习率: {learning_rate}")
print(f"\n{'轮次':>6} {'当前logp':>12} {'比率':>8} {'损失':>10} {'梯度方向':>10}")
print("-" * 60)
for iteration in range(5):
# 计算比率
ratio = np.exp(current_logp - old_logp)
# 计算损失(简化版)
loss = -ratio * advantage
# 梯度方向(正 = 增加概率)
gradient_direction = "增加↑" if advantage > 0 else "减少↓"
print(f"{iteration+1:>6} {current_logp:>12.4f} {ratio:>8.4f} {loss:>10.4f} {gradient_direction:>10}")
# 更新(简化的梯度下降)
current_logp += learning_rate * advantage * ratio
print(f"\n最终 log prob: {current_logp:.4f}")
print(f"变化: {current_logp - initial_logp:+.4f}")
print("\n观察:")
print("- 随着迭代,模型逐渐增加好输出的概率")
print("- ratio 逐渐偏离 1.0,但被裁剪机制控制")
print("- old_logp 保持不变,作为参考点")
if __name__ == "__main__":
print("\n🎓 GRPO 算法完整演示\n")
# 1. 优势函数计算
advantages = grpo_advantage_calculation()
# 2. 损失函数计算
loss = grpo_loss_calculation()
# 3. 可视化效果
visualize_grpo_effect()
# 4. 裁剪机制
compare_with_without_clipping()
# 5. 多轮迭代
simulate_multi_iteration()
print("\n" + "=" * 60)
print("🎉 演示完成!")
print("=" * 60)
print("\n关键要点:")
print("1. 优势函数:组内归一化 → 相对排名学习")
print("2. 裁剪机制:防止概率变化过大 → 稳定训练")
print("3. KL 惩罚:保持生成的自然性")
print("4. 多轮迭代:充分利用生成的数据")
print("\n这就是 GRPO 的核心!🚀")
Qwen2.5vl实机演示
首先是要设置config:

GRPOScriptArguments是自己定义的
GRPOConfig是官方定义好的,但是有些参数要自己在命令行中加上
ModelConfig也是官方定义好的,同上
然后是设置奖励函数和加载数据:

数据集加载是写成了一个函数



example是个字典

最后直接dataset = Dataset.from_list(examples)

这个函数也是先写好再绑定的:
dataset.getitem = getitem.get(dataset, Dataset) # Bind getitem to the dataset
设置训练器:

训练器中init会初始化模型,以及reference模型

这里可以看到使用deepspeed之后refer模型就是初始化一个最开始的模型
GRPO的核心计算
每一组生成4个答案
输入阶段
视频信息:
- 视频路径:
video_001.mp4 - 总时长:40秒
- 事件描述:
"person picking up the phone"
问题模板:
To accurately pinpoint the event "person picking up the phone" in the video,
determine the precise time period of the event.
Output in format: "start time to end time"
生成阶段(Generate 4 Completions)
模型生成4个不同的预测:
num_generations = 4 # 每个prompt生成4个补全
# 生成的4个补全
completions = [
"<think>分析视频...</think><answer>10.00 to 12.00</answer>", # 补全0:预测10-12秒
"<think>分析视频...</think><answer>15.30 to 18.50</answer>", # 补全1:预测15.3-18.5秒
"<think>分析视频...</think><answer>15.00 to 18.00</answer>", # 补全2:预测15-18秒
"<think>分析视频...</think><answer>14.80 to 18.20</answer>", # 补全3:预测14.8-18.2秒
]
# Ground Truth(真实标注)
ground_truth = (15.0, 18.0) # 真实事件发生在15-18秒
🎯 奖励计算阶段
``python
代码中使用两个奖励函数(见第682行)
self.reward_funcs = [REWARD_FUNCS[“iou”], REWARD_FUNCS[“format”]]
对每个补全计算奖励
rewards_per_func = torch.tensor([
# [IoU奖励, 格式奖励]
[0.0, 1.0], # 补全0: IoU=0(完全不重叠),格式正确+1
[0.6, 1.0], # 补全1: IoU=0.6(部分重叠),格式正确+1
[0.9, 1.0], # 补全2: IoU=0.9(高度重叠),格式正确+1
[0.8, 1.0], # 补全3: IoU=0.8(较高重叠),格式正确+1
])
假设reward_weights = [5.0, 1.0](IoU权重更高)
最终奖励 = IoU奖励 × 5.0 + 格式奖励 × 1.0
rewards = torch.tensor([
0.0 * 5.0 + 1.0 * 1.0, # = 1.0
0.6 * 5.0 + 1.0 * 1.0, # = 4.0
0.9 * 5.0 + 1.0 * 1.0, # = 5.5
0.8 * 5.0 + 1.0 * 1.0, # = 5.0
])
实际使用你给的奖励值
rewards = torch.tensor([0.0, 1.6, 5.4, 8.8])
---
## 🔥 GRPO核心计算(重点!)
### **Step 1: 组内归一化(Group-wise Normalization)**
```python
# 这4个补全来自同一个prompt,属于同一组
num_generations = 4
# 计算组内均值和标准差(第710-711行)
mean_grouped_rewards = rewards.mean()
# = (0.0 + 1.6 + 5.4 + 8.8) / 4 = 3.95
std_grouped_rewards = rewards.std()
# = sqrt(((0-3.95)² + (1.6-3.95)² + (5.4-3.95)² + (8.8-3.95)²) / 4)
# = sqrt((15.6025 + 5.5225 + 2.1025 + 23.5225) / 4)
# = sqrt(11.687) = 3.418
Step 2: 计算优势函数(Advantages)
# 计算每个补全相对于组内平均的优势(第715-718行)
advantages = rewards - mean_grouped_rewards
advantages = torch.tensor([
0.0 - 3.95, # = -3.95 (差,低于平均)
1.6 - 3.95, # = -2.35 (差,低于平均)
5.4 - 3.95, # = +1.45 (好,高于平均)
8.8 - 3.95, # = +4.85 (很好,高于平均)
])
# 如果scale_rewards=True,进行标准化(第719行)
if self.scale_rewards:
advantages = advantages / (std_grouped_rewards + 1e-4)
# = [-3.95/3.418, -2.35/3.418, 1.45/3.418, 4.85/3.418]
# = [-1.156, -0.687, 0.424, 1.419]
💡 关键理解:
- 负优势:该补全比组内平均差 → 降低其概率
- 正优势:该补全比组内平均好 → 提高其概率
Step 3: 计算Policy Ratio(策略比率)
# 假设4个补全的token长度都是20
completion_ids = torch.tensor([
[...20个token...], # 补全0
[...20个token...], # 补全1
[...20个token...], # 补全2
[...20个token...], # 补全3
])
# 每个token的旧模型log概率(生成时的模型)
old_per_token_logps = torch.tensor([
[-2.1, -1.8, -2.3, ...], # 补全0的20个token
[-1.9, -2.0, -1.7, ...], # 补全1的20个token
[-2.2, -1.9, -2.1, ...], # 补全2的20个token
[-2.0, -2.2, -1.8, ...], # 补全3的20个token
]) # shape: (4, 20)
# 当前训练中的模型log概率(更新后的模型)
per_token_logps = torch.tensor([
[-2.3, -1.9, -2.5, ...], # 补全0
[-1.7, -1.8, -1.5, ...], # 补全1
[-2.0, -1.7, -1.9, ...], # 补全2
[-1.8, -2.0, -1.6, ...], # 补全3
]) # shape: (4, 20)
# 计算概率比率(第807行)
coef_1 = torch.exp(per_token_logps - old_per_token_logps)
# 例如第一个补全的第一个token:
# exp(-2.3 - (-2.1)) = exp(-0.2) = 0.819
# 含义:新策略给这个token的概率是旧策略的0.819倍(下降了)
Step 4: Clipped PPO Loss(GRPO的核心)
# 设置clip范围(第813行)
epsilon_low = 0.2 # 下界:允许概率下降到80%
epsilon_high = 0.2 # 上界:允许概率上升到120%
# 对coef_1进行裁剪(第809行)
coef_2 = torch.clamp(coef_1, 1 - epsilon_low, 1 + epsilon_high)
# = torch.clamp(coef_1, 0.8, 1.2)
# 计算两个损失项(第810-811行)
# 对于补全2(好的样本,advantage=+1.45)
token_0_advantage = 1.45
# 未裁剪损失
per_token_loss1 = coef_1[2, 0] * token_0_advantage
# 假设 coef_1[2,0] = 1.15
# = 1.15 * 1.45 = 1.668
# 裁剪损失
per_token_loss2 = coef_2[2, 0] * token_0_advantage
# coef_2[2,0] = clamp(1.15, 0.8, 1.2) = 1.15(未被裁剪)
# = 1.15 * 1.45 = 1.668
# 取最小值作为最终损失(保守策略)
per_token_loss = -min(per_token_loss1, per_token_loss2)
# = -1.668(负号是因为我们要最大化,但用梯度下降最小化)
📊 直观理解裁剪机制:
| 样本 | Advantage | Ratio | 裁剪前 | 裁剪后 | 效果 |
|---|---|---|---|---|---|
| 补全0 | -3.95 | 0.85 | -3.36 | -3.16 | 限制过度惩罚差样本 |
| 补全1 | -2.35 | 0.90 | -2.12 | -2.12 | 未裁剪 |
| 补全2 | +1.45 | 1.15 | +1.67 | +1.67 | 未裁剪 |
| 补全3 | +4.85 | 1.25 | +6.06 | +5.82 | 限制过度奖励好样本 |
Step 5: 最终Loss计算
# 对所有token求平均(第815-820行)
if self.loss_type == "grpo":
# 每个补全内部平均,然后对4个补全平均
loss = ((per_token_loss * completion_mask).sum(-1) /
completion_mask.sum(-1)).mean()
# 具体数值(假设):
# 补全0: mean(per_token_loss[0]) = +2.5 (惩罚)
# 补全1: mean(per_token_loss[1]) = +1.2 (惩罚)
# 补全2: mean(per_token_loss[2]) = -1.0 (奖励)
# 补全3: mean(per_token_loss[3]) = -3.5 (奖励)
loss = (2.5 + 1.2 - 1.0 - 3.5) / 4 = -0.2
🎓 总结:为什么这样设计?
GRPO vs 传统RL的优势
# 传统RL(如REINFORCE):
# - 需要baseline来减小方差(如critic网络)
# - 可能不稳定
# GRPO的创新:
# 1️⃣ 组内对比:同一prompt的多个补全互相比较
# → 自动提供baseline(组内均值)
# → 不需要额外的value network
# 2️⃣ 相对奖励:只关心谁更好,不关心绝对分数
# → 对奖励scale不敏感
# → 训练更稳定
# 3️⃣ PPO裁剪:防止策略更新过大
# → 避免灾难性遗忘
# → 保证训练稳定性
完整流程图
输入视频 + 问题
↓
生成4个补全 [c0, c1, c2, c3]
↓
计算奖励 [0.0, 1.6, 5.4, 8.8]
↓
组内归一化 → 优势 [-3.95, -2.35, +1.45, +4.85]
↓
计算policy ratio (新策略/旧策略)
↓
PPO裁剪 [0.8, 1.2]
↓
计算loss = -min(未裁剪loss, 裁剪loss)
↓
反向传播 → 更新模型
↓
结果:
- 降低补全0和1的概率(差)
- 提高补全2和3的概率(好)
实际代码中的Advantage计算

更多推荐
所有评论(0)