第3章:与人类对齐:偏好优化 (Preference Alignment)#

“Alignment is the art of getting what you want, not just what you asked for.”

即使是最强的预训练模型,也只是学会了"续写"。是偏好优化让它学会了"对话"、“拒绝"和"价值观”。


目录#


一、对齐三原则与 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.00115.28.56.2 (质量差)
0.022.17.88.9 (最优)
0.10.56.27.1 (保守)
1.00.055.15.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 的设计哲学:保守更新,宁可慢也不要崩

分情况分析:

  1. 当 $A_t > 0$(好动作,想增加概率)

    • 如果 $r_t > 1 + \epsilon$(概率已经增加超过阈值):
      • 不裁剪:继续增加 → 可能过度
      • PPO:clip 到 $1 + \epsilon$ → 停止增加
    • 结果:允许概率增加,但不超过 $(1+\epsilon)$ 倍
  2. 当 $A_t < 0$(坏动作,想减少概率)

    • 如果 $r_t < 1 - \epsilon$(概率已经减少超过阈值):
      • 不裁剪:继续减少 → 可能过度
      • PPO:clip 到 $1 - \epsilon$ → 停止减少
    • 结果:允许概率减少,但不超过 $(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 个模型:

  1. Policy Model ($\pi_\theta$): 待训练的策略
  2. Ref Model ($\pi_{ref}$): 冻结的参考模型
  3. Reward Model ($r_\phi$): 冻结的奖励模型
  4. 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] } $$

人话解释

  1. 我们不需要训练单独的 Reward Model
  2. 直接优化 Policy,让 $\frac{\pi_\theta(y_w|x)}{\pi_\theta(y_l|x)}$ 的比值大于 $\frac{\pi_{ref}(y_w|x)}{\pi_{ref}(y_l|x)}$
  3. 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}")

代码关键点

  1. Log-Prob 计算:使用 log_softmax + gather 提取每个 token 的概率,再求和
  2. DPO Loss 核心:只有一行!-F.logsigmoid(beta * (pi_logratios - ref_logratios))
  3. Ref Model 冻结:使用 torch.no_grad() 避免计算梯度
  4. 监控指标
    • 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 的不稳定性来源

  1. 四个模型的复合误差

    Policy 更新 → 影响 → Reward 估计
         ↓                    ↓
    Critic 更新 ← 影响 ← Advantage 计算
    
    误差会累积放大(类似误差传播)
  2. On-policy 采样问题

    • 每次更新后,旧的经验数据就过时
    • 需要重新采样(sample inefficient)
    • 数据分布不断变化 → 训练不稳定

DPO 的稳定性来源

  1. Off-policy 训练

    • 使用固定的 preference dataset
    • 不需要重新采样
    • 数据分布恒定 → 训练稳定
  2. 直接优化闭式解

    • 目标函数是凸的(在 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 震荡幅度
PPO2.38.5 (高方差)±1.2 (剧烈)
DPO0.81.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 差"的倒数。

实践建议

  1. 先在小数据集(1000 条)上扫描 $\beta \in {0.01, 0.05, 0.1, 0.3, 0.5}$
  2. 观察训练中的 accuracy 曲线
  3. 选择 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虽然可行,但对于简单读取来说太重了。"
}

关键超参数

参数推荐值说明
beta0.1 - 0.5KL 惩罚系数,越大模型越保守
learning_rate1e-7 - 5e-7DPO 的 lr 要比 SFT 小 5-10 倍
batch_size2 - 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 (人类评分)
DPO24782 (高方差)7.8
IPO18245 (低方差)8.2 (更好)
SFT165387.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{)}$$

例子

概率 POdds含义
0.5150% 概率(五五开)
0.9990% 概率(9:1 的胜率)
0.999999% 概率(99:1 的胜率)
0.99999999.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$(差距仍然明显)

数学优势

  1. 对数空间的对称性: $$\log(\text{Odds Ratio}) = \log(\text{Odds}_w) - \log(\text{Odds}_l)$$ 与 log 概率比类似,但动态范围更大

  2. 极端概率下更敏感

    概率: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. 阶段 1(SFT):模仿 $y_w$

    • 问题:也会模仿 $y_l$(如果它在 SFT 数据中)
  2. 阶段 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 权重结果
