第三阶段:LLM 对齐实战:从 RLHF 到 DPO,大模型是如何学会“说人话”的

“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 S F T = − log ⁡ P ( y l a b e l ∣ x ) L_{SFT} = -\log P(y_{label} \mid x) LSFT​=−logP(ylabel​∣x)

SFT 只能学会**“模仿"标准答案,但无法理解"好与坏”**的程度。

  • 对于问题 “如何制造炸弹?”,SFT 模型可能会模仿训练集里的高智商回答,给出一份完美的炸弹制作教程。这很 Helpful,但不 Harmless。
  • 我们希望模型知道:即使你的回答在语法上很完美,但因为它是有害的,所以分数极低。
2.1 深层原理:SFT 与偏好学习的数学空间本质不同

为什么 SFT 学不会偏好?

SFT 的目标是最大化 P ( y ∣ x ) P(y|x) P(y∣x),这是一个生成目标(Generative Objective)。
偏好学习需要的是比较目标(Comparative Objective): P ( y w ≻ y l ∣ x ) P(y_w \succ y_l | x) P(yw​≻yl​∣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 ϕ ( x , y ) r_\phi(x, y) rϕ​(x,y)。
输入:提示词 x x x,回答 y y y。输出:标量分数。

训练数据:成对比较数据 (Pairwise Data)。
Human: “写首诗”

  • A: “窗前明月光…” (人类觉得更好)
  • B: “月亮很大…”

Bradley-Terry 模型假设:人类偏好可以用奖励差的 sigmoid 建模。

给定两个回答 y w y_w yw​ (winner) 和 y l y_l yl​ (loser),人类选择 y w y_w yw​ 的概率为:
P ( y w ≻ y l ∣ x ) = exp ⁡ ( r ϕ ( x , y w ) ) exp ⁡ ( r ϕ ( x , y w ) ) + exp ⁡ ( r ϕ ( x , y l ) ) = σ ( r ϕ ( x , y w ) − r ϕ ( x , y l ) ) 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)) P(yw​≻yl​∣x)=exp(rϕ​(x,yw​))+exp(rϕ​(x,yl​))exp(rϕ​(x,yw​))​=σ(rϕ​(x,yw​)−rϕ​(x,yl​))

其中 σ ( z ) = 1 1 + e − z \sigma(z) = \frac{1}{1 + e^{-z}} σ(z)=1+e−z1​ 是 sigmoid 函数。

Loss Function (负对数似然):
L R M = − E ( x , y w , y l ) ∼ D [ log ⁡ σ ( r ϕ ( x , y w ) − r ϕ ( x , y l ) ) ] 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] LRM​=−E(x,yw​,yl​)∼D​[logσ(rϕ​(x,yw​)−rϕ​(x,yl​))]

直观理解:

  • 如果 r ϕ ( x , y w ) ≫ r ϕ ( x , y l ) r_\phi(x, y_w) \gg r_\phi(x, y_l) rϕ​(x,yw​)≫rϕ​(x,yl​),则 σ ( ⋅ ) → 1 \sigma(\cdot) \to 1 σ(⋅)→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 ⁡ θ E x ∼ D , y ∼ π θ ( ⋅ ∣ x ) [ r ϕ ( x , y ) − β log ⁡ π θ ( y ∣ x ) π r e f ( y ∣ x ) ] \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] θmax​Ex∼D,y∼πθ​(⋅∣x)​[rϕ​(x,y)−βlogπref​(y∣x)πθ​(y∣x)​]

其中:

  • π θ \pi_\theta πθ​: 当前策略模型(待优化)
  • π r e f \pi_{ref} πref​: 参考模型(通常是 SFT 模型,frozen)
  • r ϕ ( x , y ) r_\phi(x, y) rϕ​(x,y): 奖励模型的评分
  • β \beta β: KL 惩罚系数(通常取 0.01-0.1)
  • D \mathcal{D} D: 提示词分布

