qinghuan
← 监督微调与偏好对齐 / 全部笔记

单元 35 / 监督微调与偏好对齐

P0 · 必会主线L3 · 深入

DPO:用偏好对直接优化策略

根据成对偏好调整回答概率,从带 KL 约束的奖励目标推导 DPO,并明确参考概率、哪些回答位置参与计算、怎样按长度汇总。

本篇目录 · 按需要跳转

读完这篇,你能做什么

手算初始DPO损失为log(2),核对掩码聚合,并在CPU两候选策略上验证梯度更新方向。

学习理由、准备要求与知识点

为什么学它

DPO 代码虽短,每个概率也必须对应正确的回答与策略。漏掉参考项,或把回答对数概率求和改成平均,都会改变目标。

先读这些内容

知识点清单

  • KL约束最优策略
  • 奖励重参数化
  • 配分函数消去
  • 参考策略冻结
  • 回答序列对数概率
  • 标签移位与掩码
  • beta与梯度尺度
  • 离线偏好分布偏移

运行前先确认论文机制按文中版本说明;Java 17 完整算法,Python 3.14 与 PyTorch 2.9.1+cpu 的小型 CPU 实验。未执行 GPU、多机或大规模模型训练。

01先弄懂它是什么

手头已有同题两份回答、并标出了哪份更好时,DPO 可以直接用这些偏好对训练策略。它不先单独训练奖励模型,标准离线训练步骤中也不必重新生成回答。但这些偏好对仍需要先采样、再标注,数据质量依然关键。因此“无需强化学习循环”描述的是训练流程的变化,不代表可以忽略好数据,也不代表训练数据与实际使用场景的差异会自动消失。

计算时,要同时比较当前策略和一个固定的参考策略。设 x 是提示,yw 是优选回答,yl 是拒选回答;pi_theta(y|x) 表示整段回答的条件概率,pi_ref 是参照基线。一批有 B 对样本,就需要四个 [B] 向量:当前策略、参考策略分别给优选和拒选回答的序列对数概率。这里不直接输入“这份回答得九十分”,所说的隐式奖励来自当前策略相对参考策略的概率变化。

训练数据能表达的只是已出现候选之间的偏好。某类工具错误从未进入拒选样本,DPO不会因为损失下降就自动学会避免它。还应检查候选生成策略、标注者、模板及任务家族,按提示来源隔离测试集。已有SFT基线提供可用行为,偏好阶段仍需要验证它是否损伤原有任务能力。

文中这几个词,先认一认

参考策略
这次 DPO 训练中固定不变、用于作比较的策略概率基线。
序列对数概率
把回答中各个有效 token 在给定历史下的条件对数概率加起来,得到整段回答的对数概率。
隐式奖励
对同一个提示和回答,比较当前策略与固定参考策略各给了它多少概率:两者的对数概率相减,再乘正的系数 beta,得到本文用于比较回答的奖励分数。这个分数来自模型概率的相对变化,不是额外收集的一份人工评分。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

推导从“提高平均奖励,同时扣除偏离参考分布的代价”开始,后者写成 beta×KL。beta 必须为正,并满足参考分布支持集等条件,例如所讨论的回答要有非零参考概率,归一化总量也要有限且非零。

此时最优策略满足 pi_star(y|x) 正比于 pi_ref(y|x)×exp(r(x,y)/beta)。把两边取对数并整理,得到 r=beta×log(pi_star/pi_ref)+beta×logZ(x)。

Z(x) 是把这些权重归一成概率所需的总量。比较同一提示下两份回答的奖励时,二者共有的 beta×logZ(x) 正好抵消,因此这一步不必枚举全部回答来算 Z;参考概率项仍然保留。

DPO 比较的是当前策略对两份回答的偏好差,相对参考策略又改变了多少。令 z=beta×[(logpi_theta(yw)-logpi_theta(yl))-(logpi_ref(yw)-logpi_ref(yl))],损失为 -log(sigmoid(z)),稳定计算可写成 softplus(-z)。

当前策略与参考完全相同时,z=0、损失为 log(2),不管参考原本更喜欢哪份。参考项虽然不用对策略求梯度,却会改变这个样本的梯度权重,因此不能随意删掉。

语言模型原始logits形状为[B,T,V],V是词表大小。先沿词表做log_softmax,按正确移位的目标token索引取值,再用[B,T]回答掩码筛掉提示和padding,最后对时间求和。原始DPO使用序列概率;改为每token平均是一种新的目标选择,可能减弱长度影响,却不能在论文公式名下悄悄替换。空回答的策略也应在数据契约中明确,而非让全零掩码悄悄参与训练。

