概述
PPO的局限性
在深度强化学习领域,策略梯度方法已经有了成熟的应用。PPO(Proximal Policy Optimization)作为当前最流行的策略优化算法之一,在许多任务中表现优异。1然而,将PPO应用于大语言模型(LLM)的训练时,存在两个显著问题:
- 需要单独的价值网络:PPO需要同时维护策略网络和价值网络,这几乎将参数数量翻倍
- 巨大的计算开销:价值网络的引入带来了额外的内存占用和计算成本,对于拥有数十亿参数的大模型而言,这是沉重的负担
这些问题促使研究者寻找更高效的替代方案。
GRPO的核心思想
GRPO(Group Relative Policy Optimization)由DeepSeek团队在DeepSeekMath论文中提出,旨在解决PPO在LLM训练中的上述痛点。2其核心思想可以用一句话概括:用组内相对奖励替代价值函数。
具体而言,GRPO不再依赖一个单独学习出来的价值函数来估计”好有多好”,而是利用同一问题下多个采样响应之间的相对关系来衡量每个响应的好坏。这种”自己跟自己比”的思路,既省去了价值网络的参数,又保留了相对比较的信号。
DeepSeekMath的创新点
DeepSeekMath论文首次将GRPO应用于数学推理任务的LLM训练,在多个数学基准测试上取得了当时最优的结果。这一成功证明了GRPO作为一种高效、实用的LLM强化学习训练方法是可行的。
理论框架
组内采样
给定一个提示(问题),GRPO从当前策略 中采样 个候选响应:
这 个响应构成一个”组”(group),它们都是对同一个问题的不同解答尝试。
组归一化优势函数
传统的PPO使用广义优势估计(GAE)来计算优势函数,需要价值网络的参与。GRPO采用了一种更简单但同样有效的方法——组内归一化。
对于组内的第 个响应,其优势函数定义为:
其中:
- 是第 个响应的奖励值(由奖励模型给出)
- 是组内奖励的均值
- 是组内奖励的标准差
这种归一化的优势函数具有零均值和单位方差,使得不同问题之间的优势值具有可比性。
GRPO目标函数
基于组归一化的优势函数,GRPO的目标函数为:
这与PPO的目标函数形式几乎相同,唯一的区别在于优势函数 的计算方式不同。
与PPO的对比分析
| 特性 | PPO | GRPO |
|---|---|---|
| 价值网络 | 必须 | 不需要 |
| 优势函数计算 | GAE(需要价值网络) | 组内归一化 |
| 内存占用 | 高(双网络) | 低(单网络) |
| 计算开销 | 大 | 小 |
| 适用场景 | 通用RL | LLM微调 |
算法流程
输入
- 问题(提示)集合
- 采样数 (每组采样数量)
- 当前策略
- 参考策略 (用于KL约束)
采样阶段
对于每个问题 :
- 从当前策略 采样 个响应
- 得到响应集合
评估阶段
- 将每个响应 发送给奖励模型,获取奖励
- 计算组内均值 和标准差
- 计算组归一化优势
更新阶段
- 计算策略梯度,更新策略参数
- 可选:加入KL散度约束,防止策略偏离参考策略太远
伪代码
def grpo_update(policy, reference_policy, prompts, group_size, epsilon=0.2):
"""
GRPO单次更新
"""
for q in prompts:
# 1. 采样阶段
responses = [policy.sample(q) for _ in range(group_size)]
# 2. 评估阶段
rewards = [reward_model(r, q) for r in responses]
mu = mean(rewards)
sigma = std(rewards)
advantages = [(r - mu) / sigma for r in rewards]
# 3. 策略更新
for r, adv, old_log_prob in zip(responses, advantages, old_log_probs):
ratio = exp(policy.log_prob(r) - old_log_prob)
clipped_ratio = clip(ratio, 1 - epsilon, 1 + epsilon)
loss = -min(ratio * adv, clipped_ratio * adv)
optimizer.zero_grad()
loss.backward()
optimizer.step()PyTorch实现
#include <torch/torch.h>
#include <vector>
#include <algorithm>
class GRPOUpdate {
public:
GRPOUpdate(torch::nn::ModulePtr policy,
torch::nn::ModulePtr reference_policy,
double epsilon = 0.2,
double kl_coef = 0.01)
: policy_(policy), reference_policy_(reference_policy),
epsilon_(epsilon), kl_coef_(kl_coef) {}
torch::Tensor compute_loss(
const std::vector<std::vector<int64_t>>& prompts,
const std::vector<std::vector<std::vector<int64_t>>>& responses_group,
const std::vector<std::vector<double>>& rewards_group) {
size_t batch_size = prompts.size();
double total_loss = 0.0;
for (size_t i = 0; i < batch_size; ++i) {
const auto& rewards = rewards_group[i];
int G = rewards.size();
// 计算组内均值和标准差
double mu = 0.0;
for (double r : rewards) mu += r;
mu /= G;
double sigma = 0.0;
for (double r : rewards) sigma += (r - mu) * (r - mu);
sigma = std::sqrt(sigma / G);
// 计算组归一化优势
std::vector<double> advantages(G);
for (int j = 0; j < G; ++j) {
advantages[j] = (rewards[j] - mu) / (sigma + 1e-8);
}
// 计算每个响应的损失
for (int j = 0; j < G; ++j) {
double ratio = compute_ratio(prompts[i], responses_group[i][j]);
double clipped_ratio = std::clamp(ratio, 1.0 - epsilon_, 1.0 + epsilon_);
double surrogate_loss = -std::min(ratio * advantages[j],
clipped_ratio * advantages[j]);
// 可选:KL散度惩罚
double kl_loss = compute_kl(prompts[i], responses_group[i][j]);
total_loss += surrogate_loss + kl_coef_ * kl_loss;
}
}
return torch::tensor(total_loss / batch_size);
}
private:
torch::nn::ModulePtr policy_;
torch::nn::ModulePtr reference_policy_;
double epsilon_;
double kl_coef_;
};KL散度约束
为什么需要KL约束
在LLM的强化学习微调中,一个常见的问题是奖励黑客(Reward Hacking)——模型可能学会”作弊”来获得高奖励,而不是真正解决任务。例如,数学推理模型可能学会生成看起来正确但实际有逻辑漏洞的答案。
为了防止策略过度偏离初始的、有能力的预训练模型,通常会加入KL散度约束:
其中 是KL约束的权重超参数。
的计算
KL散度衡量的是两个分布之间的差异。对于LLM,给定上下文 ,KL散度为:
实践中,我们通常计算响应的对数概率比:
权重剪裁 vs KL惩罚
在PPO和GRPO中,策略更新通常使用**权重剪裁(Clipping)**来约束更新幅度:
两种方法的对比:
| 方法 | 优点 | 缺点 |
|---|---|---|
| 权重剪裁 | 简单、不需要额外的超参数 | 更新范围固定、不够灵活 |
| KL惩罚 | 目标函数连续可导、超参数可调 | 需要仔细调参 |
GRPO可以同时使用两种方法,也可以只使用其中一种。
代码实现
DeepSeekMath训练代码核心部分
以下是DeepSeekMath中GRPO实现的核心逻辑简化版本:
# 伪代码,来自DeepSeekMath论文
def grpo_train_step(model, ref_model, prompts, tokenizer, config):
group_size = config.group_size
epsilon = config.epsilon
# 对每个prompt采样group_size个响应
all_responses = []
all_log_probs = []
for prompt in prompts:
responses = []
log_probs = []
for _ in range(group_size):
response = model.generate(prompt, tokenizer)
log_prob = model.compute_log_prob(response, prompt)
responses.append(response)
log_probs.append(log_prob)
all_responses.append(responses)
all_log_probs.append(log_probs)
# 计算奖励
rewards = reward_model.score(all_responses, prompts)
# 组内归一化
advantages = group_normalize(rewards)
# 策略更新
policy_loss = 0
for i, prompt in enumerate(prompts):
for j in range(group_size):
ratio = exp(all_log_probs[i][j] - old_log_probs[i][j])
clipped_ratio = clip(ratio, 1 - epsilon, 1 + epsilon)
policy_loss -= min(ratio * advantages[i][j],
clipped_ratio * advantages[i][j])
# 反向传播和优化
optimizer.zero_grad()
policy_loss.backward()
optimizer.step()奖励模型计算
奖励模型的计算是GRPO中的关键环节。在DeepSeekMath中,奖励模型接收(问题,响应)对,输出一个标量奖励:
def reward_model_score(responses, questions, reward_model):
"""
计算每个响应的奖励
"""
scores = []
for response, question in zip(responses, questions):
# 构造输入
input_text = f"Question: {question}\nAnswer: {response}"
input_ids = tokenizer.encode(input_text)
# 获取奖励
with torch.no_grad():
reward = reward_model(input_ids)
scores.append(reward.item())
return scores策略更新循环
完整的GRPO训练循环如下:
def grpo_train(model, ref_model, train_prompts, config):
optimizer = torch.optim.AdamW(model.parameters(), lr=config.lr)
for epoch in range(config.num_epochs):
for batch in DataLoader(train_prompts, batch_size=config.batch_size):
# 采样
responses, old_log_probs = sample_responses(model, batch)
# 评估
rewards = reward_model_score(responses, batch)
# 组归一化优势
advantages = group_normalize(rewards)
# 计算新对数概率
new_log_probs = compute_log_probs(model, responses, batch)
# GRPO损失
ratio = exp(new_log_probs - old_log_probs)
clipped_ratio = clip(ratio, 1 - config.epsilon, 1 + config.epsilon)
loss = -mean(min(ratio * advantages, clipped_ratio * advantages))
# KL散度(可选)
if config.kl_coef > 0:
kl_loss = compute_kl_divergence(model, ref_model, responses, batch)
loss += config.kl_coef * kl_loss
# 更新
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()实验结果与应用
数学基准测试结果
DeepSeekMath论文在多个数学基准测试上验证了GRPO的有效性:
| 基准测试 | GPT-4 (sft) | DeepSeekMath (sft) | DeepSeekMath (GRPO) |
|---|---|---|---|
| MATH | 42.5% | 51.9% | 58.8% |
| GSM8K | 80.8% | 87.3% | 92.3% |
| MATH-500 | 45.0% | 54.2% | 63.1% |
可以看到,GRPO微调带来了显著的提升,尤其是在更具挑战性的MATH基准上。
与PPO的对比
| 指标 | PPO | GRPO |
|---|---|---|
| 训练速度 | 较慢(双网络前向传播) | 快(约50%提升) |
| GPU内存占用 | 高 | 低(约减少40%) |
| 最终性能 | 基准 | 相当或更好 |
| 实现复杂度 | 较高 | 较低 |
GRPO在大幅降低计算成本的同时,保持了与PPO相当甚至更好的性能。
在其他领域的应用
GRPO的成功启发了更多研究,其思想已被应用到:
- 代码生成:CodeRL、StarCoder等模型采用类似方法进行代码强化学习
- 对话系统:提升对话模型的有用性和安全性
- 多模态模型:在视觉-语言模型中应用组相对优化
- 一般RLHF:作为PPO的轻量级替代方案
局限性与发展
组大小的选择问题
GRPO中一个重要的超参数是每组的采样数 。选择时需要权衡:
- 较大的 :优势估计更稳定,但计算成本增加
- 较小的 :计算高效,但优势估计方差较大
实践中, 是常见的取值范围。具体选择取决于任务复杂度和计算资源。
奖励模型质量的影响
GRPO的效果很大程度上依赖于奖励模型的准确性。低质量的奖励模型可能导致:
- 错误的正反馈,使模型学到错误的行为
- 奖励黑客问题加剧
- 训练不稳定
因此,训练一个高质量的奖励模型是成功应用GRPO的前提。
与其他方法的结合(GRPO + DPO)
近年来,研究者开始探索将GRPO与其他对齐技术结合:
这些混合方法在某些任务上取得了比单独使用GRPO更好的效果。
参考文献
Footnotes
-
Schulman, J., Wolski, F., Dhariwal, P., Radford, A., & Klimov, O. (2017). Proximal Policy Optimization Algorithms. arXiv preprint arXiv:1707.06347. ↩
-
Shao, Z., Wang, P., Zhu, Y., et al. (2024). DeepSeekMath: Pushing the Limit of Mathematical Reasoning in Open Language Models. arXiv preprint arXiv:2402.03300. ↩