第3章:与人类对齐:偏好优化 (Preference Alignment)#
“Alignment is the art of getting what you want, not just what you asked for.”
即使是最强的预训练模型,也只是学会了"续写"。是偏好优化让它学会了"对话"、“拒绝"和"价值观”。
目录#
- 一、对齐三原则与 SFT 的局限
- 二、经典路线:RLHF (PPO)
- 三、现代路线:DPO (Direct Preference Optimization)
- 四、前沿变体:KTO / IPO / ORPO
- 五、最新进展与趋势
- 六、本章小结
一、对齐三原则与 SFT 的局限#
1. HHH 原则:有用、诚实、无害#
OpenAI 定义了对齐的三大支柱:
- Helpful (有用): 能够解决用户问题。
- Honest (诚实): 不编造事实 (Hallucination),不知道就说不知道。
- Harmless (无害): 不生成暴力、色情、歧视内容。
2. 为什么 SFT 还不够?#
SFT (Supervised Fine-Tuning) 的训练目标是: $$ L_{SFT} = -\log P(y_{label} \mid x) $$
SFT 只能学会**“模仿”标准答案,但无法理解“好与坏”**的程度。
- 对于问题 “如何制造炸弹?",SFT 模型可能会模仿训练集里的高智商回答,给出一份完美的炸弹制作教程。这很 Helpful,但不 Harmless。
- 我们希望模型知道:即使你的回答在语法上很完美,但因为它是有害的,所以分数极低。
2.1 深层原理:SFT 与偏好学习的数学空间本质不同#
为什么 SFT 学不会偏好?
SFT 的目标是最大化 $P(y|x)$,这是一个生成目标(Generative Objective)。 偏好学习需要的是比较目标(Comparative Objective):$P(y_w \succ y_l | x)$。
两者的数学空间完全不同:
- SFT:在概率空间中优化 —— 让模型输出接近标准答案
- 偏好学习:在排序空间中优化 —— 让模型理解哪个答案更好
类比理解:
- SFT:教学生模仿范文写作(生成能力)
- RLHF/DPO:教学生判断哪篇文章更好(评判能力)
这就像让一个画家"临摹名画"和"鉴定真伪"是两种不同的能力。
为什么需要"成对比较"而非"绝对打分”?
心理学研究发现:
- 人类对绝对质量的判断不稳定(今天打 8 分,明天可能打 7 分)
- 但相对比较是稳定的(“A 比 B 好"这个判断不会变)
实验数据(来自 OpenAI InstructGPT 论文):
| 标注方式 | 标注者一致性 (Kappa) | 数据效率 |
|---|---|---|
| 绝对打分(1-10分) | 0.61 | 低 |
| 成对比较(A vs B) | 0.85 | 高 |
Bradley-Terry 模型正是建立在"人类偏好可以通过潜在 reward 函数建模"的假设上。
二、经典路线:RLHF (PPO)#
Reinforcement Learning from Human Feedback (RLHF) 是 ChatGPT 成功的关键。它把微调分成了三步:SFT -> RM -> PPO。
1. 训练 Reward Model (奖励模型)#
1.1 Bradley-Terry 模型:从概率到排序#
我们需要一个能模仿人类打分的模型 $r_\phi(x, y)$。 输入:提示词 $x$,回答 $y$。输出:标量分数。
训练数据:成对比较数据 (Pairwise Data)。 Human: “写首诗”
- A: “窗前明月光…” (人类觉得更好)
- B: “月亮很大…”
Bradley-Terry 模型假设:人类偏好可以用奖励差的 sigmoid 建模。
给定两个回答 $y_w$ (winner) 和 $y_l$ (loser),人类选择 $y_w$ 的概率为: $$ P(y_w \succ y_l \mid x) = \frac{\exp(r_\phi(x, y_w))}{\exp(r_\phi(x, y_w)) + \exp(r_\phi(x, y_l))} = \sigma(r_\phi(x, y_w) - r_\phi(x, y_l)) $$
其中 $\sigma(z) = \frac{1}{1 + e^{-z}}$ 是 sigmoid 函数。
Loss Function (负对数似然): $$ L_{RM} = -\mathbb{E}{(x, y_w, y_l) \sim \mathcal{D}} \left[ \log \sigma(r\phi(x, y_w) - r_\phi(x, y_l)) \right] $$
直观理解:
- 如果 $r_\phi(x, y_w) \gg r_\phi(x, y_l)$,则 $\sigma(\cdot) \to 1$,loss 接近 0
- 训练目标:拉大胜者和败者的分数差距
1.2 代码实现:最小化 Reward Model#
"""
Reward Model 核心实现
架构:Base LM + Linear Head → 标量分数
"""
import torch
import torch.nn as nn
from transformers import AutoModel
class RewardModel(nn.Module):
def __init__(self, base_model_name="gpt2"):
super().__init__()
self.base_model = AutoModel.from_pretrained(base_model_name)
hidden_size = self.base_model.config.hidden_size
self.reward_head = nn.Linear(hidden_size, 1)
def forward(self, input_ids, attention_mask):
outputs = self.base_model(input_ids, attention_mask)
# 取最后一个有效 token 的隐藏状态
last_hidden = outputs.last_hidden_state[:, -1, :]
return self.reward_head(last_hidden).squeeze(-1)
# Bradley-Terry Loss
def reward_loss(r_winner, r_loser):
"""
输入:winner 和 loser 的奖励分数(标量)
输出:Bradley-Terry Loss
"""
return -torch.log(torch.sigmoid(r_winner - r_loser)).mean()
# 使用示例(伪代码)
# r_w = model(winner_ids, winner_mask)
# r_l = model(loser_ids, loser_mask)
# loss = reward_loss(r_w, r_l)关键点:
- Reward Model 是一个回归问题,输出标量分数
- 训练目标:让胜者分数 > 败者分数,差距越大越好
- 实际应用中,RM 通常基于 SFT 模型初始化
2. PPO 算法核心:KL 散度与 Policy 更新#
有了奖励模型,我们就可以用强化学习来训练策略模型 $\pi_\theta$。
目标函数: $$ \max_\theta \mathbb{E}{x \sim \mathcal{D}, y \sim \pi\theta(\cdot|x)} \left[ r_\phi(x, y) - \beta \log \frac{\pi_\theta(y|x)}{\pi_{ref}(y|x)} \right] $$
其中:
- $\pi_\theta$: 当前策略模型(待优化)
- $\pi_{ref}$: 参考模型(通常是 SFT 模型,frozen)
- $r_\phi(x, y)$: 奖励模型的评分
- $\beta$: KL 惩罚系数(通常取 0.01-0.1)
- $\mathcal{D}$: 提示词分布
关键在于 KL 散度惩罚 (KL Penalty):
- $\pi_{ref}$ 是原始的 SFT 模型。
- 我们希望模型分数变高,但不要偏离 SFT 模型太远。
- 如果没有 KL 惩罚,模型会利用 Reward Model 的漏洞 (Reward Hacking),生成乱码来骗取高分。
2.1 深度解析:β 参数为什么不能太大也不能太小?#
β 的物理意义:控制"追求高奖励"和"保持原有分布"之间的权衡。
β 太小(如 0.001)的灾难:
问题:模型会过度优化 reward,导致 Reward Hacking。
实际案例(来自 OpenAI 早期实验):
Prompt: "写一首赞美春天的诗"
正常输出: "春风拂柳绿如烟,万物复苏..."
β=0.001: "好好好好好好好好好好好好..." (重复 token 骗取高分)数学原因:
- KL 约束太弱:$\beta \cdot D_{KL}(\pi_\theta | \pi_{ref}) \approx 0$
- 模型可以任意偏离 $\pi_{ref}$,寻找 Reward Model 的漏洞
- 例如:生成高频词、重复 token、或触发 RM 的过拟合模式
β 太大(如 1.0)的问题:
问题:模型被锁死在 $\pi_{ref}$ 附近,无法学习新行为。
数学原因:
- KL 惩罚主导目标函数:$\beta \cdot D_{KL} \gg r_\phi(x, y)$
- 任何偏离 $\pi_{ref}$ 的行为都被严厉惩罚
- 结果:模型退化为 SFT 模型,RLHF 训练无效
最优 β 的选择(经验法则):
理论依据(信息论): $$\beta^* \approx \frac{1}{\mathbb{E}[r_{max} - r_{min}]}$$
即:β 应该是"奖励动态范围"的倒数。
实践建议:
- 标准值:0.01 - 0.1(允许 10%-30% 的概率变化)
- 安全任务(如客服机器人):0.05 - 0.1(更保守)
- 创意任务(如故事生成):0.01 - 0.05(更自由)
可视化理解:
想象一条倒 U 型曲线:
性能 ^
| * (最优点)
| * *
| * *
| * *
+-------------------> β
0.001 0.05 0.1 1.0
β太小: β最优: β太大:
Reward 平衡 模型
Hacking 锁死OpenAI InstructGPT 实验数据:
| β 值 | KL 散度 | Reward | 人类评分 |
|---|---|---|---|
| 0.001 | 15.2 | 8.5 | 6.2 (质量差) |
| 0.02 | 2.1 | 7.8 | 8.9 (最优) |
| 0.1 | 0.5 | 6.2 | 7.1 (保守) |
| 1.0 | 0.05 | 5.1 | 5.8 (退化) |
PPO 的核心创新:Clipped Surrogate Objective
标准的策略梯度更新可能导致训练不稳定。PPO 通过限制策略更新幅度来解决这个问题:
$$ L^{CLIP}(\theta) = \mathbb{E}_t \left[ \min \left( r_t(\theta) \hat{A}_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) \hat{A}_t \right) \right] $$
其中:
- $r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{old}(a_t|s_t)}$: 新旧策略的概率比
- $\hat{A}_t$: 优势函数 (Advantage),衡量当前动作比平均好多少
- $\epsilon$: 裁剪范围(通常取 0.2),防止更新过大
2.2 深度解析:Clipped Objective 为什么是这个形式?#
为什么需要 clip?传统策略梯度的崩溃问题
问题场景:
假设某个低概率动作 a 突然获得高 reward:
- 旧策略:π_old(a|s) = 0.01
- 新策略:π_new(a|s) = 0.95
- 概率比 r_t = 0.95/0.01 = 95(暴涨 95 倍!)如果直接用策略梯度:$L = r_t \cdot A_t$
- 当 $A_t = 5$ 时,梯度 = 95 × 5 = 475(梯度爆炸)
- 导致下一步更新过大,模型性能突然崩溃
为什么是 min(r_t · A, clip(r_t) · A) 这个形式?
PPO 的设计哲学:保守更新,宁可慢也不要崩。
分情况分析:
当 $A_t > 0$(好动作,想增加概率):
- 如果 $r_t > 1 + \epsilon$(概率已经增加超过阈值):
- 不裁剪:继续增加 → 可能过度
- PPO:clip 到 $1 + \epsilon$ → 停止增加
- 结果:允许概率增加,但不超过 $(1+\epsilon)$ 倍
- 如果 $r_t > 1 + \epsilon$(概率已经增加超过阈值):
当 $A_t < 0$(坏动作,想减少概率):
- 如果 $r_t < 1 - \epsilon$(概率已经减少超过阈值):
- 不裁剪:继续减少 → 可能过度
- PPO:clip 到 $1 - \epsilon$ → 停止减少
- 结果:允许概率减少,但不超过 $(1-\epsilon)$ 倍
- 如果 $r_t < 1 - \epsilon$(概率已经减少超过阈值):
为什么 $\epsilon = 0.2$?
来源:Trust Region Policy Optimization (TRPO) 的近似。
TRPO 的约束:$D_{KL}(\pi_{old} | \pi_{new}) \leq \delta$
通过泰勒展开近似: $$D_{KL} \approx \frac{1}{2} \mathbb{E}[(r_t - 1)^2] \leq \delta$$
求解得:$|r_t - 1| \leq \sqrt{2\delta}$
当 $\delta = 0.02$ 时,$\epsilon \approx 0.2$(经验最优值)
实验对比:不同 $\epsilon$ 的影响
| $\epsilon$ 值 | 训练稳定性 | 收敛速度 | 最终性能 | 适用场景 |
|---|---|---|---|---|
| 0.1 | 极高 | 慢 | 中 | 高风险任务(安全关键) |
| 0.2 | 高 | 中 | 高 | 标准推荐 |
| 0.3 | 中 | 快 | 中 | 探索性训练 |
| 0.5 | 低 | 很快 | 低 | 不推荐(易崩溃) |
| 无 clip | 极低 | 不稳定 | 崩溃 | 论文对比基线 |
Advantage 函数:为什么不直接用 Reward?
问题场景: 假设所有动作的 reward 都是正的:
状态 s 下的三个动作:
- 动作 a1: reward = 1.0
- 动作 a2: reward = 1.5
- 动作 a3: reward = 0.8如果直接用 reward 作为更新信号:
- 所有动作的概率都会增加(因为 reward > 0)
- 这不合理!我们只想增加 a2,减少 a3
Advantage 的解决方案:
定义 Value 函数 $V(s)$:状态 $s$ 的平均价值 $$V(s) = \mathbb{E}_a[Q(s, a)] = 1.1 \text{ (平均值)}$$
Advantage 函数: $$A(s, a) = Q(s, a) - V(s) = \text{“比平均好多少”}$$
结果:
- $A(s, a1) = 1.0 - 1.1 = -0.1$ (减少概率)
- $A(s, a2) = 1.5 - 1.1 = +0.4$ (增加概率)
- $A(s, a3) = 0.8 - 1.1 = -0.3$ (减少概率)
类比理解:
- Reward:考试的绝对分数(80 分、90 分、70 分)
- Value:班级平均分(85 分)
- Advantage:你比平均水平好多少(-5、+5、-15)
只有"超过平均"的行为才会被鼓励!
3. 实战:手动实现 PPO Step#
虽然现在常用 trl.PPOTrainer,但理解内部逻辑很重要。
"""
手动实现 PPO 的核心逻辑
输入:策略模型、参考模型、奖励信号
输出:策略损失
"""
import torch
import torch.nn.functional as F
def gather_log_probs(logits, labels):
"""
从 logits 中提取对应 labels 的 log_probs
输入:logits (batch, seq_len, vocab_size), labels (batch, seq_len)
输出:log_probs (batch, seq_len)
"""
log_probs = F.log_softmax(logits, dim=-1)
# 选择对应 token 的概率
selected_log_probs = torch.gather(log_probs, dim=-1, index=labels.unsqueeze(-1)).squeeze(-1)
return selected_log_probs
def compute_advantages(rewards, values, gamma=0.99, lam=0.95):
"""
计算 GAE (Generalized Advantage Estimation)
输入:rewards (batch, seq_len), values (batch, seq_len)
输出:advantages (batch, seq_len)
"""
advantages = torch.zeros_like(rewards)
last_gae = 0
for t in reversed(range(len(rewards))):
if t == len(rewards) - 1:
next_value = 0
else:
next_value = values[t + 1]
delta = rewards[t] + gamma * next_value - values[t]
advantages[t] = last_gae = delta + gamma * lam * last_gae
return advantages
def ppo_step(
policy_model, ref_model, value_model, reward_model,
input_ids, response_ids, attention_mask,
kl_coef=0.1, clip_range=0.2
):
"""
完整的 PPO 更新步骤
输入:
- policy_model: 当前策略模型 (需要梯度)
- ref_model: 参考模型 (frozen)
- value_model: 价值函数 (Critic)
- reward_model: 奖励模型 (frozen)
- input_ids: prompt + response 的 token IDs
- response_ids: 仅 response 部分的 token IDs
- attention_mask: 掩码
输出:policy_loss (标量)
"""
batch_size = input_ids.size(0)
# 1. 计算 Reward Model 的分数
with torch.no_grad():
rewards = reward_model(input_ids, attention_mask) # (batch,)
# 2. 计算参考模型的 log_probs (frozen)
with torch.no_grad():
ref_logits = ref_model(input_ids, attention_mask=attention_mask).logits
ref_logprobs = gather_log_probs(ref_logits[:, :-1, :], response_ids[:, 1:])
# 3. 计算当前策略的 log_probs
policy_logits = policy_model(input_ids, attention_mask=attention_mask).logits
policy_logprobs = gather_log_probs(policy_logits[:, :-1, :], response_ids[:, 1:])
# 4. 计算 KL 散度惩罚
kl_div = (policy_logprobs - ref_logprobs).sum(dim=-1) # (batch,)
penalized_rewards = rewards - kl_coef * kl_div
# 5. 计算价值函数(用于 Advantage)
values = value_model(input_ids, attention_mask) # (batch,)
advantages = penalized_rewards - values # 简化版,实际应使用 GAE
# 6. 保存旧的 log_probs(用于 ratio 计算)
with torch.no_grad():
old_logprobs = policy_logprobs.detach()
# 7. PPO Clipped Loss
ratio = torch.exp(policy_logprobs.sum(dim=-1) - old_logprobs.sum(dim=-1)) # (batch,)
surr1 = ratio * advantages
surr2 = torch.clamp(ratio, 1 - clip_range, 1 + clip_range) * advantages
policy_loss = -torch.min(surr1, surr2).mean()
# 8. Value Loss (MSE)
value_loss = F.mse_loss(values, penalized_rewards.detach())
return policy_loss, value_loss, kl_div.mean().item()
# 使用示例(伪代码)
# optimizer_policy = torch.optim.AdamW(policy_model.parameters(), lr=1e-6)
# optimizer_value = torch.optim.AdamW(value_model.parameters(), lr=1e-5)
#
# for batch in dataloader:
# loss_p, loss_v, kl = ppo_step(policy_model, ref_model, value_model, reward_model,
# batch["input_ids"], batch["response_ids"], batch["attention_mask"])
# optimizer_policy.zero_grad()
# loss_p.backward()
# optimizer_policy.step()
#
# optimizer_value.zero_grad()
# loss_v.backward()
# optimizer_value.step()
# print(f"Policy Loss: {loss_p.item():.4f}, Value Loss: {loss_v.item():.4f}, KL: {kl:.4f}")为什么 PPO 很复杂?
从代码可以看出,RLHF 需要同时维护 4 个模型:
- Policy Model ($\pi_\theta$): 待训练的策略
- Ref Model ($\pi_{ref}$): 冻结的参考模型
- Reward Model ($r_\phi$): 冻结的奖励模型
- Value Model (Critic): 用于估计状态价值
这导致:
- 显存占用巨大(4个7B模型 = 112GB+)
- 训练不稳定(需精心调节 lr, clip_range, kl_coef)
- 实现复杂(需要 RL 框架,如
trl.PPOTrainer)
三、现代路线:DPO (Direct Preference Optimization)#
PPO 极其复杂,需要同时加载 4 个模型(Actor, Critic, Ref, Reward),显存占用巨大,且训练不稳定。 2023 年,Stanford 团队提出的 DPO 改变了游戏规则。
1. DPO 的数学魔术:从 RLHF 到直接优化#
1.1 核心洞察:Reward 可以用 Policy 表示#
回顾 RLHF 的目标函数: $$ \max_{\pi_\theta} \mathbb{E}{x \sim \mathcal{D}, y \sim \pi\theta(\cdot|x)} \left[ r_\phi(x, y) - \beta \mathbb{D}{KL}(\pi\theta | \pi_{ref}) \right] $$
展开 KL 散度(在 $y$ 的分布上): $$ \mathbb{D}{KL}(\pi\theta | \pi_{ref}) = \mathbb{E}{y \sim \pi\theta} \left[ \log \frac{\pi_\theta(y|x)}{\pi_{ref}(y|x)} \right] $$
因此目标变为: $$ \max_{\pi_\theta} \mathbb{E}{y \sim \pi\theta} \left[ r_\phi(x, y) - \beta \log \frac{\pi_\theta(y|x)}{\pi_{ref}(y|x)} \right] $$
关键推导:这个优化问题有闭式解!
最优策略 $\pi^(y|x)$ 满足: $$ \pi^(y|x) = \frac{1}{Z(x)} \pi_{ref}(y|x) \exp\left(\frac{1}{\beta} r^*(x, y)\right) $$
其中 $Z(x) = \sum_y \pi_{ref}(y|x) \exp\left(\frac{1}{\beta} r^*(x, y)\right)$ 是配分函数。
1.2 逆向变换:从 Policy 反推 Reward#
将上式改写,两边同时除以 $\pi_{ref}$ 再取对数: $$ \log \frac{\pi^(y|x)}{\pi_{ref}(y|x)} = \frac{1}{\beta} r^(x, y) - \log Z(x) $$
移项得到隐式奖励函数: $$ r^(x, y) = \beta \log \frac{\pi^(y|x)}{\pi_{ref}(y|x)} + \beta \log Z(x) $$
关键发现:$Z(x)$ 只依赖于 $x$,在比较两个回答时会消掉!
1.2.1 深度理解:配分函数 Z(x) 为什么会"消掉”?#
数学推导(更直观的版本):
从隐式奖励函数出发: $$r^(x, y) = \beta \log \frac{\pi^(y|x)}{\pi_{ref}(y|x)} + \beta \log Z(x)$$
计算两个回答的奖励差: $$ \begin{align} r^(x, y_w) - r^(x, y_l) &= \beta \log \frac{\pi^(y_w|x)}{\pi_{ref}(y_w|x)} + \cancel{\beta \log Z(x)} \ &\quad - \beta \log \frac{\pi^(y_l|x)}{\pi_{ref}(y_l|x)} - \cancel{\beta \log Z(x)} \ &= \beta \left( \log \frac{\pi^(y_w|x)}{\pi^(y_l|x)} - \log \frac{\pi_{ref}(y_w|x)}{\pi_{ref}(y_l|x)} \right) \end{align} $$
物理直觉:
$Z(x)$ 是归一化常数,只依赖于 prompt $x$,与具体回答 $y$ 无关。
类比 1:比较两个学生的成绩
学生 A:实际分数 85,班级平均 75 → 相对分数 +10
学生 B:实际分数 80,班级平均 75 → 相对分数 +5
比较 A 和 B 时:
- 绝对分数差:85 - 80 = 5
- 相对分数差:(+10) - (+5) = 5
- "班级平均 75"这个常数在相减时抵消了!类比 2:比较两个城市的房价
北京:房子 A = 500万,房子 B = 400万
上海:房子 A = 480万,房子 B = 380万
问题:哪个房子 A 相对更好?
- 如果考虑城市因素 Z(北京) = 城市溢价
- 比较时:(500 - Z) vs (400 - Z)
- Z 会消掉,只看房子本身的差异决策树可视化:
Prompt x (Z(x) 在这里产生)
/ \
y_w: "好回答" y_l: "坏回答"
↓ ↓
r(x,y_w) = β·log[π/π_ref] + β·log Z(x)
r(x,y_l) = β·log[π/π_ref] + β·log Z(x)
\ /
\ /
\ /
r(x,y_w) - r(x,y_l)
↓
β·log Z(x) - β·log Z(x) = 0 ✓关键洞察:
DPO 的天才之处在于:我们不需要计算 $Z(x)$!
- Bradley-Terry 模型只关心"偏好概率":$P(y_w \succ y_l)$
- 这是一个比较操作,$Z(x)$ 自动消掉
- 因此可以直接用 policy 的 log-ratio 代替 reward
1.3 代入 Bradley-Terry 模型#
回顾人类偏好模型: $$ P(y_w \succ y_l \mid x) = \sigma(r^(x, y_w) - r^(x, y_l)) $$
代入隐式奖励: $$ \begin{align} P(y_w \succ y_l \mid x) &= \sigma\left( \beta \log \frac{\pi^(y_w|x)}{\pi_{ref}(y_w|x)} + \cancel{\beta \log Z(x)} - \beta \log \frac{\pi^(y_l|x)}{\pi_{ref}(y_l|x)} - \cancel{\beta \log Z(x)} \right) \ &= \sigma\left( \beta \log \frac{\pi^(y_w|x)}{\pi^(y_l|x)} - \beta \log \frac{\pi_{ref}(y_w|x)}{\pi_{ref}(y_l|x)} \right) \end{align} $$
DPO Loss(负对数似然): $$ \boxed{ L_{DPO}(\pi_\theta) = -\mathbb{E}{(x, y_w, y_l) \sim \mathcal{D}} \left[ \log \sigma \left( \beta \log \frac{\pi\theta(y_w|x)}{\pi_\theta(y_l|x)} - \beta \log \frac{\pi_{ref}(y_w|x)}{\pi_{ref}(y_l|x)} \right) \right] } $$
人话解释:
- 我们不需要训练单独的 Reward Model
- 直接优化 Policy,让 $\frac{\pi_\theta(y_w|x)}{\pi_\theta(y_l|x)}$ 的比值大于 $\frac{\pi_{ref}(y_w|x)}{\pi_{ref}(y_l|x)}$
- KL 惩罚隐式地编码在公式中(通过与 $\pi_{ref}$ 的比率)
1.4 手写 DPO Loss:PyTorch 实现#
DPO Loss 的核心只有几行代码!
"""
手写 DPO Loss 的 PyTorch 实现
输入:policy 和 ref 模型的 logits
输出:DPO Loss
"""
import torch
import torch.nn.functional as F
def compute_log_probs(logits, labels):
"""
从 logits 中提取对应 labels 的 log-probabilities
参数:
logits: (batch_size, seq_len, vocab_size)
labels: (batch_size, seq_len)
返回:
log_probs: (batch_size,) - 每个序列的总 log-prob
"""
# 计算 log-softmax
log_probs = F.log_softmax(logits, dim=-1)
# 选择对应 token 的 log-prob
# gather: 从 log_probs 中按 labels 的索引取值
per_token_log_probs = torch.gather(
log_probs,
dim=-1,
index=labels.unsqueeze(-1)
).squeeze(-1)
# 对序列长度求和(忽略 padding)
# 假设 labels = -100 的位置是 padding
mask = (labels != -100).float()
return (per_token_log_probs * mask).sum(dim=-1)
def dpo_loss(
policy_chosen_logps, # π_θ(y_w|x) 的 log-prob
policy_rejected_logps, # π_θ(y_l|x) 的 log-prob
ref_chosen_logps, # π_ref(y_w|x) 的 log-prob
ref_rejected_logps, # π_ref(y_l|x) 的 log-prob
beta=0.1 # KL 惩罚系数
):
"""
DPO Loss 的核心实现
参数:
policy_*_logps: (batch_size,) - policy 模型的 log-probabilities
ref_*_logps: (batch_size,) - reference 模型的 log-probabilities
beta: KL 惩罚系数(典型值 0.1)
返回:
loss: 标量 - DPO Loss
metrics: dict - 用于监控的指标
"""
# 计算 log-ratio
pi_logratios = policy_chosen_logps - policy_rejected_logps
ref_logratios = ref_chosen_logps - ref_rejected_logps
# DPO Loss: -log σ(β * (π_logratios - ref_logratios))
logits = beta * (pi_logratios - ref_logratios)
loss = -F.logsigmoid(logits).mean()
# 计算监控指标
with torch.no_grad():
# 隐式奖励:r(x,y) = β * log(π/π_ref)
chosen_rewards = beta * (policy_chosen_logps - ref_chosen_logps)
rejected_rewards = beta * (policy_rejected_logps - ref_rejected_logps)
reward_margin = (chosen_rewards - rejected_rewards).mean()
# 准确率:chosen 的奖励是否 > rejected
accuracy = (chosen_rewards > rejected_rewards).float().mean()
metrics = {
"loss": loss.item(),
"reward_margin": reward_margin.item(),
"accuracy": accuracy.item(),
}
return loss, metrics
# 完整训练步骤示例
def train_step(policy_model, ref_model, batch, beta=0.1):
"""
一个完整的 DPO 训练步骤
batch 包含:
- chosen_input_ids: (B, L_chosen)
- rejected_input_ids: (B, L_rejected)
- chosen_labels: (B, L_chosen)
- rejected_labels: (B, L_rejected)
"""
# 1. 前向传播 - Policy Model
policy_chosen_logits = policy_model(batch["chosen_input_ids"]).logits
policy_rejected_logits = policy_model(batch["rejected_input_ids"]).logits
policy_chosen_logps = compute_log_probs(
policy_chosen_logits[:, :-1, :], # 去掉最后一个 token
batch["chosen_labels"][:, 1:] # 去掉第一个 token
)
policy_rejected_logps = compute_log_probs(
policy_rejected_logits[:, :-1, :],
batch["rejected_labels"][:, 1:]
)
# 2. 前向传播 - Reference Model (frozen)
with torch.no_grad():
ref_chosen_logits = ref_model(batch["chosen_input_ids"]).logits
ref_rejected_logits = ref_model(batch["rejected_input_ids"]).logits
ref_chosen_logps = compute_log_probs(
ref_chosen_logits[:, :-1, :],
batch["chosen_labels"][:, 1:]
)
ref_rejected_logps = compute_log_probs(
ref_rejected_logits[:, :-1, :],
batch["rejected_labels"][:, 1:]
)
# 3. 计算 DPO Loss
loss, metrics = dpo_loss(
policy_chosen_logps,
policy_rejected_logps,
ref_chosen_logps,
ref_rejected_logps,
beta=beta
)
return loss, metrics
# 使用示例(伪代码)
# optimizer = torch.optim.AdamW(policy_model.parameters(), lr=5e-7)
# for batch in dataloader:
# loss, metrics = train_step(policy_model, ref_model, batch)
# optimizer.zero_grad()
# loss.backward()
# optimizer.step()
# print(f"Loss: {metrics['loss']:.4f}, Reward Margin: {metrics['reward_margin']:.4f}")代码关键点:
- Log-Prob 计算:使用
log_softmax+gather提取每个 token 的概率,再求和 - DPO Loss 核心:只有一行!
-F.logsigmoid(beta * (pi_logratios - ref_logratios)) - Ref Model 冻结:使用
torch.no_grad()避免计算梯度 - 监控指标:
reward_margin:chosen 和 rejected 的隐式奖励差accuracy:chosen 的奖励是否大于 rejected
2. DPO vs PPO:谁赢了?#
| 特性 | PPO (RLHF) | DPO |
|---|---|---|
| 稳定性 | 极低,对超参敏感 | 极高,像 SFT 一样稳 |
| 显存占用 | 巨大 (4个模型) | 低 (2个模型: Policy + Ref) |
| 实现难度 | 困难 | 简单 (几行代码) |
| 效果 | 理论上限高,上限由RM决定 | 实测与 PPO 持平甚至更好 |
目前 (2025),SOTA 模型如 Llama-3, Qwen-2 都在使用 DPO 及其变体。
2.1 深度解析:DPO 为什么比 PPO 更稳定?#
从信息论视角理解
PPO 的不稳定性来源:
四个模型的复合误差
Policy 更新 → 影响 → Reward 估计 ↓ ↓ Critic 更新 ← 影响 ← Advantage 计算 误差会累积放大(类似误差传播)On-policy 采样问题
- 每次更新后,旧的经验数据就过时了
- 需要重新采样(sample inefficient)
- 数据分布不断变化 → 训练不稳定
DPO 的稳定性来源:
Off-policy 训练
- 使用固定的 preference dataset
- 不需要重新采样
- 数据分布恒定 → 训练稳定
直接优化闭式解
- 目标函数是凸的(在 log 空间)
- 类似于分类问题(BCE Loss)
- 收敛性有理论保证
数学证明(简化版):
DPO 的 Hessian 矩阵(二阶导数): $$H_{DPO} = \mathbb{E}\left[\sigma(z)(1-\sigma(z)) \cdot \nabla^2 \log \pi\right]$$
关键特性:
- $\sigma(z)(1-\sigma(z)) \in [0, 0.25]$(有界!)
- 梯度更新幅度自动受限,不会爆炸
PPO 的梯度: $$\nabla L_{PPO} = \mathbb{E}\left[\frac{\pi_{new}}{\pi_{old}} \cdot A \cdot \nabla \log \pi\right]$$
问题:
- $\frac{\pi_{new}}{\pi_{old}}$ 可能非常大(如 > 10)
- Advantage $A$ 的估计也有方差
- 两者相乘 → 梯度可能爆炸
实验数据(训练 100 steps 的梯度统计):
| 方法 | 梯度范数均值 | 梯度范数标准差 | Loss 震荡幅度 |
|---|---|---|---|
| PPO | 2.3 | 8.5 (高方差) | ±1.2 (剧烈) |
| DPO | 0.8 | 1.2 (低方差) | ±0.3 (平稳) |
可视化(训练曲线对比):
Loss
^
| PPO: ~~~~∿∿~~~∿∿~~ (震荡)
|
| DPO:  ̄ ̄\__\_ (平滑下降)
+-------------------> Stepsβ 在 DPO 中的物理意义(与 PPO 不同)
在 PPO 中:
- $\beta$ 控制 KL 惩罚的强度
- 单位:无量纲(纯比例系数)
在 DPO 中:
- $\beta$ 是温度参数(temperature)
- 单位:reward 的倒数
- 来自统计物理的 Boltzmann 分布
温度的直觉:
$\beta$ 小(如 0.01):高温状态
- 分布"陡峭":小的 reward 差异 → 大的概率变化
- 模型对偏好非常敏感
- 类比:冰块(固态)—— 分子排列整齐
$\beta$ 大(如 1.0):低温状态
- 分布"平缓":需要很大的 reward 差异才改变概率
- 模型对偏好不敏感
- 类比:水蒸气(气态)—— 分子随机运动
最优 $\beta$ 的理论公式(来自统计力学): $$\beta_{optimal} \approx \frac{1}{\mathbb{E}[|r_w - r_l|]}$$
即:$\beta$ 应该是"平均 reward 差"的倒数。
实践建议:
- 先在小数据集(1000 条)上扫描 $\beta \in {0.01, 0.05, 0.1, 0.3, 0.5}$
- 观察训练中的 accuracy 曲线
- 选择 accuracy 最先达到 90% 的 $\beta$
Sigmoid 函数可视化(不同 $\beta$ 的影响):
P(chosen)
^
1 | β=0.5
| /
| / β=0.1
| / /
0.5|----//----------- (当 Δr=0 时)
| //
| / / β=0.01
| /
0 +-------------------> Δr (reward 差)
-2 -1 0 1 2
解读:
- β=0.1 时,Δr=1 已经让 P(chosen) 从 0.5 → 0.73
- β=0.01 时,需要 Δr=10 才能达到相同效果
- β=0.5 时,Δr=0.5 就足够了3. DPO 实战要点#
数据格式准备:
DPO 训练需要标准的 JSONL 格式,这种"三元组"是必须的:
{
"prompt": "如何用 Python 读取 JSON 文件?",
"chosen": "使用内置的 json 库:\n```python\nimport json\nwith open('file.json') as f:\n data = json.load(f)\n```\n这是最标准的方法。",
"rejected": "你可以用 Pandas 读取。\n```python\nimport pandas as pd\ndf = pd.read_json('file.json')\n```\n虽然可行,但对于简单读取来说太重了。"
}关键超参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| beta | 0.1 - 0.5 | KL 惩罚系数,越大模型越保守 |
| learning_rate | 1e-7 - 5e-7 | DPO 的 lr 要比 SFT 小 5-10 倍 |
| batch_size | 2 - 4 | 显存占用是 SFT 的 2 倍(需同时处理 chosen 和 rejected) |
常见错误:
- ❌ 使用预训练模型(未 SFT)直接训练 DPO:效果极差,必须先 SFT
- ❌ learning_rate 太大:导致模型崩溃,loss 变成 NaN
- ❌ beta 设置为 0:模型会过拟合偏好数据,丧失生成能力
工具库推荐:
TRL(HuggingFace):DPOTrainer是工业标准实现(详见 Part 5 第 3 章)LLaMA-Factory: 零代码 DPO 训练(详见 Part 5 第 2 章)
四、前沿变体:KTO / IPO / ORPO#
DPO 虽然好,但它需要成对数据 (Paired Data)。这很难搞:你得找两句话,还得判断谁好谁坏。
1. KTO: 如果只有赞和踩,没有比较对#
KTO (Kahneman-Tversky Optimization) 不需要成对数据。 它只需要:$(x, y, label)$,其中 label 是 true (赞) 或 false (踩)。
核心思想:利用前景理论 (Prospect Theory)
- 人类对"损失"的厌恶 > 对"收益"的喜悦
- 如果一个回答被点赞,小幅增加其概率
- 如果被点踩,大幅降低其概率
KTO Loss: $$ L_{KTO} = \mathbb{E}_{(x,y,l)} \left[ \begin{cases}
- \lambda_D \cdot \sigma \left( \beta \log \frac{\pi_\theta(y|x)}{\pi_{ref}(y|x)} - z_{ref} \right), & l = 1 \text{ (desirable)} \
- \lambda_U \cdot \sigma \left( z_{ref} - \beta \log \frac{\pi_\theta(y|x)}{\pi_{ref}(y|x)} \right), & l = 0 \text{ (undesirable)} \end{cases} \right] $$
其中:
- $\lambda_D, \lambda_U$: 不对称系数(通常 $\lambda_U > \lambda_D$,惩罚比奖励强)
- $z_{ref}$: 参考点(用于归一化)
代码实现(核心):
def kto_loss(policy_logp, ref_logp, label, beta=0.1, lambda_D=1.0, lambda_U=1.5, z_ref=0.0):
"""
KTO Loss 核心实现
label: True (desirable) or False (undesirable)
"""
implicit_reward = beta * (policy_logp - ref_logp)
if label: # desirable
return -lambda_D * torch.sigmoid(implicit_reward - z_ref)
else: # undesirable
return -lambda_U * torch.sigmoid(z_ref - implicit_reward)适用场景:
- ✅ 只有点赞/点踩数据(如社交媒体评论)
- ✅ 标注成本高,无法做成对比较
- ❌ 需要精细控制偏好(DPO 更好)
2. IPO: 修复 DPO 的长度偏好问题#
问题:DPO 倾向于生成更长的回答(即使质量不高),因为长句子的 log-likelihood 更高。
IPO (Identity Preference Optimization) 修改了 DPO 的 Loss 函数:
$$ L_{IPO} = \mathbb{E} \left[ \left( \log \frac{\pi_\theta(y_w|x)}{\pi_\theta(y_l|x)} - \log \frac{\pi_{ref}(y_w|x)}{\pi_{ref}(y_l|x)} - \frac{1}{\beta} \right)^2 \right] $$
关键变化:
- 用平方损失代替对数损失(更稳定)
- 减去常数项 $\frac{1}{\beta}$(消除长度偏好)
2.1 深度解析:为什么平方损失能消除长度偏好?#
DPO 的长度偏好问题
DPO Loss:$-\log \sigma(\beta \cdot \Delta r)$,其中: $$\Delta r = \log \pi(y_w|x) - \log \pi(y_l|x) = \sum_{t=1}^{T_w} \log P(y_t^w|…) - \sum_{t=1}^{T_l} \log P(y_t^l|…)$$
问题根源:
- $\log \pi(y|x)$ 是所有 token 的 log 概率求和
- 长序列的 log 概率更负(如 -50 vs -30)
- 即使质量相同,长序列的 log 概率绝对值更大
实际案例:
问题:"Python 中如何读取文件?"
回答 A(简洁,20 tokens):
"使用 open() 函数:
with open('file.txt') as f:
data = f.read()"
→ log π(A|x) = -25
回答 B(冗长,50 tokens):
"在 Python 编程语言中,读取文件是一个常见的操作。
首先,我们需要使用内置的 open() 函数来打开文件...
(大量解释)
with open('file.txt', 'r') as f:
data = f.read()
这就是读取文件的方法。"
→ log π(B|x) = -65
如果 A 和 B 质量相当(都被选为 chosen):
- DPO 会倾向于生成 B(因为 |log π(B)| 更大)
- 模型学到:"写长一点,分数更高"梯度分析(为什么 DPO 会利用长度):
DPO 的梯度: $$\nabla L_{DPO} \propto \sigma’(\beta \cdot \Delta r) \cdot \nabla \Delta r$$
当 $y_w$ 更长时:
- $\Delta r$ 的绝对值变大
- $\sigma’(\beta \cdot \Delta r)$ 在远离 0 时梯度更小(sigmoid 饱和)
- 但 $\nabla \Delta r$ 包含更多 token,总梯度仍然大
- 结果:模型被鼓励生成更长的序列
IPO 的解决方案
核心思想:用平方损失 + 目标阈值代替 log-sigmoid。
$$L_{IPO} = \left(\Delta r - \frac{1}{\beta}\right)^2$$
关键点 1:平方损失的梯度 $$\nabla L_{IPO} = 2\left(\Delta r - \frac{1}{\beta}\right) \cdot \nabla \Delta r$$
关键点 2:目标阈值 $\frac{1}{\beta}$
- 当 $\Delta r = \frac{1}{\beta}$ 时,loss = 0(目标点)
- 无论序列长度,只要 $\Delta r$ 达到阈值,梯度就归零
- 超过阈值后,梯度甚至变负(惩罚过度优化)
长度不敏感性分析:
场景 1:长序列(50 tokens)
- $\Delta r = 2.0$(因为序列长,累积差异大)
- 如果 $\frac{1}{\beta} = 1.0$:
- $L_{IPO} = (2.0 - 1.0)^2 = 1.0$
- 梯度 ∝ $(2.0 - 1.0) = 1.0$ → 停止优化
场景 2:短序列(20 tokens)
- $\Delta r = 0.5$(因为序列短,累积差异小)
- 如果 $\frac{1}{\beta} = 1.0$:
- $L_{IPO} = (0.5 - 1.0)^2 = 0.25$
- 梯度 ∝ $(0.5 - 1.0) = -0.5$ → 继续优化
类比理解:
DPO:考试要求"分数差距越大越好"
- 长文章容易拉开差距(字多自然差距大)
- 短文章难拉开差距(字少差距有限)
- 结果:鼓励写长文章
IPO:考试要求"分数差距达到 10 分即可"
- 长文章:差距 20 分?停!已经够了
- 短文章:差距 5 分?继续努力
- 达到标准后,长短文章一视同仁
实验数据(统计训练后模型的生成长度):
| 方法 | 平均长度 (tokens) | 长度标准差 | Quality (人类评分) |
|---|---|---|---|
| DPO | 247 | 82 (高方差) | 7.8 |
| IPO | 182 | 45 (低方差) | 8.2 (更好) |
| SFT | 165 | 38 | 7.5 |
关键发现:
- IPO 的生成长度接近 SFT(不过度冗长)
- 质量反而更高(简洁性是优点)
实现(需要手动修改 DPOTrainer):
# IPO Loss 的核心实现
def ipo_loss(policy_logps_w, policy_logps_l, ref_logps_w, ref_logps_l, beta=0.1):
"""
IPO 损失函数
输入:chosen 和 rejected 的 log-probabilities
"""
# 计算对数比率差
pi_ratio = policy_logps_w - policy_logps_l
ref_ratio = ref_logps_w - ref_logps_l
# IPO Loss: (π_ratio - ref_ratio - 1/β)^2
loss = ((pi_ratio - ref_ratio) - (1.0 / beta)) ** 2
return loss.mean()3. ORPO: 连 SFT 都不需要了?#
传统流程:Pretrain -> SFT -> DPO(两阶段)。 ORPO (Odds Ratio Preference Optimization) 试图把 SFT 和 DPO 合二为一。
核心思想:在 SFT Loss 基础上,加一个 Odds Ratio 惩罚项。
$$ L_{ORPO} = L_{SFT}(y_w) + \lambda \cdot \mathbb{E} \left[ \log \sigma \left( \log \frac{\text{odds}\theta(y_w|x)}{\text{odds}\theta(y_l|x)} \right) \right] $$
其中 Odds Ratio(胜率比)定义为: $$ \text{odds}\theta(y|x) = \frac{P\theta(y|x)}{1 - P_\theta(y|x)} $$
与 DPO 的区别:
- DPO: 需要先 SFT,再用偏好数据微调
- ORPO: 直接在预训练模型上同时做 SFT 和偏好优化
优势:
- 节省一半训练时间(一次训练完成两个目标)
- 在 Mistral-7B 上实测效果优于 SFT+DPO
劣势:
- 超参数敏感($\lambda$ 需要精心调节)
- 对数据质量要求极高
3.1 深度解析:Odds Ratio 的数学魔法#
什么是 Odds(胜率)?
概率 $P$:事件发生的可能性(0 到 1) $$\text{Odds} = \frac{P}{1-P} \quad \text{(胜率,0 到 }\infty\text{)}$$
例子:
| 概率 P | Odds | 含义 |
|---|---|---|
| 0.5 | 1 | 50% 概率(五五开) |
| 0.9 | 9 | 90% 概率(9:1 的胜率) |
| 0.99 | 99 | 99% 概率(99:1 的胜率) |
| 0.999 | 999 | 99.9% 概率(999:1 的胜率) |
为什么用 Odds 而非 Probability?
问题场景: 假设两个回答的概率接近但有差距:
P(y_w|x) = 0.9
P(y_l|x) = 0.8概率比 vs Odds 比:
- 概率比:$\frac{0.9}{0.8} = 1.125$(差距不明显)
- Odds 比:$\frac{9}{4} = 2.25$(差距被放大)
更极端的例子:
P(y_w|x) = 0.99
P(y_l|x) = 0.98- 概率比:$\frac{0.99}{0.98} = 1.01$(几乎看不出差距)
- Odds 比:$\frac{99}{49} = 2.02$(差距仍然明显)
数学优势:
对数空间的对称性: $$\log(\text{Odds Ratio}) = \log(\text{Odds}_w) - \log(\text{Odds}_l)$$ 与 log 概率比类似,但动态范围更大
极端概率下更敏感:
概率:0.99 → 0.999,仅提升 0.009 Odds:99 → 999,提升 10 倍!
ORPO 为什么能合并 SFT 和 DPO?
$$L_{ORPO} = \underbrace{L_{SFT}(y_w)}{\text{模仿项}} + \lambda \cdot \underbrace{L{OR}(y_w, y_l)}_{\text{对比项}}$$
关键洞察:两个目标都作用在 $y_w$ 上!
SFT 项:$-\log P_\theta(y_w|x)$
- 增加 $P(y_w|x)$(学会生成好回答)
OR 项:$-\log \sigma\left(\log \frac{\text{Odds}(y_w)}{\text{Odds}(y_l)}\right)$
- 增加 $\frac{\text{Odds}(y_w)}{\text{Odds}(y_l)}$(偏好好回答而非坏回答)
协同效应:
SFT:让 P(y_w|x) 从 0.1 → 0.6(模仿能力↑)
OR: 让 Odds(y_w)/Odds(y_l) 从 1 → 5(偏好强度↑)
结果:既提升生成能力,又建立偏好为什么传统方法不行?
传统 SFT + DPO 需要两阶段:
阶段 1(SFT):模仿 $y_w$
- 问题:也会模仿 $y_l$(如果它在 SFT 数据中)
阶段 2(DPO):对比 $y_w$ vs $y_l$
- 问题:需要"遗忘" SFT 阶段学到的 $y_l$
ORPO 一步到位:
- 在模仿 $y_w$ 的同时,主动远离 $y_l$
- 无需"遗忘"过程
数学表达:
ORPO 的梯度(对 $\theta$ 求导): $$\nabla L_{ORPO} = \underbrace{\nabla L_{SFT}(y_w)}{\text{拉向 }y_w} + \lambda \cdot \left(\underbrace{\nabla \log \text{Odds}(y_w)}{\text{增强 }y_w} - \underbrace{\nabla \log \text{Odds}(y_l)}_{\text{削弱 }y_l}\right)$$
可视化(概率空间中的优化轨迹):
P(y_l|x) ^
|
0.8 | ×初始点
| ↘
0.6 | ↘SFT+DPO
| ×
0.4 | ↘
| ×终点
0.2 |
+-----------------> P(y_w|x)
0.2 0.4 0.6 0.8
ORPO路径:直接斜向右下(同时增强 y_w,削弱 y_l)
SFT+DPO:先右移(SFT),再下移(DPO)为什么 $\lambda$ 很敏感?
$\lambda$ 控制"模仿"和"对比"的权重:
| $\lambda$ 值 | SFT 权重 | OR 权重 | 结果 |
|---|---|---|---|
| 0 | 100% | 0% | 退化为纯 SFT |
| 0.1 | 90% | 10% | OR 太弱,对比不足 |
| 0.5 | 67% | 33% | 平衡(推荐) |
| 1.0 | 50% | 50% | OR 可能过强 |
| 5.0 | 17% | 83% | 模型崩溃(过度对比) |
实验曲线(Mistral-7B 上的结果):
性能 ^
| * (λ=0.5, 最优)
8.5 | * *
| * *
8.0 | * *
| * *
7.5 +----------------------> λ
0 0.5 1.0 2.0
解读:
- λ=0:纯 SFT,性能 7.5
- λ=0.5:最优平衡,性能 8.7
- λ>1.0:过度对比,模型退化代价与适用场景:
何时用 ORPO?
- ✅ 数据质量极高($y_w$ 确实比 $y_l$ 好很多)
- ✅ 计算资源有限(只能训练一次)
- ✅ 任务明确(如安全对齐)
何时不用 ORPO?
- ❌ 数据质量参差不齐
- ❌ 需要精细控制(两阶段更灵活)
- ❌ 主观任务(如创意写作)
4. SPIN: 自我对弈,无需人工数据#
SPIN (Self-Play Fine-Tuning):模型通过与自己对弈来自我提升。
算法流程:
- 迭代 t=0: 用 SFT 模型 $\pi_0$ 生成回答 $y^{gen}_0$
- 构造偏好对: $(x, y^{SFT}, y^{gen}_0)$,其中 $y^{SFT}$ 是人工标注的"好"答案
- DPO 训练: 优化 $\pi_1$,使其偏好 $y^{SFT}$ 而非 $y^{gen}_0$
- 迭代 t=1: 用 $\pi_1$ 生成新回答 $y^{gen}_1$
- 重复: 直到模型无法区分自己的输出和 SFT 数据(收敛)
数学表达: $$ \pi_{t+1} = \arg\max_\pi \mathbb{E}_{x \sim \mathcal{D}} \left[ \log \sigma \left( \beta \log \frac{\pi(y^{SFT}|x)}{\pi(y^{gen}_t|x)} \right) \right] $$
代码实现(伪代码):
"""
SPIN 自我对弈训练流程
"""
from transformers import AutoModelForCausalLM
from trl import DPOTrainer
# 初始化模型(SFT 后的模型)
model = AutoModelForCausalLM.from_pretrained("sft_model")
# 迭代训练
for iteration in range(3): # 通常 3-5 轮即可
print(f"=== SPIN Iteration {iteration} ===")
# 1. 用当前模型生成回答
generated_responses = []
for prompt in prompts:
response = model.generate(prompt)
generated_responses.append(response)
# 2. 构造偏好数据(SFT 数据为 chosen,生成数据为 rejected)
preference_data = [
{"prompt": p, "chosen": sft_response, "rejected": gen_response}
for p, sft_response, gen_response in zip(prompts, sft_responses, generated_responses)
]
# 3. DPO 训练一轮
trainer = DPOTrainer(model=model, train_dataset=preference_data, beta=0.1)
trainer.train()
# 4. 评估:当生成质量接近 SFT 数据时停止
accuracy = evaluate_model(model)
if accuracy > 0.95:
break实验结果:
- 在 GSM8K(数学推理)上,SPIN 使 Llama-2-7B 从 36% 提升到 58%
- 无需额外标注数据,仅靠自我对弈
适用场景:
- ✅ 有高质量 SFT 数据,但无偏好标注
- ✅ 任务有明确对错(数学、代码)
- ❌ 主观任务(创意写作)效果不明显
五、最新进展与趋势#
1. SimPO:连 Reference Model 都不需要了#
问题:DPO 虽然比 PPO 简单,但仍需要加载一个冻结的 Reference Model,占用显存。
SimPO (Simple Preference Optimization, 2024) 的核心洞察:
用序列长度归一化代替 Reference Model!
1.1 SimPO 的数学推导#
传统 DPO Loss: $$ L_{DPO} = -\log \sigma \left( \beta \log \frac{\pi_\theta(y_w|x)}{\pi_\theta(y_l|x)} - \beta \log \frac{\pi_{ref}(y_w|x)}{\pi_{ref}(y_l|x)} \right) $$
SimPO 的替换:
- 移除 $\pi_{ref}$ 项
- 对 log-prob 进行长度归一化(average log-prob)
- 引入 reward margin $\gamma$(类似 SVM 的 margin)
$$ \boxed{ L_{SimPO} = -\log \sigma \left( \beta \left( \frac{\log \pi_\theta(y_w|x)}{|y_w|} - \frac{\log \pi_\theta(y_l|x)}{|y_l|} \right) - \gamma \right) } $$
其中:
- $|y|$: 序列长度(token 数)
- $\gamma$: reward margin,通常取 0.5-2.0
关键优势:
- 零显存开销:不需要加载 Reference Model
- 更好的长度泛化:长度归一化天然避免长度偏好
- 隐式奖励更稳定:margin $\gamma$ 提供了更大的容错空间
1.2 手写 SimPO Loss#
"""
SimPO Loss 的 PyTorch 实现
对比 DPO:无需 Reference Model!
"""
import torch
import torch.nn.functional as F
def simpo_loss(
policy_chosen_logps, # π_θ(y_w|x) 的 log-prob
policy_rejected_logps, # π_θ(y_l|x) 的 log-prob
chosen_lengths, # y_w 的长度
rejected_lengths, # y_l 的长度
beta=2.0, # 温度系数(SimPO 通常用更大的 beta)
gamma=1.0 # reward margin
):
"""
SimPO Loss 核心实现
参数:
policy_*_logps: (batch_size,) - log-probabilities(总和)
*_lengths: (batch_size,) - 序列长度
beta: 温度系数(典型值 1.0-5.0)
gamma: reward margin(典型值 0.5-2.0)
返回:
loss: 标量 - SimPO Loss
"""
# 长度归一化:平均 log-prob
avg_log_prob_chosen = policy_chosen_logps / chosen_lengths
avg_log_prob_rejected = policy_rejected_logps / rejected_lengths
# SimPO Loss: -log σ(β * (avg_logp_w - avg_logp_l) - γ)
logits = beta * (avg_log_prob_chosen - avg_log_prob_rejected) - gamma
loss = -F.logsigmoid(logits).mean()
# 监控指标
with torch.no_grad():
# 隐式奖励(归一化后的)
reward_margin = (avg_log_prob_chosen - avg_log_prob_rejected).mean()
accuracy = (avg_log_prob_chosen > avg_log_prob_rejected).float().mean()
metrics = {
"loss": loss.item(),
"reward_margin": reward_margin.item(),
"accuracy": accuracy.item(),
}
return loss, metrics
# 对比:DPO vs SimPO
# DPO: 需要 2 个模型(policy + ref),需要计算 ref_logps
# SimPO: 只需 1 个模型(policy),直接用长度归一化实验结果(2024 年论文):
| 任务 | DPO (7B) | SimPO (7B) | 显存节省 |
|---|---|---|---|
| AlpacaEval | 82.3% | 85.7% | 40% |
| MT-Bench | 7.45 | 7.68 | 40% |
适用场景:
- ✅ 显存受限(单卡训练 7B 模型)
- ✅ 需要更好的长度泛化
- ❌ 需要强约束(DPO 的 ref model 提供更强的 KL 约束)
2. 从 RLHF 到 RLAIF (AI Feedback)#
问题:人类标注成本高($10-30/小时),速度慢,还有标注者偏见。
解决方案:用 GPT-4/Claude 等强模型来生成偏好数据。
Constitutional AI (Anthropic):
- 定义"宪法"(价值观规则),如"拒绝有害请求"
- 让 AI 自己评判回答是否符合宪法
- 用 AI 生成的偏好数据训练模型
实战:通用 Judge Prompt 模板
JUDGE_PROMPT = """
其实你是一个公正的裁判。请根据以下用户问题,对比两个助手的回答,选出更好的一个。
[用户问题]
{prompt}
[助手 A]
{response_a}
[助手 B]
{response_b}
[评判标准]
1. 有用性:是否解决了问题?
2. 安全性:是否有害?
3. 简洁性:是否啰嗦?
请输出 JSON 格式:
{{
"reasoning": "简短的分析理由...",
"winner": "A" // 或 "B" 或 "tie"
}}
"""效果:Claude-2 的 Harmlessness 指标提升 40%,完全基于 RLAIF。
2. Online DPO: 摆脱静态数据集#
传统 DPO 问题:使用固定的偏好数据集,无法适应模型迭代。
Online DPO:
- 训练过程中实时生成 rejected 样本
- 每个 epoch 使用当前模型生成新的负样本
- 类似 SPIN,但不需要 SFT 数据作为 chosen
优势:
- 数据永不过时(always on-policy)
- 避免分布偏移(distribution shift)
3. 多目标对齐:不只是 HHH#
现代对齐不止考虑 Helpful、Honest、Harmless,还包括:
- Factuality (事实性):减少幻觉
- Safety (安全性):防止 Jailbreak
- Reasoning (推理能力):保持逻辑链
- Efficiency (效率):生成简洁的回答(避免冗长)
多目标 DPO: $$ L = \alpha_1 L_{helpful} + \alpha_2 L_{harmless} + \alpha_3 L_{factual} + \alpha_4 L_{concise} $$
每个目标使用不同的偏好数据集,联合优化。
4. 对齐税 (Alignment Tax)#
现象:对齐训练会损害模型的原始能力(如代码生成、数学推理)。
原因:
- 过度的 safety 训练导致模型"过于谨慎"
- KL 惩罚限制了模型的表达能力
解决方案:
- Targeted Alignment: 只对特定领域(如 safety)做对齐,保留其他能力
- Iterative DPO: 多轮小步迭代,而非一次大步
- Weak-to-Strong Generalization: 用弱模型的偏好数据训练强模型
5. 主流模型的对齐策略#
| 模型 | 对齐方法 | 数据来源 |
|---|---|---|
| GPT-4 | RLHF (PPO) | 人工标注 + RLAIF |
| Claude 3 | Constitutional AI (DPO) | 完全 RLAIF |
| Llama-3 | DPO + IPO | 人工标注 |
| Qwen-2 | ORPO (单阶段) | 人工标注 + 自我对弈 |
| DeepSeek-V2 | Online DPO | RLAIF + 多目标对齐 |
趋势总结:
- ✅ DPO 取代 PPO(稳定性 + 效率)
- ✅ RLAIF 取代人工标注(成本 + 规模)
- ✅ 单阶段训练(ORPO)成为新宠
- ✅ 多目标对齐成为标配
六、本章小结#
核心要点#
对齐是刚需:没有对齐的模型是危险且不可用的。HHH(Helpful, Honest, Harmless)是基本原则。
RLHF 已成过去式:PPO 虽然理论优雅,但工程上复杂度太高(4 个模型,不稳定),已被 DPO 取代。
DPO 是现代标准:通过数学推导消除 Reward Model,将 RL 转化为分类问题,训练稳定且效果出色。
多种变体各有千秋:
- KTO: 适用于只有点赞/踩数据的场景
- IPO: 修复 DPO 的长度偏好问题
- ORPO: 单阶段训练,省时省力
- SPIN: 自我对弈,无需额外标注
未来趋势:
- 从 Human Feedback 走向 AI Feedback (RLAIF)
- 从离线训练走向在线训练 (Online DPO)
- 从单目标走向多目标对齐
- 从两阶段(SFT+DPO)走向单阶段(ORPO)
实践建议#
如果你是工程师:
- 优先使用
TRL库的DPOTrainer(成熟稳定) - beta 参数从 0.1 开始调试
- 确保先做 SFT,再做 DPO(除非用 ORPO)
- 监控 KL 散度,避免模型偏离过远
如果你是研究员:
- 探索 ORPO/SPIN 等单阶段方法
- 尝试 RLAIF(用 GPT-4 生成偏好数据)
- 研究多目标对齐(factuality + safety + reasoning)
延伸阅读#
核心论文:
- RLHF: Training language models to follow instructions with human feedback (OpenAI, 2022)
- DPO: Direct Preference Optimization: Your Language Model is Secretly a Reward Model (Stanford, 2023)
- KTO: Kahneman-Tversky Optimization (Cornell, 2024)
- ORPO: Odds Ratio Preference Optimization (KAIST, 2024)
- SPIN: Self-Play Fine-Tuning Converts Weak Language Models to Strong Language Models (UCLA, 2024)
工具库:
- TRL (Transformer Reinforcement Learning) - HuggingFace 官方库
- LLaMA-Factory - 零代码 DPO 训练
- OpenRLHF - 开源 RLHF 框架
下一章预告: 第4章 - 创建更优的嵌入模型
除了生成模型,Embedding 模型也是 LLM 生态的重要部分。下一章我们将探讨对比学习、InfoNCE 和 MTEB 榜单,教你训练媲美 OpenAI Ada-002 的嵌入模型。