AI Hero Club

大模型后训练 · 第 7 章

已自行标记完成0 / 8

仅保存在当前浏览器;完成标记不代表通过项目验收。

本章提纲 8 个小节
全课目录 8 章
完整课程计划 ↗
大模型后训练 / 课程计划 ↗

CHAPTER 07 / 8建议学习 120 分钟

把理论接到完整decoder的SFT与DPO

把四个回答的公式接到完整 decoder 后,怎样核对掩码、整条路径分数与冻结参考?

本章要解决的 3 个问题
  • 按真实batch与response_logps接口重建首回答/EOS mask和完整路径评分
  • 核算11472参数decoder并验证SFT/DPO全参数梯度与冻结reference
  • 运行独立载入评估,定位候选条件归一化与绝对回答概率的差别
课程项目 ZIP ↓

任务与依赖:两个实现各自负责什么

前六章的有限策略是可穷举参考,不是课程最终模型。本章增加真实自写decoder序列实现,读者需已掌握Transformer实战的注意力、preLN、位置与反向传播,并能读PyTorch张量索引。项目根目录同时有lab.py与sequence_lab.py:前者用八参数策略检验SFT/BT/DPO公式,后者从随机权重训练完整decoder,回答由多个字节token自回归生成。两者都离线可跑,互不导入兄弟课程路径;decoder.py是已声明来源的本地副本,不使用nn.Transformer或预训练权重。

序列版沿用同一数值x分区,但把提示格式固定成ASCII字符串,例如“x=-3.0;answer:”。四响应变成“NEG”、“POS”以及各自加“ because of sign.”的冗长版本。固定UTF-8基础字节词表为零到二百五十五,BOS二百五十六、EOS二百五十七,没有UNK与独立PAD。编码不拟合新词表,因此未见字节仍有表示;这只保证编码覆盖,不能保证新提示格式有语义泛化。第五项格式探针会展示合法编码仍失败。

从原始字符串到精确响应目标

batch(rows)的每行是(x,a)。prefix为BOS加提示bytes,answer为回答bytes加EOS,whole为两者连接。输入取whole去掉最后一项,目标取whole去掉第一项。有效mask先放len(prefix)减一项False,再放len(answer)项True。这个长度看似少一,正是因为prefix最后一个输入位置预测首回答token;若放len(prefix)个False,就漏掉首回答。目标无效处设负一百,输入右填充EOS,因果性确保未来右填充不影响左侧有效查询。

Xj=wholej,Yj=wholej+1,Mj=1[P−1≤j<P−1+A].X_j=\mathrm{whole}_j,\quad Y_j=\mathrm{whole}_{j+1},\quad M_j=\mathbf1[P-1\le j<P-1+A].
P为prefix长度,A为answer(含EOS)长度,j从0开始;无效Y设为−100。

response_logps(model,rows)先前向得到B×T×258 logits,对词表轴做log_softmax;把目标负一百暂时clamp为零仅用于安全gather索引,再乘有效mask,使其贡献与梯度为零。沿时间求和返回每条完整回答的logprob,以及每行真实回答目标数。真实EOS包括在answer中,目标负一百不进入分母。模型没有显式任意左padding接口,也不通过输入等于EOS判定无效性。

log⁡πθ(y∣x)=∑t∈response+EOSlog⁡softmax⁡(Zt)yt,LSFT=−1B∑ilog⁡πθ(yi∣xi).\log\pi_\theta(y|x)=\sum_{t\in\mathrm{response+EOS}}\log\operatorname{softmax}(Z_t)_{y_t},\qquad \mathcal L_{SFT}=-\frac1B\sum_i\log\pi_\theta(y_i|x_i).
本项目按回答总NLL平均;负一百目标和prompt目标均不评分。

算例一:一个真实负数提示的首尾账

对x=-3.0,提示字符串“x=-3.0;answer:”有十四个字节,prefix加BOS为十五。短回答NEG三个字节加EOS共四个目标。输入总长十八,前十四个目标位置不评分,索引十四首次预测N,之后E、G、EOS,共四个有效目标。长回答“NEG because of sign.”有二十正文bytes,连EOS二十一目标;同样prefix下输入总长三十五。两种回答同批时短行会右填充十七格,填充不产生额外目标。

