任务与依赖:两个实现各自负责什么
前六章的有限策略是可穷举参考,不是课程最终模型。本章增加真实自写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,因果性确保未来右填充不影响左侧有效查询。
response_logps(model,rows)先前向得到B×T×258 logits,对词表轴做log_softmax;把目标负一百暂时clamp为零仅用于安全gather索引,再乘有效mask,使其贡献与梯度为零。沿时间求和返回每条完整回答的logprob,以及每行真实回答目标数。真实EOS包括在answer中,目标负一百不进入分母。模型没有显式任意左padding接口,也不通过输入等于EOS判定无效性。
算例一:一个真实负数提示的首尾账
对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、0.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先深复制并冻结。对每个目标、每个参数张量,选择解析梯度绝对值最大的一个非零坐标,再独立计算正负扰动的损失。这避免随手抽到未使用的位置行而获得零对零的空检查。
这里只检查每张量一个坐标: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-08 | 1.091571e-01 / 1.090e-09 |
| position_embedding.weight | -1.692161e+00 / 2.450e-08 | 1.092751e-01 / 1.100e-09 |
| blocks.0.ln_attention.weight | -7.415200e-03 / 3.585e-11 | 4.421344e-04 / 4.063e-12 |
| blocks.0.ln_attention.bias | -2.057150e-02 / 2.133e-10 | 1.238321e-03 / 9.191e-12 |
| blocks.0.attention.query.weight | 1.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.weight | 1.394703e-01 / 6.440e-10 | -7.096439e-03 / 2.240e-13 |
| blocks.0.attention.output.weight | 1.153704e-01 / 1.730e-10 | 9.926346e-03 / 1.649e-12 |
| blocks.0.ln_feedforward.weight | 1.318550e-02 / 1.600e-10 | -8.482224e-04 / 2.376e-11 |
| blocks.0.ln_feedforward.bias | 1.533317e-02 / 3.725e-10 | -1.067783e-03 / 1.673e-11 |
| blocks.0.feedforward.0.weight | 2.520447e-01 / 2.206e-10 | 1.338431e-02 / 8.082e-12 |
| blocks.0.feedforward.0.bias | -1.460617e-01 / 5.195e-10 | 1.276220e-02 / 1.543e-13 |
| blocks.0.feedforward.2.weight | 2.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.weight | 9.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.weight | 1.857298e+00 / 2.846e-10 | -9.897809e-02 / 5.488e-12 |
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'])
.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权重及;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有限差分均正确,最终用 绝对容差覆盖这种累计顺序误差,另保留原失败和修复理由。没有改训练loss、mask或权重以追求通过;计数、编号和参考hash仍要求精确相等。
两类验证需要各自容差:离散目标成员、response计数和参考状态身份要求精确一致;连续浮点损失与有限差分按精度和规约尺度给出容差。把所有检查统一改成“差不多”会放过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)
python sequence_lab.py train
python sequence_lab.py evaluate --split test
python probe_formats.py
python -m unittest discover -v -s tests
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
提示 2
对照参考解答
TRY IT YOURSELF
练习二:条件概率的陷阱
四候选总路径质量为0.1,winner条件概率为0.99,winner真实路径概率是多少?能否说随机生成99%得到它?
提示 1
提示 2
对照参考解答
TRY IT YOURSELF
练习三:权重文件与恢复
sequence-state.pt可独立评价,是否足以精确继续AdamW训练?需要怎样如实描述?