读完这篇,你能做什么
手算初始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,训练固定两候选策略并核对回答掩码。只用内置小张量,不加载语言模型。
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按顺序看,每一步做了什么
- 固定参考策略与偏好数据
- 对齐目标标签和回答掩码
- 计算四份序列对数概率
- 形成相对参考的偏好差
- 优化并评估保留任务退化
03对照实现代码
Rafailov等,arXiv:2305.18290v3;算子基线PyTorch 2.9.1 · 第4节式4至7;附录梯度推导
根据固定论文公式原创的PyTorch教学函数;不是完整DPO训练器,输入必须是已正确聚合的序列概率。
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实验只训练两候选类别分布,证明公式与梯度方向,不代表完成大模型对齐。
代码先调用谁,再调用谁
- 读取同提示的优选与拒选序列
- 策略和冻结参考分别前向
- 移位取token对数概率并按掩码求和
- 计算式7偏好损失
- 只更新策略并独立验证
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始终比较当前策略偏好差与同提示参考偏好差。
逐项聚合,不创建时间维中间数组;模型前向成本不在本题范围。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
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与梯度尺度
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
从含 KL 约束的策略形式出发,说明成对比较中哪些项依赖提示、哪些依赖回答;写清消去项成立所需的共享条件。
- 提交内容
- 符号分类与推导
- 怎样算完成
- 同提示条件明确
- 参考概率支持条件完整
练到的知识点KL约束最优策略 · 奖励重参数化 · 配分函数消去
- 作业 02代码推演
自行构造两条长度不同的回答,各列出四个以内token的策略及参考对数概率,并构造回答掩码;手工形成一对DPO目标,明确偏好标签和所用系数。
- 提交内容
- token 账本与目标记录
- 怎样算完成
- 只聚合声明的目标位置
- 参考项身份固定
练到的知识点参考策略冻结 · 回答序列对数概率 · 标签移位与掩码
- 作业 03代码推演
自造两条回答的逐 token 策略/参考对数概率,分别用求和与平均形成偏好目标;改变回答长度但不改变每 token 统计,分析目标定义变化。
- 提交内容
- 两种目标的长度对照表
- 怎样算完成
- 目标归约明示
- 长度变化方式可复核
练到的知识点回答序列对数概率 · beta与梯度尺度
- 作业 04原理问答
参考模型对某个候选给零概率时,序列对数比目标还需什么支持集条件?给出输入校验、拒绝或改目标的明确接口选择。
- 提交内容
- 数学定义域与处理契约
- 怎样算完成
- 零概率和极小有限值分开
- 改目标必须显式记录
练到的知识点参考策略冻结 · 回答序列对数概率
综合应用
- 作业 05编程实践
实现 CPU DPO 损失与解析输入梯度,测试相同策略参考、极端对数比及零有效目标;与自动求导核对。
- 提交内容
- 实现与数值测试
- 怎样算完成
- 稳定计算可验证
- beta 与归约显式
练到的知识点参考策略冻结 · 回答序列对数概率 · 标签移位与掩码 · beta与梯度尺度
- 作业 06排错实验
为参考概率缓存注入模板变化、分词器版本变化和标签重复移位,设计拒绝不兼容缓存的测试。
- 提交内容
- 缓存契约与故障报告
- 怎样算完成
- 缓存身份包含计算条件
- 测试能定位失配来源
练到的知识点参考策略冻结 · 标签移位与掩码
- 作业 07面试场景
离线偏好来自旧模型短回答,新策略开始生成更长文本。制定长度分层与分布偏移评价,不用总体偏好率代替全部诊断。
- 提交内容
- 评价协议及分层表
- 怎样算完成
- 长度和质量分开
- 保留评价不参与训练选样
练到的知识点beta与梯度尺度 · 离线偏好分布偏移
- 作业 08编程实践
CPU 两候选离散策略分别使用固定参考和随策略更新的参考,比较五步梯度与策略变化;后者仅作为错误或替代目标实验,不称标准 DPO。
- 提交内容
- 双目标实验与梯度日志
- 怎样算完成
- 参考更新规则完整
- 算法命名与实现一致
练到的知识点参考策略冻结 · beta与梯度尺度
- 作业 09排错实验
同一偏好对在批次中重复不同次数,同时使用 sum 或 mean 归约。用 CPU 梯度实验审计其权重和 beta 作用是否被混淆。
- 提交内容
- 重复权重与梯度表
- 怎样算完成
- 重复次数有身份记录
- 缩放来源可分别追踪
练到的知识点回答序列对数概率 · beta与梯度尺度
进阶挑战 · 掌握前置后选做
- 作业 10设计评审
用 CPU 离散候选回答模型比较两个 beta 和两种偏好噪声水平,固定基础策略及数据量,报告策略偏移与偏好拟合。
- 提交内容
- 受控实验及限制说明
- 怎样算完成
- 干预因素清晰
- 玩具实验不冒充语言模型结论
练到的知识点KL约束最优策略 · beta与梯度尺度 · 离线偏好分布偏移
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 11思维拓展 · 反例构造
“只要调整 beta,就能抵消任意参考策略变化”是否成立?在三候选离散分布中构造验证或反例,明确比较全部偏好对还是单对。
- 提交内容
- 离散分布与条件论证
- 怎样算完成
- 概率分布合法
- 单对拟合不冒充全局等价
练到的知识点参考策略冻结 · beta与梯度尺度
- 作业 12思维拓展 · 跨域迁移
将 DPO 的序列聚合迁移到不同粒度的动作轨迹:把一步拆成两步后,偏好目标能否保持相同?定义概率分解与终止约定后分析。
- 提交内容
- 动作分解模型及等价条件
- 怎样算完成
- 概率事件身份明确
- 拆步改变与归约改变分开
练到的知识点回答序列对数概率 · beta与梯度尺度
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
DPO原论文v3 (新标签页)↗
重点阅读式4至7与附录梯度推导。
- 实践参考文档英文
作者公开训练器 (新标签页)↗
对照标签移位和回答概率聚合;可变分支用于阅读,复现实验需另固定提交。
- 实践参考文档英文
PyTorch 2.9 log_softmax (新标签页)↗
理解稳定计算与dim选择。
- 实践参考文档英文
PyTorch v2.9.1算子入口 (新标签页)↗
核对真实logsigmoid与
log_softmax接口。