关键在于 KL 散度惩罚 (KL Penalty):

  • π r e f \pi_{ref} πref​ 是原始的 SFT 模型。
  • 我们希望模型分数变高,但不要偏离 SFT 模型太远。
  • 如果没有 KL 惩罚,模型会利用 Reward Model 的漏洞 (Reward Hacking),生成乱码来骗取高分。
2.1 深度解析:β 参数为什么不能太大也不能太小?

β 的物理意义:控制"追求高奖励"和"保持原有分布"之间的权衡。

β 太小(如 0.001)的灾难:

问题:模型会过度优化 reward,导致 Reward Hacking。

实际案例(来自 OpenAI 早期实验):

Prompt: "写一首赞美春天的诗"
正常输出: "春风拂柳绿如烟,万物复苏..."
β=0.001: "好好好好好好好好好好好好..." (重复 token 骗取高分)

数学原因:

  • KL 约束太弱: β ⋅ D K L ( π θ ∥ π r e f ) ≈ 0 \beta \cdot D_{KL}(\pi_\theta \| \pi_{ref}) \approx 0 β⋅DKL​(πθ​∥πref​)≈0
  • 模型可以任意偏离 π r e f \pi_{ref} πref​,寻找 Reward Model 的漏洞
  • 例如:生成高频词、重复 token、或触发 RM 的过拟合模式

β 太大(如 1.0)的问题:

问题:模型被锁死在 π r e f \pi_{ref} πref​ 附近,无法学习新行为。

数学原因:

  • KL 惩罚主导目标函数: β ⋅ D K L ≫ r ϕ ( x , y ) \beta \cdot D_{KL} \gg r_\phi(x, y) β⋅DKL​≫rϕ​(x,y)
  • 任何偏离 π r e f \pi_{ref} πref​ 的行为都被严厉惩罚
  • 结果:模型退化为 SFT 模型,RLHF 训练无效

最优 β 的选择(经验法则):

理论依据(信息论):
β ∗ ≈ 1 E [ r m a x − r m i n ] \beta^* \approx \frac{1}{\mathbb{E}[r_{max} - r_{min}]} β∗≈E[rmax​−rmin​]1​

即:β 应该是"奖励动态范围"的倒数。

实践建议:

  • 标准值: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 C L I P ( θ ) = E t [ min ⁡ ( r t ( θ ) A ^ t , clip ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A ^ t ) ] 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] LCLIP(θ)=Et​[min(rt​(θ)A^t​,clip(rt​(θ),1−ϵ,1+ϵ)A^t​)]

其中:

  • r t ( θ ) = π θ ( a t ∣ s t ) π o l d ( a t ∣ s t ) r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{old}(a_t|s_t)} rt​(θ)=πold​(at​∣st​)πθ​(at​∣st​)​: 新旧策略的概率比
  • A ^ t \hat{A}_t 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 ⋅ A t L = r_t \cdot A_t L=rt​⋅At​

  • 当 A t = 5 A_t = 5 At​=5 时,梯度 = 95 × 5 = 475(梯度爆炸)
  • 导致下一步更新过大,模型性能突然崩溃

为什么是 min(r_t · A, clip(r_t) · A) 这个形式?

PPO 的设计哲学:保守更新,宁可慢也不要崩。

分情况分析:

  1. 当 A t > 0 A_t > 0 At​>0(好动作,想增加概率):

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

    • 如果 r t < 1 − ϵ r_t < 1 - \epsilon rt​<1−ϵ(概率已经减少超过阈值):
      • 不裁剪:继续减少 → 可能过度
      • PPO:clip 到 1 − ϵ 1 - \epsilon 1−ϵ → 停止减少
    • 结果:允许概率减少,但不超过 ( 1 − ϵ ) (1-\epsilon) (1−ϵ) 倍

为什么 ϵ = 0.2 \epsilon = 0.2 ϵ=0.2?

来源:Trust Region Policy Optimization (TRPO) 的近似。

TRPO 的约束: D K L ( π o l d ∥ π n e w ) ≤ δ D_{KL}(\pi_{old} \| \pi_{new}) \leq \delta DKL​(πold​∥πnew​)≤δ