先跑通一个最小例子

在PyTorch 2.9.1+cpu中执行experiment.py,训练固定两候选策略并核对回答掩码。只用内置小张量,不加载语言模型。

experiment.py · python
import torch
import torch.nn.functional as F
torch.manual_seed(13)
torch.set_num_threads(1)
torch.set_default_dtype(torch.float64)

logits = torch.nn.Parameter(torch.zeros(2))
reference = torch.zeros(2)  # 固定参考分布,不放进优化器。
ref_logp = F.log_softmax(reference, dim=0).detach()
def dpo():
    lp = F.log_softmax(logits, dim=0)
    margin = .5 * ((lp[0]-lp[1])-(ref_logp[0]-ref_logp[1]))
    return -F.logsigmoid(margin)
initial = dpo().item()
assert abs(initial-0.6931471805599453)<1e-12
optimizer = torch.optim.SGD([logits],lr=.3)
for _ in range(30):
    optimizer.zero_grad()
    loss = dpo()
    loss.backward()
    optimizer.step()  # 真正更新两候选策略,数据对和参考概率保持固定。
assert dpo().item()<initial
assert F.softmax(logits,dim=0)[0].item()>.6
assert reference.grad is None
token_logp = torch.tensor([[-100.,-.5,-1.,-9.],[-7.,-.2,-4.,-8.]])
mask = torch.tensor([[False,True,True,False],[False,True,False,False]])
totals = token_logp.masked_fill(~mask,0).sum(dim=1)  # 排除提示和padding,只求回答概率和。
assert torch.allclose(totals,torch.tensor([-1.5,-.2]))
assert torch.isfinite(-F.logsigmoid(torch.tensor([-1000.,1000.]))).all()
assert torch.allclose(ref_logp,F.log_softmax(torch.zeros(2),dim=0))
print(f"initial={initial:.6f} final={dpo().item():.6f} preferred_probability={F.softmax(logits,dim=0)[0].item():.6f}")
print("PASS 7 checks; two-candidate CPU policy, not a language model")

核对运行结果初始损失约0.693147,最终下降,优选概率大于0.6;输出PASS 7 checks。

正在加载流程图…

策略与参考都对同一对回答评分,掩码一致后才比较相对偏好差。
查看流程图源码(Mermaid)
flowchart TD
  N0["固定参考策略与偏好数据"]
  N1["对齐目标标签和回答掩码"]
  N2["计算四份序列对数概率"]
  N3["形成相对参考的偏好差"]
  N4["优化并评估保留任务退化"]
  N0 --> N1
  N1 --> N2
  N2 --> N3
  N3 --> N4

按顺序看,每一步做了什么

  1. 固定参考策略与偏好数据
  2. 对齐目标标签和回答掩码
  3. 计算四份序列对数概率
  4. 形成相对参考的偏好差
  5. 优化并评估保留任务退化

03对照实现代码

DPO论文式7及其概率重参数化 (新标签页)↗

Rafailov等,arXiv:2305.18290v3;算子基线PyTorch 2.9.1 · 第4节式4至7;附录梯度推导

根据固定论文公式原创的PyTorch教学函数;不是完整DPO训练器,输入必须是已正确聚合的序列概率。

核心机制代码 · python
import torch.nn.functional as F
def dpo_loss(policy_w, policy_l, reference_w, reference_l, beta):
    policy_gap = policy_w - policy_l  # 当前策略的序列偏好差。
    reference_gap = (reference_w - reference_l).detach()  # 固定概率基线,不回传梯度。
    return -F.logsigmoid(beta * (policy_gap-reference_gap)).mean()  # 稳定二分类目标。

论文第4节先从KL约束目标得到最优策略形式,再把奖励写成策略比值与提示常数。成对比较只依赖奖励差,提示常数因此消去。这个推导说明DPO为何不必显式拟合奖励网络,并不证明任意噪声数据和有限神经优化都能达到理论最优。

式7的两个参考对数概率有相同条件x,优选和拒选不能来自不同提示。教学代码接收已经聚合的[B]分数,省略分词与模型前向;四个输入形状必须一致,否则广播可能产生[B,B]并把不同样本交叉比较。真实训练实现应明确断言形状,而不是依赖自动广播帮忙。

