强化学习中的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​−G1​j=1∑G​rj​
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.61.3
2“光合作用是植物利用光能、水和二氧化碳合成有机物并释放氧气的过程”0.91.1
3“不知道”0.10.8
4“植物吸收阳光”0.41.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的优势

  1. 无需Critic模型 - 节省50%的显存和计算
  2. 稳定训练 - 组内比较减少方差
  3. 易于实现 - 相比PPO更简单
  4. 高效采样 - 一次生成多个样本用于训练

与PPO的对比

特性PPOGRPO
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.80.7
作用0.90.85
是0.950.9
植物0.850.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​ratioLPGL_{PG}LPG​KLLGRPO(i)L_{GRPO}^{(i)}LGRPO(i)​
10.11.30.130.080.1292
20.41.10.440.050.4395
3-0.40.8-0.32-0.02-0.3198
4-0.11.2-0.120.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的额外补充

Deepseek的公式

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(负号是因为我们要最大化,但用梯度下降最小化)

📊 直观理解裁剪机制:

样本AdvantageRatio裁剪前裁剪后效果
补全0-3.950.85-3.36-3.16限制过度惩罚差样本
补全1-2.350.90-2.12-2.12未裁剪
补全2+1.451.15+1.67+1.67未裁剪
补全3+4.851.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计算
在这里插入图片描述

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