AI Hero Club

从零训练语言模型 · 第 6 章

已自行标记完成0 / 8

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

本章提纲 5 个小节
全课目录 8 章
完整课程计划 ↗
从零训练语言模型 / 课程计划 ↗

CHAPTER 06 / 8建议学习 120 分钟

把训练变成可恢复的状态转移

同样的权重和种子载入后,为什么下一步更新仍可能不同?

本章要解决的 3 个问题
  • 逐项执行清梯度、前向、反向、裁剪与AdamW更新,区分loss与预算指标
  • 手算AdamW首步并解释为何只恢复权重不能恢复优化轨迹
  • 保存并核验模型、优化器、编码、数据、抽样与随机状态,复现同环境下一步
课程项目 ZIP ↓

任务与先修:八十次更新必须有身份

现在管道已经可以产生正确目标,完整 decoder 也能反传。本章将训练定义为有状态过程,并让中断恢复成为可验收行为。读者需会向量范数、链式法则和 Python 状态字典;Adam 的具体约定在本章给出。项目固定 CPU 单线程、PyTorch 2.6.0、随机种子七、batch最多四个窗口、学习率0.003、权重衰减0.01、全局梯度范数裁剪阈值一,运行八十次更新。没有学习率调度、混合精度或分布式通信,不能把这些未实现机制写进收据。

一次更新的顺序具有数学含义:先 model.train;抽取下一批并拼成 x、y;optimizer.zero_grad 清除上一步残留;前向得到当批有效目标均值 loss;backward 沿完整计算图求梯度;记录裁剪前范数与各参数张量范数;全局裁剪;optimizer.step 更新。漏清梯度会把不同步的导数累积,先更新再反向则使用错的状态;用验证损失反向会直接破坏留出集合。日志存 float(loss.detach()),不保留带计算图张量,避免无意持有历次图。

裁剪与AdamW的精确对象

把所有参数的梯度展平连接成向量 g,范数 ∥g∥2\|g\|_2 超过阈值 c 时乘缩放因子 c/范数,否则不变。裁剪保留方向但改变幅度,各层不能分别裁成 c 再声称全局范数也不超过 c。实现使用框架函数,其中会加入很小数避免除零,因此本课手算忽略那个保护项,仅对非零且远离阈值的数值比较。梯度非有限应被视为失败;记录所有分量范数是为了发现数值故障,而不是把高梯度一律解释为学习更好。

g~t=gtmin⁡(1,c/∥gt∥2),mt=β1mt−1+(1−β1)g~t,vt=β2vt−1+(1−β2)g~t2.\tilde g_t=g_t\min(1,c/\|g_t\|_2),\quad m_t=\beta_1m_{t-1}+(1-\beta_1)\tilde g_t,\quad v_t=\beta_2v_{t-1}+(1-\beta_2)\tilde g_t^2.
梯度裁剪后进入Adam矩估计;平方、除法与开方均按参数元素执行。

项目用 AdamW 的默认 beta1=0.9、beta2=0.999、epsilon=1e−8。矩估计初值零,步数 t 从一开始。偏差修正为 m^t=mt/(1−β1t)\hat m_t=m_t/(1-\beta_1^t) 与 v^t=vt/(1−β2t)\hat v_t=v_t/(1-\beta_2^t)。权重衰减与梯度矩分开:先使参数乘以 1−ηλ1-\eta\lambda,再减去 ηm^t/(v^t+ϵ)\eta\hat m_t/(\sqrt{\hat v_t}+\epsilon)。本项目统一给全部参数施加该权重衰减,包括归一化和bias;这是一项简单实验约定,不声称是所有大模型训练的最佳参数分组。

θt=(1−ηλ)θt−1−ηm^tv^t+ϵ.\theta_t=(1-\eta\lambda)\theta_{t-1}-\eta\frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon}.
固定学习率的AdamW更新;本项目没有把L2项加进梯度矩,因此不是直接用Adam优化L加二次罚项。

算例一:一维首步与矩状态

令参数初值二,原始梯度0.5,裁剪阈值一,故梯度不变。采用学习率0.1、衰减0.01,首步 m=0.05、v=0.00025,偏差修正后分别为0.5与0.25。忽略极小epsilon时,自适应步长为0.1;衰减先将二变为1.998,最后参数约1.898。若误把衰减项0.02加入梯度再估计矩,首步虽也可能约减少0.1,但之后轨迹不同,因为矩状态已经不同。仅看一次下降不能区分两种算法。

第二步即使梯度换为−0.25,m仍含上一梯度记忆,v也含其平方记忆。因此只保存参数1.898、重新把m和v设零,与保留原状态继续会走不同方向或步幅。独立检查一是用标准库完整展开首步数值;独立检查二是把连续运行三步与运行两步后序列化全部状态再运行一步比较。只有权重文件相同不是状态等价,因为优化器下一步依赖历史。

算例二:同权重、不同下一批

