第3章:与人类对齐:偏好优化 (Preference Alignment)
“Alignment is the art of getting what you want, not just what you asked for.”
即使是最强的预训练模型,也只是学会了"续写"。是偏好优化让它学会了"对话"、“拒绝"和"价值观”。
目录
- 一、对齐三原则与 SFT 的局限
- 1. HHH 原则:有用、诚实、无害
- 2. 为什么 SFT 还不够?
- 二、经典路线:RLHF (PPO)
- 1. 训练 Reward Model (奖励模型)
- 2. PPO 算法核心:KL 散度与 Policy 更新
- 3. 实战:手动实现 PPO Step
- 三、现代路线:DPO (Direct Preference Optimization)
- 1. DPO 的数学魔术:无需 Reward Model
- 2. DPO vs PPO:谁赢了?
- 3. 实战:使用 TRL 训练 DPO
- 四、前沿变体:KTO / IPO / ORPO
- 1. KTO: 如果只有赞和踩,没有比较对
- 2. IPO: 修复 DPO 的长度偏好问题
- 3. ORPO: 连 SFT 都不需要了?
- 4. SPIN: 自我对弈,无需人工数据
- 五、2025 年最新进展与趋势
- 1. 从 RLHF 到 RLAIF (AI Feedback)
- 2. Online DPO: 摆脱静态数据集
- 3. 多目标对齐:不只是 HHH
- 4. 对齐税 (Alignment Tax)
- 5. 主流模型的对齐策略 (2025)
- 六、本章小结
一、对齐三原则与 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。
- 我们希望模型知道:即使你的回答在语法上很完美,但因为它是有害的,所以分数极低。
二、经典路线: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]
θmaxEx∼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),生成乱码来骗取高分。
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),防止更新过大
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 个模型:
π
θ
\\pi_\\theta
πθ): 待训练的策略
π
r
e
f
\\pi_{ref}
πref): 冻结的参考模型
r
ϕ
r_\\phi
rϕ): 冻结的奖励模型
这导致:
- 显存占用巨大(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]
πθmaxEx∼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]
πθmaxEy∼πθ[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(β1r∗(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(β1r∗(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)=β1r∗(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.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))]
人话解释:
π
θ
(
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)
π
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}")
代码关键点:
- reward_margin:chosen 和 rejected 的隐式奖励差
- accuracy:chosen 的奖励是否大于 rejected
2. DPO vs PPO:谁赢了?
| 稳定性 | 极低,对超参敏感 | 极高,像 SFT 一样稳 |
| 显存占用 | 巨大 (4个模型) | 低 (2个模型: Policy + Ref) |
| 实现难度 | 困难 | 简单 (几行代码) |
| 效果 | 理论上限高,上限由RM决定 | 实测与 PPO 持平甚至更好 |
目前 (2025),SOTA 模型如 Llama-3, Qwen-2 都在使用 DPO 及其变体。
3. DPO 实战要点
关键超参数:
| beta | 0.1 – 0.5 | KL 惩罚系数,越大模型越保守 |
| learning_rate | 1e-7 – 5e-7 | DPO 的 lr 要比 SFT 小 5-10 倍 |
| batch_size | 2 – 4 | 显存占用是 SFT 的 2 倍(需同时处理 chosen 和 rejected) |
常见错误:
- ❌ 使用预训练模型(未 SFT)直接训练 DPO:效果极差,必须先 SFT
- ❌ learning_rate 太大:导致模型崩溃,loss 变成 NaN
- ❌ beta 设置为 0:模型会过拟合偏好数据,丧失生成能力
工具库推荐:
- TRL (HuggingFace): DPOTrainer 是工业标准实现(详见 Part 5 第 3 章)
- LLaMA-Factory: 零代码 DPO 训练(详见 Part 5 第 2 章)
四、前沿变体:KTO / IPO / ORPO
DPO 虽然好,但它需要成对数据 (Paired Data)。这很难搞:你得找两句话,还得判断谁好谁坏。
1. KTO: 如果只有赞和踩,没有比较对
KTO (Kahneman-Tversky Optimization) 不需要成对数据。 它只需要:
(
x
,
y
,
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(消除长度偏好)
实现(需要手动修改 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
λ 需要精心调节) - 对数据质量要求极高
4. SPIN: 自我对弈,无需人工数据
SPIN (Self-Play Fine-Tuning):模型通过与自己对弈来自我提升。
算法流程:
π
0
\\pi_0
π0 生成回答
y
0
g
e
n
y^{gen}_0
y0gen
(
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 是人工标注的"好"答案
π
1
\\pi_1
π1,使其偏好
y
S
F
T
y^{SFT}
ySFT 而非
y
0
g
e
n
y^{gen}_0
y0gen
π
1
\\pi_1
π1 生成新回答
y
1
g
e
n
y^{gen}_1
y1gen
数学表达:
π
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πmaxEx∼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 数据,但无偏好标注
- ✅ 任务有明确对错(数学、代码)
- ❌ 主观任务(创意写作)效果不明显
五、2025 年最新进展与趋势
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
关键优势:
γ
\\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 年论文):
| AlpacaEval | 82.3% | 85.7% | 40% |
| MT-Bench | 7.45 | 7.68 | 40% |
适用场景:
- ✅ 显存受限(单卡训练 7B 模型)
- ✅ 需要更好的长度泛化
- ❌ 需要强约束(DPO 的 ref model 提供更强的 KL 约束)
2. 从 RLHF 到 RLAIF (AI Feedback)
问题:人类标注成本高($10-30/小时),速度慢,还有标注者偏见。
解决方案:用 GPT-4/Claude 等强模型来生成偏好数据。
Constitutional AI (Anthropic):
效果: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=α1Lhelpful+α2Lharmless+α3Lfactual+α4Lconcise
每个目标使用不同的偏好数据集,联合优化。
4. 对齐税 (Alignment Tax)
现象:对齐训练会损害模型的原始能力(如代码生成、数学推理)。
原因:
- 过度的 safety 训练导致模型"过于谨慎"
- KL 惩罚限制了模型的表达能力
解决方案:
- Targeted Alignment: 只对特定领域(如 safety)做对齐,保留其他能力
- Iterative DPO: 多轮小步迭代,而非一次大步
- Weak-to-Strong Generalization: 用弱模型的偏好数据训练强模型
5. 主流模型的对齐策略 (2025)
| GPT-4 | RLHF (PPO) | 人工标注 + RLAIF |
| Claude 3 | Constitutional AI (DPO) | 完全 RLAIF |
| Llama-3 | DPO + IPO | 人工标注 |
| Qwen-2 | ORPO (单阶段) | 人工标注 + 自我对弈 |
| DeepSeek-V2 | Online DPO | RLAIF + 多目标对齐 |
趋势总结:
- ✅ DPO 取代 PPO(稳定性 + 效率)
- ✅ RLAIF 取代人工标注(成本 + 规模)
- ✅ 单阶段训练(ORPO)成为新宠
- ✅ 多目标对齐成为标配
六、本章小结
核心要点
对齐是刚需:没有对齐的模型是危险且不可用的。HHH(Helpful, Honest, Harmless)是基本原则。
RLHF 已成过去式:PPO 虽然理论优雅,但工程上复杂度太高(4 个模型,不稳定),已被 DPO 取代。
DPO 是现代标准:通过数学推导消除 Reward Model,将 RL 转化为分类问题,训练稳定且效果出色。
多种变体各有千秋:
- KTO: 适用于只有点赞/踩数据的场景
- IPO: 修复 DPO 的长度偏好问题
- ORPO: 单阶段训练,省时省力
- SPIN: 自我对弈,无需额外标注
未来趋势:
- 从 Human Feedback 走向 AI Feedback (RLAIF)
- 从离线训练走向在线训练 (Online DPO)
- 从单目标走向多目标对齐
- 从两阶段(SFT+DPO)走向单阶段(ORPO)
实践建议
如果你是工程师:
- 优先使用 TRL 库的 DPOTrainer(成熟稳定)
- beta 参数从 0.1 开始调试
- 确保先做 SFT,再做 DPO(除非用 ORPO)
- 监控 KL 散度,避免模型偏离过远
如果你是研究员:
- 探索 ORPO/SPIN 等单阶段方法
- 尝试 RLAIF(用 GPT-4 生成偏好数据)
- 研究多目标对齐(factuality + safety + reasoning)
延伸阅读
核心论文:
工具库:
- TRL (Transformer Reinforcement Learning) – HuggingFace 官方库
- LLaMA-Factory – 零代码 DPO 训练
- OpenRLHF – 开源 RLHF 框架
下一章预告: 第4章 – 创建更优的嵌入模型
除了生成模型,Embedding 模型也是 LLM 生态的重要部分。下一章我们将探讨对比学习、InfoNCE 和 MTEB 榜单,教你训练媲美 OpenAI Ada-002 的嵌入模型。


