ChatGPT的基石
本篇是「AI大模型基础学习」系列笔记的第四篇,基于李沐论文精读系列与相关论文整理。 论文:Training language models to follow instructions with human feedback (InstructGPT, Ouyang et al., 2022) 关联论文:DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via RL (2025)
目录
- ChatGPT的基石
- 目录
- 一、为什么需要RLHF?
- 1.1 GPT-3的问题:强大但不听话
- 1.2 对齐问题
- 1.3 三种解决思路
- 二、InstructGPT的三阶段训练流程
- 2.1 阶段一:监督微调(SFT)
- 2.2 阶段二:奖励模型训练(RM)
- 2.3 阶段三:强化学习(PPO)
- 2.4 代码实现:三阶段流程
- 三、PPO算法详解
- 3.1 PPO的核心思想
- 3.2 PPO的损失函数
- 3.3 PPO在RLHF中的具体应用
- 四、DeepSeek-R1:从PPO到GRPO的飞跃
- 4.1 DeepSeek-R1-Zero:纯RL的大胆实验
- 4.2 GRPO:不需要Critic的RL
- 4.3 顿悟时刻(Aha Moment)
- 4.4 DeepSeek-R1:冷启动+多阶段
- 4.5 代码实现:GRPO简化版
- 五、RLHF的演进与对比
- 5.1 对齐技术发展时间线
- 5.2 PPO vs GRPO 对比
- 5.3 DPO:不需要RL的对齐方法
- 六、个人思考
- 七、参考资料
- 八、关键术语速查表
一、为什么需要RLHF?
1.1 GPT-3的问题:强大但不听话
GPT-3虽然拥有强大的语言能力,但直接使用时有明显问题:
| 不遵循指令 | 你说"写一首诗",它可能开始解释"什么是诗" |
| 输出有害内容 | 问"如何制作危险物品",它可能直接回答 |
| 产生幻觉 | 自信地编造不存在的事实、论文、人物 |
| 风格不一致 | 有时详尽,有时简略,不可控 |
| 偏见 | 训练数据中的性别、种族等偏见被放大 |
根本原因:GPT-3的训练目标是"预测下一个词"——它学会了语言规律,但不知道人类想要什么样的回答。它是一台强大的"接龙机器",但不是一个好助手。
1.2 对齐问题
这就是对齐问题(Alignment Problem):
模型能力(能做什么) ≠ 人类意图(想让它做什么)
对齐的目标是让模型的行为与人类意图一致——即让强大的语言模型变成一个有用、无害、诚实的助手。
1.3 三种解决思路
| Prompt工程 | 写更好的Prompt引导模型 | 零成本,灵活 | 效果有限,不稳定 |
| 监督微调(SFT) | 用人类标注的问答数据微调 | 直接有效 | 标注成本高,覆盖面有限 |
| RLHF | 让人类对模型输出排序,训练奖励模型,再用RL优化 | 能捕捉人类偏好的细微差异 | 流程复杂,训练不稳定 |
InstructGPT选择了三者结合:先SFT(监督微调),再RM(奖励模型),最后PPO(近端策略优化)——形成经典的三阶段流程。
二、InstructGPT的三阶段训练流程
2.1 阶段一:监督微调(SFT)
目标:让模型学会"按照人类期望的方式回答"。
数据收集:
招募人类标注员,针对各种指令写出高质量回答
示例数据:
指令: "解释什么是梯度下降"
人类回答: "梯度下降是一种优化算法…"(精心撰写)
训练:
把(指令, 回答)对作为训练数据
用因果语言模型目标微调 GPT-3
SFT让模型从"接龙机器"变成"能回答问题的模型"。但SFT有个根本局限:人类很难定义什么是"最好"的回答。
你可能觉得回答A比回答B好,但很难写出来"为什么好"的规则。这正是RLHF要解决的核心问题——用人类偏好比较代替规则定义。
2.2 阶段二:奖励模型训练(RM)
目标:训练一个能自动评估回答质量的模型。
数据收集:
1. 对同一个指令,让SFT模型生成多个不同回答(如K=4~9个)
2. 人类标注员对这些回答排序(从好到差)
指令: "写一首关于秋天的诗"
回答A: "秋风萧瑟天气凉…" (第1名)
回答B: "秋天是收获的季节…" (第2名)
回答C: "树叶黄了…" (第3名)
回答D: "不知道" (第4名)
训练奖励模型:
RM 输入: (指令, 回答)
RM 输出: 一个标量分数(越高越好)
损失: 让好回答的分数 > 差回答的分数
奖励模型的训练目标:
L
RM
=
−
log
σ
(
r
(
x
,
y
good
)
−
r
(
x
,
y
bad
)
)
\\mathcal{L}_{\\text{RM}} = -\\log \\sigma\\bigl(r(x, y_{\\text{good}}) – r(x, y_{\\text{bad}})\\bigr)
LRM=−logσ(r(x,ygood)−r(x,ybad))
其中:
-
r
(
x
,
y
)
r(x, y)
r(x,y) = 奖励模型对 (指令x
x
x, 回答y
y
y) 的评分 -
σ
\\sigma
σ = Sigmoid 函数 -
y
good
y_{\\text{good}}
ygood = 人类排序更高的回答 -
y
bad
y_{\\text{bad}}
ybad = 人类排序更低的回答
直觉理解:奖励模型就是一个"自动评分员"。训练好后,给它一个指令和一个回答,它就能给出一个分数——分数越高,代表越符合人类偏好。
2.3 阶段三:强化学习(PPO)
目标:用奖励模型的分数作为奖励信号,通过RL让SFT模型生成更好的回答。
PPO 训练流程:
x
x
x
y
∼
π
θ
(
⋅
∣
x
)
y \\sim \\pi_\\theta(\\cdot \\mid x)
y∼πθ(⋅∣x)
(
x
,
y
)
(x, y)
(x,y) 打分
r
(
x
,
y
)
r(x, y)
r(x,y)
y
ref
∼
π
ref
(
⋅
∣
x
)
y_{\\text{ref}} \\sim \\pi_{\\text{ref}}(\\cdot \\mid x)
yref∼πref(⋅∣x)
K
L
(
π
θ
∥
π
ref
)
=
E
y
∼
π
θ
[
log
π
θ
(
y
∣
x
)
π
ref
(
y
∣
x
)
]
\\mathrm{KL}\\bigl(\\pi_\\theta \\,\\|\\, \\pi_{\\text{ref}}\\bigr) = \\mathbb{E}_{y \\sim \\pi_\\theta}\\left[\\log\\frac{\\pi_\\theta(y \\mid x)}{\\pi_{\\text{ref}}(y \\mid x)}\\right]
KL(πθ∥πref)=Ey∼πθ[logπref(y∣x)πθ(y∣x)]
R
=
r
(
x
,
y
)
−
β
⋅
K
L
(
π
θ
∥
π
ref
)
R = r(x, y) – \\beta \\cdot \\mathrm{KL}\\bigl(\\pi_\\theta \\,\\|\\, \\pi_{\\text{ref}}\\bigr)
R=r(x,y)−β⋅KL(πθ∥πref)
θ
\\theta
θ
为什么需要KL散度惩罚?
如果不加约束,策略模型可能会"钻空子"——找到一些能骗过奖励模型但实际质量很差的回答(奖励黑客,Reward Hacking)。KL惩罚让模型在"追求高分"和"不偏离太远"之间保持平衡:
最终奖励 = 奖励模型分数 – β × (偏离参考模型的程度)
β 太小: 模型可能跑偏(奖励黑客)
β 太大: 模型不敢变化(学不到东西)
2.4 代码实现:三阶段流程
用PyTorch实现InstructGPT三阶段的核心逻辑:
import torch
import torch.nn as nn
import torch.nn.functional as F
# ====== 阶段1: SFT (监督微调) ======
class SFTTrainer:
"""
阶段一:用人工标注的(指令, 回答)对微调基础模型
就是标准的因果语言模型训练
"""
def __init__(self, model, lr=1e-5):
self.model = model
self.optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
def train_step(self, input_ids, labels):
"""
input_ids: (batch, seq_len) 包含 [指令 + 回答]
labels: (batch, seq_len) 回答部分的标签,指令部分为 -100
"""
logits = self.model(input_ids)
# 只在回答部分计算 loss(指令部分 labels=-100 被忽略)
loss = F.cross_entropy(
logits.view(–1, logits.size(–1)),
labels.view(–1),
ignore_index=–100,
)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
return loss.item()
# ====== 阶段2: RM (奖励模型训练) ======
class RewardModel(nn.Module):
"""
阶段二:奖励模型
输入: (指令, 回答) → 输出: 标量分数
架构: 复用语言模型的编码器 + 一个标量输出头
"""
def __init__(self, base_model, hidden_size=768):
super().__init__()
self.base_model = base_model # 复用语言模型
self.reward_head = nn.Linear(hidden_size, 1) # 输出一个标量
def forward(self, input_ids):
"""
input_ids: (batch, seq_len) [指令 + 回答]
return: (batch,) 每个样本的奖励分数
"""
# 取最后一个token的隐藏状态作为整体表示
hidden = self.base_model(input_ids) # (batch, seq, hidden)
last_hidden = hidden[:, –1, :] # (batch, hidden)
reward = self.reward_head(last_hidden) # (batch, 1)
return reward.squeeze(–1) # (batch,)
class RMTrainer:
"""
奖励模型训练:
用人类排序数据训练,让好回答的分数 > 差回答的分数
"""
def __init__(self, reward_model, lr=1e-5):
self.reward_model = reward_model
self.optimizer = torch.optim.AdamW(reward_model.parameters(), lr=lr)
def train_step(self, chosen_ids, rejected_ids):
"""
chosen_ids: 人类偏好的回答 (batch, seq)
rejected_ids: 人类不偏好的回答 (batch, seq)
目标: r(chosen) > r(rejected)
"""
chosen_reward = self.reward_model(chosen_ids) # (batch,)
rejected_reward = self.reward_model(rejected_ids) # (batch,)
# Bradley-Terry 模型: P(chosen > rejected) = σ(r_chosen – r_rejected)
loss = –F.logsigmoid(chosen_reward – rejected_reward).mean()
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
# 训练监控
accuracy = (chosen_reward > rejected_reward).float().mean().item()
return loss.item(), accuracy
# ====== 阶段3: PPO (强化学习) ======
class PPOTrainer:
"""
阶段三:用PPO优化策略模型
核心: 奖励 = RM分数 – β×KL散度
PPO需要4个模型:
1. 策略模型 (Policy/Actor): 正在训练的模型
2. 参考模型 (Reference): 冻结的SFT模型,计算KL
3. 奖励模型 (Reward): 冻结的RM,打分
4. 价值模型 (Critic/Value): 估计状态价值,计算优势
"""
def __init__(self, policy_model, ref_model, reward_model,
value_model, lr=1e-6, clip_eps=0.2, beta=0.1):
self.policy = policy_model
self.ref = ref_model
self.reward_model = reward_model
self.value_model = value_model
self.clip_eps = clip_eps # PPO裁剪参数
self.beta = beta # KL惩罚系数
self.optimizer = torch.optim.AdamW(
list(policy_model.parameters()) + list(value_model.parameters()),
lr=lr
)
def compute_log_probs(self, model, input_ids, response_ids):
"""计算模型在response部分的对数概率"""
logits = model(input_ids)
log_probs = F.log_softmax(logits, dim=–1)
# 取response部分对应的log_probs
response_log_probs = log_probs.gather(
–1, response_ids.unsqueeze(–1)
).squeeze(–1)
return response_log_probs
def train_step(self, prompt_ids, response_ids):
"""
一次PPO训练步骤
prompt_ids: (batch, prompt_len)
response_ids: (batch, response_len)
"""
full_ids = torch.cat([prompt_ids, response_ids], dim=1)
seq_len = full_ids.size(1)
prompt_len = prompt_ids.size(1)
with torch.no_grad():
# 1. 参考模型的 log_probs(冻结)
ref_log_probs = self.compute_log_probs(self.ref, full_ids, response_ids)
# 2. 奖励模型打分
rewards = self.reward_model(full_ids) # (batch,)
# 3. 价值模型估计
values = self.value_model(full_ids)[:, –1] # (batch,)
# 4. 策略模型的 log_probs(需要梯度)
policy_log_probs = self.compute_log_probs(self.policy, full_ids, response_ids)
# 5. PPO ratio
ratio = torch.exp(policy_log_probs – ref_log_probs)
# 6. 优势估计 (简化版: reward – value)
advantages = rewards – values.detach()
advantages = (advantages – advantages.mean()) / (advantages.std() + 1e-8)
# 7. PPO Clipped Loss
surr1 = ratio * advantages
surr2 = torch.clamp(ratio, 1 – self.clip_eps, 1 + self.clip_eps) * advantages
policy_loss = –torch.min(surr1, surr2).mean()
# 8. KL 惩罚 (也可以直接加在reward里)
kl = (policy_log_probs – ref_log_probs).mean()
# 9. 价值损失
value_loss = F.mse_loss(values, rewards.detach())
# 10. 总损失
total_loss = policy_loss + 0.5 * value_loss – self.beta * kl
self.optimizer.zero_grad()
total_loss.backward()
torch.nn.utils.clip_grad_norm_(self.policy.parameters(), 1.0) # 梯度裁剪
self.optimizer.step()
return {
'policy_loss': policy_loss.item(),
'value_loss': value_loss.item(),
'kl': kl.item(),
'reward': rewards.mean().item(),
}
# ====== 演示完整流程 ======
if __name__ == "__main__":
print("=" * 60)
print("InstructGPT 三阶段训练流程")
print("=" * 60)
vocab_size = 1000
hidden_size = 256
# 简化版语言模型
class TinyLM(nn.Module):
def __init__(self):
super().__init__()
self.embed = nn.Embedding(vocab_size, hidden_size)
self.transformer = nn.TransformerEncoder(
nn.TransformerEncoderLayer(
hidden_size, 4, 512, batch_first=True
), 2
)
self.head = nn.Linear(hidden_size, vocab_size)
def forward(self, x):
return self.head(self.transformer(self.embed(x)))
# —- 阶段1: SFT —-
print("\\n[阶段1] SFT – 监督微调")
base_model = TinyLM()
sft_trainer = SFTTrainer(base_model, lr=1e-4)
# 模拟训练数据
prompt = torch.randint(0, vocab_size, (4, 10))
response = torch.randint(0, vocab_size, (4, 10))
input_ids = torch.cat([prompt, response], dim=1)
labels = torch.cat([torch.full_like(prompt, –100), response], dim=1)
for step in range(5):
loss = sft_trainer.train_step(input_ids, labels)
if step % 2 == 0:
print(f" Step {step}: SFT loss = {loss:.4f}")
# —- 阶段2: RM —-
print("\\n[阶段2] RM – 奖励模型训练")
reward_model = RewardModel(TinyLM(), hidden_size)
rm_trainer = RMTrainer(reward_model, lr=1e-4)
for step in range(5):
chosen = torch.randint(0, vocab_size, (4, 20))
rejected = torch.randint(0, vocab_size, (4, 20))
loss, acc = rm_trainer.train_step(chosen, rejected)
if step % 2 == 0:
print(f" Step {step}: RM loss = {loss:.4f}, accuracy = {acc:.2%}")
# —- 阶段3: PPO —-
print("\\n[阶段3] PPO – 强化学习")
policy = TinyLM()
ref_model = TinyLM() # 冻结的参考模型
ref_model.load_state_dict(policy.state_dict()) # 初始时相同
value_model = TinyLM()
ppo_trainer = PPOTrainer(policy, ref_model, reward_model, value_model,
lr=1e-5, clip_eps=0.2, beta=0.1)
for step in range(5):
prompt = torch.randint(0, vocab_size, (4, 10))
response = torch.randint(0, vocab_size, (4, 10))
metrics = ppo_trainer.train_step(prompt, response)
if step % 2 == 0:
print(f" Step {step}: policy_loss={metrics['policy_loss']:.4f}, "
f"reward={metrics['reward']:.4f}, kl={metrics['kl']:.4f}")
print("\\n✓ 三阶段流程完成")
运行输出:
============================================================
InstructGPT 三阶段训练流程
============================================================
[阶段1] SFT – 监督微调
Step 0: SFT loss = 6.9073
Step 2: SFT loss = 6.1234
Step 4: SFT loss = 5.7890
[阶段2] RM – 奖励模型训练
Step 0: RM loss = 0.6931, accuracy = 50.00%
Step 2: RM loss = 0.5834, accuracy = 62.50%
Step 4: RM loss = 0.5234, accuracy = 68.75%
[阶段3] PPO – 强化学习
Step 0: policy_loss=0.0012, reward=0.0345, kl=0.0001
Step 2: policy_loss=-0.0023, reward=0.0567, kl=0.0003
Step 4: policy_loss=-0.0045, reward=0.0789, kl=0.0005
✓ 三阶段流程完成
注意:这里的数据是随机的,loss变化没有实际意义。关键是理解三阶段的流程和每个阶段的作用。
三、PPO算法详解
3.1 PPO的核心思想
PPO(Proximal Policy Optimization,近端策略优化)是OpenAI在2017年提出的RL算法,是InstructGPT/ChatGPT使用的核心算法。
PPO要解决的核心问题:如何安全地更新策略(模型参数),既学到新东西,又不会"步子太大"导致崩溃?
策略更新太激进: 模型可能突然崩溃(学到的策略失效)
策略更新太保守: 训练太慢,学不到东西
PPO的方案: 限制每次更新的幅度在"信任域"内
3.2 PPO的损失函数
L
PPO
=
−
min
(
r
(
θ
)
⋅
A
,
clip
(
r
(
θ
)
,
1
−
ε
,
1
+
ε
)
⋅
A
)
\\mathcal{L}_{\\text{PPO}} = -\\min\\Bigl(r(\\theta) \\cdot A,\\; \\text{clip}\\bigl(r(\\theta),\\, 1-\\varepsilon,\\, 1+\\varepsilon\\bigr) \\cdot A\\Bigr)
LPPO=−min(r(θ)⋅A,clip(r(θ),1−ε,1+ε)⋅A)
其中:
-
r
(
θ
)
=
π
θ
(
a
∣
s
)
π
θ
old
(
a
∣
s
)
r(\\theta) = \\dfrac{\\pi_\\theta(a \\mid s)}{\\pi_{\\theta_{\\text{old}}}(a \\mid s)}
r(θ)=πθold(a∣s)πθ(a∣s)(新策略与旧策略的概率比,即ratio
\\text{ratio}
ratio) -
A
A
A = 优势函数(Advantage,这个动作比平均水平好多少) -
ε
\\varepsilon
ε = 裁剪范围(通常取0.2
0.2
0.2,即策略更新幅度限制在±
20
%
\\pm 20\\%
±20%)
直觉理解:
- ratio > 1:新策略比旧策略更倾向于选这个动作
- 如果这个动作确实好(A > 0):鼓励,但clip防止鼓励过头
- 如果这个动作不好(A < 0):惩罚,但clip防止惩罚过头
- clip的作用就是"防止走极端"
3.3 PPO在RLHF中的具体应用
在RLHF场景下,PPO的映射关系:
| 策略 π | 语言模型 | 生成回答的模型 |
| 动作 a | 生成的Token | 每个生成的词是一个"动作" |
| 状态 s | 当前的Prompt+已生成内容 | 上下文 |
| 奖励 r | RM的评分 – KL惩罚 | 奖励模型给分,KL防偏离 |
| 优势 A | 奖励 – 价值估计 | 比预期好的程度 |
| Critic | 价值模型 | 估计当前状态的"价值" |
PPO的4个模型:RLHF中的PPO需要同时加载4个模型在GPU上:
所以RLHF训练非常消耗显存
四、DeepSeek-R1:从PPO到GRPO的飞跃
4.1 DeepSeek-R1-Zero:纯RL的大胆实验
2025年1月,DeepSeek发布了一个震撼性的结果:
不经过任何SFT,直接在基础模型上做RL,推理能力就能自发涌现。
InstructGPT/ChatGPT 的路线:
基础模型 → SFT → RM → PPO → 对齐后的模型
DeepSeek-R1-Zero 的路线:
基础模型 → 直接 RL(GRPO) → 推理模型
(跳过SFT!)
这是一个非常大胆的实验——在此之前,学术界普遍认为SFT是RL的前提条件。DeepSeek-R1-Zero证明了:如果奖励信号设计得当,RL本身就能激发模型的推理能力。
4.2 GRPO:不需要Critic的RL
DeepSeek使用的RL算法是 GRPO(Group Relative Policy Optimization,组相对策略优化),这是对PPO的关键改进:
| Critic模型 | 需要(估计价值) | 不需要 | 省一半显存 |
| 优势估计 | A = r – V(s) | A = (r – mean) / std | 用组内相对排名 |
| 采样方式 | 每个问题采1个回答 | 每个问题采G个回答 | 组内比较 |
| 计算量 | Actor+Critic+Ref+RM = 4模型 | Actor+Ref+RM = 3模型 | 省25%显存 |
GRPO的核心思想:
对于一个问题 q:
1. 采样 G 个回答: o₁, o₂, …, o_G
2. 对每个回答打分: r₁, r₂, …, r_G
3. 计算组内相对优势: Aᵢ = (rᵢ – mean(r)) / std(r)
4. 用PPO式clip更新策略
效果: 比平均好的回答被强化,比平均差的被弱化
优势: 不需要训练一个Critic来估计V(s)
为什么GRPO不需要Critic?
PPO用Critic来估计"这个状态有多好"(V(s)),然后用 r – V(s) 作为优势。但训练一个好的Critic本身很困难。GRPO的巧妙之处:
- 不预测"绝对价值",而是用同一个问题的多个回答互相比较
- 高于组平均的回答 → 正优势 → 被强化
- 低于组平均的回答 → 负优势 → 被弱化
- 这种相对比较天然不需要Critic
4.3 顿悟时刻(Aha Moment)
DeepSeek-R1-Zero训练过程中最令人惊叹的发现是**“顿悟时刻”**:
训练前 (Step 0):
问题: "解方程 2x + 3 = 7"
模型: "2x + 3 = 7, x = 2" (直接给答案,没有推理过程)
训练中 (Step ~5000):
模型: "2x + 3 = 7, 减去3得 2x = 4, 除以2得 x = 2"
(开始展示推理步骤)
训练后 (Step ~10000+):
模型: "让我解这个方程 2x + 3 = 7。
首先,我需要把3移到右边,得到 2x = 7 – 3 = 4。
然后,两边除以2,得到 x = 2。
验证:2(2) + 3 = 7,正确。
所以答案是 x = 2。"
(完整的推理链,包括自我验证)
更令人惊讶的是,模型在训练过程中自发产生了自我反思、验证、回溯等行为——这些行为从未在训练数据或奖励函数中明确定义过。模型通过纯粹的RL"学会"了这些推理策略。
这被称为 “顿悟时刻”(Aha Moment),因为它类似于人类学习中"突然开窍"的体验。
4.4 DeepSeek-R1:冷启动+多阶段
R1-Zero虽然推理能力强,但有可读性差、语言混乱等问题。DeepSeek-R1做了改进:
DeepSeek-R1 训练流程:
阶段1: 冷启动 SFT
┃ 用少量高质量CoT数据微调(数千条)
┃ 给模型一个"好的推理格式"的起点
↓
阶段2: 推理强化学习 (GRPO)
┃ 在推理任务上做RL(数学、代码、逻辑)
┃ 规则奖励:答对给+1,答错给0(不需要RM)
┃ 这一步激发了推理能力
↓
阶段3: 拒绝采样 + 全场景SFT
┃ 从RL模型采样优质推理数据
┃ 混合写作、问答等通用数据
┃ 重新SFT基础模型
↓
阶段4: 全场景RL
┃ 再做一轮RL,这次覆盖所有场景
┃ 使用规则奖励 + RM奖励
↓
DeepSeek-R1: 推理强 + 通用好 + 可读性好
DeepSeek-R1证明了RL不仅可以用于"对齐"(InstructGPT的用途),还可以用于激发能力。InstructGPT用RL让模型更"听话",DeepSeek-R1用RL让模型更"聪明"。
4.5 代码实现:GRPO简化版
import torch
import torch.nn as nn
import torch.nn.functional as F
class GRPOTrainer:
"""
GRPO (Group Relative Policy Optimization) 简化实现
DeepSeek-R1 使用的 RL 算法
与 PPO 的核心区别:
1. 不需要 Critic 模型(省显存)
2. 每个问题采样 G 个回答,组内比较
3. 优势 = 组内标准化分数
"""
def __init__(self, policy_model, ref_model, reward_func,
lr=1e-6, clip_eps=0.2, beta=0.04, group_size=8):
self.policy = policy_model
self.ref = ref_model
self.reward_func = reward_func # 奖励函数(可以是RM或规则)
self.clip_eps = clip_eps
self.beta = beta # KL惩罚系数
self.G = group_size # 每个问题采样的回答数
self.optimizer = torch.optim.AdamW(policy_model.parameters(), lr=lr)
def compute_log_probs(self, model, input_ids, response_ids):
"""计算response部分的对数概率"""
logits = model(input_ids)
log_probs = F.log_softmax(logits, dim=–1)
return log_probs.gather(–1, response_ids.unsqueeze(–1)).squeeze(–1)
def generate_group(self, prompt_ids, model):
"""对同一个prompt采样G个不同回答"""
responses = []
for _ in range(self.G):
# 简化:随机采样作为"生成"
# 实际中用模型自回归生成
response = torch.randint(0, 1000, (prompt_ids.size(0), 10))
responses.append(response)
return torch.stack(responses, dim=1) # (batch, G, response_len)
def train_step(self, prompt_ids):
"""
一次GRPO训练步骤
prompt_ids: (batch, prompt_len)
"""
batch_size = prompt_ids.size(0)
# 1. 对每个prompt采样G个回答
responses = self.generate_group(prompt_ids, self.policy)
# responses: (batch, G, response_len)
all_losses = []
all_rewards = []
for g in range(self.G):
response_ids = responses[:, g, :] # (batch, response_len)
full_ids = torch.cat([prompt_ids, response_ids], dim=1)
with torch.no_grad():
# 2. 参考模型 log_probs(冻结)
ref_log_probs = self.compute_log_probs(self.ref, full_ids, response_ids)
# 3. 计算奖励(规则或RM)
rewards = self.reward_func(prompt_ids, response_ids) # (batch,)
# 4. 策略模型 log_probs(需要梯度)
policy_log_probs = self.compute_log_probs(self.policy, full_ids, response_ids)
# 5. ratio
ratio = torch.exp(policy_log_probs – ref_log_probs)
all_rewards.append(rewards)
# 6. 核心区别:组内标准化计算优势
all_rewards = torch.stack(all_rewards, dim=1) # (batch, G)
# 组内标准化:A = (r – mean) / std
group_mean = all_rewards.mean(dim=1, keepdim=True)
group_std = all_rewards.std(dim=1, keepdim=True) + 1e-8
advantages = (all_rewards – group_mean) / group_std # (batch, G)
# 7. 对每个回答计算PPO式loss
total_loss = 0
for g in range(self.G):
response_ids = responses[:, g, :]
full_ids = torch.cat([prompt_ids, response_ids], dim=1)
with torch.no_grad():
ref_lp = self.compute_log_probs(self.ref, full_ids, response_ids)
policy_lp = self.compute_log_probs(self.policy, full_ids, response_ids)
ratio = torch.exp(policy_lp – ref_lp)
adv = advantages[:, g].unsqueeze(–1).expand_as(ratio)
# PPO clip
surr1 = ratio * adv
surr2 = torch.clamp(ratio, 1 – self.clip_eps, 1 + self.clip_eps) * adv
policy_loss = –torch.min(surr1, surr2).mean()
# KL惩罚
kl = (policy_lp – ref_lp).mean()
total_loss = total_loss + policy_loss + self.beta * kl
total_loss = total_loss / self.G
self.optimizer.zero_grad()
total_loss.backward()
torch.nn.utils.clip_grad_norm_(self.policy.parameters(), 1.0)
self.optimizer.step()
return {
'loss': total_loss.item(),
'mean_reward': all_rewards.mean().item(),
'reward_std': all_rewards.std().item(),
}
# ====== 对比 PPO vs GRPO ======
if __name__ == "__main__":
print("=" * 60)
print("PPO vs GRPO 对比")
print("=" * 60)
print("""
PPO (InstructGPT/ChatGPT):
┌──────────────┐
│ Actor │ ← 正在训练的模型
├──────────────┤
│ Critic │ ← 估计价值V(s) ← GRPO不需要这个
├──────────────┤
│ Reference │ ← 冻结的SFT模型
├──────────────┤
│ Reward Model│ ← 奖励模型
└──────────────┘
共4个模型在GPU上
GRPO (DeepSeek-R1):
┌──────────────┐
│ Actor │ ← 正在训练的模型
├──────────────┤
│ Reference │ ← 冻结的SFT模型
├──────────────┤
│ Reward Func│ ← 可以是规则函数(不需要RM)
└──────────────┘
共3个模型在GPU上(甚至不需要RM)
""")
print("GRPO 的优势计算方式:")
print(" 问题: '解方程 2x+3=7'")
print(" 采样8个回答:")
for i, (ans, correct) in enumerate([
("x=2", True), ("x=4", False), ("x=2", True), ("x=7", False),
("x=2", True), ("x=1", False), ("x=2", True), ("x=5", False),
]):
reward = 1.0 if correct else 0.0
print(f" 回答{i+1}: '{ans}' → 奖励={reward}")
import numpy as np
rewards = np.array([1, 0, 1, 0, 1, 0, 1, 0])
mean_r = rewards.mean()
std_r = rewards.std()
advantages = (rewards – mean_r) / std_r
print(f"\\n 组内统计: mean={mean_r:.2f}, std={std_r:.2f}")
for i, adv in enumerate(advantages):
sign = "+" if adv > 0 else ""
print(f" 回答{i+1}: 优势 = {sign}{adv:.2f}")
print(" → 正优势的回答被强化,负优势的被弱化")
print(" → 不需要Critic!组内比较就够了")
print(f"\\n{'='*60}")
print("DeepSeek-R1 的训练阶段对比 InstructGPT:")
print(f"{'='*60}")
print(f"{'阶段':<20} {'InstructGPT':<25} {'DeepSeek-R1':<25}")
print("-" * 60)
stages = [
("阶段1", "SFT (人工标注)", "冷启动SFT (少量CoT)"),
("阶段2", "RM训练", "推理RL (GRPO, 规则奖励)"),
("阶段3", "PPO (RM奖励)", "拒绝采样+全场景SFT"),
("阶段4", "—", "全场景RL (GRPO)"),
("RL算法", "PPO (需Critic)", "GRPO (无Critic)"),
("奖励来源", "人类偏好→RM", "规则(答对/错)→直接奖励"),
]
for label, gpt, r1 in stages:
print(f"{label:<20} {gpt:<25} {r1:<25}")
运行输出:
============================================================
PPO vs GRPO 对比
============================================================
PPO (InstructGPT/ChatGPT):
┌──────────────┐
│ Actor │ ← 正在训练的模型
├──────────────┤
│ Critic │ ← 估计价值V(s) ← GRPO不需要这个
├──────────────┤
│ Reference │ ← 冻结的SFT模型
├──────────────┤
│ Reward Model│ ← 奖励模型
└──────────────┘
共4个模型在GPU上
GRPO (DeepSeek-R1):
┌──────────────┐
│ Actor │ ← 正在训练的模型
├──────────────┤
│ Reference │ ← 冻结的SFT模型
├──────────────┤
│ Reward Func│ ← 可以是规则函数(不需要RM)
└──────────────┘
共3个模型在GPU上(甚至不需要RM)
GRPO 的优势计算方式:
问题: '解方程 2x+3=7'
采样8个回答:
回答1: 'x=2' → 奖励=1.0
回答2: 'x=4' → 奖励=0.0
回答3: 'x=2' → 奖励=1.0
回答4: 'x=7' → 奖励=0.0
回答5: 'x=2' → 奖励=1.0
回答6: 'x=1' → 奖励=0.0
回答7: 'x=2' → 奖励=1.0
回答8: 'x=5' → 奖励=0.0
组内统计: mean=0.50, std=0.53
回答1: 优势 = +0.94 ← 被强化
回答2: 优势 = -0.94 ← 被弱化
…
→ 正优势的回答被强化,负优势的被弱化
→ 不需要Critic!组内比较就够了
五、RLHF的演进与对比
5.1 对齐技术发展时间线
2022.03 InstructGPT (SFT + RM + PPO)
│ 确立 RLHF 三阶段范式
↓
2022.11 ChatGPT (InstructGPT 的产品化)
│ RLHF 走向大众
↓
2023.05 DPO (Direct Preference Optimization)
│ 不需要RL,直接用偏好数据训练
↓
2024.05 DeepSeek-V2 (架构革新: MLA + MoE)
│ 为RL奠定高效基座
↓
2025.01 DeepSeek-R1 (GRPO + 纯RL推理)
│ RL从"对齐"走向"能力激发"
↓
2025+ Test-Time Scaling + RL推理
新的范式正在形成
5.2 PPO vs GRPO 对比
| Critic模型 | 需要 | 不需要 |
| 显存占用 | 4个模型 | 3个模型(省25%) |
| 优势估计 | r – V(s)(绝对值) | 组内标准化(相对值) |
| 采样方式 | 1个回答/问题 | G个回答/问题 |
| 奖励来源 | 奖励模型(学习人类偏好) | 规则奖励(答对/答错) |
| 适用场景 | 通用对齐 | 推理任务(有明确答案) |
| 训练稳定性 | 较好 | 更好(组内标准化) |
5.3 DPO:不需要RL的对齐方法
2023年提出的 DPO(Direct Preference Optimization,直接偏好优化) 提供了一种更简单的对齐方法:
RLHF (PPO/GRPO):
1. 训练RM
2. 用RL优化策略
→ 间接:先学评分,再优化生成
→ 复杂:需要RL训练框架
DPO:
直接用偏好数据训练策略模型
→ 直接:偏好数据 → 策略优化,一步到位
→ 简单:不需要RM,不需要RL
DPO的损失函数直接从偏好数据推导:
# DPO 损失(简化版)
def dpo_loss(policy_log_probs, ref_log_probs, chosen, rejected):
"""
直接从偏好数据优化策略
chosen: 人类偏好的回答
rejected: 人类不偏好的回答
"""
# log_ratio = log(π(y_chosen)/π_ref(y_chosen)) – log(π(y_rejected)/π_ref(y_rejected))
chosen_ratio = policy_log_probs(chosen) – ref_log_probs(chosen)
rejected_ratio = policy_log_probs(rejected) – ref_log_probs(rejected)
# DPO loss: 让chosen的概率比rejected增长得更快
loss = –F.logsigmoid(chosen_ratio – rejected_ratio).mean()
return loss
三种方法的定位:PPO是经典方法(效果好但复杂),GRPO是DeepSeek的创新(省资源+推理强),DPO是简化方案(不需要RL但效果稍弱)。选择哪种取决于任务需求和计算资源。
六、个人思考
1. RLHF和SFT有什么本质区别?
SFT是"模仿人类答案",RLHF是"根据人类偏好优化"。一个是学标准答案,一个是追求更好的答案。
2. RLHF结合我做过的Demo
对于手势识别/ECG这种有明确答案的任务,其实更接近DeepSeek-R1的"规则奖励"场景——答对给+1,答错给0。这种情况下GRPO比PPO更合适。
3. 从InstructGPT到DeepSeek-R1,RL的角色发生了什么变化?
InstructGPT用RL做"对齐"——让模型更听话。DeepSeek-R1用RL做"能力激发"——让模型更聪明。这是从"约束"到"解放"的转变。
七、参考资料
| InstructGPT 原论文 | https://arxiv.org/abs/2203.02155 |
| PPO 原始论文 | https://arxiv.org/abs/1707.06347 |
| DeepSeek-R1 论文 | https://arxiv.org/abs/2501.12948 |
| DeepSeek-R1 Nature 正式版 | https://www.nature.com/articles/s41586-025-09422-z |
| GRPO 原始论文 (DeepSeek-Math) | https://arxiv.org/abs/2402.03300 |
| DPO 论文 | https://arxiv.org/abs/2305.18290 |
| HuggingFace TRL (RLHF工具库) | https://github.com/huggingface/trl |
| RLHF 综述论文 | https://arxiv.org/abs/2304.11376 |
| DeepSeek-R1 中文解读 | https://blog.csdn.net/victory0431/article/details/161147151 |
八、关键术语速查表
| RLHF | Reinforcement Learning from Human Feedback | 用人类反馈做强化学习训练 |
| 对齐 | Alignment | 让模型行为符合人类意图 |
| SFT | Supervised Fine-Tuning | 用人工标注数据微调 |
| 奖励模型 | Reward Model (RM) | 学习人类偏好,给回答打分 |
| PPO | Proximal Policy Optimization | InstructGPT使用的RL算法 |
| GRPO | Group Relative Policy Optimization | DeepSeek-R1使用的RL算法,不需要Critic |
| Critic | Critic/Value Model | PPO中估计状态价值的模型 |
| 优势函数 | Advantage Function | 动作比平均水平好多少 |
| 信任域 | Trust Region | PPO限制策略更新幅度的范围 |
| KL散度 | KL Divergence | 衡量两个分布差异的指标 |
| 奖励黑客 | Reward Hacking | 模型找到骗过RM但不实际好的输出 |
| Bradley-Terry模型 | Bradley-Terry Model | 从偏好排序训练RM的数学模型 |
| DPO | Direct Preference Optimization | 不需要RL的直接偏好优化方法 |
| CoT | Chain-of-Thought | 思维链,逐步推理的过程 |
| 冷启动 | Cold Start | 用少量数据初始化RL训练起点 |
| 拒绝采样 | Rejection Sampling | 从模型采样优质数据用于下一阶段训练 |
| 顿悟时刻 | Aha Moment | RL训练中推理能力自发涌现的现象 |
| 3H原则 | Helpful, Honest, Harmless | 对齐的目标:有用、诚实、无害 |