第一项检查先构造有效位置掩码 valid = (y != -100),再按原顺序取出 y[valid]。得到的每个目标编号都必须与“回答的 UTF-8 字节编号,随后接一个 EOS”逐项相等;只比较目标数量不足以发现位置错移。第二项检查将同一短回答分别单独前向、与长回答拼批前向,比较其完整回答对数概率。第三项检查逐位置取出正确类别的 log_softmax 值并求和,与向量化的 gather 求和对照。编号与成员关系要求精确一致;浮点求和按已声明的精度与容差比较。

完整模型与所有参数

序列模型配置V=258、d=16、四头、一层、前馈宽度32、最大位置64、dropout零。注意力Q/K/V/O无bias,前馈有bias,learned absolute位置,preLN与精确GELU,输入输出矩阵未共享。参数账为两张词表矩阵8256、位置1024、该层注意力1024、前馈矩阵1024、前馈bias48、两次LayerNorm64、最终LayerNorm32,总11472。这个账与实际parameter numel严格对齐,不能把微型说成只有输出头。

take_step接收已计算loss,清梯度、backward,逐命名参数检查grad存在且全部有限,再做全局范数阈值一的裁剪,最后AdamW step。若loss或梯度非有限直接报错;不能把NaN替成零继续制造通过结果。十七个张量包含embedding、位置、注意力四投影、两层前馈和所有归一化。收据保存各张量范数与整阶段最大参数变化;测试用一批SFT和一批DPO检查这些链路,而不以总loss下降代替完整训练的证据。

SFT、reference与DPO的实际API

seq_sft_loss对每条回答logprob取负,再对十八条示范平均,和第二章按回答总NLL目标一致。固定SFT六十步,AdamW学习率0.006、衰减0.01。结束后deepcopy出reference,eval并关闭全部requires_grad;以其完整参数bytes计算hash。DPO另建AdamW,学习率0.002、衰减0.01、β\beta0.1,训练四十步。重建optimizer是明确的阶段边界,不声称从SFT保留矩状态继续优化同一目标。

seq_dpo_loss把十八个pair拆成winner与loser两组,各自计算policy完整response logprob;reference在no_grad中对同一两组评分。四组分数按第六章差分log比得到margin,再softplus负margin求平均。reference的hash在训练后必须不变,其参数grad始终None;policy十七张量都有有限梯度和实际变化。代码不会把有限策略的简化梯度照抄到decoder,也没有对回答logprob除长度。

独立的选定参数有限差分使用double模型,对输出头一个参数加减十万分之一,以整回答SFT损失差近似导数,和autograd对比到七位小数。它只核验一条序列链路,不能称完整网络全部元素已逐项数值验证;全部张量的存在性、有限性与更新由其他测试覆盖。有限参考则确实对其全部八参数做SFT与DPO中心差分,两个实现以不同验证成本形成互补。

独立实验:把差分检查送到全部17种参数

旧测试只对输出头一个坐标做SFT差分,不能据此声称所有网络分支的数值导数都验证过。新增gradient_audit.py建立seed=919的随机float64 decoder,关闭dropout,完全不读冻结训练权重,也不执行优化器。它分别使用两条长短SFT回答和两条偏好比较;reference先深复制并冻结。对每个目标、每个参数张量,选择解析梯度绝对值最大的一个非零坐标,再独立计算正负扰动的损失。这避免随手抽到未使用的位置行而获得零对零的空检查。

gi=∂L∂θi,g^i(h)=L(θ+hei)−L(θ−hei)2h,ϵi=∣gi−g^i(h)∣.g_i=\frac{\partial L}{\partial\theta_i},\qquad \widehat g_i(h)=\frac{L(\theta+h e_i)-L(\theta-h e_i)}{2h},\qquad \epsilon_i=|g_i-\widehat g_i(h)|.
e_i为第i坐标单位向量,h=10⁻⁵;SFT与DPO分别重算自身完整回答目标。每次扰动后原值精确恢复。

