查看全课大纲(第 28 / 40 节)
  1. 1.从规则到学习:深度学习解决什么问题
  2. 2.表示学习与端到端训练
  3. 3.张量、计算图与自动微分
  4. 4.训练三要素:数据、模型、目标函数
  5. 5.从感知机到多层感知机
  6. 6.反向传播:手推一个两层网络
  7. 7.激活函数与初始化
  8. 8.优化器:SGD → Momentum → Adam
  9. 9.正则化:权重衰减、Dropout、BN/LN
  10. 10.卷积的直觉与参数共享
  11. 11.池化、感受野与下采样
  12. 12.经典网络演进:LeNet → AlexNet → VGG → ResNet
  13. 13.残差连接与归一化为什么有效
  14. 14.迁移学习与特征可视化
  15. 15.RNN 与 BPTT
  16. 16.LSTM/GRU:门控如何缓解梯度问题
  17. 17.Seq2Seq 与编码器-解码器
  18. 18.注意力前史:Bahdanau 加性注意力
  19. 19.Self-Attention:QKV 与缩放点积
  20. 20.多头注意力:并行的多种关系
  21. 21.位置编码:正弦与可学习
  22. 22.残差、LayerNorm 与前馈层
  23. 23.从零实现一个最小 Transformer block
  24. 24.训练稳定性、复杂度与常见误区
  25. 25.自监督与语言建模目标
  26. 26.BERT:掩码语言模型与双向编码
  27. 27.GPT:自回归生成与缩放定律
  28. 28.指令微调与对齐:SFT → RLHF/DPO
  29. 29.参数高效微调:LoRA 与适配器
  30. 30.自编码器与 VAE
  31. 31.GAN:对抗训练与模式崩塌
  32. 32.扩散模型:前向加噪与反向去噪
  33. 33.扩散模型的应用与采样加速
  34. 34.对比学习与 CLIP
  35. 35.ViT:把图像当序列
  36. 36.多模态架构:对齐、融合与生成
  37. 37.能力边界、幻觉与评测
  38. 38.PyTorch 2.x 训练循环与混合精度
  39. 39.实验管理、评测与复现
  40. 40.推理部署与成本:量化/蒸馏概览

指令微调与对齐:SFT → RLHF/DPO

小象实战讲义 · 深度学习基础:从神经网络到 Transformer

预训练出来的模型只会“接龙”,不会“听话”。本节拆解让模型对齐人类意图的三段路:监督微调(SFT)、基于人类反馈的强化学习(RLHF),以及它的简化替代——直接偏好优化(DPO)。学完你将能回答:对齐到底在对什么?奖励模型在学什么?DPO 凭什么能省掉强化学习?

先修:自回归语言建模与缩放定律(本章 6.3 节)、交叉熵与 KL 散度。后续:下一节《参数高效微调:LoRA 与适配器》,之后进入第 7 章《生成模型》。

💡 核心导读

  1. 问题根源:大规模无监督语言模型(如 GPT-3)的训练目标(预测下一个词)与“遵循用户意图”的目标存在错位,导致模型可能生成不真实、有害或不遵循指令的内容。
  2. 经典解法:RLHF(Reinforcement Learning from Human Feedback)通过三步对齐模型:监督微调(SFT)建立基础、奖励模型(RM)学习人类偏好、强化学习(PPO)优化策略。
  3. 简化突破:DPO(Direct Preference Optimization)通过数学变换,将复杂的 RLHF 问题转化为一个简单的分类损失,无需训练独立的奖励模型或进行强化学习,大幅简化流程。
  4. 核心机制:理解 Bradley-Terry 偏好模型如何将人类对回答的排序转化为可优化的概率,以及 KL 散度约束如何防止优化过程偏离原始模型太远。
  5. 代码实践:我们将用极简代码模拟 SFT、奖励模型训练、PPO 优化步骤,并实现 DPO 的核心损失函数,直观感受其简洁性。