作者公开实现的训练器把目标移位并屏蔽忽略标签,然后计算策略与参考的回答概率。核对这一调用顺序比只抄loss函数更重要:如果模型预测第t+1个token,取值目标却仍是第t个,数值仍然有限,训练也可能下降,但学习的已不是约定的语言概率。

F.log_softmax避免先softmax再log导致极小概率下溢,F.logsigmoid负责成对目标的稳定计算。这些是真实PyTorch算子;教学片段由本文重新组合,并不是作者训练器的原样副本。CPU实验只训练两候选类别分布,证明公式与梯度方向,不代表完成大模型对齐。

代码先调用谁,再调用谁

  1. 读取同提示的优选与拒选序列
  2. 策略和冻结参考分别前向
  3. 移位取token对数概率并按掩码求和
  4. 计算式7偏好损失
  5. 只更新策略并独立验证

04动手写一个例子

回答掩码聚合与DPO损失核对

聚合有效回答位置的有限非正对数概率,再计算单对DPO损失;无有效token、正对数概率或非法beta拒绝。

输入 1
策略与参考的优拒分数相同
输出 1
log(2)
输入 2
策略[-.5,-2],参考[-1,-1],beta=1
输出 2
约0.201413

先把序列归约独立成sum函数,能够对提示与padding是否被排除作最小测试。例如[-100,-.5]配合[false,true]应得到-.5。没有这种局部断言时,极大的提示损失可能在总体均值里被掩盖,难以从训练曲线判断错误来自哪里。

聚合结果进入loss函数前检查有限且非正,因为概率不超过一。这里输入是某些已选token的对数概率,不包含完整词表,程序不能验证所有词表概率之和为一;这一前提必须由上游log_softmax保证。beta要求正且有限,计算溢出则明确报告,不能把NaN作为一次有效实验。

对z使用稳定softplus公式后,极端负偏好差仍产生有限大损失。算法在固定浮点容差内比较手算结果,确认参考差、括号与符号顺序。它不是优化器,也不估计泛化效果;其用途是作为训练管线的算术契约测试,帮助定位掩码和概率语义错误。

为什么这样写是对的序列和只包含mask为true的位置;z始终比较当前策略偏好差与同提示参考偏好差。

时间 聚合O(T),单对损失O(1)额外空间 O(1)额外空间

逐项聚合,不创建时间维中间数组;模型前向成本不在本题范围。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 40 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    static void near(double expected,double actual) {
        if (!Double.isFinite(actual) || Math.abs(expected-actual)>1e-9*Math.max(1,Math.abs(expected)))
            throw new AssertionError(expected+" != "+actual);
    }
    static void check(boolean ok) { if(!ok) throw new AssertionError(); }
    static void bad(Runnable r) {
        try { r.run(); } catch(IllegalArgumentException e) { return; }
        throw new AssertionError("invalid input accepted");
    }
static double sum(double[] logp,boolean[] mask) {
        if(logp==null||mask==null||logp.length!=mask.length) throw new IllegalArgumentException("shape");
        double s=0; int used=0;
        for(int i=0;i<logp.length;i++) {
            if(!Double.isFinite(logp[i])||logp[i]>0) throw new IllegalArgumentException("log probability");
            if(mask[i]) { s+=logp[i];used++; } // 只累计回答中有效标签的位置。
        }
        if(used==0||!Double.isFinite(s)) throw new IllegalArgumentException("empty or overflow");
        return s;
    }
    static double loss(double pc,double pr,double rc,double rr,double beta) {
        if(!Double.isFinite(pc)||!Double.isFinite(pr)||!Double.isFinite(rc)||!Double.isFinite(rr)
            ||!Double.isFinite(beta)||beta<=0||pc>0||pr>0||rc>0||rr>0) throw new IllegalArgumentException("scores");
        double z=beta*((pc-pr)-(rc-rr)); // 减去参考策略原本的偏好差。
        if(!Double.isFinite(z)) throw new IllegalArgumentException("margin overflow");
        return Math.max(-z,0)+Math.log1p(Math.exp(-Math.abs(z))); // 稳定负logsigmoid。
    }
    public static void main(String[] args) {
near(Math.log(2),loss(-1,-2,-1,-2,.1));
        near(.2014132779827524,loss(-.5,-2,-1,-1,1));
        near(-.5,sum(new double[]{-100,-.5},new boolean[]{false,true}));
        near(-1.5,sum(new double[]{-.5,-1},new boolean[]{true,true}));
        near(1000,loss(-1001,-1,-1,-1,1));
        bad(()->sum(new double[]{-1},new boolean[]{false}));
        bad(()->sum(new double[]{.1},new boolean[]{true}));
        bad(()->loss(-1,-1,-1,-1,0));
        System.out.println("PASS 8 cases");
    }
}