这里只检查每张量一个坐标:17张量乘两目标,共34个选点;不是11472个参数的逐坐标穷举。绝对误差门槛在执行前固定为10⁻⁶,实际SFT最大误差2.450343×10⁻⁸,DPO为2.420345×10⁻⁹。下表每格是该选点的“解析梯度 / 差分绝对误差”;具体展平索引、数值差分结果、配置和源码摘要存于runs/gradient-audit-reference.json。不同张量选点不同,不应比较表格数值来给层的重要性排名。

参数张量SFT:梯度 / 误差DPO:梯度 / 误差
token_embedding.weight-1.692161e+00 / 2.450e-081.091571e-01 / 1.090e-09
position_embedding.weight-1.692161e+00 / 2.450e-081.092751e-01 / 1.100e-09
blocks.0.ln_attention.weight-7.415200e-03 / 3.585e-114.421344e-04 / 4.063e-12
blocks.0.ln_attention.bias-2.057150e-02 / 2.133e-101.238321e-03 / 9.191e-12
blocks.0.attention.query.weight1.261474e-03 / 2.105e-10-7.014403e-05 / 1.712e-11
blocks.0.attention.key.weight-7.604298e-04 / 1.638e-10-5.048501e-05 / 1.432e-11
blocks.0.attention.value.weight1.394703e-01 / 6.440e-10-7.096439e-03 / 2.240e-13
blocks.0.attention.output.weight1.153704e-01 / 1.730e-109.926346e-03 / 1.649e-12
blocks.0.ln_feedforward.weight1.318550e-02 / 1.600e-10-8.482224e-04 / 2.376e-11
blocks.0.ln_feedforward.bias1.533317e-02 / 3.725e-10-1.067783e-03 / 1.673e-11
blocks.0.feedforward.0.weight2.520447e-01 / 2.206e-101.338431e-02 / 8.082e-12
blocks.0.feedforward.0.bias-1.460617e-01 / 5.195e-101.276220e-02 / 1.543e-13
blocks.0.feedforward.2.weight2.281106e-01 / 3.840e-10-1.305459e-02 / 5.973e-12
blocks.0.feedforward.2.bias-3.363219e+00 / 1.554e-08-2.944112e-01 / 2.420e-09
final_norm.weight9.809701e-02 / 6.935e-10-4.587868e-03 / 9.939e-12
final_norm.bias-7.284020e-02 / 7.628e-10-5.707167e-03 / 1.171e-11
head.weight1.857298e+00 / 2.846e-10-9.897809e-02 / 5.488e-12
CPU单线程、float64、seed919随机模型上的真实检查;这张表不属于原SFT60步+DPO40步训练成绩。
在课程项目目录运行在本课项目根目录运行,重建全部34个选点;不读写历史evidence。
from gradient_audit import audit
r = audit()
assert r['random_model_restored_exactly'] and not r['training_performed']
for name, phase in r['phases'].items():
    assert len(phase['coordinates']) == 17
    print(name, phase['max_absolute_error'])
终端命令保存自己的独立收据;随包reference收据与原训练收据分别保留。
.venv/bin/python gradient_audit.py --output runs/gradient-audit.json

选点差分只在给定随机模型和输入上检错,不能替代一般链式法则证明,更不能证明冻结模型的泛化。reference冻结、整数mask精确成员、右填充前向一致、源文件身份仍由其他检查负责。脚本结束时核对随机模型所有参数与开始时逐值一致;它是独立验收,不是一轮新训练。

算例二:候选概率外流的真实记录

固定测试x=-3.5,SFT时正确短回答NEG的logprob约−0.707623,绝对概率0.492814;四个指定候选的总路径概率约0.575782。DPO之后该正确短答logprob约−2.265495,概率降到0.103779;其他三个候选更快降到极小,四候选总质量也降到约0.103779。它们以外仍有大量可能字符串,概率没有凭空消失,而是转移到未枚举结果。

如果只对四个候选logprob做softmax,DPO后正确短答的条件概率几乎等于一,因为其他三个更低。这个一不是完整生成分布给它的概率,不能替代0.103779。独立检查是先对每个原始logprob取exp再求和得到候选总质量,再以某个候选绝对概率除总质量得到条件概率;两个步骤必须分开存。序列版因此报告candidate_total_path_mass与candidate_conditioned_probabilities,并把候选KL明确限定在这个再归一化空间。