抽样器有六个窗口,batch大小四,某轮排列为 [3,0,5,1,4,2]。取第一批后游标是四,下一批应为 [4,2]。若恢复时仅用相同种子新建抽样器,游标重置为零,下一批又是 [3,0,5,1],重复了已见数据。即使权重、优化器和全局随机种子都相同,轨迹也会改变,因为抽样器有自己独立的 generator。需要同时保存排列、游标、轮号、生成器内部状态与窗口数、batch大小。

另一个故障是构造模型时消耗全局随机数:先恢复 RNG,再实例化随机初始化模型,构造过程已经推进了 RNG,之后 dropout 或采样将不一致。正确顺序是读取状态,构造模型与优化器,载入权重和状态,构造并恢复抽样器,最后恢复全局 torch RNG。本项目 dropout为零、生成用greedy,当前更新本身不消耗全局随机数;测试仍额外读取随机向量比较,为以后增加随机机制保留一个真实状态校验,而不虚称已测试dropout恢复。

独立运行标准库独立验证:AdamW首步、完整状态恢复与只恢复权重的反例。
import json, math

def step(state,g):
    theta,m,v,t=state
    g=g*min(1.,1./abs(g)) if g else g
    t+=1; m=.9*m+.1*g; v=.999*v+.001*g*g
    mh=m/(1-.9**t); vh=v/(1-.999**t)
    theta=theta*(1-.1*.01)-.1*mh/(math.sqrt(vh)+1e-8)
    return [theta,m,v,t]
state=[2.,0.,0.,0]
first=step(state,.5)
assert abs(first[0]-1.898)<1e-7
continuous=state
for g in [.5,-.25,.1]: continuous=step(continuous,g)
prefix=state
for g in [.5,-.25]: prefix=step(prefix,g)
resumed=step(json.loads(json.dumps(prefix)),.1)
assert continuous==resumed
wrong=step([prefix[0],0.,0.,0],.1)
assert abs(wrong[0]-continuous[0])>1e-3
print('first',first,'continuous',continuous[0],'resume',resumed[0],'weights-only',wrong[0])

checkpoint契约与真正的恢复测试

项目 checkpoint 保存配置、模型 state_dict、optimizer state_dict、tokenizer规则与摘要、语料摘要、抽样器状态、全局 RNG、已完成步数和 targets_seen。载入时以当前语料重新计算摘要,不匹配即拒绝;编码规则也重算摘要。checkpoint 中的配置用于重建同形状模型,不能默默把词表或窗口改大后继续。这个协议解决本实验已知状态身份,不保证任何来自未知来源的文件都安全,也不把不同框架版本的反序列化结果称为逐位可重现。

真实项目测试先运行三次更新,保存状态;连续执行第四次,记录每个参数、Adam状态、loss、抽样位置与随后随机向量;再从第三步文件恢复,重新执行第四次并逐项比较。当前同一CPU环境下均逐值相等。另加错误语料摘要必须抛异常的故障测试。这比“save后load没有报错”强,因为后者可能只证明文件可读,并未检验后续转移;也比只比loss强,因为不同权重可能给某一批同样的标量。

本次冻结训练的损失与预算

训练收据同时区分初始与最终整套训练损失、逐检查点验证损失、最后一批损失。当前固定实验八十更新,累计4585个有效目标;训练集合单次覆盖只有690个目标,因此4585不是独特语料规模。初始训练NLL为5.682412,最终为1.653204;验证由5.681924降至3.190052。最后一批loss约1.509825,不能拿它替代整套训练平均。曲线说明该环境下优化链路和合成分布的统计拟合在工作,不证明自然语言泛化。

本次计时的范围

墙钟训练段约0.416秒,包含定期验证但不包含解释器启动、导入框架和构造模型;其名称和计时边界记录于文件,机器负载变化时可不同。短运行时间被固定开销和系统抖动主导,不能从这一数推导大规模吞吐。测试通过之后不继续无目标地增加训练预算,只保存给定协议下的真实结果。下一章把冻结 checkpoint 用于验证和一次预先固定配置的测试评估,并把可读生成与定量似然分别记录。

训练状态转移可写成一个确定函数:给定模型参数、优化器矩与步数、数据窗口、抽样器排列游标和随机状态,产生新状态与一条收据。确定性依赖环境与算子,不是只设置一个整数seed就自动成立。本项目设置单线程、手动种子和确定性算法,并把实际torch版本写入training.json;如果将来加上Python随机或其他库的生成器,也必须分别保存其状态。当前源码没有使用那些额外随机源,不能声称已覆盖任意扩展。

保存发生在完整optimizer.step之后、下一次抽样之前。这个边界让已完成步数、权重和游标互相对应:若写在backward之后却未step,必须额外保存尚未应用的梯度,否则恢复会丢失半步。本项目不提供半步恢复,也没有对多个并行写者设计事务。文件写入中断可能产生损坏文件;当前单进程小实验把成功写完的checkpoint作为恢复前提,不宣称具备生产级原子轮换和容错日志。