这些特殊情况也要试一试

  • 全零回答掩码拒绝
  • 提示极端分数应被排除
  • 参考等于策略时固定log(2)
  • 正对数概率和非正beta拒绝
进阶选读 · 偏好噪声、长度与分布外变化

场景DPO提升总体偏好率,却损伤短答案或工具格式。

做法建立长度匹配对照、标签噪声审计与独立任务保留集;一次只改变beta、归约或采样规则,记录新目标区别。

复杂度N对、总有效token数L的损失归约O(L),神经网络前向和反向单独计算。

好处与代价直接离线训练较简洁,但覆盖取决于偏好数据;在线更新候选增加覆盖也增加成本与漂移。

05项目里怎么用

beta在理论目标中对应偏离参考的约束强度,在具体有限训练中也缩放损失输入和梯度,不能把某个数值直接解释为“允许百分之几变化”。比较不同beta时要同时报告步数、学习率、KL、长度和保留集质量,不能只按训练损失大小排序优劣。

参考模型可预计算分数以减少训练计算,但缓存必须绑定模型权重、分词器、模板、截断与掩码版本。修改任一环节后旧分数可能失效。使用参数高效适配时还需确认参考前向不会启用正在更新的适配器,否则参考随着策略移动,式7的基线语义就变了。

研究报告同时展示SFT基线、DPO后模型和失败样本;客观格式检查与人类偏好可以分开计量。只有通过训练集几对合成样本,不能声称模型在开放对话中对齐。大规模GPU吞吐、量化误差和分布式优化器恢复属于独立实验,本文CPU程序没有验证这些能力。

容易踩的坑

  • 将四份分数广播成错误矩阵
  • 没有冻结参考或缓存失效
  • 把提示token加入回答概率
  • 把求和换平均却不注明目标变化
  • 只报告偏好率而不看原能力退化

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
原始序列概率DPO对应论文推导存在长度与数据分布影响复现原始目标
预计算参考分数减少重复前向强依赖数据和模型版本一致固定离线数据
在线生成新偏好对覆盖当前策略输出标注成本及目标漂移增加有稳定独立评价者的迭代

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01为什么logZ能消去?

展开参考答案与推导

同一提示下常数相同,奖励差中相减为零。

02参考项不求梯度为何仍重要?

展开参考答案与推导

它改变logsigmoid输入,从而改变每个样本的梯度权重。

03平均token概率等于序列概率吗?

展开参考答案与推导

不等于;序列对数概率是求和,平均引入长度归一化。

换个情况,还会处理吗

01初始损失不是log(2)应检查什么?

展开参考答案与推导

策略是否真正等于参考、dropout状态、适配器开关、缓存版本和掩码是否一致。

02训练后答案变长如何判断?

展开参考答案与推导

按长度分层比较,加入长度匹配偏好对,分别看真实质量和长度捷径。

03更换模板后沿用参考缓存行吗?

展开参考答案与推导

不行;模板改变条件序列与token位置,需要重新计算或严格验证缓存等价。

写代码试一试

01聚合回答概率。

输入 / 条件
[-100,-.5,-1]与[false,true,true]
输出 / 预期
-1.5

提示 按掩码求和

展开参考答案与推导

忽略提示位置,拒绝全零掩码。

02计算初始DPO损失。

输入 / 条件
策略和参考均优=-1、拒=-2
输出 / 预期
log(2)

提示 先算两个相同偏好差

展开参考答案与推导

z为零,使用稳定负logsigmoid。

03验证极端值。

输入 / 条件
z=-1000
输出 / 预期
损失约1000

提示 不要exp(1000)

展开参考答案与推导

用max和log1p恒等式避免溢出。

读完之后,独立完成

课后作业:自己做一遍

本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

手算初始DPO损失为log(2),核对掩码聚合,并在CPU两候选策略上验证梯度更新方向。

本篇重点加练

