冻结实验之前先确定预算
训练计划在运行前固定:随机种子二十九,CPU单线程,四十步监督暖启动,一百步on-policy强化学习。模型配置与第五章一致,监督和RL都使用AdamW但权重衰减为零;监督学习率0.006,RL学习率0.0015,梯度范数裁剪上限一。RL每步选五道训练题,每题独立抽八条轨迹,因此每步四十条、一百步四千条训练轨迹,不计每阶段独立评价用的额外样本。不能把评价采样也算成训练证据或偷偷追加训练预算。
监督阶段使用全部二十训练题的正确两步响应,目标是平均完整响应负对数概率。四十步后复制为冻结参考,再保存策略作为RL前起点;这是同一个权重快照,不从不同候选挑选某些指标最好的拼成基线。当前实验没有多种训练种子,没有依据测试挑停止点,也没有做“多跑几次直到成功”的选择。所得结果是单条真实训练轨迹,统计泛化结论必须相应收窄。
监督目标与后续奖励目标不同
监督负对数似然直接把正确响应当标签,每次都有梯度;严格奖励只能告诉采到的完整轨迹是否通过,未采到正确路径时不直接给出正确数字。这两种训练信号不可混为一谈。若只报告随机初始化到最终的提升,会把监督学习获得的能力也算作RL收益;本项目明确分别报告监督后的before与RL后的after,测试也同时评价二者。
监督阶段给每道题的完整响应相同权重,每条都含两个随机动作,因而按响应求和再平均与按响应词元平均只差固定二倍系数。若以后允许不同长度,这个等价不再成立,按词元平均会改变长短响应权重。项目只有固定二步,仍在文档中明确目标单位,不让长度归一习惯悄悄换掉本课推导的完整轨迹目标。
算例一:从一组奖励走到损失
假设某题八条样本中只有第一条成功,严格奖励为[1,0,0,0,0,0,0,0]。若当前策略恰等于参考,所有对数概率比零,正则回报就是这些奖励。留一基线给第一条零,其余七条各为1/7,所以优势为[1,−1/7,−1/7,−1/7,−1/7,−1/7,−1/7,−1/7]。损失是这八个优势各乘对应完整log概率后求平均,再取负号。
如果某条错误路径的log概率为−3,优势为−1/7,它对损失的数值贡献为−3/56,数值可以为负。损失为负不意味着程序错误,也不能直接把这个代理损失大小当作任务成功率。真正梯度来自优势乘log概率导数;优化器下降损失,倾向降低负优势路径概率、提高正优势路径概率,但共享参数可能带来其他路径的联动,单步不保证每条行为都按口头描述独立变化。
当当前策略已经偏离参考
若某路径当前比参考更常见,对数比为正,beta0.03乘这个值会从其奖励里扣除;较少见路径对数比为负,减去负数相当于增加回报。这是KL正则期望中的正确组成,不是给错误答案人工加奖励。由于留一中心化,组内比较同时反映正确性和参考偏离。beta只在明确的目标中有意义,不应该因为看见负回报就临时把它裁零,那会换成另一目标。
以一条正确路径对数比0.5、一条错误路径对数比−0.2为例,回报分别0.985和0.006。错误路径数值正,不代表验证器接受错误,只是正则鼓励恢复相对参考被压低的质量。报告把严格奖励和采样log比单独记录,使读者能分辨“训练更接近参考”与“解题更成功”。当beta过大,正则可能压过任务奖励;本实验固定0.03,未搜索一个所谓普适最优系数。
批内任务与随机流怎样组织
每步按固定循环从二十训练题中选连续五题,对每题扩展八次,形成按任务连续的四十行。sampler连续两次前向,各产生一位数字;之后用同一网络重新评分,参考在no_grad下评分。奖励和参考差构造回报,loo_advantages按八条reshape;这一批只反传一次,更新后立即丢弃轨迹。没有用高奖励样本筛选或复用数轮,所以本章实现符合on-policy估计器的采样条件。
固定任务循环每四步覆盖一次二十题,一百步覆盖二十五次;每题训练采样二百条。但采样结果依赖逐步变化的策略,不能把同题二百条当作来自一个固定成功率的IID样本来计算简单二项置信区间。评价阶段冻结策略后再独立采样,才符合固定参数下的重复试验解释。这是日志中的“采样成功率”与最终评价成功率的区别。
算例二:全零组什么时候应该不更新
训练刚开始,当前模型等于参考。若某一组奖励全零,对数比也全零,所有回报和优势都零,该组对梯度贡献零,这是预期行为。两步各十分类、共一百轨迹的空间稀疏奖励本来会遇到许多这种组。若一整批都如此且无额外正则,更新确实可能为零。tests.py 的两步烟雾训练检查完整流程和有限值,不要求每次短训练都改变全部参数;冻结的一百步真实运行才另外核实十七张量实际更新。
当策略已变、参考不变,即使严格奖励全零,各路径log比不同仍能产生正则信号。若实现每次更新前又把参考同步成当前策略,会消除这部分约束,测试参考摘要能抓到这个错误。相反,若所有奖励相同但程序仍因不清空旧梯度产生很大更新,也不是合理“持续学习”;必须确认梯度清零、回报detach和当前批图的边界。
从数学估计到实际优化还隔着几步
REINFORCE无偏结论针对未裁剪梯度在固定参数下的期望。项目先记录原始梯度范数,再按范数上限一进行裁剪,随后AdamW依据历史一二阶矩更新。裁剪与Adam都是实际算法的一部分,不再声称每个参数步等于无偏梯度乘固定学习率。记录这些设置是为了可复现,而不是把它们包装成某个严格收敛定理。
每次backward后逐张量检查梯度存在、全为有限数,再记录范数;出现NaN或缺失梯度立即失败,不把nan_to_num当作正常修复。优化完成后记录RL前后各张量最大绝对差;实际十七个张量都有正变化,最后一批也各有非零梯度。参考模型完整字节摘要前后相同,且没有梯度。这个证据支持“完整网络参数参与RL”,不支持“每个内部神经元都学到了算术算法”。
日志与检查点必须属于同一运行
train输出包含config、数据摘要、随机种子、步数、beta、采样数、分阶段history、before/after全题评价、每张量变化及参考摘要。checkpoint.pt同时保存最终策略与冻结参考,评价命令重新加载后重算精确成功概率和KL,测试确认与results.json一致。不能只把一个终端截图当结果,也不能把另一轮更好权重替换到旧报告旁边;清单中的摘要把源码、检查点和指标捆在一起。
读者修改学习率或步数时,建议通过--output创建新目录,让新实验与随包evidence并存。输出不同目录不是多此一举,因为重写原始证据后,就无法再证明讲义所引数值来自哪次运行。独立复跑可以出现环境舍入差异;应比较概率、梯度和任务结果,并标明是否逐位重放。下一章会读这次冻结运行的实际训练提升和测试明显下降,解释为什么一个实现通过所有测试的系统仍可能没有解决研究问题。
import torch
from run import loo_advantages
r=torch.tensor([1.,0.,0.,0.,0.,0.,0.,0.])
a=loo_advantages(r,8)
assert torch.allclose(a,torch.tensor([1.]+[-1/7]*7))
assert abs(float(a.sum()))<1e-6
print(a.tolist())python run.py train --output my-run
python run.py evaluate --output my-run --split test独立实验:整棵轨迹树的梯度走完17个张量
本课只有100条允许轨迹,所以可以在同一个模型、同一道题上把期望完全枚举,比较第四章的两条求导路线。新增gradient_audit.py从seed919创建随机float64模型和冻结reference,然后仅给当前head一个坐标加0.1,使KL项不是全零退化例;这次人为扰动用于检验公式,不是优化器更新,也不使用历史训练权重。
脚本对17张量的每个坐标比较∇J与∇S,而不再只查head。所有坐标最大差为5.204170×10⁻¹⁷,小于预定10⁻¹²门槛;100路径的概率和也在该门槛内等于一。下表第二列为直接梯度的整张量L2范数,第三列为两路线最大逐坐标绝对差。范数不用于判断哪一层更重要,仅用于避免梯度全零而虚假通过。
| 参数张量 | 直接梯度范数 | 两路线最大误差 |
|---|---|---|
| token_embedding.weight | 5.204599e-02 | 2.776e-17 |
| position_embedding.weight | 5.293632e-02 | 3.383e-17 |
| blocks.0.ln_attention.weight | 2.022516e-04 | 2.711e-19 |
| blocks.0.ln_attention.bias | 3.909932e-04 | 8.945e-19 |
| blocks.0.attention.query.weight | 4.406804e-05 | 1.186e-20 |
| blocks.0.attention.key.weight | 4.168311e-05 | 2.795e-20 |
| blocks.0.attention.value.weight | 8.320259e-03 | 5.204e-18 |
| blocks.0.attention.output.weight | 8.771058e-03 | 3.578e-18 |
| blocks.0.ln_feedforward.weight | 3.317937e-04 | 1.355e-19 |
| blocks.0.ln_feedforward.bias | 2.445171e-04 | 1.423e-19 |
| blocks.0.feedforward.0.weight | 1.228797e-02 | 4.770e-18 |
| blocks.0.feedforward.0.bias | 3.367637e-03 | 1.030e-18 |
| blocks.0.feedforward.2.weight | 1.336448e-02 | 4.987e-18 |
| blocks.0.feedforward.2.bias | 5.428007e-02 | 2.342e-17 |
| final_norm.weight | 1.220381e-03 | 5.313e-18 |
| final_norm.bias | 1.404165e-03 | 7.589e-19 |
| head.weight | 5.522263e-02 | 5.204e-17 |
阴性对照故意删除概率权重和回报上的stopgrad,再对“pF乘logp”求导。它与真正∇J的最大差为0.1358048393,检查器明确拒绝;这说明本例不是任意写一条代理都能通过。直接路线和score路线共享path_logps,因此二者一致仍不能单独证明路径索引正确;前面独立的两次前向评分、语法支持和概率和检查继续保留。
from gradient_audit import audit
r = audit()
assert len(r['tensors']) == 17
assert r['negative_control']['status'] == 'REJECTED'
assert r['model_unchanged'] and not r['training_performed']
print(r['max_absolute_error'], r['negative_control']['max_absolute_gradient_error'])
.venv/bin/python gradient_audit.py --output runs/gradient-audit.json这里证明的是这个有限生成树、这个参数点上两种实现的一致性;期望恒等式的一般理由仍是score均值为零的推导。它不意味着随机小批梯度与精确梯度逐次相同,也不意味着裁剪后的Adam增量无偏。脚本不执行optimizer,不读冻结checkpoint,结束时核对被检验的模型参数没有变化。
TRY IT YOURSELF
练习一:预算逐项计数
每步5题、每题8条、100步,每题被均匀轮转25次,共多少训练轨迹?
提示 1
提示 2
对照参考解答
TRY IT YOURSELF
练习二:正则回报
beta0.03,正确路径log比0.5、错误路径log比−0.2,回报各是多少?
提示 1
提示 2
对照参考解答
TRY IT YOURSELF
练习三:短烟雾训练没有更新
随机初始策略等于参考,整批全零奖励,为什么不应在测试中硬要求每张量变化?
提示 1
提示 2
对照参考解答
循环抽题时,哪一个目标的梯度无偏?
固定输入的留一无偏不等于每步对全部题目的均匀目标无偏。记 J_x(θ) 为一道题在当前策略下的正则目标,全部训练题的目标 J(θ)=Σ_x J_x(θ)/20。本项目第 k 步确定性取当前五题 B_k,轨迹随机性只保证未裁剪估计器的条件期望等于 ∇J_Bk(θ_k),其中 J_Bk=Σ_(x∈Bk) J_x/5。它一般不等于 ∇J(θ_k)。 最短反例:两题目标 J₁(θ)=θ、J₂(θ)=−θ,全体均匀目标梯度为零;固定抽第一题,估计梯度均值仍是一。轮到第二题后参数已改变,四步走遍二十题也不是在同一参数点做平均。只有冻结同一个 θ 后等权累计四批,才精确回到总体梯度;若每步独立均匀抽题,才可对全体目标逐步无偏。 这不把现有循环训练变成 off-policy:每条轨迹仍由更新前的当前策略采出,满足本章 on-policy 的含义。实际优化还包含梯度裁剪和 AdamW,参数增量本就不能直接当作原始无偏梯度。这里保留循环实现与旧实验结果,只把各层结论的范围写清。