这个实测正是第六章反例在完整模型中的出现:偏好margin改善不保证winner绝对概率上升。它也说明greedy正确不足以判断分布质量;只要某条路径逐步argmax,整串概率仍可能不高,而随机采样会落到很多未训练字符串。课程没有运行大规模随机采样成功率,因此不把四个greedy正确样本当作策略期望准确率,更不把候选条件概率当作真实采样成功概率。

保存、独立评价与状态边界

sequence-state.pt保存格式版本、序列数据契约摘要、完整配置、policy与reference权重及β\beta;load校验摘要后重建两模型并载入。摘要包含提示格式、响应字符串、BOS/EOS、mask和SFT/DPO求和约定,避免只核对数值x却忽略格式改变。该文件用于冻结推理与独立评价,没有保存Adam状态、阶段游标或RNG,因此不提供序列训练的精确恢复承诺。有限参考JSON可以在无状态全批SGD下继续一步,这不等于序列AdamW也能只载权重恢复。

CLI在项目根目录运行:sequence_lab.py train生成真实训练收据,sequence_lab.py evaluate --split test独立载入冻结权重评分;probe_formats.py在固定五种格式变更上保留全部输出。参考实现的lab.py与evaluate.py分别训练和评价有限策略。两套结果文件名称分开,避免把八参数的精确KL与decoder候选诊断混在一起。压缩包包括decoder、两实现、数据规则、测试和状态,不需要其他课程目录。

本章项目增量是完整序列后训练链。第八章将把训练loss、留出示范NLL、隐式pair指标、greedy行为与格式失败并排解释,指出哪些目标冲突确实发生。进入评估前应能从batch成员、路径分数与参考hash重建每一步,而不是只相信某条“训练成功”的文字日志。

只读分样式诊断进一步拆开了SFT分布变化。test正确短回答平均完整NLL从0.690142到2.658300,正确长回答从2.472356到91.301003;按两短一长混合,分别恢复约1.284213和32.205868。长回答大幅下降是偏好明确抑制冗长的预期方向之一,不能单凭混合均值说“所有正确回答都退化”。但短回答的平均NLL也上升,且NEG绝对概率下降有单独路径证据,二者说明比目标混合变化更具体的质量外流。

greedy函数还施加一条训练评分中没有的约束:把BOS候选logit设负无穷,再取argmax。response_logps始终在未屏蔽的258类softmax上评分。因此greedy正确率属于另设的解码策略,既非从原始pi随机采样,也不是候选条件分布采样。若改为受约束随机采样,允许token概率会按一减BOS质量重归一化,完整路径概率相应变化;本课未把这种新概率冒充训练的原始分布。

复核:BOS 质量与原始评分分布

只读BOS诊断显示,SFT正确短答位置平均BOS质量约数万分之一,DPO部分正数位置可到约0.002755;它们都被原始logprob分母计入。这个量能说明解码规则差异,却不能单独解释大部分概率外流,更不能以“BOS屏蔽很小”抹去两个分布定义不同。所有逐x、逐阶段数值保存sequence-path-diagnostics.json,无参数更新,不据此重选训练。

复核:浮点规约与原容差失败

数值验证曾出现一个被保留的测试失败:把float32向量化规约与逐位置标量连加以百万分之一绝对容差比较,长回答差约0.00000763。逐项有效成员与独立double有限差分均正确,最终用 2×10−52\times10^{-5} 绝对容差覆盖这种累计顺序误差,另保留原失败和修复理由。没有改训练loss、mask或权重以追求通过;计数、编号和参考hash仍要求精确相等。

两类验证需要各自容差:离散目标成员、response计数和参考状态身份要求精确一致;连续浮点损失与有限差分按精度和规约尺度给出容差。把所有检查统一改成“差不多”会放过mask错位,把所有浮点规约要求逐位相同又会把合法顺序差误报为算法失败。