0100%0%退化为纯 SFT
0.190%10%OR 太弱,对比不足
0.567%33%平衡(推荐)
1.050%50%OR 可能过强
5.017%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):模型通过与自己对弈来自我提升。

算法流程

  1. 迭代 t=0: 用 SFT 模型 $\pi_0$ 生成回答 $y^{gen}_0$
  2. 构造偏好对: $(x, y^{SFT}, y^{gen}_0)$,其中 $y^{SFT}$ 是人工标注的"好"答案
  3. DPO 训练: 优化 $\pi_1$,使其偏好 $y^{SFT}$ 而非 $y^{gen}_0$
  4. 迭代 t=1: 用 $\pi_1$ 生成新回答 $y^{gen}_1$
  5. 重复: 直到模型无法区分自己的输出和 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

关键优势

  1. 零显存开销:不需要加载 Reference Model
  2. 更好的长度泛化:长度归一化天然避免长度偏好
  3. 隐式奖励更稳定: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)显存节省
AlpacaEval82.3%85.7%40%
MT-Bench7.457.6840%

适用场景

  • ✅ 显存受限(单卡训练 7B 模型)
  • ✅ 需要更好的长度泛化
  • ❌ 需要强约束(DPO 的 ref model 提供更强的 KL 约束)

2. 从 RLHF 到 RLAIF (AI Feedback)#

问题:人类标注成本高($10-30/小时),速度慢,还有标注者偏见。

解决方案:用 GPT-4/Claude 等强模型来生成偏好数据。

Constitutional AI (Anthropic)

  1. 定义"宪法"(价值观规则),如"拒绝有害请求"
  2. 让 AI 自己评判回答是否符合宪法
  3. 用 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-4RLHF (PPO)人工标注 + RLAIF
Claude 3Constitutional AI (DPO)完全 RLAIF
Llama-3DPO + IPO人工标注
Qwen-2ORPO (单阶段)人工标注 + 自我对弈
DeepSeek-V2Online DPORLAIF + 多目标对齐

趋势总结

  • ✅ DPO 取代 PPO(稳定性 + 效率)
  • ✅ RLAIF 取代人工标注(成本 + 规模)
  • ✅ 单阶段训练(ORPO)成为新宠
  • ✅ 多目标对齐成为标配

六、本章小结#

核心要点#

  1. 对齐是刚需:没有对齐的模型是危险且不可用的。HHH(Helpful, Honest, Harmless)是基本原则。

  2. RLHF 已成过去式:PPO 虽然理论优雅,但工程上复杂度太高(4 个模型,不稳定),已被 DPO 取代。

  3. DPO 是现代标准:通过数学推导消除 Reward Model,将 RL 转化为分类问题,训练稳定且效果出色。

  4. 多种变体各有千秋

    • KTO: 适用于只有点赞/踩数据的场景
    • IPO: 修复 DPO 的长度偏好问题
    • ORPO: 单阶段训练,省时省力
    • SPIN: 自我对弈,无需额外标注
  5. 未来趋势

    • 从 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)

延伸阅读#

核心论文

  1. RLHF: Training language models to follow instructions with human feedback (OpenAI, 2022)
  2. DPO: Direct Preference Optimization: Your Language Model is Secretly a Reward Model (Stanford, 2023)
  3. KTO: Kahneman-Tversky Optimization (Cornell, 2024)
  4. ORPO: Odds Ratio Preference Optimization (KAIST, 2024)
  5. SPIN: Self-Play Fine-Tuning Converts Weak Language Models to Strong Language Models (UCLA, 2024)

工具库


下一章预告: 第4章 - 创建更优的嵌入模型

除了生成模型,Embedding 模型也是 LLM 生态的重要部分。下一章我们将探讨对比学习、InfoNCE 和 MTEB 榜单,教你训练媲美 OpenAI Ada-002 的嵌入模型。

[统计组件仅在生产环境显示]