跳转到正文

B.3 DPO 及其变体

DPO Loss 是后训练岗位面试中考查频率最高的手写代码题,几乎每场都会考。本节给出 DPO、IPO、KTO、SimPO 四个变体的速记。


DPO Loss

核心问题:绕开 RLHF 中"训 reward model + PPO"的整套流程,直接用偏好对 监督微调策略,等价于在隐式 reward 上做 Bradley-Terry 最大似然,无需在线采样和 critic。

核心变量

  • pi_chosen / pi_rejected:当前策略 对好/坏回答的 log 概率
  • ref_chosen / ref_rejected:参考策略 对好/坏回答的 log 概率(需 detach
  • log_ratio_w / log_ratio_l,每个回答的隐式奖励(除以
  • beta):温度,控制策略偏离 reference 的强度;越大越敏感,典型 0.1~0.5

一句话记忆

4 条 logp(2 模型 × 2 回答):每条算"当前 − 参考";好减坏、乘 β、过 sigmoid、取负 log。

伪代码

log_ratio_w = log_pi_theta(y_w|x) - log_pi_ref(y_w|x)   # 好回答的隐式奖励
log_ratio_l = log_pi_theta(y_l|x) - log_pi_ref(y_l|x)   # 坏回答的隐式奖励
logits      = beta * (log_ratio_w - log_ratio_l)        # 好坏优势 × 温度
loss        = -log_sigmoid(logits)                       # 最大化优势 → 等价 BT 似然

Python 实现

python
import numpy as np

def log_sigmoid(x):
    return -np.logaddexp(0, -x)  # 数值稳定

def dpo_loss(logp_chosen, logp_rejected,
             logp_ref_chosen, logp_ref_rejected,
             beta=0.1):
    """
    所有参数: scalar 或 [B],返回标量 loss
    """
    log_ratio_w = logp_chosen - logp_ref_chosen
    log_ratio_l = logp_rejected - logp_ref_rejected
    logits = beta * (log_ratio_w - log_ratio_l)
    return -log_sigmoid(logits).mean()

PyTorch 实现

python
import torch
import torch.nn.functional as F

def dpo_loss(policy_chosen_logps, policy_rejected_logps,
             ref_chosen_logps, ref_rejected_logps,
             beta=0.1):
    """所有参数: [B]"""
    log_ratio_w = policy_chosen_logps - ref_chosen_logps
    log_ratio_l = policy_rejected_logps - ref_rejected_logps
    logits = beta * (log_ratio_w - log_ratio_l)
    return -F.logsigmoid(logits).mean()

IPO

核心问题:DPO 的 损失在偏好差距很大时梯度饱和,且对噪声偏好过拟合。IPO 改用平方损失,把好坏优势推向固定目标 ,惩罚双向偏离,训练更稳。

核心变量

  • delta,不带 β 的好坏优势
  • 目标间隔 ,由最优性条件推出

一句话记忆

把 DPO 的 换成 ——饱和没了,目标变成定点。

伪代码

delta = log_ratio_w - log_ratio_l     # 不带 β 的好坏优势
loss  = (delta - 1 / (2 * beta)) ** 2 # 推向固定目标

PyTorch 实现

python
def ipo_loss(log_ratio_w, log_ratio_l, beta=0.1):
    delta = log_ratio_w - log_ratio_l
    return ((delta - 1.0 / (2 * beta)) ** 2).mean()

KTO

核心问题:DPO/IPO 必须有 chosen-rejected 配对,但实际业务常只有"用户点赞/点踩"这种单条二元反馈。KTO 借用前景理论改造为单样本损失,无需配对,且对坏样本加权更高(损失厌恶)。

核心变量

  • log_ratio:单条样本的
  • z_ref:基线,是 desirable 样本 logit 比的 EMA,即 ,需 detach
  • lambda_D / lambda_U:好/坏样本权重,通常 (损失厌恶)

一句话记忆

无需配对:好样本 推过 ,坏样本压到 下,各自

伪代码

logit = beta * log_ratio                  # 单条样本的隐式奖励 × 温度
loss_desirable   = -log_sigmoid(logit - z_ref)        # 好样本: logit 超过基线
loss_undesirable = -log_sigmoid(z_ref - logit)        # 坏样本: logit 低于基线
loss = lambda_D * loss_desirable + lambda_U * loss_undesirable

PyTorch 实现

python
import torch
import torch.nn.functional as F

def kto_loss(log_ratio, is_desirable, z_ref=0.0,
             beta=0.1, lambda_D=1.0, lambda_U=1.33):
    """
    log_ratio: [B] = log_pi(y|x) - log_ref(y|x)
    is_desirable: [B] bool; z_ref 已含 β(TRL 约定)
    """
    logit = beta * log_ratio
    loss = torch.zeros_like(log_ratio)
    d = is_desirable
    u = ~is_desirable
    if d.any():
        loss[d] = lambda_D * -F.logsigmoid(logit[d] - z_ref)
    if u.any():
        loss[u] = lambda_U * -F.logsigmoid(z_ref - logit[u])
    return loss.mean()

SimPO

核心问题:DPO 需要在显存里同时驻留当前策略和参考策略,开销大;且长回答天然 log 概率更低,会被不公平地惩罚。SimPO 用长度归一化代替参考模型,去掉 ref 同时消除长度偏差。

核心变量

  • chosen_logps / rejected_logps:当前策略对好/坏回答的 log 概率(序列求和)
  • chosen_lengths / rejected_lengths:好/坏回答的 token 数
  • beta):偏好差距放大系数(SimPO 典型 2.0,比 DPO 大)
  • gamma):目标 margin,相当于可学的"好回答要比坏回答高出多少"阈值

