RLHF 深度解析:从人类反馈到 PPO/GRPO/DPO 的对齐革命
1. 引言:为什么需要"对齐"?
1.1 从 SFT 到 RLHF 的进化
如果说预训练给学生灌输了整个互联网的知识,SFT 教会学生"听人话做事",那么RLHF 就是让学生学会**“做正确的事”**——不仅按指令,还要符合人类的价值观、偏好与安全约束。
训练范式的演进:
预训练 (Pre-training)
↓ 学习"语言 + 世界知识"
↓ 能力:续写、模仿
SFT (Supervised Fine-Tuning)
↓ 学习"指令 → 回答"
↓ 能力:助手、对话
RLHF (Reinforcement Learning from Human Feedback)
↓ 学习"偏好 + 价值观"
↓ 能力:有用 (Helpful) + 无害 (Harmless) + 诚实 (Honest)
1.2 SFT 解决不了的问题
即使做完 SFT,模型仍会表现出:
| 问题 | 表现 | 例子 |
|---|---|---|
| 胡编乱造 | 一本正经说错信息 | 编造不存在的论文 |
| 有害输出 | 教用户做危险的事 | 制造武器、绕过安全 |
| 过度冗长 | 不必要的客套话 | “作为 AI 助手…” |
| 风格不适配 | 不符合期望语气 | 客服场景过于正式 |
| 幻觉 + 自信 | 错误但充满信心 | 错误医学建议 |
这些问题是主观偏好与价值观判断,无法用"标准答案"完全标注——SFT 的最大似然损失没有偏好信号。
1.3 RLHF 的核心思想
两阶段训练:
- 奖励建模(Reward Modeling):学习一个人类偏好的"打分器"
- 策略优化(Policy Optimization):用 RL 让 LLM 生成高分回答
┌──────────────────────────────────────────────────────────────┐
│ RLHF 流程总览 │
├──────────────────────────────────────────────────────────────┤
│ │
│ ① 收集偏好数据 │
│ prompt + response_A + response_B + 人类标注 A>B │
│ ↓ │
│ ② 训练奖励模型 RM(y|x) │
│ loss = -log σ( RM(y_w|x) - RM(y_l|x) ) │
│ ↓ │
│ ③ 用 PPO 优化 SFT 模型 │
│ reward = RM(y|x) - β·KL(π_θ || π_ref) │
│ │
└──────────────────────────────────────────────────────────────┘
2. 偏好数据:从反馈到标注
2.1 数据形式
每个偏好样本是一个三元组或四元组:
(x, yw, yl) (x,\ y_w,\ y_l) (x, yw, yl)
其中:
- xxx:提示(prompt)
- ywy_wyw:被人类标注为"更好"的回答(winner)
- yly_lyl:被人类标注为"更差"的回答(loser)
2.2 数据来源
preference_data_sources = {
# 1. 人类标注
"human_annotation": "标注员对同一 prompt 的多个回答进行排序",
# 2. AI 反馈(RLAIF / Constitutional AI)
"ai_feedback": "用强模型按宪法原则评判弱模型回答",
# 3. 用户隐式反馈
"implicit_feedback": "点赞、采纳、停留时间、复制按钮",
# 4. 合成数据
"synthetic": "用 prompt 模板生成 y_w, 再故意制造 y_l 错误",
}
2.3 数据质量的关键
| 维度 | 影响 | 工程经验 |
|---|---|---|
| 标注一致性 | RM 学到正确偏好 | 多标注员 + Krippendorff α |
| 多样性 | RM 不偏科 | 跨领域、跨任务、跨长度 |
| 边界 case | RM 鲁棒性 | 故意加入对抗样本 |
| 失败模式覆盖 | 知道"哪里不好" | 红队测试数据 |
[!WARNING] 数据陷阱
标注员有风格偏好(偏好长回答),导致 RM 学会"越长越好"。解决:在 prompt 中明确告知"长度不是评判标准"。
3. 奖励模型(Reward Model)
3.1 Bradley-Terry 模型
假设人类偏好遵循 Bradley-Terry 模型:
P(yw≻yl∣x)=exp(R(x,yw))exp(R(x,yw))+exp(R(x,yl))=σ(R(x,yw)−R(x,yl)) P(y_w \succ y_l \mid x) = \frac{\exp(R(x, y_w))}{\exp(R(x, y_w)) + \exp(R(x, y_l))} = \sigma\big(R(x, y_w) - R(x, y_l)\big) P(yw≻yl∣x)=exp(R(x,yw))+exp(R(x,yl))exp(R(x,yw))=σ(R(x,yw)−R(x,yl))
其中 R(x,y)R(x, y)R(x,y) 是真实的"人类偏好分数"。我们用一个神经网络 RθR_\thetaRθ 来近似它。
3.2 损失函数
对数似然:
LRM=−E(x,yw,yl)∼D[logσ(Rθ(x,yw)−Rθ(x,yl))] \mathcal{L}_{\text{RM}} = -\mathbb{E}_{(x,y_w,y_l)\sim D}\Big[\log \sigma\big(R_\theta(x,y_w) - R_\theta(x,y_l)\big)\Big] LRM=−E(x,yw,yl)∼D[logσ(Rθ(x,yw)−Rθ(x,yl))]
直觉:拉大 winner 和 loser 的分数差。
3.3 实现
import torch
import torch.nn as nn
from transformers import AutoModelForCausalLM
class RewardModel(nn.Module):
"""
基于 LLM 的奖励模型。
通常是一个 LLM + 一个标量回归头,预测回答的"人类偏好分数"。
"""
def __init__(self, base_model_path: str, cache_dir: str = None):
super().__init__()
# 加载与 SFT 相同架构的 LLM
self.backbone = AutoModelForCausalLM.from_pretrained(
base_model_path, cache_dir=cache_dir, torch_dtype=torch.bfloat16
)
hidden_size = self.backbone.config.hidden_size
# 标量奖励头
self.score_head = nn.Linear(hidden_size, 1, bias=False)
# 初始化为接近 0(让初始分数居中)
nn.init.zeros_(self.score_head.weight)
def forward(self, input_ids: torch.Tensor, attention_mask: torch.Tensor):
"""
输入: 拼接 [prompt; response]
输出: 最后一个 token 的标量分数
"""
outputs = self.backbone(
input_ids=input_ids,
attention_mask=attention_mask,
output_hidden_states=True,
)
# 取每个序列最后一个有效 token 的隐藏状态
last_hidden = outputs.hidden_states[-1] # (B, L, H)
sequence_lengths = attention_mask.sum(dim=1) - 1
batch_idx = torch.arange(last_hidden.size(0), device=last_hidden.device)
last_token_hidden = last_hidden[batch_idx, sequence_lengths]
score = self.score_head(last_token_hidden).squeeze(-1)
return score
class RewardModelLoss(nn.Module):
"""Bradley-Terry 偏好损失"""
def forward(self, score_w: torch.Tensor, score_l: torch.Tensor) -> torch.Tensor:
return -torch.nn.functional.logsigmoid(score_w - score_l).mean()
3.4 训练实践
def train_reward_model(model, dataset, optimizer, epochs=2, batch_size=4):
for epoch in range(epochs):
for batch in dataset:
# batch 包含 (x, y_w, y_l) 三组 tokenized 输入
score_w = model(batch["input_ids_w"], batch["attention_mask_w"])
score_l = model(batch["input_ids_l"], batch["attention_mask_l"])
loss = -torch.nn.functional.logsigmoid(score_w - score_l).mean()
optimizer.zero_grad()
loss.backward()
optimizer.step()
3.5 评估 RM 的方法
def evaluate_rm(model, eval_pairs):
"""
评估 RM 的"判别准确率":
在测试集上,正确判定 y_w > y_l 的比例。
"""
correct = 0
for pair in eval_pairs:
score_w = model(pair["x_w"])
score_l = model(pair["x_l"])
if score_w > score_l:
correct += 1
return correct / len(eval_pairs)
4. PPO:经典的策略优化算法
4.1 为什么不用 SFT 直接拟合 RM?
直觉:用 RM 给回答打分,然后 SFT 用 reward⋅p(y∣x)\text{reward} \cdot p(y|x)reward⋅p(y∣x) 加权拟合。问题:
- 分布偏移:RM 只在 SFT 数据上学过,对"模型自己生成的分布"外推差
- Reward hacking:模型学会 RM 的盲点(比如生成"看起来很好但其实乱码"的回答)
- 无约束:可能偏离原始 SFT 模型,丧失能力
4.2 RL 基础
把 LLM 视为一个自回归策略 πθ(y∣x)\pi_\theta(y|x)πθ(y∣x):
πθ(y∣x)=∏t=1∣y∣πθ(yt∣x,y<t) \pi_\theta(y|x) = \prod_{t=1}^{|y|} \pi_\theta(y_t | x, y_{<t}) πθ(y∣x)=t=1∏∣y∣πθ(yt∣x,y<t)
目标是最大化期望奖励:
J(θ)=Ey∼πθ[R(x,y)] J(\theta) = \mathbb{E}_{y\sim\pi_\theta}\big[R(x,y)\big] J(θ)=Ey∼πθ[R(x,y)]
4.3 KL 约束:锚定参考策略
为防止 πθ\pi_\thetaπθ 偏离太远,加入 KL 惩罚:
JRLHF(θ)=Ey∼πθ[R(x,y)−β KL(πθ(⋅∣x) ∥ πref(⋅∣x))] J_{\text{RLHF}}(\theta) = \mathbb{E}_{y\sim\pi_\theta}\Big[R(x,y) - \beta\,\text{KL}\big(\pi_\theta(\cdot|x)\,\|\,\pi_{\text{ref}}(\cdot|x)\big)\Big] JRLHF(θ)=Ey∼πθ[R(x,y)−βKL(πθ(⋅∣x)∥πref(⋅∣x))]
其中 πref\pi_{\text{ref}}πref 是 SFT 模型(参数冻结),β\betaβ 控制约束强度。
[!TIP] 直觉
KL 项相当于"不许忘本"。模型可以优化 RM,但不能变成另一个完全不同的模型。
4.4 PPO 目标函数
PPO 是信赖域优化的实用版本:
LPPO(θ)=−Et[min(rt(θ) At, clip(rt(θ),1−ϵ,1+ϵ) At)] \mathcal{L}_{\text{PPO}}(\theta) = -\mathbb{E}_t\Big[\min\big(r_t(\theta)\,A_t,\ \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon)\,A_t\big)\Big] LPPO(θ)=−Et[min(rt(θ)At, clip(rt(θ),1−ϵ,1+ϵ)At)]
其中:
- 重要性比率:rt(θ)=πθ(at∣st)πθold(at∣st)r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{\text{old}}}(a_t|s_t)}rt(θ)=πθold(at∣st)πθ(at∣st)
- 优势函数:AtA_tAt(用 GAE 或 critic 估计)
- ϵ\epsilonϵ 通常为 0.2
clip 机制:限制新旧策略的比率不超出 [1−ϵ,1+ϵ][1-\epsilon, 1+\epsilon][1−ϵ,1+ϵ],避免一次更新步子过大。
4.5 RM + KL 的奖励设计
PPO 中每个 token 的"奖励":
rt={R(x,y)−β (logπθ(yt∣⋅)−logπref(yt∣⋅))t=T−β (logπθ(yt∣⋅)−logπref(yt∣⋅))t<T r_t = \begin{cases} R(x, y) - \beta\,\big(\log \pi_\theta(y_t|\cdot) - \log \pi_{\text{ref}}(y_t|\cdot)\big) & t = T \\ -\beta\,\big(\log \pi_\theta(y_t|\cdot) - \log \pi_{\text{ref}}(y_t|\cdot)\big) & t < T \end{cases} rt={R(x,y)−β(logπθ(yt∣⋅)−logπref(yt∣⋅))−β(logπθ(yt∣⋅)−logπref(yt∣⋅))t=Tt<T
只有最后一个 token获得真实 RM 分数,前面所有 token 的"奖励"只是负的 KL 惩罚——鼓励每个位置的策略不偏离参考。
4.6 PPO for LLM 的完整实现
import torch
import torch.nn.functional as F
def compute_gae(rewards, values, dones, gamma=0.99, lam=0.95):
"""广义优势估计 GAE"""
advantages = torch.zeros_like(rewards)
last_adv = 0
T = rewards.size(1)
for t in reversed(range(T)):
if t == T - 1:
next_value = 0
else:
next_value = values[:, t + 1]
nonterminal = 1.0 - dones[:, t]
delta = rewards[:, t] + gamma * next_value * nonterminal - values[:, t]
last_adv = delta + gamma * lam * nonterminal * last_adv
advantages[:, t] = last_adv
returns = advantages + values
return advantages, returns
def ppo_loss(
log_probs_new, # 新策略对每个 token 的 log prob: (B, T)
log_probs_old, # 旧策略(rollout 时)的 log prob
advantages, # GAE 优势
clip_ratio=0.2,
):
"""PPO clipped surrogate loss"""
ratio = torch.exp(log_probs_new - log_probs_old)
surr1 = ratio * advantages
surr2 = torch.clamp(ratio, 1 - clip_ratio, 1 + clip_ratio) * advantages
policy_loss = -torch.min(surr1, surr2).mean()
return policy_loss
def rlhf_train_step(
policy_model, # π_θ,待优化
ref_model, # π_ref,冻结
reward_model, # R(x,y)
critic, # 价值网络 V(s)
batch, # prompt 列表
beta=0.1, # KL 系数
clip_ratio=0.2,
):
"""
RLHF 单步训练流程(简化的 PPO):
1. 用当前 policy 生成回答(rollout)
2. 用 RM 评分
3. 计算 KL 惩罚
4. 用 GAE 计算优势
5. PPO 更新 policy 与 critic
"""
prompts = batch["prompts"]
# === 1. Rollout:用当前策略生成回答 ===
with torch.no_grad():
responses, response_mask = policy_model.generate(
prompts, max_new_tokens=256, do_sample=True, temperature=1.0
)
# === 2. 计算旧策略的 log prob(用于重要性采样) ===
with torch.no_grad():
log_probs_old = compute_log_probs(policy_model, responses)
# === 3. 用奖励模型打分 ===
with torch.no_grad():
rewards = reward_model(prompts + responses)
# === 4. 计算 KL 惩罚(per token) ===
with torch.no_grad():
log_probs_ref = compute_log_probs(ref_model, responses)
kl_per_token = log_probs_old - log_probs_ref # = log(π_θ / π_ref)
# 最后一个 token 获得 RM 分数;其余 token 仅 KL 惩罚
T = responses.size(1)
token_rewards = -beta * kl_per_token
token_rewards[:, -1] += rewards
# === 5. GAE 计算优势 ===
with torch.no_grad():
values = critic(responses)
dones = torch.zeros_like(token_rewards)
dones[:, -1] = 1.0 # 序列结束
advantages, returns = compute_gae(token_rewards, values, dones)
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
# === 6. PPO 更新(通常做 K_mini_epochs 次) ===
for _ in range(4):
log_probs_new = compute_log_probs(policy_model, responses)
policy_loss = ppo_loss(log_probs_new, log_probs_old, advantages, clip_ratio)
value_pred = critic(responses)
value_loss = F.mse_loss(value_pred, returns)
loss = policy_loss + 0.5 * value_loss
loss.backward()
# ... optimizer.step()
4.7 PPO 的工程痛点
| 痛点 | 原因 | 缓解 |
|---|---|---|
| 显存爆炸 | 同时持有 policy、ref、RM、critic | LoRA + 共享参数 |
| 训练不稳定 | 大 reward 方差 | Reward 归一化、KL warmup |
| Reward hacking | 模型找到 RM 漏洞 | 多 RM ensemble |
| 慢 | 每步 4 模型前向 | vLLM rollout |
| 崩溃 | 极端 KL 时崩 | 早停 + checkpoint rollback |
[!WARNING] 经典教训
InstructGPT 论文报告:RLHF 训练中,约 10-30% 的训练会因奖励坍缩或 KL 爆炸而中断。需要持续监控。
5. GRPO:DeepSeek 提出的高效替代
5.1 动机
PPO 的最大开销:
- 一个额外的 critic 网络(与 policy 同尺寸)
- 每个 prompt 一次采样 → GAE 优势估计
- 4 个模型同时在显存
5.2 GRPO 核心思想
**GRPO(Group Relative Policy Optimization)**由 DeepSeek 提出:
不需要 critic,对同一 prompt 采样一组回答,用组内相对排名作为优势。
5.3 算法流程
对每个 prompt x:
1. 用 π_θ 采样 G 个回答 {y_1, y_2, ..., y_G}
2. 用 RM 给每个回答打分 {r_1, r_2, ..., r_G}
3. 组内归一化得到优势:
A_i = (r_i - mean(r)) / std(r)
4. 用优势做 PPO 风格更新
5.4 优势计算
Ai=ri−mean(r)std(r),i=1,…,G A_i = \frac{r_i - \text{mean}(\mathbf{r})}{\text{std}(\mathbf{r})}, \quad i = 1,\ldots,G Ai=std(r)ri−mean(r),i=1,…,G
直觉:比平均好的回答获得正优势,比平均差的获得负优势。无需 critic,无需 GAE。
5.5 完整损失
LGRPO(θ)=−E[1G∑i=1G(min(riAi,clip(ri,1−ϵ,1+ϵ)Ai)−β KL(πθ∥πref))] \mathcal{L}_{\text{GRPO}}(\theta) = -\mathbb{E}\Big[\frac{1}{G}\sum_{i=1}^{G}\big(\min(r_i A_i, \text{clip}(r_i, 1-\epsilon, 1+\epsilon) A_i) - \beta\,\text{KL}(\pi_\theta \| \pi_{\text{ref}})\big)\Big] LGRPO(θ)=−E[G1i=1∑G(min(riAi,clip(ri,1−ϵ,1+ϵ)Ai)−βKL(πθ∥πref))]
其中重要性比率:
ri(θ)=πθ(yi∣x)πθold(yi∣x) r_i(\theta) = \frac{\pi_\theta(y_i|x)}{\pi_{\theta_{\text{old}}}(y_i|x)} ri(θ)=πθold(yi∣x)πθ(yi∣x)
5.6 GRPO 实现
import torch
import torch.nn.functional as F
def grpo_loss(
log_probs_new, # (B, G, T) — B 个 prompt,每个 G 个回答
log_probs_old, # 同上
rewards_group, # (B, G) — 组内每个回答的 RM 分数
ref_log_probs, # (B, G, T)
beta=0.04,
clip_ratio=0.2,
):
"""
GRPO 损失(DeepSeek-R1 风格)。
log_probs_new, log_probs_old, ref_log_probs 形状: (B, G, T)
rewards_group 形状: (B, G)
"""
# === 组内归一化得到优势 ===
mean_r = rewards_group.mean(dim=-1, keepdim=True)
std_r = rewards_group.std(dim=-1, keepdim=True) + 1e-8
advantages = (rewards_group - mean_r) / std_r # (B, G)
# === 重要性比率 ===
# 这里我们用平均 log prob(per sequence)作为比率
ratio = torch.exp(
(log_probs_new - log_probs_old).sum(dim=-1)
) # (B, G)
# === PPO-style clipped surrogate ===
surr1 = ratio * advantages
surr2 = torch.clamp(ratio, 1 - clip_ratio, 1 + clip_ratio) * advantages
policy_loss = -torch.min(surr1, surr2).mean()
# === KL 惩罚 ===
kl = (log_probs_new - ref_log_probs).sum(dim=-1).mean()
kl_loss = beta * kl
total_loss = policy_loss + kl_loss
return total_loss, {"policy_loss": policy_loss.item(), "kl": kl.item()}
5.7 PPO vs GRPO 对比
| 维度 | PPO | GRPO |
|---|---|---|
| Critic | 需要 | 不需要 |
| 优势估计 | GAE + Value Net | 组内排名 |
| 显存 | 4 模型 | 2 模型 (policy + ref) |
| 采样数 | 1/G | G(按 prompt) |
| 训练稳定 | 较稳定 | 需要足够大的 G |
| 代表工作 | InstructGPT, ChatGPT | DeepSeek-R1 |
6. DPO:免强化学习的对齐
6.1 核心洞见
DPO(Direct Preference Optimization)由 Stanford 在 2023 年提出,关键问题是:
能不能绕过 RM 和 RL,直接用偏好数据训练 LLM?
答案是:可以。RLHF 的目标函数有一个闭式解。
6.2 推导:从 RLHF 到 DPO
RLHF 目标:
maxπEx,y∼π[R(x,y)]−β KL(π(⋅∣x) ∥ πref(⋅∣x)) \max_\pi \mathbb{E}_{x,y\sim\pi}\Big[R(x,y)\Big] - \beta\,\text{KL}\big(\pi(\cdot|x)\,\|\,\pi_{\text{ref}}(\cdot|x)\big) πmaxEx,y∼π[R(x,y)]−βKL(π(⋅∣x)∥πref(⋅∣x))
其最优解为:
π∗(y∣x)=1Z(x)πref(y∣x)exp (1βR(x,y)) \pi^*(y|x) = \frac{1}{Z(x)}\pi_{\text{ref}}(y|x)\exp\!\Big(\frac{1}{\beta}R(x,y)\Big) π∗(y∣x)=Z(x)1πref(y∣x)exp(β1R(x,y))
其中 Z(x)=∑yπref(y∣x)exp(R(x,y)/β)Z(x) = \sum_y \pi_{\text{ref}}(y|x)\exp(R(x,y)/\beta)Z(x)=∑yπref(y∣x)exp(R(x,y)/β) 是归一化常数。
反解出 RM:
R(x,y)=βlogπ∗(y∣x)πref(y∣x)+βlogZ(x) R(x,y) = \beta\log\frac{\pi^*(y|x)}{\pi_{\text{ref}}(y|x)} + \beta\log Z(x) R(x,y)=βlogπref(y∣x)π∗(y∣x)+βlogZ(x)
代入 Bradley-Terry 偏好模型(Z(x)Z(x)Z(x) 抵消):
P(yw≻yl∣x)=σ(βlogπθ(yw∣x)πref(yw∣x)−βlogπθ(yl∣x)πref(yl∣x)) P(y_w \succ y_l | x) = \sigma\Big(\beta\log\frac{\pi_\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)} - \beta\log\frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)}\Big) P(yw≻yl∣x)=σ(βlogπref(yw∣x)πθ(yw∣x)−βlogπref(yl∣x)πθ(yl∣x))
这就是 DPO 损失!
6.3 DPO 损失
LDPO(θ)=−E(x,yw,yl)[logσ(β Δθ(x,yw,yl))] \mathcal{L}_{\text{DPO}}(\theta) = -\mathbb{E}_{(x,y_w,y_l)}\Big[\log\sigma\big(\beta\,\Delta_\theta(x,y_w,y_l)\big)\Big] LDPO(θ)=−E(x,yw,yl)[logσ(βΔθ(x,yw,yl))]
其中:
Δθ=logπθ(yw∣x)πref(yw∣x)−logπθ(yl∣x)πref(yl∣x) \Delta_\theta = \log\frac{\pi_\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)} - \log\frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)} Δθ=logπref(yw∣x)πθ(yw∣x)−logπref(yl∣x)πθ(yl∣x)
6.4 关键优势
RLHF 流程:
偏好数据 → 训练 RM → PPO 优化 policy
(3 个阶段,复杂)
DPO 流程:
偏好数据 → 直接训练 policy
(1 个阶段,简单)
| 维度 | RLHF/PPO | DPO |
|---|---|---|
| 训练阶段 | 3(RM、critic、policy) | 1(policy) |
| 显存 | 4 模型 | 2 模型 |
| 超参 | β\betaβ + clip + GAE + KL | 只需 β\betaβ |
| 在线采样 | 需要 | 不需要(offline) |
| 稳定性 | 易崩 | 稳定 |
| 性能 | 略好(一般) | 与 PPO 相当 |
| 代表模型 | GPT-4, Claude 2 | Llama 3 Instruct, Mistral |
6.5 DPO 实现
import torch
import torch.nn.functional as F
def compute_log_probs(model, input_ids, attention_mask, response_mask):
"""计算回答部分的 log π(y|x) 之和"""
outputs = model(input_ids=input_ids, attention_mask=attention_mask)
logits = outputs.logits[:, :-1, :] # 预测下一个 token
labels = input_ids[:, 1:]
log_probs = F.log_softmax(logits, dim=-1)
# 取出真实 token 的 log prob
token_log_probs = log_probs.gather(2, labels.unsqueeze(-1)).squeeze(-1)
# 只对 response 部分求和(mask 掉 prompt)
response_token_log_probs = token_log_probs * response_mask[:, 1:]
return response_token_log_probs.sum(dim=-1)
def dpo_loss(
policy_chosen_logps, # π_θ(y_w|x) 对数和
policy_rejected_logps,
ref_chosen_logps, # π_ref(y_w|x) 对数和
ref_rejected_logps,
beta=0.1,
):
"""
DPO 损失。
policy_chosen_logps: (B,)
policy_rejected_logps: (B,)
ref_chosen_logps: (B,)
ref_rejected_logps: (B,)
"""
# log π_θ(y|x) - log π_ref(y|x)
policy_logratios = policy_chosen_logps - policy_rejected_logps
ref_logratios = ref_chosen_logps - ref_rejected_logps
# DPO 目标:β * (Δ_θ - Δ_ref)
logits = beta * (policy_logratios - ref_logratios)
# 偏好损失
loss = -F.logsigmoid(logits).mean()
# 额外监控:chosen reward margin
chosen_rewards = beta * (policy_chosen_logps - ref_chosen_logps).detach()
rejected_rewards = beta * (policy_rejected_logps - ref_rejected_logps).detach()
return loss, {
"chosen_rewards": chosen_rewards.mean().item(),
"rejected_rewards": rejected_rewards.mean().item(),
"accuracy": (chosen_rewards > rejected_rewards).float().mean().item(),
}
6.6 DPO 训练循环
from torch.optim import AdamW
def train_dpo(policy_model, ref_model, dataloader, beta=0.1, lr=1e-6, epochs=1):
optimizer = AdamW(policy_model.parameters(), lr=lr)
ref_model.eval() # 冻结
for epoch in range(epochs):
for batch in dataloader:
# batch: {chosen: input_ids, mask, response_mask, rejected: ...}
# Policy 对 chosen 和 rejected 的 log prob
policy_chosen_logps = compute_log_probs(
policy_model, batch["chosen_input_ids"],
batch["chosen_attention_mask"], batch["chosen_response_mask"]
)
policy_rejected_logps = compute_log_probs(
policy_model, batch["rejected_input_ids"],
batch["rejected_attention_mask"], batch["rejected_response_mask"]
)
# Ref(不计算梯度)
with torch.no_grad():
ref_chosen_logps = compute_log_probs(
ref_model, batch["chosen_input_ids"],
batch["chosen_attention_mask"], batch["chosen_response_mask"]
)
ref_rejected_logps = compute_log_probs(
ref_model, batch["rejected_input_ids"],
batch["rejected_attention_mask"], batch["rejected_response_mask"]
)
loss, metrics = dpo_loss(
policy_chosen_logps, policy_rejected_logps,
ref_chosen_logps, ref_rejected_logps,
beta=beta,
)
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(policy_model.parameters(), 1.0)
optimizer.step()
print(f"loss={loss.item():.4f} "
f"acc={metrics['accuracy']:.2f} "
f"chosen_r={metrics['chosen_rewards']:.2f} "
f"rejected_r={metrics['rejected_rewards']:.2f}")
6.7 DPO 的局限与变体
[!WARNING] DPO 偏差
DPO 容易过拟合到偏好数据,并出现"verbosity bias"——偏好变长但不一定变好。
主要变体:
| 算法 | 改进点 | 公式 |
|---|---|---|
| IPO | 防止过拟合 | L=(logπθ(yw)πref(yw)−logπθ(yl)πref(yl)−12β)2\mathcal{L} = (\log\frac{\pi_\theta(y_w)}{\pi_{\text{ref}}(y_w)} - \log\frac{\pi_\theta(y_l)}{\pi_{\text{ref}}(y_l)} - \frac{1}{2\beta})^2L=(logπref(yw)πθ(yw)−logπref(yl)πθ(yl)−2β1)2 |
| KTO | 不需要成对偏好(单回答 + 标签) | Kahneman-Tversky 前景理论 |
| SimPO | 移除 reference model | L=−logσ(∣yw∣∣yw∣R(yw)−∣yl∣∣yl∣R(yl)−γ)\mathcal{L} = -\log\sigma(\frac{|y_w|}{|y_w|}R(y_w) - \frac{|y_l|}{|y_l|}R(y_l) - \gamma)L=−logσ(∣yw∣∣yw∣R(yw)−∣yl∣∣yl∣R(yl)−γ) |
| ORPO | SFT + DPO 联合 | 联合 SFT 与 odds ratio 损失 |
| CPO | SFT + DPO 简化 | 加 SFT 正则项 |
7. 进阶:从 RLHF 到 RLAIF / Constitutional AI
7.1 RLAIF (RL from AI Feedback)
动机:人类标注慢、贵、不一致。用强模型替代人类打标。
传统 RLHF:
人类对 (y_A, y_B) 排序 → RM → PPO
RLAIF:
GPT-4 对 (y_A, y_B) 按"宪法原则"排序 → RM → PPO
7.2 Constitutional AI (CAI)
Anthropic 提出,两阶段:
阶段 1:监督式(SL-CAI)
1. 模型生成回答 y
2. 模型基于"宪法原则"(如"不要有害")批评 y
3. 模型根据批评改写 y'
4. 用 (prompt, y') 做监督微调
阶段 2:RL-CAI
1. 模型生成回答 y_A, y_B
2. 模型按宪法原则判断哪个更好
3. 用这些 AI 偏好训练 RM
4. PPO 训练
宪法原则示例:
## 宪法(Constitution)
1. 请选择更**有帮助**的回答。
2. 请选择更**诚实**的回答。
3. 请选择更**无害**的回答,不应协助暴力、欺骗。
4. 请避免**歧视性**内容。
5. 请避免**违反隐私**的回答。
7.3 自我奖励语言模型(Meta,2024)
迭代过程:
1. 用 CAI 生成偏好数据
2. 训练 RM
3. 用 DPO 训练 LLM
4. 重复:用更强的 LLM 重新打标
→ 模型自己当老师
8. 实战:完整 RLHF 训练流水线
8.1 三阶段流水线
┌──────────────────────────────────────────────────────────────┐
│ 完整 RLHF 训练流水线 │
├──────────────────────────────────────────────────────────────┤
│ │
│ Stage 0: Base Model (预训练 LLM) │
│ ↓ │
│ Stage 1: SFT (监督微调) │
│ ↓ 用高质量指令数据训练 π_SFT │
│ Stage 2: Reward Model Training │
│ ↓ 用偏好数据训练 R_θ │
│ Stage 3: RL Fine-tuning (PPO / GRPO / DPO) │
│ ↓ │
│ Final: Aligned Model │
│ │
└──────────────────────────────────────────────────────────────┘
8.2 推荐训练顺序(基于 Llama 2/3 论文经验)
1. 预训练(基模型) ............................. 1-3 月,~1万 GPU·小时
2. SFT(指令微调) ............................... 数小时
3. RLHF 阶段 A:采样 + RM 训练 .................. 数小时
4. RLHF 阶段 B:PPO/DPO ......................... 数十小时
5. 安全对齐(可选的第二轮 RLHF) ............... 数小时
8.3 关键超参表
| 超参 | 推荐值 | 备注 |
|---|---|---|
| 学习率(policy) | 1e-6 ~ 5e-6 | 比 SFT 小一个数量级 |
| 学习率(RM) | 1e-5 ~ 5e-5 | RM 可以稍大 |
| β\betaβ(KL 系数) | 0.04 ~ 0.2 | DPO 论文用 0.1 |
| PPO clip ϵ\epsilonϵ | 0.2 | 标准值 |
| Rollout 采样数 GGG | 4 ~ 16 | GRPO 用更大 |
| Mini-batch 大小 | 1 ~ 4 | 受显存限制 |
| GAE λ\lambdaλ | 0.95 | |
| 折扣 γ\gammaγ | 1.0 | 序列级任务 |
8.4 监控指标
metrics_to_log = {
# === RM 训练 ===
"rm/loss": "RM Bradley-Terry 损失",
"rm/accuracy": "RM 在测试集上判别准确率",
"rm/score_margin": "winner - loser 平均分数差",
# === PPO/DPO 训练 ===
"policy/kl": "π_θ 与 π_ref 的 KL",
"policy/reward": "RM 给当前生成回答的平均分",
"policy/reward_std": "RM 分数方差",
"policy/clip_fraction": "被 clip 的 token 比例",
"policy/advantages": "优势均值",
# === 任务指标 ===
"eval/helpfulness": "GPT-4 评分(1-10)",
"eval/harmlessness": "安全率",
"eval/win_rate": "相对基模型的胜率",
}
8.5 常见 Failure Mode
┌────────────────────────┬──────────────────────────────────┐
│ 现象 │ 原因 / 解决 │
├────────────────────────┼──────────────────────────────────┤
│ KL 爆炸 │ β 太小 / LR 太大 → 增大 β、降低 LR │
│ Reward 坍缩到 0 │ RM 过拟合 → 加正则、做 ensemble │
│ 模型输出变成乱码 │ Reward hacking → 多 RM、加规则 │
│ 模型变得太保守(拒绝一切)│ RM 偏负样本 → 重新平衡数据 │
│ 偏好过拟合(DPO) │ 学习率太大 / epoch 多 → 减小 │
│ Verbosity bias(越长越好)│ RM 没控制长度 → 加长度归一化 │
└────────────────────────┴──────────────────────────────────┘
9. 算法全景对比
9.1 主流对齐方法
┌─────────────────────────────────────────────────────────────┐
│ 对齐方法谱系 │
├─────────────────────────────────────────────────────────────┤
│ │
│ RL-based: │
│ ├── PPO (2017) ── 经典,4 模型,复杂 │
│ ├── GRPO (2024) ── DeepSeek-R1,无 critic │
│ ├── REINFORCE ── 最简,公式简洁 │
│ └── A2C / A3C ── 通用 RL,LLM 中较少用 │
│ │
│ RL-free (Direct Preference): │
│ ├── DPO (2023) ── 开山之作,1 阶段 │
│ ├── IPO (2023) ── 防过拟合 │
│ ├── KTO (2024) ── 不需成对偏好 │
│ ├── SimPO (2024) ── 无参考模型 │
│ ├── ORPO (2024) ── SFT + DPO 联合 │
│ └── CPO (2024) ── SFT + DPO 正则 │
│ │
│ Self-Play / Iterative: │
│ ├── Constitutional AI ── AI 当标注员 │
│ ├── Self-Rewarding LM ── 自我奖励迭代 │
│ └── SPIN / Iterative DPO── 多轮迭代偏好对齐 │
│ │
└─────────────────────────────────────────────────────────────┘
9.2 决策树
需要对齐 LLM
│
├── 你有多少 GPU?多少数据?
│ │
│ ├── 资源受限(< 8 卡 A100)
│ │ └── ✅ DPO / SimPO(最便宜、最稳)
│ │
│ ├── 中等资源(8-32 卡)
│ │ ├── 数据多 + 想追最强性能 → ✅ GRPO
│ │ └── 数据少 + 想稳定 → ✅ DPO
│ │
│ └── 大量资源(> 64 卡)
│ └── ✅ PPO + 大 RM ensemble
│
├── 你能采集偏好数据吗?
│ │
│ ├── 可以(人工标注)
│ │ └── ✅ PPO / DPO
│ │
│ ├── 不能人工,但有强模型
│ │ └── ✅ RLAIF + PPO / DPO
│ │
│ └── 都没有
│ └── ✅ KTO(只需要"好/坏"标签)
│
└── 是否需要在线探索?
│
├── 是 → ✅ PPO / GRPO
└── 否 → ✅ DPO(offline)
9.3 性能 vs 复杂度矩阵
| 算法 | 实现复杂度 | 显存 | 性能 | 稳定性 | 推荐场景 |
|---|---|---|---|---|---|
| PPO | 高 | 高 | ★★★★★ | ★★ | 大厂、追 SOTA |
| GRPO | 中 | 中 | ★★★★★ | ★★★ | DeepSeek 复现 |
| DPO | 低 | 低 | ★★★★ | ★★★★★ | 快速迭代、初创 |
| IPO | 低 | 低 | ★★★★ | ★★★★ | DPO 过拟合时 |
| KTO | 低 | 低 | ★★★ | ★★★★ | 只有二值标签 |
| SimPO | 低 | 最低 | ★★★★ | ★★★★ | 显存紧张 |
10. 核心数学公式汇总
10.1 Bradley-Terry 偏好模型
P(yw≻yl∣x)=σ(R(x,yw)−R(x,yl)) P(y_w \succ y_l | x) = \sigma\big(R(x,y_w) - R(x,y_l)\big) P(yw≻yl∣x)=σ(R(x,yw)−R(x,yl))
10.2 RM 损失
LRM=−E[logσ(Rθ(x,yw)−Rθ(x,yl))] \mathcal{L}_{\text{RM}} = -\mathbb{E}\Big[\log\sigma\big(R_\theta(x,y_w) - R_\theta(x,y_l)\big)\Big] LRM=−E[logσ(Rθ(x,yw)−Rθ(x,yl))]
10.3 RLHF 目标
J(πθ)=E[R(x,y)]−β KL(πθ(⋅∣x) ∥ πref(⋅∣x)) J(\pi_\theta) = \mathbb{E}\Big[R(x,y)\Big] - \beta\,\text{KL}\big(\pi_\theta(\cdot|x)\,\|\,\pi_{\text{ref}}(\cdot|x)\big) J(πθ)=E[R(x,y)]−βKL(πθ(⋅∣x)∥πref(⋅∣x))
10.4 PPO Clipped Surrogate
LCLIP=−Et[min(rtAt, clip(rt,1−ϵ,1+ϵ) At)] \mathcal{L}^{\text{CLIP}} = -\mathbb{E}_t\Big[\min\big(r_t A_t,\ \text{clip}(r_t, 1-\epsilon, 1+\epsilon)\,A_t\big)\Big] LCLIP=−Et[min(rtAt, clip(rt,1−ϵ,1+ϵ)At)]
10.5 GRPO 组内归一化
Ai=ri−mean(r)std(r) A_i = \frac{r_i - \text{mean}(\mathbf{r})}{\text{std}(\mathbf{r})} Ai=std(r)ri−mean(r)
10.6 DPO 损失
LDPO=−E[logσ(β Δθ)],Δθ=logπθ(yw)πref(yw)−logπθ(yl)πref(yl) \mathcal{L}_{\text{DPO}} = -\mathbb{E}\Big[\log\sigma\big(\beta\,\Delta_\theta\big)\Big], \quad \Delta_\theta = \log\frac{\pi_\theta(y_w)}{\pi_{\text{ref}}(y_w)} - \log\frac{\pi_\theta(y_l)}{\pi_{\text{ref}}(y_l)} LDPO=−E[logσ(βΔθ)],Δθ=logπref(yw)πθ(yw)−logπref(yl)πθ(yl)
10.7 IPO 损失
LIPO=E[(logπθ(yw)πref(yw)−logπθ(yl)πref(yl)−12β)2] \mathcal{L}_{\text{IPO}} = \mathbb{E}\Big[\big(\log\frac{\pi_\theta(y_w)}{\pi_{\text{ref}}(y_w)} - \log\frac{\pi_\theta(y_l)}{\pi_{\text{ref}}(y_l)} - \frac{1}{2\beta}\big)^2\Big] LIPO=E[(logπref(yw)πθ(yw)−logπref(yl)πθ(yl)−2β1)2]
11. 总结
11.1 对齐范式的演进
阶段 1: SFT only
→ 学会"按指令回答"
→ 无法处理偏好、价值观
阶段 2: RLHF (InstructGPT, ChatGPT)
→ 引入 RM + PPO
→ 性能强但训练复杂
阶段 3: Direct Preference (DPO)
→ 闭式解,无需 RL
→ 工程简单,性能相当
阶段 4: Self-Improvement (RLAIF, Self-Rewarding)
→ AI 替代人类标注
→ 持续自我改进
11.2 选择建议
项目初期 / 资源受限:
→ DPO 是最好的起点。简单、稳定、性能强。
追求 SOTA:
→ PPO + 大 RM(如果算力足够)
→ 或 GRPO(DeepSeek 路径)
数据形式特殊:
→ 只有"好/坏"标签 → KTO
→ 没有 reference model → SimPO
长期迭代:
→ Constitutional AI + 自我奖励
→ SPIN / Iterative DPO
``
更多推荐





所有评论(0)