🧭 前置知识

  1. 交叉熵损失:用于分类任务的标准损失函数。
  2. softmax 函数:将 logits 转换为概率分布。
  3. KL 散度:衡量两个概率分布之间的差异。
  4. 逻辑回归与 Bradley-Terry 模型:理解如何用分数差异建模 pairwise 偏好概率(不展开数学推导,见站内数学基础课)。
  5. 策略梯度与 PPO(概念):知道强化学习可用于优化参数化策略,PPO 是一种稳定策略梯度算法(细节不展开)。

对齐问题:为什么语言模型需要“调教”?

大型语言模型(LLM)通过在互联网规模的文本上进行下一个词预测(语言建模)任务进行预训练。这个目标让模型掌握了丰富的语言知识和世界知识,但并未明确教导模型去遵循用户的特定指令或意图(Ouyang et al., 2022,arXiv:2203.02155)。

这导致了“对齐问题”:

  • 不遵循指令:用户问“用 Python 写个快速排序”,模型可能开始解释快速排序的原理。
  • 虚构事实:模型可能自信地编造不存在的信息(“幻觉”)。
  • 生成有害内容:可能产生带有偏见、歧视或毒性的文本。

因此,我们需要一个“对齐”过程,使模型的行为与人类期望的“有帮助、诚实、无害”相一致(Askell et al., 2021,arXiv:2112.00861)。指令微调与对齐技术就是解决这一问题的关键。

经典路线:基于人类反馈的强化学习(RLHF)

RLHF 是 InstructGPT(ChatGPT 的技术基础)采用的方法。其流程分为三步,如下图所示(概念源自 Ouyang et al., 2022):

         [人类标注者]
             |
             | 1. 撰写示范
             v
        [提示X] -> [期望回答Y]
             |
             | 监督学习
             v
    [SFT模型] (初始策略 π_SFT)
             |
             | 2. 采样并比较
             v
[提示X] -> [回答A] vs [回答B]
             |           |
             |           | 人类标注偏好 (A ≻ B)
             v           v
        [偏好数据集 D]
             |
             | 训练奖励模型
             v
        [奖励模型 r_φ(x, y)]
             |
             | 3. 强化学习优化
             v
    [PPO优化] -> [最终策略 π_RL]
             (约束:不与 π_SFT 偏离太远)

第一步:监督微调(SFT)

这套流程的效果有一个著名数字:InstructGPT 论文里,1.3B 的 PPO 模型在人类评测中比 175B 的 GPT-3 更受偏好——参数少 100 倍,回答却更受欢迎。它说明对齐信号的价值不在参数量,而在把“人类觉得好”这件事显式地喂给模型。下面看三步各自在做什么。

目标:收集人类标注者根据提示写出的高质量回答,用标准的监督学习(交叉熵损失)微调预训练模型,得到一个初步的指令跟随模型 π_SFT。

数据:提示 x 和对应的理想回答 y。 损失函数:语言建模损失。

import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(0)

# 模拟一个极简的语言模型(仅用于示意流程)
class TinyLM(nn.Module):
    def __init__(self, vocab_size=100, hidden_size=16):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, hidden_size)
        self.lm_head = nn.Linear(hidden_size, vocab_size)
    def forward(self, input_ids):
        # 简化:直接对每个token的embedding做线性投影
        embeds = self.embed(input_ids)  # (batch, seq, hidden)
        logits = self.lm_head(embeds)   # (batch, seq, vocab)
        return logits

# 模拟SFT训练步骤(一个batch)
model_sft = TinyLM()
optimizer = torch.optim.Adam(model_sft.parameters(), lr=1e-4)

# 构造模拟数据:batch=2, seq_len=8
input_ids = torch.randint(0, 100, (2, 8))  # 提示+回答的token id
labels = torch.randint(0, 100, (2, 8))     # 下一个token的目标id,回答部分需要shift

# 前向传播
logits = model_sft(input_ids)  # (2, 8, 100)
# 计算交叉熵损失(忽略padding等细节)
loss = F.cross_entropy(logits.view(-1, 100), labels.view(-1))

print(f"SFT步骤 - 损失值(示意): {loss.item():.4f}")
# 反向传播与优化(此处仅示意,不实际更新)
# optimizer.zero_grad()
# loss.backward()
# optimizer.step()
SFT步骤 - 损失值(示意): 4.9099