通过泰勒展开近似:
D K L ≈ 1 2 E [ ( r t − 1 ) 2 ] ≤ δ D_{KL} \approx \frac{1}{2} \mathbb{E}[(r_t - 1)^2] \leq \delta DKL​≈21​E[(rt​−1)2]≤δ

求解得: ∣ r t − 1 ∣ ≤ 2 δ |r_t - 1| \leq \sqrt{2\delta} ∣rt​−1∣≤2δ ​

当 δ = 0.02 \delta = 0.02 δ=0.02 时, ϵ ≈ 0.2 \epsilon \approx 0.2 ϵ≈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 ) V(s) V(s):状态 s s s 的平均价值
V ( s ) = E a [ Q ( s , a ) ] = 1.1  (平均值) V(s) = \mathbb{E}_a[Q(s, a)] = 1.1 \text{ (平均值)} V(s)=Ea​[Q(s,a)]=1.1 (平均值)

Advantage 函数:
A ( s , a ) = Q ( s , a ) − V ( s ) = "比平均好多少" A(s, a) = Q(s, a) - V(s) = \text{"比平均好多少"} A(s,a)=Q(s,a)−V(s)="比平均好多少"

结果:

  • A ( s , a 1 ) = 1.0 − 1.1 = − 0.1 A(s, a1) = 1.0 - 1.1 = -0.1 A(s,a1)=1.0−1.1=−0.1 (减少概率)
  • A ( s , a 2 ) = 1.5 − 1.1 = + 0.4 A(s, a2) = 1.5 - 1.1 = +0.4 A(s,a2)=1.5−1.1=+0.4 (增加概率)
  • A ( s , a 3 ) = 0.8 − 1.1 = − 0.3 A(s, a3) = 0.8 - 1.1 = -0.3 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 ( π r e f \pi_{ref} πref​): 冻结的参考模型
  3. Reward Model ( r ϕ r_\phi rϕ​): 冻结的奖励模型
  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 ⁡ π θ E x ∼ D , y ∼ π θ ( ⋅ ∣ x ) [ r ϕ ( x , y ) − β D K L ( π θ ∥ π r e f ) ] \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] πθ​max​Ex∼D,y∼πθ​(⋅∣x)​[rϕ​(x,y)−βDKL​(πθ​∥πref​)]

展开 KL 散度(在 y y y 的分布上):
D K L ( π θ ∥ π r e f ) = E y ∼ π θ [ log ⁡ π θ ( y ∣ x ) π r e f ( y ∣ x ) ] \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] DKL​(πθ​∥πref​)=Ey∼πθ​​[logπref​(y∣x)πθ​(y∣x)​]

因此目标变为:
max ⁡ π θ E y ∼ π θ [ r ϕ ( x , y ) − β log ⁡ π θ ( y ∣ x ) π r e f ( y ∣ x ) ] \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] πθ​max​Ey∼πθ​​[rϕ​(x,y)−βlogπref​(y∣x)πθ​(y∣x)​]

关键推导:这个优化问题有闭式解!

最优策略 π ∗ ( y ∣ x ) \pi^*(y|x) π∗(y∣x) 满足:
π ∗ ( y ∣ x ) = 1 Z ( x ) π r e f ( y ∣ x ) exp ⁡ ( 1 β r ∗ ( x , y ) ) \pi^*(y|x) = \frac{1}{Z(x)} \pi_{ref}(y|x) \exp\left(\frac{1}{\beta} r^*(x, y)\right) π∗(y∣x)=Z(x)1​πref​(y∣x)exp(β1​r∗(x,y))

其中 Z ( x ) = ∑ y π r e f ( y ∣ x ) exp ⁡ ( 1 β r ∗ ( x , y ) ) Z(x) = \sum_y \pi_{ref}(y|x) \exp\left(\frac{1}{\beta} r^*(x, y)\right) Z(x)=∑y​πref​(y∣x)exp(β1​r∗(x,y)) 是配分函数。

1.2 逆向变换:从 Policy 反推 Reward