运行时校验与来源记录

恢复语料摘要通过仍不保证所有超参数适配。模型配置来自文件,optimizer状态中还包含实际学习率与衰减,抽样器状态有窗口数和batch大小;窗口由相同tokenizer和block_size重建。若只改了正文但总窗口数巧合一样,语料摘要仍会拒绝;若只改tokenizer编号,规则摘要会拒绝。对源码实现的变化则必须看source_manifest和测试收据,当前restore不会自动证明两个不同程序语义相同。分清运行时硬校验与旁路来源记录的责任很重要。

有效预算可以用两条独立路径核对:更新返回本批目标数,把历次相加得到targets_seen;抽样器已取窗口索引也能映射回各窗长度求和。若resume把seen恢复了却把游标重置,第一条预算数仍可能看似连续,但训练对象已重复;所以恢复测试同时比较下一批所隐含的loss、全参数、优化器与游标。只盯一个计数无法证明轨迹没断裂。最终固定实验的4585还可与每个检查点1170、2328、3450、4585核对单调性。

拓展:更换学习率调度

学习率固定并不是没有设计。它减少了恢复时遗漏scheduler的状态,但也限制训练效率。若将来采用warmup加余弦衰减,必须首定义时间轴按更新数还是有效目标数推进,并把调度器当前状态或可重算的完整规则保存;两个不同长度batch会让这两种时间轴不同。我们没有为了使曲线更漂亮临时更换学习率,也没有把这份小预算训练称为收敛到全局最优。损失降低是局部经验观察,非凸网络没有由本实验获得最优性证明。

优化器的步数是实际执行更新次数,而非前向次数。一次验证会做许多前向但不推进Adam状态;若误把验证batch也计为训练步,偏差修正时间轴或调度时间轴便会错。项目只在update内部执行step,evaluate返回后不会修改参数或优化器。恢复测试也比较Adam中的step张量,防止权重相同而计数偏移。记录八十次更新与周期验证次数的区别,才能让别人从命令重建相同预算,而不是只复制日志最后一行。

终端命令在下载项目的根目录、已安装固定依赖的Python环境中运行;这是项目命令,不是Python源码。
python run.py train
python -m unittest -v tests.test_pipeline.PipelineTests.test_exact_same_environment_resume

TRY IT YOURSELF

练习一:全局裁剪

两个参数的梯度是3与4,阈值为1。求裁剪后梯度,并说明分别裁剪会得到什么。

提示 1
先求两个坐标构成的总二范数,再统一缩放。
提示 2
分别裁标量后重新计算整体范数,检查是否真的满足全局阈值,而非只满足每坐标阈值。
对照参考解答
全局范数为5,统一缩放因子1/5,得到0.6与0.8,范数恰好1。若分别把每个标量裁至绝对值1,得到1与1,全局范数为根号2,大于阈值。矩估计应接收前一种全局裁剪结果,本项目记录的是裁剪前总范数,不能把日志值误当裁剪后界。

TRY IT YOURSELF

练习二:恢复缺项

checkpoint已有模型、optimizer、全局RNG,但没有抽样器游标。为什么不能声称精确恢复?写出最小可复现失败。

提示 1
模型状态相同不保证下一批输入相同,抽样器还停在哪一格?
提示 2
写一个固定排列与保存游标,分别列正确下一批及从头重建所得批,再比较更新。
对照参考解答
用六窗口排列[3,0,5,1,4,2],batch4,保存发生在第一批后。正确下一批为[4,2]。重建抽样器从零开始会重复前四个窗口,即使所有模型状态相同也有不同输入梯度。保存并恢复窗口数、batch大小、排列、游标、epoch与独立generator状态,再比较下一批和下一步参数,才覆盖此故障。

TRY IT YOURSELF

练习三:定义收据边界

同CPU环境恢复逐值相等,能否宣称换GPU和新版框架也逐位一致?给出足够而不过度的结论。

提示 1
先限定已验收的是哪套设备、版本、线程与随机实现。
提示 2
将“同环境逐值相等”和“跨设备容差比较”分成两个命题,未运行的后者不能从前者外推。
对照参考解答
不能。浮点规约顺序、内核与随机实现可能改变。足够结论是记录Python/torch版本、CPU线程数、确定性设置、源码与数据身份,并声明在该环境中下一步参数、状态、loss和RNG逐值相等。跨设备应另设容差与协议重新验收;当前收据不覆盖该范围,也不能把未跑结果写为通过。

CHECK YOUR UNDERSTANDING

检验理解

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

1. 为什么每步先zero_grad?

查看解析梯度默认可累积,项目并未采用梯度累积协议。
2. AdamW恢复不能遗漏哪个状态?

查看解析下一步的偏差修正和自适应步幅依赖这些历史状态。
3. 恢复全局RNG应放在何时?

查看解析构造会推进RNG;应最后载入保存的随机状态。
4. 4585 targets_seen能说明什么?

查看解析它是更新预算,不是独特数据量或模型容量。

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