第二步:训练奖励模型(RM)

目标:学习一个反映人类偏好的奖励函数 r_φ(x, y)。

数据收集:用 π_SFT 对大量提示 x 生成多个回答(如 y_A, y_B),让标注者选出更好的那个,形成偏好对 (x, y_w, y_l),其中 y_w 优于 y_l。 建模偏好:常用 Bradley-Terry 模型 假设偏好概率与奖励差值的 sigmoid 函数相关: P(y_w ≻ y_l | x) = σ(r_φ(x, y_w) - r_φ(x, y_l)),其中 σ 是 sigmoid 函数。

损失函数:二元交叉熵损失。

import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(0)

# 块自包含:重新定义极简语言模型(与上一块同结构)
class TinyLM(nn.Module):
    def __init__(self, vocab_size=100, hidden_size=16):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, hidden_size)
        self.lm_head = nn.Linear(hidden_size, vocab_size)
    def forward(self, input_ids):
        embeds = self.embed(input_ids)
        return self.lm_head(embeds)

# 奖励模型:通常基于SFT模型,在顶部加一个标量输出层
class RewardModel(nn.Module):
    def __init__(self, base_lm):
        super().__init__()
        self.base_lm = base_lm
        # 假设我们取最后一个token的隐藏状态作为序列表示
        self.reward_head = nn.Linear(16, 1)  # hidden_size=16 -> 1
    def forward(self, input_ids):
        # 获取序列表示(这里简化处理)
        embeds = self.base_lm.embed(input_ids)  # (batch, seq, hidden)
        last_hidden = embeds[:, -1, :]  # 取最后一个token (batch, hidden)
        reward = self.reward_head(last_hidden).squeeze(-1)  # (batch,)
        return reward

# 模拟数据:一个偏好对 (x, y_w, y_l)
# 假设 prompt 长度为 4,回答长度为 4
prompt_ids = torch.randint(0, 100, (1, 4))
response_w_ids = torch.randint(0, 100, (1, 4))  # 优选回答
response_l_ids = torch.randint(0, 100, (1, 4))  # 劣选回答

# 拼接成完整序列 [prompt; response]
input_w = torch.cat([prompt_ids, response_w_ids], dim=1)  # (1, 8)
input_l = torch.cat([prompt_ids, response_l_ids], dim=1)  # (1, 8)

# 初始化模型
base_lm = TinyLM()
rm = RewardModel(base_lm)

# 计算奖励
reward_w = rm(input_w)  # 标量
reward_l = rm(input_l)  # 标量

# 计算奖励模型损失 (负对数似然)
# 根据 Bradley-Terry 模型:log σ(r_w - r_l)
loss_rm = -F.logsigmoid(reward_w - reward_l).mean()

print(f"奖励差值 (rw - rl): {(reward_w - reward_l).item():.4f}")
print(f"奖励模型损失: {loss_rm.item():.4f}")
奖励差值 (rw - rl): 1.5127
奖励模型损失: 0.1991

第三步:使用 PPO 进行强化学习优化

目标:微调 π_SFT(现在称为策略 π_θ),使其生成的回答能获得奖励模型 r_φ 给出的高奖励,同时防止策略变得与原始 π_SFT 相差太远(避免退化或遗忘通用能力)。

目标函数(简化): max_θ E_(x∼D, y∼π_θ(·|x)) [r_φ(x, y)] - β * KL(π_θ(y|x) || π_SFT(y|x)) 其中 β 是控制偏离程度的超参数,KL 散度作为约束。

这个 β 是 RLHF 里最敏感的一个旋钮:太小,模型会去钻奖励模型的空子(reward hacking,生成 RM 打高分但人类觉得莫名其妙的文本);太大,又几乎学不动。也正因如此,各家对齐配方里它的取值往往是最不肯公开的细节之一。

流程:使用近端策略优化(PPO)算法迭代地采样回答、计算奖励(来自 RM 和 KL 惩罚)、更新策略。

import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(0)