将上式改写,两边同时除以 π r e f \pi_{ref} πref​ 再取对数:
log ⁡ π ∗ ( y ∣ x ) π r e f ( y ∣ x ) = 1 β r ∗ ( x , y ) − log ⁡ Z ( x ) \log \frac{\pi^*(y|x)}{\pi_{ref}(y|x)} = \frac{1}{\beta} r^*(x, y) - \log Z(x) logπref​(y∣x)π∗(y∣x)​=β1​r∗(x,y)−logZ(x)

移项得到隐式奖励函数:
r ∗ ( x , y ) = β log ⁡ π ∗ ( y ∣ x ) π r e f ( y ∣ x ) + β log ⁡ Z ( x ) r^*(x, y) = \beta \log \frac{\pi^*(y|x)}{\pi_{ref}(y|x)} + \beta \log Z(x) r∗(x,y)=βlogπref​(y∣x)π∗(y∣x)​+βlogZ(x)

关键发现: Z ( x ) Z(x) Z(x) 只依赖于 x x x,在比较两个回答时会消掉!

1.2.1 深度理解:配分函数 Z(x) 为什么会"消掉"?

数学推导(更直观的版本):

从隐式奖励函数出发:
r ∗ ( x , y ) = β log ⁡ π ∗ ( y ∣ x ) π r e f ( y ∣ x ) + β log ⁡ Z ( x ) r^*(x, y) = \beta \log \frac{\pi^*(y|x)}{\pi_{ref}(y|x)} + \beta \log Z(x) r∗(x,y)=βlogπref​(y∣x)π∗(y∣x)​+βlogZ(x)

计算两个回答的奖励差:
r ∗ ( x , y w ) − r ∗ ( x , y l ) = β log ⁡ π ∗ ( y w ∣ x ) π r e f ( y w ∣ x ) + β log ⁡ Z ( x ) − β log ⁡ π ∗ ( y l ∣ x ) π r e f ( y l ∣ x ) − β log ⁡ Z ( x ) = β ( log ⁡ π ∗ ( y w ∣ x ) π ∗ ( y l ∣ x ) − log ⁡ π r e f ( y w ∣ x ) π r e f ( y l ∣ 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} r∗(x,yw​)−r∗(x,yl​)​=βlogπref​(yw​∣x)π∗(yw​∣x)​+βlogZ(x) ​−βlogπref​(yl​∣x)π∗(yl​∣x)​−βlogZ(x) ​=β(logπ∗(yl​∣x)π∗(yw​∣x)​−logπref​(yl​∣x)πref​(yw​∣x)​)​​

物理直觉:

Z ( x ) Z(x) Z(x) 是归一化常数,只依赖于 prompt x x x,与具体回答 y y 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 ) Z(x) Z(x)!

  • Bradley-Terry 模型只关心"偏好概率": P ( y w ≻ y l ) P(y_w \succ y_l) P(yw​≻yl​)
  • 这是一个比较操作, Z ( x ) Z(x) Z(x) 自动消掉
  • 因此可以直接用 policy 的 log-ratio 代替 reward
1.3 代入 Bradley-Terry 模型

回顾人类偏好模型:
P ( y w ≻ y l ∣ x ) = σ ( r ∗ ( x , y w ) − r ∗ ( x , y l ) ) P(y_w \succ y_l \mid x) = \sigma(r^*(x, y_w) - r^*(x, y_l)) P(yw​≻yl​∣x)=σ(r∗(x,yw​)−r∗(x,yl​))

代入隐式奖励:
P ( y w ≻ y l ∣ x ) = σ ( β log ⁡ π ∗ ( y w ∣ x ) π r e f ( y w ∣ x ) + β log ⁡ Z ( x ) − β log ⁡ π ∗ ( y l ∣ x ) π r e f ( y l ∣ x ) − β log ⁡ Z ( x ) ) = σ ( β log ⁡ π ∗ ( y w ∣ x ) π ∗ ( y l ∣ x ) − β log ⁡ π r e f ( y w ∣ x ) π r e f ( y l ∣ x ) ) \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} P(yw​≻yl​∣x)​=σ(βlogπref​(yw​∣x)π∗(yw​∣x)​+βlogZ(x) ​−βlogπref​(yl​∣x)π∗(yl​∣x)​−βlogZ(x) ​)=σ(βlogπ∗(yl​∣x)π∗(yw​∣x)​−βlogπref​(yl​∣x)πref​(yw​∣x)​)​​

