读完这篇,你能做什么
能手算两步更新,解释状态保存和偏差校正的作用,并实现明确处理边界输入的标量 AdamW。
学习理由、准备要求与知识点
为什么学它
优化器决定参数怎样改变。知道它保存了什么状态,才能解释恢复训练、梯度缺失和权重衰减时的行为。
先读这些内容
知识点清单
- 全批与小批随机梯度
- 学习率与梯度噪声
- 动量历史缓冲
- Adam 一二阶矩
- 零初始化偏差校正
- epsilon 数值尺度
AdamW解耦衰减- 梯度缺失与零梯度
- 优化器状态恢复
运行前先确认Java 17;Python 3.14 标准库手工 CPU 更新,无需安装训练框架。源码对照 PyTorch v2.9.1;算法是标量 AdamW 教学实现,未实现完整框架优化器。
01先弄懂它是什么
每一步训练都看完全部数据,可能太慢。小批随机梯度的方法只用一部分样本估计当前梯度,再据此调整参数;随机性来自抽样或打乱顺序。这样算得省一些,但不保证每一步都离最优解更近。比较训练方法时,要同时看学习率、批次大小、样本是否相似,以及损失是求和还是取平均,不能只看优化器的名字。
如果每一步只听当前梯度的意见,更新方向可能来回摇摆。动量会保留一些历史方向:本篇先算 v_t=mu*v_(t-1)+g_t,再用 w_t=w_(t-1)-lr*v_t 更新,初始缓冲为零。mu 在零到一之间,g_t 是当前梯度。连续同向的梯度会积累;反向梯度先抵消一部分旧方向。因此即使这一步梯度为零,之前积累的动量仍可能让参数移动。
Adam 维护一阶矩 m 与未中心化二阶矩 v:m=beta1*m+(1-beta1)*g,v=beta2*v+(1-beta2)*g^2。这里 v 是平方梯度的指数平均,不是减去均值平方后的方差。将方向历史除以尺度历史后,各参数得到不同有效步长,但这不是自动保证收敛或泛化更好的魔法。
文中这几个词,先认一认
- 动量
- 更新参数时保留的一部分历史梯度,让当前方向同时参考前面几步。
- 一阶矩
- 对过去的梯度值做指数移动平均:越近的梯度通常权重越大。
- 二阶矩
- 把当前梯度先平方,再与之前保存的平均值按固定比例混合,反复记录梯度数值的大小。这里没有先减去梯度均值再平方,所以不能把它当作方差。
- 解耦衰减
- 直接按比例缩小参数,且不把这一步混进梯度的一阶矩、二阶矩计算。
02看看它怎么工作
因为 m、v 从零初始化,最初几步会偏向零。Adam 用 mhat=m/(1-beta1^t)、vhat=v/(1-beta2^t) 校正,t 从第一次有效更新的一开始计数。参数更新为 w-lr*mhat/(sqrt(vhat)+eps)。eps 位于平方根之外,本篇明确采用这一约定;把它放入平方根会改变数值。
若起始 w=1、g=2、lr=0.1、beta1=0.9、beta2=0.999,第一步 m=0.2、v=0.004,校正后分别为二与四,因此更新约为零点一。第二步即使梯度改变,也不能只用当前梯度重算为全新优化器;状态包含过去信息,重建对象会改变后续轨迹。
在普通梯度下降中,若系数约定对应,把 L2 惩罚的梯度加到 g,可以等价为把参数按比例缩小一次。但 Adam 会把这部分惩罚一起放进一阶、二阶矩和归一化计算,通常就不再等价。AdamW 把两件事拆开:先按 w*(1-lr*weight_decay) 做衰减,再使用只根据任务梯度得到的自适应更新。
没有梯度,与梯度算出来正好为零,要分开处理。缺失可能说明参数没参与这次反向路径,本篇会连同该参数的更新和步数一起跳过。零梯度则是一个有效计算结果,仍推进步数、衰减旧的一阶和二阶矩,也可能发生权重衰减。使用条件分支或冻结参数时,要核对框架具体怎样区分这两种情况。
优化器超参数不是独立于目标函数的常量。把平均损失换为总损失、改变梯度累积方式或批次规模,都可能改变有效更新。Adam 对缩放存在一定适应性,但 eps、权重衰减、数值精度和训练动态会破坏简单等价;调整设置后仍要检查梯度范数与参数变化。
先跑通一个最小例子
仅需 Python 3.14 标准库。保存为 experiment.py 后执行 python experiment.py。字典存放一个参数的状态,手工追踪两步 AdamW,并比较零梯度下的普通 SGD 与动量行为。
import math
def adamw(s,g,lr=.1,b1=.9,b2=.999,eps=1e-8,decay=0.):
if g is None:
return dict(s) # 缺失梯度不推进该参数状态。
t=s["t"]+1
m=b1*s["m"]+(1-b1)*g
v=b2*s["v"]+(1-b2)*g*g
mh=m/(1-b1**t); vh=v/(1-b2**t)
w=s["w"]*(1-lr*decay)-lr*mh/(math.sqrt(vh)+eps)
return dict(w=w,m=m,v=v,t=t)
s=dict(w=1.,m=0.,v=0.,t=0)
first=adamw(s,2.)
assert math.isclose(first["w"],.9,abs_tol=1e-8)
assert math.isclose(first["m"],.2)
assert math.isclose(first["v"],.004)
second=adamw(first,2.)
assert math.isclose(second["w"],.8,abs_tol=1e-8)
assert adamw(first,None)==first
zero=adamw(dict(w=2.,m=0.,v=0.,t=0),0.,decay=.1)
assert math.isclose(zero["w"],1.98) # 零梯度仍可有解耦权重衰减。
momentum=2.
momentum=.9*momentum+0.
assert math.isclose(momentum,1.8) # 当前梯度为零,方向缓冲仍不为零。
sgd_w=1.-.1*0.
assert sgd_w==1.
print("PASS 8; first =",first,"second =",second)
核对运行结果打印 PASS 8;两次相同梯度使参数约从 1 到 0.9 再到 0.8,零梯度衰减例从 2 到 1.98。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart LR
A["当前梯度 g"] --> B["更新一阶矩 m"]
A --> C["更新二阶矩 v"]
B --> D["按步数做偏差校正"]
C --> D
E["旧参数直接衰减"] --> F["减去自适应更新"]
D --> F
F --> G["保存参数和全部状态"]按顺序看,每一步做了什么
- 明确损失尺度与当前梯度。
- 区分梯度缺失和有效零梯度。
- 更新历史矩与有效步数。
- 做偏差校正并应用解耦衰减。
- 检查数值有限并完整保存状态。
03对照实现代码
PyTorch v2.9.1;torch/optim/adam.py · _single_tensor_adam
以下为原创精简教学实现,不是原样库源码;只演示本文说明的机制,省略真实实现的优化、调度和完整兼容边界。
def corrected_moments(m,v,g,t,beta1,beta2):
# 原创数学模型:先更新原始矩,再做初始化偏差校正。
m=beta1*m+(1-beta1)*g
v=beta2*v+(1-beta2)*g*g
return m,v,m/(1-beta1**t),v/(1-beta2**t)
# t 必须是本次有效更新后的正步数;未包含框架设备分派。
固定版本 _single_tensor_adam 对传入参数依次更新步数和矩状态。普通非 capturable 分支通过 bias_correction1 与 bias_correction2 计算校正,并以 addcdiv_ 完成参数更新;不同分支可能重排运算,但应先对齐数学含义再比较代码形状。
当 decoupled_weight_decay 为真,源码直接将参数乘以 1-lr*weight_decay;否则把参数比例项加到梯度,后续矩估计也受该项影响。这给出了 AdamW 与耦合惩罚的具体区别,而不只是两种名字或默认系数不同。
是否进入这个内核由外层流程决定。Adam._init_group 只收集 p.grad 非 None 的参数,并为首次参与的参数初始化 step、exp_avg、exp_avg_sq。因而缺失梯度的跳过语义需要连同调用者阅读,不能仅观察核心数学循环。
真实文件还包含复数、AMSGrad、可微优化器、捕获、多张量及融合等路径。原创标量模型只对应普通实数的一种数学分支,未声称复现这些设备行为或逐位数值结果。动量公式也有阻尼与初始化变体,使用库时应核对文档约定。
代码先调用谁,再调用谁
Adam.step遍历参数组_init_group收集有梯度参数并初始化状态- 函数分派选择单张量等路径
- 更新步数、衰减与矩估计
- 校正后写回参数
04动手写一个例子
状态明确的标量 AdamW
使用有限实数状态和合法超参数进行一次标量 AdamW 更新;null 梯度跳过,零梯度照常更新。返回新状态而不部分修改旧状态。
- 输入 1
w=1,m=v=0,t=0,g=2,lr=.1- 输出 1
w≈.9,t=1
- 输入 2
w=2,g=0,decay=.1,lr=.1- 输出 2
w=1.98
将参数、一阶矩、二阶矩和步数封装为不可变状态,把更新结果计算完成后统一返回。入口先验证超参数和状态,防止负二阶矩、非法 beta 或非有限值进入平方根;步数加一使用精确整数溢出检查。
有效梯度触发一次矩更新与偏差校正。对 null 则原样返回状态,刻意与零梯度区分。这个选择模仿所核验框架分支的外层筛选语义,仍然只是本篇标量契约,不代表每一种优化器都使用相同跳过规则。
计算 g*g 及平方根分母时仍可能超出 double 表达范围,因此检查新矩、分母与最终参数。测试手算首步、连续两步、缺失、零梯度衰减与非法输入;只通过这些例子不能推导任意优化问题的收敛性。
为什么这样写是对的每个返回状态的 t 表示该参数已处理的有效梯度次数,m、v 保留相应次数的指数历史,v 非负。
每个参数使用固定次数的标量运算并保存两个矩与步数;这里不计产生梯度的模型前向反向成本。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
public class Main {
record State(double w,double m,double v,long t){}
record Config(double lr,double b1,double b2,double eps,double decay){}
static boolean finite(double... a){for(double v:a)if(!Double.isFinite(v))return false;return true;}
static State step(State s,Double g,Config h){
if(s==null||h==null||!finite(s.w,s.m,s.v,h.lr,h.b1,h.b2,h.eps,h.decay)||s.v<0||s.t<0||h.lr<0||h.b1<0||h.b1>=1||h.b2<0||h.b2>=1||h.eps<=0||h.decay<0)throw new IllegalArgumentException("state or hyperparameter");
if(g==null)return s; // 缺失梯度不等于数值零。
if(!Double.isFinite(g))throw new IllegalArgumentException("gradient");
long t=Math.addExact(s.t,1);
double m=h.b1*s.m+(1-h.b1)*g,v=h.b2*s.v+(1-h.b2)*g*g;
double mh=m/(1-Math.pow(h.b1,t)),vh=v/(1-Math.pow(h.b2,t)); // 步数从一开始校正。
double denominator=Math.sqrt(vh)+h.eps;
double w=s.w*(1-h.lr*h.decay)-h.lr*mh/denominator; // 衰减不进入矩估计。
if(!finite(m,v,mh,vh,denominator,w))throw new ArithmeticException("range");
return new State(w,m,v,t);
}
static void near(double a,double b){if(!Double.isFinite(a)||Math.abs(a-b)>1e-8)throw new AssertionError();}
static void bad(Runnable r){try{r.run();}catch(IllegalArgumentException e){return;}throw new AssertionError();}
public static void main(String[] args){
Config h=new Config(.1,.9,.999,1e-8,0);State start=new State(1,0,0,0);
State first=step(start,2.,h);near(first.w,.9);near(first.m,.2);near(first.v,.004);
State second=step(first,2.,h);near(second.w,.8);if(second.t!=2)throw new AssertionError();
if(step(first,null,h)!=first)throw new AssertionError();
near(step(new State(2,0,0,0),0.,new Config(.1,.9,.999,1e-8,.1)).w,1.98);
bad(()->step(start,1.,new Config(.1,1,.999,1e-8,0)));
bad(()->step(start,Double.NaN,h));
System.out.println("PASS 6");
}
}这些特殊情况也要试一试
null跳过而零梯度可能继续移动。- beta 不能等于一,否则校正分母为零。
- 平方梯度可能在有限输入下溢出或上溢。
- 恢复训练必须同时恢复矩与步数。
进阶选读 · 优化器状态与断点恢复
场景训练中断后希望延续同一优化轨迹。
做法保存模型、优化器、学习率计划、随机数与数据进度;恢复后比较下一步梯度和参数变化,而非只看模型权重能加载。
复杂度Adam 类状态存储随参数量 O(d) 增长;检查点 I/O 也随规模增加。
好处与代价仅保存权重适合推理,不能冒称等价恢复训练;跨版本与设备还需核对兼容性。
05项目里怎么用
优化器的选择应通过同一数据边界、评价指标与预算比较。SGD 加动量状态较少,Adam 类方法多保存两组矩;大模型中这些状态会形成显著内存开销。某方法在一个小实验上下降更快,并不能推出所有任务都更好,也不能忽略调参成本。
记录学习率、beta、eps、权重衰减和参数组规则,并明确哪些参数应用惩罚。实践中可能对偏置或归一化参数使用不同规则,但这应是可解释的配置,不能依靠名称猜测。遇到参数不动时,先检查梯度是否存在、更新是否执行,再分析矩和实际步长。
出现 NaN 或无穷时,先找最早在哪里发生,别只把 eps 调大来遮住症状。问题可能先出在输入、损失、梯度平方,也可能出在参数更新,记录异常位置和当时数值尺度更便于排查。本篇标量测试只核对更新公式与状态处理,没有验证大型模型、低精度、GPU 融合内核或分布式训练的一致性。
容易踩的坑
- 把二阶矩直接称为梯度方差。
- 重建优化器后认为仅恢复权重即可继续原轨迹。
- 混淆耦合 L2 惩罚和
AdamW解耦衰减。 - 把 None 梯度与零梯度视为同一状态。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| SGD | 状态少、更新容易解释 | 可能对曲率和学习率更敏感 | 清楚基线与资源受限场景 |
| 动量 SGD | 保留方向历史、可减轻部分振荡 | 需要缓冲与动量调节 | 成熟训练基线 |
AdamW | 逐参数尺度适应,衰减定义清楚 | 额外状态较多,仍需调参 | 常见神经网络训练 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01偏差校正为何依赖步数?
展开参考答案与推导
零初始化使初期指数平均偏小,除以一减 beta 的 t 次方校正这种初始化效应。t 应对应该参数的有效更新次数,而非随意使用全局样本编号。
02二阶矩是方差吗?
展开参考答案与推导
不是。这里平均 g 的平方,没有减去梯度均值的平方;称为未中心化二阶矩更准确。
03AdamW 为什么不等同于把 L2 加进 Adam 梯度?
展开参考答案与推导
后者让惩罚影响历史矩和自适应分母,前者直接缩放参数且矩只跟踪任务梯度,通常得到不同轨迹。
换个情况,还会处理吗
01梯度为零,参数还在动,是否一定有错?
展开参考答案与推导
不一定。已有动量或一阶矩可能仍非零,权重衰减也可改变参数;应查看完整状态和更新公式。
02恢复权重后损失曲线不同,先查什么?
展开参考答案与推导
检查优化器矩、步数、学习率计划以及数据和随机状态是否恢复。权重相同并不决定下一步更新相同。
03小批次变大后能直接复用旧学习率吗?
展开参考答案与推导
可作为起点但不保证等价。梯度噪声、更新次数和归约尺度可能改变,应对应预算和有效梯度后验证。
写代码试一试
01手算初始一阶矩。
- 输入 / 条件
m=0,g=2,beta1=.9- 输出 / 预期
m=.2,mhat=2
提示 第一次t=1。
展开参考答案与推导
先得到零点二,再除以一减零点九得到二,不能漏掉初始化校正。
02计算纯衰减。
- 输入 / 条件
w=2,lr=.1,decay=.1,g=0,历史矩=0- 输出 / 预期
1.98
提示 参数乘一减lr乘decay。
展开参考答案与推导
衰减因子为零点九九,乘二得到一点九八;这一步仍推进有效步数。
03定义缺失梯度。
- 输入 / 条件
g=null- 输出 / 预期
状态不变
提示 与零梯度区别处理。
展开参考答案与推导
在本篇契约下直接返回旧状态,不推进 t,也不衰减参数。真正框架行为还需核对参数筛选代码。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能手算两步更新,解释状态保存和偏差校正的作用,并实现明确处理边界输入的标量 AdamW。
本篇重点加练
优化器状态会改变后续更新,重点检验参数相同但训练历史不同的情况。
- Adam 一二阶矩
- 优化器状态恢复
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
参数初值 2,梯度序列 [1,-2,0,3],学习率 0.1、动量 0.8。按你明确声明的动量约定记录每步状态和参数。
- 提交内容
- 逐步状态表
- 怎样算完成
- 优化器约定无歧义
- 历史状态完整
练到的知识点全批与小批随机梯度 · 学习率与梯度噪声 · 动量历史缓冲
- 作业 02原理问答
解释 Adam 的一阶矩、二阶矩及校正后统计量各代表什么;比较 epsilon 放在平方根内外的量纲与接口兼容要求。
- 提交内容
- 变量表与公式审阅
- 怎样算完成
- 统计量不混称
- 数值约定影响清楚
练到的知识点Adam 一二阶矩 · 零初始化偏差校正 · epsilon 数值尺度
- 作业 03原理问答
两次训练到达相同参数但具有不同 Adam 一二阶矩,接收相同下一梯度是否必然继续重合?自拟合法状态并写出判定依据。
- 提交内容
- 状态对照与条件论证
- 怎样算完成
- 状态来源或合法范围明确
- 参数身份与优化器身份分开
练到的知识点Adam 一二阶矩 · 优化器状态恢复
综合应用
- 作业 04编程实践
仅用 Python 标准库实现含两个参数的
AdamW,对固定梯度流运行十步,并与独立手算或高精度数值记录对照;保存超参数与状态。- 提交内容
- CPU 实现与逐步误差表
- 怎样算完成
- 衰减与梯度项区分
- 默认参数全部显式
练到的知识点Adam 一二阶矩 · 零初始化偏差校正 · epsilon 数值尺度 · AdamW 解耦衰减
- 作业 05排错实验
交替制造一个参数 grad=None、grad=0、非零梯度的情况,测试保存/恢复后下一步的行为;核对哪些状态被恢复。
- 提交内容
- 最小复现与断点差异表
- 怎样算完成
- 缺失与零梯度分开
- 恢复比较使用同一后续输入
练到的知识点梯度缺失与零梯度 · 优化器状态恢复
- 作业 06面试场景
小批量由 8 改为 32,训练总样本访问次数不变。制定比较学习率、更新次数和衰减累计效应的实验记录格式。
- 提交内容
- 受控对照协议
- 怎样算完成
- 预算口径明确
- 不能归因的变化列明
练到的知识点全批与小批随机梯度 · 学习率与梯度噪声 · AdamW 解耦衰减
- 作业 07编程实践
仅用 Python 标准库运行两参数 Adam:在第六步交换两参数的状态记录而保留权重,然后继续相同四步梯度,审计参数名与状态的关联。
- 提交内容
- CPU 脚本和逐步差异
- 怎样算完成
- 梯度流固定
- 状态关联错误可定位
练到的知识点Adam 一二阶矩 · 优化器状态恢复
- 作业 08设计评审
训练恢复后新增一组参数而保留旧参数。定义新旧参数的优化器状态迁移契约,覆盖缺失步数、重复参数身份和超参数变化。
- 提交内容
- 迁移表与拒绝案例
- 怎样算完成
- 每项状态归属明确
- 不兼容情况有处理规则
练到的知识点优化器状态恢复
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
为长期训练的优化器状态建立检查工具:参数重命名、顺序改变和新增参数时必须发现不兼容。设计输入契约并做三种故障注入。
- 提交内容
- 状态检查器与故障报告
- 怎样算完成
- 参数身份可验证
- 拒绝行为不破坏原断点
练到的知识点动量历史缓冲 · Adam 一二阶矩 · 优化器状态恢复
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 跨域迁移
把优化器视作带记忆的动态系统:能否仅由当前参数与下一梯度唯一预测下一参数?构造两段训练历史并比较与无状态更新器的区别。
- 提交内容
- 历史对照与系统状态图
- 怎样算完成
- 历史确实产生声明状态
- 可预测性结论对应状态集合
练到的知识点Adam 一二阶矩 · 优化器状态恢复
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
PyTorch v2.9.1:Adam 单张量路径与解耦权重衰减 (新标签页)↗
核对单张量矩更新、偏差校正、解耦衰减与外层缺失梯度筛选。
- 深入参考文档英文
PyTorch 2.9 AdamW (新标签页)↗
对照算法公式、参数组和
state_dict相关约定。 - 深入参考文档英文
PyTorch 2.9 SGD (新标签页)↗
阅读动量、阻尼及与其他实现不同的初始化规则。
- 深入参考文档英文
Stanford CS229 Notes (新标签页)↗
回顾批量和随机梯度的目标、学习率与优化基本条件。
- 实践实践指南英文
Distill:带一点历史方向,下降路线会怎样变 (新标签页)↗
先看开头轨迹和 First Steps,再对照动量的两行更新式。保持步长相同,画出 β=0 与 β>0 时第二步会用到哪些旧值;若本机控件能用,再记录一组轨迹变化。首轮跳过特征分解证明和最优收敛率,不把动量解释成永远更快或永不振荡。 作者原文公开可读;若本机无法使用互动控件,可先对照更新式阅读静态图。理论示例是简化函数,不能直接当作真实模型训练速度的结论。