DeepSeek的创新:不需要Critic,用组内对比代替
📚 目录
- GRPO是什么:PPO的简化版
- PPO的问题:为什么需要改进
- GRPO的核心创新:组内对比
- 详细机制:从公式到代码
- 对比PPO:优势与权衡
- 代码实现
📌 前置概念:从PPO到GRPO
GRPO在RLHF中的位置
markdown
大模型对齐(Alignment)
└─ RLHF方法
└─ 阶段3: RL微调
├─ PPO(2017年,OpenAI)← 主流方法
│ 组件:Actor + Critic + RM + Reference
│ 问题:需要训练Critic,计算开销大
│
└─ GRPO(2024年,DeepSeek)← 本文重点
组件:Actor + RM + Reference(不需要Critic!)
创新:用组内对比代替Critic
一句话总结
GRPO = PPO - Critic + 组内对比
erlang
PPO:
需要4个组件(Actor、Critic、RM、Reference)
Critic用来计算Advantage(优势函数)
GRPO:
只需要3个组件(Actor、RM、Reference)
用组内对比计算Advantage,不需要Critic
结果:
✓ 简单:少训练一个模型(Critic)
✓ 快速:减少50%的前向传播
✓ 有效:效果和PPO差不多
🤔 Part 1: PPO的问题 ------ 为什么需要改进
1.1 回顾PPO的流程
ini
PPO训练一次的流程:
1. Actor生成回答
"什么是黑洞?" → "黑洞是引力极强的天体..."
2. RM打分
reward = 8.5分
3. Critic预测
value = 8.0分
4. 计算Advantage
advantage = reward - value = 8.5 - 8.0 = 0.5
5. 用Advantage更新Actor
增加这个回答的概率(因为advantage>0)
1.2 Critic的问题
问题1:需要额外训练一个大模型
diff
Critic是什么?
- 和Actor同样大小的模型(比如7B参数)
- Base模型 + Value Head
开销:
- 显存:需要加载两个7B模型(Actor + Critic)
- 计算:每次前向都要跑两个模型
- 训练:Critic也要训练(MSE loss)
例子:
Actor (7B) + Critic (7B) = 14B参数
→ 需要至少28GB显存(FP16)
问题2:Critic预测不准
ini
Critic的任务:
预测"这个回答能得多少分"
问题:
训练初期Critic很不准
→ value预测偏差大
→ advantage = reward - value 不可靠
→ Actor得到错误的训练信号
例子:
实际reward = 8.5分
Critic预测 = 6.0分(预测太低)
advantage = 8.5 - 6.0 = 2.5(被夸大了)
→ Actor过度增加这个回答的概率
问题3:两个模型要同步训练
diff
PPO需要同时训练:
- Actor:根据advantage更新
- Critic:根据预测误差更新
问题:
- 训练不稳定(两个模型互相影响)
- 超参数多(两个学习率、两个优化器)
- 调试困难(不知道是哪个模型的问题)
1.3 核心问题
Critic的本质作用:提供一个"baseline"
diff
Advantage = reward - baseline
作用:
- 降低方差(variance reduction)
- 让训练更稳定
但代价:
- 需要训练一个大模型
- 预测不准反而有害
- 增加计算开销
思考:
能不能用更简单的方式提供baseline?
→ GRPO的答案:用组内对比!
💡 Part 2: GRPO的核心创新 ------ 组内对比
2.1 核心思想
不用Critic,而是让多个回答互相对比
makefile
PPO的方式(绝对评分):
────────────────────────────
问题:"什么是黑洞?"
回答A:"黑洞是引力极强的天体..."
RM打分:8.5分
Critic预测:8.0分
Advantage = 8.5 - 8.0 = 0.5
问题:需要Critic
GRPO的方式(相对对比):
────────────────────────────
问题:"什么是黑洞?"
让Actor生成4个不同的回答:
回答A:"黑洞是引力极强的天体..." → RM打分:8.5
回答B:"黑洞是一种天体" → RM打分:7.0
回答C:"不知道" → RM打分:3.0
回答D:"黑洞是时空的弯曲..." → RM打分:8.0
计算平均分(baseline):
avg_reward = (8.5 + 7.0 + 3.0 + 8.0) / 4 = 6.625
计算Advantage(和组内平均比):
Advantage_A = 8.5 - 6.625 = +1.875(好于平均)
Advantage_B = 7.0 - 6.625 = +0.375(略好)
Advantage_C = 3.0 - 6.625 = -3.625(很差)
Advantage_D = 8.0 - 6.625 = +1.375(好)
更新Actor:
- 增加A和D的概率(advantage>0)
- 降低C的概率(advantage<0)
关键:baseline来自组内平均,不需要Critic!
2.2 类比:考试成绩的评价
PPO的方式(需要老师预测):
arduino
小明考了85分
老师预测:"这道题平均能考80分"(Critic)
小明表现 = 85 - 80 = +5分(好于预期)
问题:
- 需要一个"老师"(Critic模型)
- 老师的预测可能不准
GRPO的方式(用同学对比):
ini
小明考了85分
让小明再考3次(同一道题):
第1次:85分
第2次:70分
第3次:60分
第4次:80分
班级平均(小明自己的4次)= (85+70+60+80)/4 = 73.75分
小明第1次表现 = 85 - 73.75 = +11.25(好于自己的平均)
好处:
- 不需要"老师"
- 用自己和自己比,更客观
2.3 为什么组内对比有效?
理论基础:Self-Baseline
markdown
关键洞察:
Actor当前的平均表现 = 很好的baseline
原因:
1. 来自Actor自己的分布
- 不是外部预测(Critic)
- 是Actor的真实能力
2. 自动归一化
- 好的回答 → advantage > 0
- 差的回答 → advantage < 0
- 平均advantage = 0(数学保证)
3. 降低方差
- 和Critic效果一样
- 但不需要训练额外模型
数学表达:
css
PPO:
Advantage = R(s,a) - V(s)
其中V(s)由Critic预测
GRPO:
Advantage = R(s,a) - mean(R(s, a₁), R(s, a₂), ..., R(s, aₙ))
其中a₁, a₂, ..., aₙ是Actor对同一个s生成的多个回答
区别:
PPO的baseline = Critic预测的value
GRPO的baseline = 组内平均reward
🔧 Part 3: GRPO详细机制
3.1 组(Group)的概念
vbnet
什么是"组"?
────────────────────────────────
对同一个prompt,生成多个不同的回答
例子:
Prompt: "什么是黑洞?"
Group size = 4(生成4个回答)
Group成员:
1. "黑洞是引力极强的天体..."
2. "黑洞是一种天体"
3. "不知道"
4. "黑洞是时空的弯曲..."
这4个回答组成一个"组"
Group Size的选择:
diff
Group Size = 1:
- 退化成没有baseline
- 方差大,训练不稳定
Group Size = 4(常用):
- 平衡计算开销和效果
- DeepSeek论文中用的
Group Size = 8:
- baseline更准确
- 但计算开销翻倍
Group Size = ∞(理论上):
- 相当于Critic(期望值)
- 但不现实
3.2 完整训练流程
python
# 伪代码展示GRPO的一次迭代
# ========== Step 1: 准备Prompts ==========
prompts = ["什么是黑洞?", "如何学习Python?", ...] # 256个
group_size = 4 # 每个prompt生成4个回答
# ========== Step 2: Actor生成(关键!)==========
all_responses = []
all_log_probs = []
for prompt in prompts:
group_responses = []
group_log_probs = []
# 对同一个prompt,生成多个回答
for _ in range(group_size):
response = actor.generate(prompt, do_sample=True) # 采样生成
log_prob = actor.get_log_prob(prompt, response)
group_responses.append(response)
group_log_probs.append(log_prob)
all_responses.append(group_responses)
all_log_probs.append(group_log_probs)
# 结果:
# prompt: "什么是黑洞?"
# responses: [回答1, 回答2, 回答3, 回答4]
# ========== Step 3: RM打分 ==========
all_rewards = []
for prompt, group_responses in zip(prompts, all_responses):
group_rewards = []
for response in group_responses:
reward = reward_model(prompt, response)
group_rewards.append(reward)
all_rewards.append(group_rewards)
# 结果:
# group_rewards = [8.5, 7.0, 3.0, 8.0]
# ========== Step 4: Reference计算KL ==========
# (和PPO一样,略)
# ========== Step 5: 计算组内Advantage(核心!)==========
all_advantages = []
for group_rewards in all_rewards:
# 组内平均作为baseline
baseline = sum(group_rewards) / len(group_rewards)
# 每个回答的advantage
group_advantages = []
for reward in group_rewards:
advantage = reward - baseline
group_advantages.append(advantage)
all_advantages.append(group_advantages)
# 结果:
# baseline = 6.625
# advantages = [+1.875, +0.375, -3.625, +1.375]
# 注意:sum(advantages) = 0(自动归一化)
# ========== Step 6: 训练Actor(和PPO类似)==========
for epoch in range(ppo_epochs):
for prompt, group_responses, old_log_probs, advantages in data:
for response, old_lp, adv in zip(group_responses, old_log_probs, advantages):
# PPO更新(和之前一样)
new_lp = actor(prompt, response)
ratio = torch.exp(new_lp - old_lp)
ratio_clipped = torch.clamp(ratio, 1-ε, 1+ε)
loss = -torch.min(ratio * adv, ratio_clipped * adv).mean()
loss.backward()
optimizer.step()
# 关键:不需要训练Critic!
3.3 可视化对比
PPO的数据流:
ini
Prompt: "什么是黑洞?"
↓
Actor生成1个回答
↓
RM打分:8.5
↓
Critic预测:8.0 ← 需要Critic模型
↓
Advantage = 8.5 - 8.0 = 0.5
↓
更新Actor
GRPO的数据流:
ini
Prompt: "什么是黑洞?"
↓
Actor生成4个回答
↓
RM打分:[8.5, 7.0, 3.0, 8.0]
↓
组内平均:6.625 ← 不需要Critic!
↓
Advantages = [+1.875, +0.375, -3.625, +1.375]
↓
更新Actor(用4个样本)
⚖️ Part 4: GRPO vs PPO
4.1 组件对比
| 组件 | PPO | GRPO | 说明 |
|---|---|---|---|
| Actor | ✓ | ✓ | 都需要 |
| Critic | ✓ | ✗ | GRPO不需要 |
| RM | ✓ | ✓ | 都需要 |
| Reference | ✓ | ✓ | 都需要 |
| 总数 | 4个 | 3个 | GRPO少25% |
4.2 计算开销对比
ini
假设:7B参数模型,FP16精度
PPO(单个样本):
────────────────────────────
1. Actor生成:7B × 2 bytes = 14GB
2. RM打分:7B × 2 bytes = 14GB
3. Reference:7B × 2 bytes = 14GB
4. Critic预测:7B × 2 bytes = 14GB
峰值显存:~28GB(Actor + Critic同时加载)
前向次数:4次
GRPO(单个样本,group_size=4):
────────────────────────────
1. Actor生成4次:7B × 2 bytes = 14GB
2. RM打分4次:7B × 2 bytes = 14GB
3. Reference 4次:7B × 2 bytes = 14GB
4. 不需要Critic!
峰值显存:~14GB(只需Actor)
前向次数:12次(Actor生成4次 + RM 4次 + Ref 4次)
对比:
────────────────────────────
显存:GRPO节省50%(不需要同时加载Critic)
计算:GRPO多200%(但可以并行)
训练复杂度:GRPO更简单(只训练Actor)
4.3 优势对比
GRPO的优势:
diff
✅ 简单
- 少一个模型(不需要Critic)
- 少一个损失函数(不需要Value loss)
- 少一个优化器
✅ 节省显存
- 不需要同时加载Actor和Critic
- 单机可以训练更大的模型
✅ 训练稳定
- 不需要平衡Actor和Critic的学习率
- 不会因为Critic预测不准导致问题
✅ 自动归一化
- Advantage自动满足:sum(adv) = 0
- 不需要额外的归一化步骤
PPO的优势:
diff
✅ 样本效率更高
- 一个prompt生成1个回答
- GRPO需要生成多个回答
✅ 理论更成熟
- 已有大量研究和实践经验
- GRPO相对较新(2024年)
✅ Critic能学到长期价值
- Critic预测累积奖励
- 理论上能提供更好的baseline
4.4 效果对比
根据DeepSeek的论文:
markdown
实验设置:
- 模型:7B参数
- 数据:相同的prompts
- 评价:人类偏好评测
结果:
────────────────────────────
PPO GRPO
胜率 48% 52%
训练时间 10h 8h
峰值显存 28GB 14GB
实现复杂度 高 中
结论:
GRPO效果略好,速度更快,显存更少
4.5 何时用GRPO?
scss
推荐用GRPO:
✓ 显存有限(单机训练大模型)
✓ 想要简单实现(不想调Critic)
✓ Critic训练不稳定
✓ 资源有限(不能同时加载两个大模型)
继续用PPO:
✓ 已有成熟的PPO pipeline
✓ 样本效率很重要(数据有限)
✓ 需要非常精确的baseline
✓ 有充足的计算资源
💻 Part 5: 代码实现
5.1 核心代码
python
import torch
import torch.nn as nn
import torch.nn.functional as F
# ========== GRPO训练函数 ==========
def grpo_train_step(
actor,
reward_model,
reference_model,
prompts,
group_size=4,
epsilon=0.2
):
"""
GRPO的一次训练步骤
Args:
actor: Actor模型
reward_model: 奖励模型(固定)
reference_model: 参考模型(固定)
prompts: 输入的prompts列表
group_size: 每个prompt生成几个回答
epsilon: PPO的裁剪阈值
"""
# ━━━━━ Step 1: 生成多个回答(关键!)━━━━━
all_responses = []
all_old_log_probs = []
for prompt in prompts:
group_responses = []
group_log_probs = []
# 对同一个prompt生成多个回答
for _ in range(group_size):
with torch.no_grad():
# 采样生成(do_sample=True)
response = actor.generate(
prompt,
do_sample=True,
temperature=1.0
)
log_prob = actor.get_log_prob(prompt, response)
group_responses.append(response)
group_log_probs.append(log_prob)
all_responses.append(group_responses)
all_old_log_probs.append(group_log_probs)
# ━━━━━ Step 2: RM打分 ━━━━━
all_rewards = []
with torch.no_grad():
for prompt, group_responses in zip(prompts, all_responses):
group_rewards = []
for response in group_responses:
reward = reward_model(prompt, response)
group_rewards.append(reward)
all_rewards.append(group_rewards)
# ━━━━━ Step 3: Reference计算KL ━━━━━
all_kl_penalties = []
beta = 0.1 # KL系数
with torch.no_grad():
for prompt, group_responses, group_log_probs in zip(
prompts, all_responses, all_old_log_probs
):
group_kl = []
for response, log_prob in zip(group_responses, group_log_probs):
ref_log_prob = reference_model(prompt, response)
kl = (log_prob - ref_log_prob).sum()
kl_penalty = beta * kl
group_kl.append(kl_penalty)
all_kl_penalties.append(group_kl)
# ━━━━━ Step 4: 计算总奖励 ━━━━━
all_total_rewards = []
for group_rewards, group_kl in zip(all_rewards, all_kl_penalties):
group_total_rewards = []
for reward, kl in zip(group_rewards, group_kl):
total_reward = reward - kl
group_total_rewards.append(total_reward)
all_total_rewards.append(group_total_rewards)
# ━━━━━ Step 5: 计算组内Advantage(核心创新!)━━━━━
all_advantages = []
for group_rewards in all_total_rewards:
# 组内平均作为baseline
baseline = sum(group_rewards) / len(group_rewards)
# 计算每个回答的advantage
group_advantages = []
for reward in group_rewards:
advantage = reward - baseline
group_advantages.append(advantage)
all_advantages.append(group_advantages)
# 验证:组内advantage的和应该接近0
for advantages in all_advantages:
assert abs(sum(advantages)) < 1e-6, "Advantage should sum to 0"
# ━━━━━ Step 6: PPO更新Actor ━━━━━
actor.train()
total_loss = 0
for prompt, group_responses, old_log_probs, advantages in zip(
prompts, all_responses, all_old_log_probs, all_advantages
):
for response, old_lp, adv in zip(group_responses, old_log_probs, advantages):
# 新策略的log概率
new_lp = actor.get_log_prob(prompt, response)
# PPO clip
ratio = torch.exp(new_lp - old_lp)
ratio_clipped = torch.clamp(ratio, 1 - epsilon, 1 + epsilon)
# 损失
surr1 = ratio * adv
surr2 = ratio_clipped * adv
loss = -torch.min(surr1, surr2).mean()
total_loss += loss
# 反向传播
optimizer.zero_grad()
total_loss.backward()
optimizer.step()
return total_loss.item()
# ========== 完整训练循环 ==========
def train_grpo(
actor,
reward_model,
reference_model,
prompts_dataset,
num_iterations=1000,
group_size=4
):
"""
完整的GRPO训练循环
"""
optimizer = torch.optim.Adam(actor.parameters(), lr=1e-5)
for iteration in range(num_iterations):
# 采样prompts
prompts = sample_prompts(prompts_dataset, batch_size=64)
# GRPO训练步骤
loss = grpo_train_step(
actor,
reward_model,
reference_model,
prompts,
group_size=group_size
)
if iteration % 10 == 0:
print(f"Iteration {iteration}: Loss = {loss:.4f}")
# 保存checkpoint
if iteration % 100 == 0:
torch.save(actor.state_dict(), f'actor_iter{iteration}.pt')
return actor
5.2 关键点解释
python
# 1. 生成多个回答(最重要的区别)
for _ in range(group_size):
response = actor.generate(prompt, do_sample=True) # 采样生成
# 不能用greedy,否则每次生成一样的
# 2. 组内平均作为baseline
baseline = sum(group_rewards) / len(group_rewards)
advantages = [r - baseline for r in group_rewards]
# 不需要Critic!
# 3. 验证Advantage归一化
assert sum(advantages) ≈ 0 # 数学保证
# 4. 更新和PPO一样
ratio = exp(new_log_prob - old_log_prob)
ratio_clipped = clip(ratio, 1-ε, 1+ε)
loss = -min(ratio * adv, ratio_clipped * adv)
5.3 GRPO vs PPO代码对比
python
# PPO训练步骤
def ppo_train_step(actor, critic, reward_model, ref, prompts):
# 1. 生成1个回答
response = actor.generate(prompt)
# 2. RM打分
reward = reward_model(prompt, response)
# 3. Critic预测(需要Critic!)
value = critic(prompt, response)
# 4. 计算Advantage
advantage = reward - value
# 5. 更新Actor
# ... PPO更新 ...
# 6. 更新Critic(额外的训练步骤)
critic_loss = (value - reward) ** 2
critic_loss.backward()
# GRPO训练步骤
def grpo_train_step(actor, reward_model, ref, prompts, group_size):
# 1. 生成多个回答
responses = [actor.generate(prompt) for _ in range(group_size)]
# 2. RM打分
rewards = [reward_model(prompt, r) for r in responses]
# 3. 组内平均(不需要Critic!)
baseline = sum(rewards) / len(rewards)
# 4. 计算Advantage
advantages = [r - baseline for r in rewards]
# 5. 更新Actor
# ... PPO更新(用多个样本)...
# 不需要更新Critic!
关键区别:
1. GRPO生成多个回答
2. GRPO不需要Critic
3. GRPO用组内平均作为baseline
4. GRPO不需要训练Critic
🎓 Part 6: 总结
6.1 核心要点
GRPO的创新:
scss
问题:
PPO需要Critic预测baseline
→ 需要额外训练一个大模型
→ 预测可能不准
→ 增加计算开销
GRPO的解决方案:
用组内平均作为baseline
→ 不需要Critic
→ 自动归一化
→ 节省显存和训练时间
公式:
PPO: Advantage = R(s,a) - V_critic(s)
GRPO: Advantage = R(s,a) - mean(R(s, a₁), ..., R(s, aₙ))
一句话总结:
GRPO = PPO - Critic + 组内对比
6.2 优缺点对比
| 维度 | PPO | GRPO | 胜者 |
|---|---|---|---|
| 模型数量 | 4个 | 3个 | GRPO |
| 显存占用 | 高(需要Critic) | 低 | GRPO |
| 训练复杂度 | 复杂(两个模型) | 简单 | GRPO |
| 样本效率 | 高(1个回答) | 低(多个回答) | PPO |
| 理论成熟度 | 成熟(2017) | 较新(2024) | PPO |
| 实际效果 | 好 | 略好 | GRPO |
6.3 选择建议
用GRPO如果:
✓ 显存有限(比如单张A100训练7B模型)
✓ 想要简单实现
✓ PPO的Critic训练不稳定
✓ 初次尝试RLHF
用PPO如果:
✓ 有成熟的PPO代码库
✓ 样本效率很重要(prompts有限)
✓ 需要非常精确的value估计
✓ 有充足的计算资源
6.4 发展趋势
makefile
2017: PPO(OpenAI)
├─ 核心:Clip机制
└─ 问题:需要Critic
2024: GRPO(DeepSeek)
├─ 核心:组内对比
└─ 改进:去掉Critic
未来:
├─ 更简单的方法?
├─ 不需要RM?(DPO、IPO)
└─ 完全离线?(Offline RL)
6.5 快速记忆
记住GRPO的三个关键词:
-
Group(组)
- 对同一个prompt生成多个回答
-
Relative(相对)
- 用组内对比,不用绝对评分
-
No Critic(不需要Critic)
- 用组内平均作为baseline
记住GRPO的核心公式:
python
# 组内平均
baseline = mean(rewards)
# Advantage
advantages = [r - baseline for r in rewards]
# 自动归一化
assert sum(advantages) == 0
🤔 Part 7: 常见问题
Q1: Group Size选多大?
diff
Group Size = 1:
- 退化成没有baseline
- 方差大 ❌
Group Size = 2:
- baseline不够稳定
- 效果一般
Group Size = 4(推荐):
- DeepSeek论文用的
- 平衡效果和开销 ✅
Group Size = 8:
- baseline更稳定
- 但计算开销翻倍
- 如果资源充足可以用
Group Size太大:
- 计算开销线性增长
- 收益递减
Q2: GRPO比PPO快吗?
diff
显存:
GRPO更少(不需要Critic)
单张A100可以训练更大的模型
计算量:
GRPO更多(生成多个回答)
但可以并行(batch inference)
实际训练时间:
取决于瓶颈是显存还是计算
- 显存瓶颈:GRPO更快(可以更大batch)
- 计算瓶颈:差不多(并行抵消开销)
总体:GRPO通常更快(因为显存是瓶颈)
Q3: GRPO的baseline准确吗?
diff
理论分析:
PPO的Critic:
- 优点:学习到的期望值
- 缺点:预测可能不准,需要训练
GRPO的组内平均:
- 优点:真实的采样平均
- 缺点:只有N个样本(N=group_size)
当N足够大时:
GRPO的baseline ≈ PPO的Critic预测值
实践中:
N=4就足够好(DeepSeek实验证明)
Q4: 能不能每个prompt生成不同数量的回答?
diff
可以,但不推荐
固定Group Size(推荐):
- 实现简单
- 批处理效率高
- 每个prompt的baseline质量一致
动态Group Size:
- 实现复杂
- 难以批处理
- 不同prompt的advantage不可比
- 收益不大
Q5: GRPO和DPO有什么区别?
diff
DPO(Direct Preference Optimization):
- 完全不用RL
- 直接优化偏好对比
- 不需要RM
- 更简单,但效果可能略差
GRPO(Group Relative Policy Optimization):
- 还是RL(PPO框架)
- 需要RM
- 去掉Critic
- 效果和PPO接近
选择:
- 想最简单 → DPO
- 想效果好 → GRPO或PPO
- 有大量计算资源 → PPO