一句话记忆

DPO 不要 ref:logp 除以回答长度当好减坏,乘 β 再减 margin

伪代码

logp_w = log_pi(chosen)  / len(chosen)   # 长度归一化
logp_l = log_pi(rejected) / len(rejected)
logits = beta * (logp_w - logp_l) - gamma # 减 margin
loss   = -log_sigmoid(logits)

PyTorch 实现

python
import torch.nn.functional as F

def simpo_loss(chosen_logps, rejected_logps,
               chosen_lengths, rejected_lengths,
               beta=2.0, gamma=0.5):
    logp_w = chosen_logps / chosen_lengths
    logp_l = rejected_logps / rejected_lengths
    logits = beta * (logp_w - logp_l) - gamma
    return -F.logsigmoid(logits).mean()

DPO 家族对比速查

算法需要 ref?需要配对?核心区别
DPO是 (chosen/rejected),经典版
IPO平方损失 ,避免饱和
KTO否(好/坏标签)单样本 sigmoid + 基线 ,损失厌恶
SimPO长度归一化 log-prob + margin

注: 为好减坏的 log-ratio 差。


易错点

易错说明
四条 log_prob 搞混记住:每个模型出两条(chosen + rejected),一共四条;ref 要 detach
log-prob 用错DPO/IPO/KTO 用的是 不是 raw log-prob
log_sigmoid 数值溢出PyTorch 的 F.logsigmoid 内置处理;手写时用 logaddexp
含义 是温度,越大对偏好差距越敏感;不是学习率
IPO 没有 sigmoidIPO 用平方损失回归到 ,不用 sigmoid
KTO 的 约定TRL 中 已带 (是 logit 的 EMA);写成
SimPO 忘除长度长度归一化是 SimPO 的核心,长回答 log-prob 更小要除掉
chosen/rejected 反了检查数据集:chosen 是人类偏好的那条

现代强化学习实战课程

个人非商业离线镜像 · 内容版权归 WalkingLabs 所有,依 CC BY-NC-SA 4.0 授权 · 查看原文