# 块自包含:重新定义极简语言模型(与前面同结构)
class TinyLM(nn.Module):
    def __init__(self, vocab_size=100, hidden_size=16):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, hidden_size)
        self.lm_head = nn.Linear(hidden_size, vocab_size)
    def forward(self, input_ids):
        embeds = self.embed(input_ids)
        return self.lm_head(embeds)

# 为简化,我们直接模拟PPO中的一个核心概念:在奖励基础上增加KL惩罚
def compute_kl_penalty(logits_new, logits_ref):
    """计算 KL(π_θ ‖ π_ref)(每 token 平均,KL 不对称,方向不能反)"""
    logp_new = F.log_softmax(logits_new, dim=-1)
    logp_ref = F.log_softmax(logits_ref, dim=-1)
    kl = (logp_new.exp() * (logp_new - logp_ref)).sum(dim=-1).mean()
    return kl

# 初始化策略模型(待优化)和参考模型(固定的SFT模型)
policy_model = TinyLM()
ref_model = TinyLM()
# 模拟参考模型参数冻结(在实际中,ref_model就是SFT模型的副本)
for param in ref_model.parameters():
    param.requires_grad = False

# 模拟一个提示
prompt_ids = torch.randint(0, 100, (1, 4))

# 策略模型生成回答(简化:直接前向传播,而非采样自回归)
# 假设我们"生成"了4个token的回答
generated_ids = torch.randint(0, 100, (1, 4))
input_full = torch.cat([prompt_ids, generated_ids], dim=1)

# 获取策略模型和参考模型对生成部分的logits
# 注意:这里为简化,我们用完整序列前向传播。实际PPO会更复杂,需要处理生成过程。
logits_policy = policy_model(input_full)[:, 4:, :]  # 只取回答部分 (1, 4, vocab)
logits_ref = ref_model(input_full)[:, 4:, :]

# 模拟奖励模型给出奖励(使用之前定义的RewardModel,这里直接给个随机值示意)
reward_from_rm = torch.randn(1) * 0.5 + 2.0  # 假设正向奖励

# 计算KL惩罚
kl_penalty = compute_kl_penalty(logits_policy, logits_ref)
beta = 0.1  # KL惩罚系数

# PPO的总目标(要最大化,所以损失取其负)
total_reward = reward_from_rm - beta * kl_penalty
loss_ppo = -total_reward

print(f"模拟RM奖励: {reward_from_rm.item():.4f}")
print(f"KL散度惩罚: {kl_penalty.item():.4f}")
print(f"总奖励 (RM - β*KL): {total_reward.item():.4f}")
print(f"PPO损失 (负总奖励): {loss_ppo.item():.4f}")
模拟RM奖励: 2.7433
KL散度惩罚: 0.4318
总奖励 (RM - β*KL): 2.7001
PPO损失 (负总奖励): -2.7001

简化革命:直接偏好优化(DPO)

RLHF 流程复杂,需要训练额外的奖励模型,并依赖不稳定的强化学习训练。DPO(Rafailov et al., 2023,arXiv:2305.18290)提出了一种优雅的简化方案。

DPO 的核心洞察

DPO 的关键在于一个数学变换:将奖励函数 r(x, y) 用策略 π(y|x) 和参考策略 π_ref(y|x) 来表示。

对于带有 KL 约束的奖励最大化问题,其最优解具有如下形式: π*(y|x) ∝ π_ref(y|x) * exp(r(x, y) / β) 可以反解出奖励函数: r(x, y) = β * log(π*(y|x) / π_ref(y|x)) + constant

将这个表达式代入 Bradley-Terry 模型的偏好概率公式中,奖励函数 r 被消去,得到了一个只关于策略 π 的损失函数。

DPO 损失函数

给定偏好数据集 D = {(x, y_w, y_l)},DPO 直接优化策略模型 π_θ,其损失函数为:

L_DPO(π_θ; π_ref) = -E_(x,y_w,y_l) [ log σ( β * log (π_θ(y_w|x) / π_ref(y_w|x)) - β * log (π_θ(y_l|x) / π_ref(y_l|x)) ) ]

直观理解:损失函数鼓励模型增大优选回答 y_w 相对于参考策略的对数概率比值,同时减小劣选回答 y_l 的比值。β 参数同样控制着与参考策略的偏离程度。