DPO Loss(负对数似然):
L D P O ( π θ ) = − E ( x , y w , y l ) ∼ D [ log ⁡ σ ( β log ⁡ π θ ( y w ∣ x ) π θ ( y l ∣ x ) − β log ⁡ π r e f ( y w ∣ x ) π r e f ( y l ∣ x ) ) ] \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] } LDPO​(πθ​)=−E(x,yw​,yl​)∼D​[logσ(βlogπθ​(yl​∣x)πθ​(yw​∣x)​−βlogπref​(yl​∣x)πref​(yw​∣x)​)]​

人话解释:

  1. 我们不需要训练单独的 Reward Model
  2. 直接优化 Policy,让 π θ ( y w ∣ x ) π θ ( y l ∣ x ) \frac{\pi_\theta(y_w|x)}{\pi_\theta(y_l|x)} πθ​(yl​∣x)πθ​(yw​∣x)​ 的比值大于 π r e f ( y w ∣ x ) π r e f ( y l ∣ x ) \frac{\pi_{ref}(y_w|x)}{\pi_{ref}(y_l|x)} πref​(yl​∣x)πref​(yw​∣x)​
  3. KL 惩罚隐式地编码在公式中(通过与 π r e f \pi_{ref} π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 D P O = E [ σ ( z ) ( 1 − σ ( z ) ) ⋅ ∇ 2 log ⁡ π ] H_{DPO} = \mathbb{E}\left[\sigma(z)(1-\sigma(z)) \cdot \nabla^2 \log \pi\right] HDPO​=E[σ(z)(1−σ(z))⋅∇2logπ]

关键特性:

  • σ ( z ) ( 1 − σ ( z ) ) ∈ [ 0 , 0.25 ] \sigma(z)(1-\sigma(z)) \in [0, 0.25] σ(z)(1−σ(z))∈[0,0.25](有界!)
  • 梯度更新幅度自动受限,不会爆炸

PPO 的梯度:
∇ L P P O = E [ π n e w π o l d ⋅ A ⋅ ∇ log ⁡ π ] \nabla L_{PPO} = \mathbb{E}\left[\frac{\pi_{new}}{\pi_{old}} \cdot A \cdot \nabla \log \pi\right] ∇LPPO​=E[πold​πnew​​⋅A⋅∇logπ]

问题:

  • π n e w π o l d \frac{\pi_{new}}{\pi_{old}} πold​πnew​​ 可能非常大(如 > 10)
  • Advantage A A 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 β 的理论公式(来自统计力学):
β o p t i m a l ≈ 1 E [ ∣ r w − r l ∣ ] \beta_{optimal} \approx \frac{1}{\mathbb{E}[|r_w - r_l|]} βoptimal​≈E[∣rw​−rl​∣]1​

即: β \beta β 应该是"平均 reward 差"的倒数。

实践建议:

  1. 先在小数据集(1000 条)上扫描 β ∈ { 0.01 , 0.05 , 0.1 , 0.3 , 0.5 } \beta \in \{0.01, 0.05, 0.1, 0.3, 0.5\} β∈{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 , l a b e l ) (x, y, label) (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]
    $$

其中:

  • λ D , λ U \lambda_D, \lambda_U λD​,λU​: 不对称系数(通常 λ U > λ D \lambda_U > \lambda_D λU​>λD​,惩罚比奖励强)
  • z r e f z_{ref} zref​: 参考点(用于归一化)

代码实现(核心):

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 I P O = E [ ( log ⁡ π θ ( y w ∣ x ) π θ ( y l ∣ x ) − log ⁡ π r e f ( y w ∣ x ) π r e f ( y l ∣ x ) − 1 β ) 2 ] 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] LIPO​=E[(logπθ​(yl​∣x)πθ​(yw​∣x)​−logπref​(yl​∣x)πref​(yw​∣x)​−β1​)2]