独立运行标准库独立重算:真实概率外流与mask首尾索引。
import math
sft=[-.7076233625411987,-9.331679344177246,-2.491082191467285,-9.736905097961426]
dpo=[-2.2654953002929688,-39.93329620361328,-89.37799835205078,-123.78866577148438]
for label,lp in [('SFT',sft),('DPO',dpo)]:
    absolute=[math.exp(v) for v in lp];mass=sum(absolute)
    conditioned=[p/mass for p in absolute]
    assert abs(sum(conditioned)-1)<1e-12 and mass<1
    print(label,'winner absolute',absolute[0],'candidate mass',mass,'winner conditioned',conditioned[0])
assert math.exp(dpo[0])<math.exp(sft[0])
prefix=15;short_answer=4;long_answer=21
assert prefix+short_answer-1==18 and prefix+long_answer-1==35
print('first response target index',prefix-1,'counts',short_answer,long_answer)
终端命令在项目根目录、PyTorch2.6环境执行真实序列闭环。
python sequence_lab.py train
python sequence_lab.py evaluate --split test
python probe_formats.py
python -m unittest discover -v -s tests
在课程项目目录运行项目片段:独立载入冻结decoder并复算真实评价,不执行训练。
import json
from sequence_lab import load,evaluate,ROOT
from lab import SPLITS
model,reference,beta=load()
report=evaluate(model,reference,SPLITS['test'],beta)
assert sum(p.numel() for p in model.parameters())==11472
print('response targets',report['response_targets_with_eos'],'mean NLL',report['mean_response_nll'])
print('short and long diagnostics',json.loads((ROOT/'evidence'/'sequence-path-diagnostics.json').read_text())['by_phase']['dpo']['short_mean_response_nll'])

TRY IT YOURSELF

练习一:填充不创造目标

prefix长15,回答含EOS长4,与含EOS长21的回答拼批,短行输入长多少、填多少、有效目标多少?

提示 1
含EOS回答长r时,完整prefix+response的输入要去掉哪一项?
提示 2
分别求短长输入长度,再求右填量;有效目标只数回答含EOS,首回答位置由prefix边界定位。
对照参考解答
短行输入15+4−1=18,长行输入15+21−1=35,因此短行右填17格。有效目标仍4个,前14个提示目标不评分,首回答位于索引14,填充目标为−100。若把矩阵35格都计入分母,就错误稀释了loss。

TRY IT YOURSELF

练习二:条件概率的陷阱

四候选总路径质量为0.1,winner条件概率为0.99,winner真实路径概率是多少?能否说随机生成99%得到它?

提示 1
候选条件概率还缺少候选集合在完整分布中的总质量。
提示 2
用条件概率乘该集合概率恢复真实路径概率,再问自由采样是否被限制在四候选内。
对照参考解答
真实路径概率0.1×0.99=0.099。99%只在生成结果被限定到这四候选的条件下成立;完整分布还有90%质量在其外。未约束采样的成功率不能用候选softmax替代,greedy也不是从该条件分布随机抽样。

TRY IT YOURSELF

练习三:权重文件与恢复

sequence-state.pt可独立评价,是否足以精确继续AdamW训练?需要怎样如实描述?

提示 1
用于评价只需前向权重,继续AdamW还依赖哪些历史状态?
提示 2
列出矩、步数、阶段与RNG,再核对文件实际保存项;不要用另一条SGD支线的能力补证。
对照参考解答
不够,它没有optimizer矩、步数、阶段/RNG等完整训练状态。当前承诺是配置与policy/reference权重的冻结载入评价;若要精确恢复,应扩展保存和下一步逐值测试。不能借有限参考无状态SGD的继续测试,给序列AdamW贴上不存在的恢复能力。

CHECK YOUR UNDERSTANDING

检验理解

先作答,再决定是否展开解析。选择题用于发现理解缺口;独立实现与迁移任务仍需另行检验。

1. 短回答首token由哪个位置预测?

查看解析错位目标使提示末尾负责首回答,mask应按目标所属片段。
2. 序列项目有多少真实可训练参数?

查看解析完整D16单层decoder含17张量,所有参数参与SFT/DPO。
3. 候选条件KL0.156可以叫完整文本KL吗?

查看解析只在四条指定响应上重新归一化,没有枚举完整生成空间。
4. sequence-state.pt当前提供什么?

查看解析保存配置与两模型权重及契约,不承诺未实现训练状态恢复。

自行标记用于记录学习进度,不代表通过项目验收。