DPO 实现

与 RLHF 相比,DPO 的实现异常简洁。

import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(0)

def sequence_logp(logits, target_ids):
    """logπ(y|x) = Σ_t log_softmax(logits_t)[y_t]"""
    log_probs = F.log_softmax(logits, dim=-1)                            # (B, 2, L, V)
    picked = log_probs.gather(-1, target_ids.unsqueeze(-1)).squeeze(-1)  # (B, 2, L)
    return picked.sum(-1)                                                # (B, 2)

def dpo_loss(policy_logps, ref_logps, beta=0.1):
    """
    计算 DPO 损失。
    policy_logps / ref_logps: 形状 (batch, 2),第 0 列是 logπ(y_w),第 1 列是 logπ(y_l)
    """
    pi_logratios = policy_logps[:, 0] - policy_logps[:, 1]   # log(π(y_w)/π(y_l))
    ref_logratios = ref_logps[:, 0] - ref_logps[:, 1]
    logits = beta * (pi_logratios - ref_logratios)           # β × 奖励差值
    return -F.logsigmoid(logits).mean()                      # -log σ(奖励差值)

# 玩具数据:2 个偏好对,每个回答 4 个 token,词表 8
batch_size, seq_len, vocab = 2, 4, 8
shape = (batch_size, 2, seq_len, vocab)   # 第二维:0=y_w,1=y_l
policy_logits = torch.randn(shape)
ref_logits = torch.randn(shape)
target_ids = torch.randint(0, vocab, (batch_size, 2, seq_len))

policy_logps = sequence_logp(policy_logits, target_ids)   # 对数概率,必 ≤ 0
ref_logps = sequence_logp(ref_logits, target_ids)

loss = dpo_loss(policy_logps, ref_logps, beta=0.1)
print("策略模型的对数概率 logπ(y)(log_softmax 求和,必 ≤0):")
for i in range(batch_size):
    print(f"  样本{i}: logπ(y_w)={policy_logps[i,0]:.4f}, logπ(y_l)={policy_logps[i,1]:.4f}")
print(f"DPO 损失值: {loss.item():.4f}")
策略模型的对数概率 logπ(y)(log_softmax 求和,必 ≤0):
  样本0: logπ(y_w)=-10.1085, logπ(y_l)=-9.4395
  样本1: logπ(y_w)=-10.1951, logπ(y_l)=-13.2401
DPO 损失值: 0.7283

注意上面对数概率是用 log_softmax 后在目标 token 上求和得到的,因此全部为负值——这正是概率与对数概率的区别,也是后面练习要检查的点。

DPO 的优势:

  1. 简单:只需一个损失函数,像监督学习一样训练。
  2. 稳定:避免了 RL 训练中的不稳定性和高方差。
  3. 高效:无需在训练循环中从模型采样,也无需维护额外的奖励模型。

它不是银弹:DPO 只用一份离线偏好对,模型一旦偏离,数据分布就跟当前策略对不上了(off-policy 问题);需要探索、或奖励信号要在线更新的场景,PPO 派系仍有优势。工程上的常见折中是:先用 DPO 快速拿到对齐基线,再用少量在线反馈做迭代。

对比与总结:RLHF vs. DPO

方面RLHF (PPO)DPO
流程复杂度高。三步 pipeline,需训练 RM 和运行 PPO。低。单阶段训练,类似监督学习。
训练稳定性较低。PPO 对超参数敏感,训练可能不稳定。高。使用标准的梯度下降,更稳定。
计算开销高。需要多次前向/后向传播(策略、价值网络、RM),并在训练中采样。低。只需计算策略和参考模型的对数概率。
核心约束通过 KL 散度项在目标函数中显式约束。通过参考策略 π_ref 隐含在损失函数中。
实践效果已被 ChatGPT/InstructGPT 验证有效。在多项任务上匹配或超越 PPO,成为当前主流简化方案。

📝 动手练一练