关键变化:

  • 用平方损失代替对数损失(更稳定)
  • 减去常数项 1 β \frac{1}{\beta} β1​(消除长度偏好)
2.1 深度解析:为什么平方损失能消除长度偏好?

DPO 的长度偏好问题

DPO Loss: − log ⁡ σ ( β ⋅ Δ r ) -\log \sigma(\beta \cdot \Delta r) −logσ(β⋅Δr),其中:
Δ r = log ⁡ π ( y w ∣ x ) − log ⁡ π ( y l ∣ x ) = ∑ t = 1 T w log ⁡ P ( y t w ∣ . . . ) − ∑ t = 1 T l log ⁡ P ( y t l ∣ . . . ) \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|...) Δr=logπ(yw​∣x)−logπ(yl​∣x)=t=1∑Tw​​logP(ytw​∣...)−t=1∑Tl​​logP(ytl​∣...)

问题根源:

  • log ⁡ π ( y ∣ x ) \log \pi(y|x) logπ(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 的梯度:
∇ L D P O ∝ σ ′ ( β ⋅ Δ r ) ⋅ ∇ Δ r \nabla L_{DPO} \propto \sigma'(\beta \cdot \Delta r) \cdot \nabla \Delta r ∇LDPO​∝σ′(β⋅Δr)⋅∇Δr

当 y w y_w yw​ 更长时:

  • Δ r \Delta r Δr 的绝对值变大
  • σ ′ ( β ⋅ Δ r ) \sigma'(\beta \cdot \Delta r) σ′(β⋅Δr) 在远离 0 时梯度更小(sigmoid 饱和)
  • 但 ∇ Δ r \nabla \Delta r ∇Δr 包含更多 token,总梯度仍然大
  • 结果:模型被鼓励生成更长的序列

IPO 的解决方案

核心思想:用平方损失 + 目标阈值代替 log-sigmoid。

L I P O = ( Δ r − 1 β ) 2 L_{IPO} = \left(\Delta r - \frac{1}{\beta}\right)^2 LIPO​=(Δr−β1​)2

关键点 1:平方损失的梯度
∇ L I P O = 2 ( Δ r − 1 β ) ⋅ ∇ Δ r \nabla L_{IPO} = 2\left(\Delta r - \frac{1}{\beta}\right) \cdot \nabla \Delta r ∇LIPO​=2(Δr−β1​)⋅∇Δr

关键点 2:目标阈值 1 β \frac{1}{\beta} β1​

  • 当 Δ r = 1 β \Delta r = \frac{1}{\beta} Δr=β1​ 时,loss = 0(目标点)
  • 无论序列长度,只要 Δ r \Delta r Δr 达到阈值,梯度就归零
  • 超过阈值后,梯度甚至变负(惩罚过度优化)

长度不敏感性分析:

场景 1:长序列(50 tokens)

  • Δ r = 2.0 \Delta r = 2.0 Δr=2.0(因为序列长,累积差异大)
  • 如果 1 β = 1.0 \frac{1}{\beta} = 1.0 β1​=1.0:
    • L I P O = ( 2.0 − 1.0 ) 2 = 1.0 L_{IPO} = (2.0 - 1.0)^2 = 1.0 LIPO​=(2.0−1.0)2=1.0
    • 梯度 ∝ ( 2.0 − 1.0 ) = 1.0 (2.0 - 1.0) = 1.0 (2.0−1.0)=1.0 → 停止优化

场景 2:短序列(20 tokens)

  • Δ r = 0.5 \Delta r = 0.5 Δr=0.5(因为序列短,累积差异小)
  • 如果 1 β = 1.0 \frac{1}{\beta} = 1.0 β1​=1.0:
    • L I P O = ( 0.5 − 1.0 ) 2 = 0.25 L_{IPO} = (0.5 - 1.0)^2 = 0.25 LIPO​=(0.5−1.0)2=0.25
    • 梯度 ∝ ( 0.5 − 1.0 ) = − 0.5 (0.5 - 1.0) = -0.5 (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 O R P O = L S F T ( y w ) + λ ⋅ E [ log ⁡ σ ( log ⁡ odds θ ( y w ∣ x ) odds θ ( y l ∣ x ) ) ] 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] LORPO​=LSFT​(yw​)+λ⋅E[logσ(logoddsθ​(yl​∣x)oddsθ​(yw​∣x)​)]

其中 Odds Ratio(胜率比)定义为:
odds θ ( y ∣ x ) = P θ ( y ∣ x ) 1 − P θ ( y ∣ x ) \text{odds}_\theta(y|x) = \frac{P_\theta(y|x)}{1 - P_\theta(y|x)} oddsθ​(y∣x)=1−Pθ​(y∣x)Pθ​(y∣x)​

与 DPO 的区别:

  • DPO: 需要先 SFT,再用偏好数据微调
  • ORPO: 直接在预训练模型上同时做 SFT 和偏好优化

优势:

  • 节省一半训练时间(一次训练完成两个目标)
  • 在 Mistral-7B 上实测效果优于 SFT+DPO

劣势:

  • 超参数敏感( λ \lambda λ 需要精心调节)
  • 对数据质量要求极高
3.1 深度解析:Odds Ratio 的数学魔法

什么是 Odds(胜率)?

概率 P P P:事件发生的可能性(0 到 1)
Odds = P 1 − P (胜率,0 到  ∞ ) \text{Odds} = \frac{P}{1-P} \quad \text{(胜率,0 到 }\infty\text{)} Odds=1−PP​(胜率,0 到 ∞)

例子:

概率 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 比:

  • 概率比: 0.9 0.8 = 1.125 \frac{0.9}{0.8} = 1.125 0.80.9​=1.125(差距不明显)
  • Odds 比: 9 4 = 2.25 \frac{9}{4} = 2.25 49​=2.25(差距被放大)

更极端的例子:

P(y_w|x) = 0.99
P(y_l|x) = 0.98
  • 概率比: 0.99 0.98 = 1.01 \frac{0.99}{0.98} = 1.01 0.980.99​=1.01(几乎看不出差距)
  • Odds 比: 99 49 = 2.02 \frac{99}{49} = 2.02 4999​=2.02(差距仍然明显)

数学优势:

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

  2. 极端概率下更敏感:

    概率:0.99 → 0.999,仅提升 0.009
    Odds:99 → 999,提升 10 倍!
    

ORPO 为什么能合并 SFT 和 DPO?

L O R P O = L S F T ( y w ) ⏟ 模仿项 + λ ⋅ L O R ( y w , y l ) ⏟ 对比项 L_{ORPO} = \underbrace{L_{SFT}(y_w)}_{\text{模仿项}} + \lambda \cdot \underbrace{L_{OR}(y_w, y_l)}_{\text{对比项}} LORPO​=模仿项 LSFT​(yw​)​​+λ⋅对比项 LOR​(yw​,yl​)​​

关键洞察:两个目标都作用在 y w y_w yw​ 上!

  • SFT 项: − log ⁡ P θ ( y w ∣ x ) -\log P_\theta(y_w|x) −logPθ​(yw​∣x)

    • 增加 P ( y w ∣ x ) P(y_w|x) P(yw​∣x)(学会生成好回答)
  • OR 项: − log ⁡ σ ( log ⁡ Odds ( y w ) Odds ( y l ) ) -\log \sigma\left(\log \frac{\text{Odds}(y_w)}{\text{Odds}(y_l)}\right) −logσ(logOdds(yl​)Odds(yw​)​)

    • 增加 Odds ( y w ) Odds ( y l ) \frac{\text{Odds}(y_w)}{\text{Odds}(y_l)} Odds(yl​)Odds(yw​)​(偏好好回答而非坏回答)

协同效应:

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_w yw​

    • 问题:也会模仿 y l y_l yl​(如果它在 SFT 数据中)
  2. 阶段 2(DPO):对比 y w y_w yw​ vs y l y_l yl​

    • 问题:需要"遗忘" SFT 阶段学到的 y l y_l yl​

ORPO 一步到位:

  • 在模仿 y w y_w yw​ 的同时,主动远离 y l y_l yl​
  • 无需"遗忘"过程

数学表达:

ORPO 的梯度(对 θ \theta θ 求导):
∇ L O R P O = ∇ L S F T ( y w ) ⏟ 拉向  y w + λ ⋅ ( ∇ log ⁡ Odds ( y w ) ⏟ 增强  y w − ∇ log ⁡ Odds ( y l ) ⏟ 削弱  y l ) \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) ∇LORPO​=拉向 yw​ ∇LSFT​(yw​)​​+λ⋅ ​增强 yw​ ∇logOdds(yw​)​​−削弱 yl​ ∇logOdds(yl​)​​ ​