参考策略、序列聚合与 beta 共同决定偏好梯度,需检验等价变换与长度效应。

  • 参考策略冻结
  • 回答序列对数概率
  • beta与梯度尺度

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    从含 KL 约束的策略形式出发,说明成对比较中哪些项依赖提示、哪些依赖回答;写清消去项成立所需的共享条件。

    提交内容
    符号分类与推导
    怎样算完成
    • 同提示条件明确
    • 参考概率支持条件完整

    练到的知识点KL约束最优策略 · 奖励重参数化 · 配分函数消去

  2. 作业 02代码推演

    自行构造两条长度不同的回答,各列出四个以内token的策略及参考对数概率,并构造回答掩码;手工形成一对DPO目标,明确偏好标签和所用系数。

    提交内容
    token 账本与目标记录
    怎样算完成
    • 只聚合声明的目标位置
    • 参考项身份固定

    练到的知识点参考策略冻结 · 回答序列对数概率 · 标签移位与掩码

  3. 作业 03代码推演

    自造两条回答的逐 token 策略/参考对数概率,分别用求和与平均形成偏好目标;改变回答长度但不改变每 token 统计,分析目标定义变化。

    提交内容
    两种目标的长度对照表
    怎样算完成
    • 目标归约明示
    • 长度变化方式可复核

    练到的知识点回答序列对数概率 · beta与梯度尺度

  4. 作业 04原理问答

    参考模型对某个候选给零概率时,序列对数比目标还需什么支持集条件?给出输入校验、拒绝或改目标的明确接口选择。

    提交内容
    数学定义域与处理契约
    怎样算完成
    • 零概率和极小有限值分开
    • 改目标必须显式记录

    练到的知识点参考策略冻结 · 回答序列对数概率

综合应用

  1. 作业 05编程实践

    实现 CPU DPO 损失与解析输入梯度,测试相同策略参考、极端对数比及零有效目标;与自动求导核对。

    提交内容
    实现与数值测试
    怎样算完成
    • 稳定计算可验证
    • beta 与归约显式

    练到的知识点参考策略冻结 · 回答序列对数概率 · 标签移位与掩码 · beta与梯度尺度

  2. 作业 06排错实验

    为参考概率缓存注入模板变化、分词器版本变化和标签重复移位,设计拒绝不兼容缓存的测试。

    提交内容
    缓存契约与故障报告
    怎样算完成
    • 缓存身份包含计算条件
    • 测试能定位失配来源

    练到的知识点参考策略冻结 · 标签移位与掩码

  3. 作业 07面试场景

    离线偏好来自旧模型短回答,新策略开始生成更长文本。制定长度分层与分布偏移评价,不用总体偏好率代替全部诊断。

    提交内容
    评价协议及分层表
    怎样算完成
    • 长度和质量分开
    • 保留评价不参与训练选样

    练到的知识点beta与梯度尺度 · 离线偏好分布偏移

  4. 作业 08编程实践

    CPU 两候选离散策略分别使用固定参考和随策略更新的参考,比较五步梯度与策略变化;后者仅作为错误或替代目标实验,不称标准 DPO。

    提交内容
    双目标实验与梯度日志
    怎样算完成
    • 参考更新规则完整
    • 算法命名与实现一致

    练到的知识点参考策略冻结 · beta与梯度尺度

  5. 作业 09排错实验

    同一偏好对在批次中重复不同次数,同时使用 sum 或 mean 归约。用 CPU 梯度实验审计其权重和 beta 作用是否被混淆。

    提交内容
    重复权重与梯度表
    怎样算完成
    • 重复次数有身份记录
    • 缩放来源可分别追踪

    练到的知识点回答序列对数概率 · beta与梯度尺度

进阶挑战 · 掌握前置后选做

  1. 作业 10设计评审

    用 CPU 离散候选回答模型比较两个 beta 和两种偏好噪声水平,固定基础策略及数据量,报告策略偏移与偏好拟合。

    提交内容
    受控实验及限制说明
    怎样算完成
    • 干预因素清晰
    • 玩具实验不冒充语言模型结论

    练到的知识点KL约束最优策略 · beta与梯度尺度 · 离线偏好分布偏移

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 11思维拓展 · 反例构造

    “只要调整 beta,就能抵消任意参考策略变化”是否成立?在三候选离散分布中构造验证或反例,明确比较全部偏好对还是单对。

    提交内容
    离散分布与条件论证
    怎样算完成
    • 概率分布合法
    • 单对拟合不冒充全局等价

    练到的知识点参考策略冻结 · beta与梯度尺度

  2. 作业 12思维拓展 · 跨域迁移

    将 DPO 的序列聚合迁移到不同粒度的动作轨迹:把一步拆成两步后,偏好目标能否保持相同?定义概率分解与终止约定后分析。

    提交内容
    动作分解模型及等价条件
    怎样算完成
    • 概率事件身份明确
    • 拆步改变与归约改变分开

    练到的知识点回答序列对数概率 · beta与梯度尺度

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