题目:假设我们有一个极简的“语言模型”,它输出一个 3 维词表上的 logits。对于同一个提示 x,模型对两个候选回答 y_w(token序列 [0, 1])和 y_l(token序列 [2, 1])的 logits 如下(batch=1):

  • 策略模型 π_θ 的 logits: 对于 y_w 为 [[1.0, 0.5, -0.5], [0.2, 0.8, 0.0]],对于 y_l 为 [[-0.5, 0.0, 1.0], [0.2, 0.8, 0.0]]。
  • 参考模型 π_ref 的 logits 与策略模型初始相同(即 π_ref 是初始策略的副本)。

请手动计算(或写代码验证)该样本的 DPO 损失(β=0.5)。提示:先对每一步的 logits 做 log_softmax,取出目标 token 位置的对数概率,再沿序列求和得到 log⁡π(y∣x)\log \pi(y|x)。

👉 点击查看参考答案

思路:

  1. 先用 log_softmax 把 logits 变成对数概率,再取出目标 token 对应的值并逐 token 相加,得到整段回答的 log⁡π(y∣x)\log \pi(y|x)。不能直接把原始 logits 相加——那不是概率的对数。
  2. 代入 DPO:logits=β[(log⁡πw−log⁡πl)−(log⁡πref,w−log⁡πref,l)]\text{logits} = \beta[(\log\pi_w - \log\pi_l) - (\log\pi_{ref,w} - \log\pi_{ref,l})],损失 =−log⁡σ(logits)= -\log\sigma(\text{logits})。
  3. 初始时策略与参考模型完全相同,括号内必为 0,损失正好是 −log⁡0.5=ln⁡2≈0.6931-\log 0.5 = \ln 2 \approx 0.6931——这就是训练的起点。再模拟”训练一步后”(把 ywy_w 首 token 的 logit 调高、yly_l 首 token 的 logit 调低),看损失怎么往下走。
import torch
import torch.nn.functional as F

beta = 0.5
logits_pi_w = torch.tensor([[1.0, 0.5, -0.5], [0.2, 0.8, 0.0]])
logits_pi_l = torch.tensor([[-0.5, 0.0, 1.0], [0.2, 0.8, 0.0]])
logits_ref_w = logits_pi_w.clone()
logits_ref_l = logits_pi_l.clone()
y_w_ids = torch.tensor([0, 1])
y_l_ids = torch.tensor([2, 1])

def seq_logprob(logits, ids):
    logp = F.log_softmax(logits, dim=-1)
    return logp[torch.arange(len(ids)), ids].sum()

def dpo_loss(pi_w, pi_l, ref_w, ref_l, beta):
    return -F.logsigmoid(beta * ((pi_w - pi_l) - (ref_w - ref_l)))

pi_w, pi_l = seq_logprob(logits_pi_w, y_w_ids), seq_logprob(logits_pi_l, y_l_ids)
ref_w, ref_l = seq_logprob(logits_ref_w, y_w_ids), seq_logprob(logits_ref_l, y_l_ids)
print(f"初始: logπ_w={pi_w:.4f}, logπ_l={pi_l:.4f}")
print(f"初始: logπ_ref_w={ref_w:.4f}, logπ_ref_l={ref_l:.4f}")
print(f"初始 DPO 损失: {dpo_loss(pi_w, pi_l, ref_w, ref_l, beta):.4f}")

# 模拟训练一步后:y_w 首 token 的 logit 调高,y_l 首 token 的 logit 调低
logits_pi_w2 = logits_pi_w.clone(); logits_pi_w2[0, 0] = 3.0
logits_pi_l2 = logits_pi_l.clone(); logits_pi_l2[0, 2] = -1.0
pi_w2 = seq_logprob(logits_pi_w2, y_w_ids)
pi_l2 = seq_logprob(logits_pi_l2, y_l_ids)
print(f"\n一步之后: logπ_w={pi_w2:.4f}, logπ_l={pi_l2:.4f}")
print(f"一步之后 DPO 损失: {dpo_loss(pi_w2, pi_l2, ref_w, ref_l, beta):.4f}")
初始: logπ_w=-1.2963, logπ_l=-1.1566
初始: logπ_ref_w=-1.2963, logπ_ref_l=-1.1566
初始 DPO 损失: 0.6931