可视化(概率空间中的优化轨迹):

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_w yw​ 确实比 y l y_l yl​ 好很多)
  • ✅ 计算资源有限(只能训练一次)
  • ✅ 任务明确(如安全对齐)

何时不用 ORPO?

  • ❌ 数据质量参差不齐
  • ❌ 需要精细控制(两阶段更灵活)
  • ❌ 主观任务(如创意写作)

4. SPIN: 自我对弈,无需人工数据

SPIN (Self-Play Fine-Tuning):模型通过与自己对弈来自我提升。

算法流程:

  1. 迭代 t=0: 用 SFT 模型 π 0 \pi_0 π0​ 生成回答 y 0 g e n y^{gen}_0 y0gen​
  2. 构造偏好对: ( x , y S F T , y 0 g e n ) (x, y^{SFT}, y^{gen}_0) (x,ySFT,y0gen​),其中 y S F T y^{SFT} ySFT 是人工标注的"好"答案
  3. DPO 训练: 优化 π 1 \pi_1 π1​,使其偏好 y S F T y^{SFT} ySFT 而非 y 0 g e n y^{gen}_0 y0gen​
  4. 迭代 t=1: 用 π 1 \pi_1 π1​ 生成新回答 y 1 g e n y^{gen}_1 y1gen​
  5. 重复: 直到模型无法区分自己的输出和 SFT 数据(收敛)

数学表达:
π t + 1 = arg ⁡ max ⁡ π E x ∼ D [ log ⁡ σ ( β log ⁡ π ( y S F T ∣ x ) π ( y t g e n ∣ x ) ) ] \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] πt+1​=argπmax​Ex∼D​[logσ(βlogπ(ytgen​∣x)π(ySFT∣x)​)]

代码实现(伪代码):

"""
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 D P O = − log ⁡ σ ( β log ⁡ π θ ( y w ∣ x ) π θ ( y l ∣ x ) − β log ⁡ π r e f ( y w ∣ x ) π r e f ( y l ∣ x ) ) 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) LDPO​=−logσ(βlogπθ​(yl​∣x)πθ​(yw​∣x)​−βlogπref​(yl​∣x)πref​(yw​∣x)​)

SimPO 的替换:

  • 移除 π r e f \pi_{ref} πref​ 项
  • 对 log-prob 进行长度归一化(average log-prob)
  • 引入 reward margin γ \gamma γ(类似 SVM 的 margin)

L S i m P O = − log ⁡ σ ( β ( log ⁡ π θ ( y w ∣ x ) ∣ y w ∣ − log ⁡ π θ ( y l ∣ x ) ∣ y l ∣ ) − γ ) \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) } LSimPO​=−logσ(β(∣yw​∣logπθ​(yw​∣x)​−∣yl​∣logπθ​(yl​∣x)​)−γ)​

其中:

  • ∣ y ∣ |y| ∣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 = α 1 L h e l p f u l + α 2 L h a r m l e s s + α 3 L f a c t u a l + α 4 L c o n c i s e L = \alpha_1 L_{helpful} + \alpha_2 L_{harmless} + \alpha_3 L_{factual} + \alpha_4 L_{concise} L=α1​Lhelpful​+α2​Lharmless​+α3​Lfactual​+α4​Lconcise​

每个目标使用不同的偏好数据集,联合优化。

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 的嵌入模型。

更多推荐