一步之后: logπ_w=-0.7986, logπ_l=-2.3725
一步之后 DPO 损失: 0.3538

分析:初始时策略与参考模型完全相同,括号里的差值必为 0,损失落在 −log⁡0.5=0.6931-\log 0.5 = 0.6931——这就是 DPO 训练的起点。把 ywy_w 首 token 的 logit 从 1.0 抬到 3.0、yly_l 首 token 的 logit 从 1.0 压到 −1.0 之后,log⁡πw\log\pi_w 上升、log⁡πl\log\pi_l 下降,差值变大,损失从 0.6931 降到 0.3538。这正是 DPO 在做的事:不用奖励模型,直接沿着“优选回答概率升、劣选回答概率降”的方向走。

常见错误

错误理解或写法现象或报错原因正确做法
在 DPO 训练中忘记冻结参考模型 π_ref 的参数。参考模型与策略模型一起更新,导致 KL 约束失效,模型可能快速退化。π_ref 必须保持固定,代表对齐前的初始策略。初始化 ref_model 后,立即设置 ref_model.eval() 并遍历参数设置 param.requires_grad = False。
直接使用模型输出的原始 logits 计算对数概率,未经过 log_softmax。DPO 损失计算的值域错误,可能导致梯度爆炸或训练不稳定。π(y∣x) 是概率分布,其对数概率应为 log_softmax(logits) 后在目标 token 上的和。使用 F.log_softmax(logits, dim=-1) 获取对数概率,再通过 gather 或索引取出目标 token 的值并求和。
在 RLHF 的奖励模型中,对长度差异巨大的回答直接比较原始奖励值。模型可能倾向于生成冗长的回答以获得更高的累积奖励,而非真正优质的回答。奖励值可能随序列长度增加而累积增加,存在长度偏差。对奖励进行长度归一化(如除以序列长度),或使用在提示后特定位置的奖励(如 EOS token 的奖励)。
将偏好数据集中的 y_w 和 y_l 视为绝对好/坏,忽略其可能来自同一模型不同采样。模型难以学习到细微的质量差别,收敛慢或效果不佳。实际标注中,y_w 和 y_l 可能质量接近,只是相对偏好。理解偏好数据的相对性,在损失函数中可使用 margin 或更复杂的排序损失。

本章小结

  1. 语言模型预训练目标与用户意图存在错位,指令微调与对齐是使其“有用、诚实、无害”的关键。
  2. RLHF 是经典的解决方案,包含 SFT、奖励模型训练、PPO 强化学习优化三个步骤,效果显著但流程复杂。
  3. DPO 通过数学变换,将 RLHF 问题转化为一个简单的二元分类损失,无需奖励模型和强化学习,大大简化了对齐流程。
  4. KL 散度约束 在对齐中至关重要,防止模型在优化偏好时过度偏离其原始知识分布。
  5. 无论是 RLHF 还是 DPO,其基础都是 Bradley-Terry 等偏好模型,将人类对回答的相对排序转化为可优化的概率目标。
  6. 对齐技术仍在快速发展,但其核心思想——利用人类反馈信号来指导模型行为——是通往更安全、更可控 AI 系统的基石。

📋 行动清单

  • 尝试修改 DPO 代码示例中的 beta 参数(例如设为 0.01 和 1.0),重新运行,观察损失值的变化,理解 beta 对模型偏离参考策略约束强度的影响。
  • 在 RLHF 模拟代码中,将 KL 惩罚系数 beta 设为 0,模拟没有约束的情况,思考这可能导致什么问题。
  • 查阅 F.kl_div 函数的文档,理解其 reduction 和 log_target 参数的含义,确保在计算 KL 散度时使用正确。

延伸阅读

  1. Training language models to follow instructions with human feedback(Ouyang et al., 2022,arXiv:2203.02155)—— RLHF 的奠基性论文,详细描述了 InstructGPT 的训练流程与实验。
  2. Direct Preference Optimization: Your Language Model is Secretly a Reward Model(Rafailov et al., 2023,arXiv:2305.18290)—— DPO 的原论文,包含了完整的理论推导与实验对比。

—— 小象教研组