尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Adam优化器完全指南:原理、调参与AdamW对比

Adam优化器完全指南:原理、调参与AdamW对比 今天想把这个话题彻底讲明白。Adam优化器大概是深度学习里最常用、也最容易被当成“黑盒”的组件之一。不管是PyTorch里的torch.optim.Adam还是TensorFlow里的老牌实现一行代码就能把优化器装上训练跑起来效果还不赖。但真到了换模型、调超参、损失不收敛的时候很多人会发现自己对它的了解只停留在“能用就行”的程度出了问题根本不知道从哪里下手。这篇文章我会从梯度下降最朴素的想法开始讲一步步带出动量法、RMSProp再把Adam的每一个公式拆开揉碎聊清楚它为什么有效、有什么毛病、和AdamW到底差在哪。最后还会给出一套我在实际项目里反复验证过的选型和调参建议。无论你是刚开始接触深度学习的初学者还是已经训练过不少模型但一直没深入看过优化器内部原理的同学这篇都值得花十几分钟读一遍。1. 优化器的核心问题SGD、动量法与RMSProp为什么不够用1.1 下山比喻里的梯度下降步长和方向两个坑先把目标函数想象成一片连绵的山地我们手里拿着一个球放在山上某个位置想让球自己滚到最低点。神经网络训练要找的那组最优参数就是最低点对应的坐标。梯度是什么呢梯度是当前山坡最陡的上升方向把梯度的方向取反就是最陡的下降方向。于是最朴素的想法就是每次沿着负梯度方向走一小步不断重复直到站在低谷里。写成公式就是θ - θ - lr * g这里的g是损失对参数θ的梯度lr是学习率控制每步迈多远。这个方法叫随机梯度下降也就是常说的SGD。它足够简单问题也足够明显如果loss曲面的形状不是一个规则的碗而是一条狭长峡谷那么梯度方向往往不是指向谷底而是斜着切向峡谷两侧。结果就是球在山壁上左右横跳明明在往山谷移动却要走很多冤枉路。另一方面学习率是个“一步定生死”的值设太大可能直接越过谷底甚至loss冲到爆炸设太小又会在平原上磨蹭半天。所以我们需要对更新规则做改进这也是后面所有优化器存在的意义。1.2 动量法给梯度更新加一份“惯性”第一种重要改进就是动量法。物理上一个球从山坡滚下来遇到小坑不会立刻停下来因为之前的惯性会让它冲过去更新网络时也希望梯度方向能保留一些历史信息。动量法的更新公式很简单m β * m (1 - β) * g θ θ - lr * m这里的m可以理解成过去一段时间的平均梯度方向β通常取0.9。0.9意味着什么呢如果按迭代次数展开第t步的历史梯度在m里占的权重会以指数速度衰减大致相当于最近10步左右的梯度在做加权平均。这样当某一步梯度突然反向时m不会立刻跟着掉头而是先小幅摆动于是峡谷两侧的震荡被明显抑制而当某个方向持续有梯度时m会越滚越大相当于在不断加速。动量法很有效但它管的是“方向怎么走”没有回答“每一步该走多远”。在一个参数上梯度一直很大说明这个方向可能已经接近陡坡在另一个参数上梯度很小可能是平坦区域反而应该多走一点。如果所有参数共用同一个学习率高频特征对应的大梯度参数会被反复大改而稀疏特征对应的小梯度参数几乎一直原地踏步。这个问题是RMSProp这类自适应学习率方法要解决的。1.3 RMSProp每个参数都该有自己的步长RMSProp的核心思想是给每个参数单独维护一个二阶统计量用来估计这个参数在当前状态下梯度平方的平均水平。用v来表示v β2 * v (1 - β2) * g² θ θ - lr * g / (sqrt(v) ε)这里g²是逐元素平方。直觉很直接如果一个参数的梯度经常很大它的v就会很大分到的有效学习率lr/sqrt(v)就变小如果梯度一直很小v就小有效步长变大。于是RMSProp相当于给每个维度自适应地分配了不同步长尤其适合处理特征稀疏、梯度尺度差异明显的场景。但RMSProp只有步长的自适应没有历史方向的管理梯度噪声大的时候它仍然缺乏“惯性”来平滑。1.4 Adam诞生把动量和自适应步长合二为一Adam的全称是Adaptive Moment Estimation在一篇2014年底的论文里被正式提出之后迅速成为深度学习训练中最常用的优化器之一。它的核心想法很简单同时保留动量法里的一阶矩估计m以及RMSProp里对梯度平方的二阶矩估计v。默认参数lr0.001, β10.9, β20.999, ε1e-8在很多任务上几乎不用调就能得到一个还可以的结果这在当时是很了不起的体验。为什么Adam这么省心因为它对梯度的尺度不敏感对学习率的选择也相对宽容。梯度大分母也大更新量被自动压回来梯度小分母小更新量也还过得去。这就让训练过程少了很多“学习率设错直接跑飞”的崩溃现场。不过要真正理解Adam只记住“自适应学习率动量”还不够它的偏差校正、更新形式、与权重衰减的交互方式每一个细节都会影响最终效果。接下来我逐项拆解。2. 深入Adam公式从伪代码到手算一个例子2.1 逐行拆解Adam更新规则为了不绕弯子先直接放出Adam的伪代码后面所有讨论都围绕它展开# Adam伪代码PyTorch风格 m 0 v 0 t 0 beta1 0.9 beta2 0.999 lr 0.001 epsilon 1e-8 for step in range(T): g compute_gradient(param) t 1 m beta1 * m (1 - beta1) * g v beta2 * v (1 - beta2) * g * g m_hat m / (1 - beta1 ** t) v_hat v / (1 - beta2 ** t) param - lr * m_hat / (sqrt(v_hat) epsilon)m是梯度的指数滑动平均相当于带遗忘因子的动量v是梯度平方的指数滑动平均相当于对梯度幅度的估计。β1和β2是两个衰减率分别控制这两段“记忆”的长度。后续最容易被忽略的是计算m_hat与v_hat这一步它把原始m和v做了偏差校正校正完后再做参数更新。更新式里的epsilon主要是为了防止除零但对更新量也有实际影响后面我会细说。为什么用滑动平均而不是把所有历史梯度存下来求平均因为训练过程中梯度是流式的每次只能看到当前batch不可能存下全部历史。指数滑动平均的计算代价只有常数却能在很长的时间窗口内保留历史信息。你可以把它理解成一个对过去梯度越来越模糊的“短时记忆”参数β越接近1记忆越长对噪声越不敏感但反应也越迟钝。这也是Adam和SGD最底层的一个不同它不是在“用当前梯度”而是在“用历史梯度的综合信息”。2.2 偏差校正冷启动时必须做的补偿偏差校正bias correction这一项很多介绍资料是一笔带过的但它其实直接关系到训练初期的稳定性。假设β20.999第一步训练时v被初始化为0更新一次后v 0.999 * 0 0.001 * g² 0.001 * g²这和真实的E[g²]差了一千倍。如果不做校正第一步更新时分母就极小导致更新量被异常放大训练初期会出现一个明显的步长尖峰。m也存在类似问题只是β10.9偏差比例相对小一些但同样会影响早期更新。校正的方法很简单把m和v分别除以(1 - β1^t)和(1 - β2^t)其中t是当前迭代步数。当t1时v_hat v / (1 - β2)正好把刚才的0.001系数抵消掉当t很大时分母趋向1校正就变得可有可无。这个校正解决的是冷启动问题类似于一辆车刚起步时仪表盘的里程数据还不准你需要手动做一次校准。如果你发现训练前几百步loss出现异常尖峰第一个要怀疑的地方就是这里。2.3 手算一遍Adam更新和SGD对比更有感觉光看公式还是容易飘我拿一个极简例子手算一遍。假设目标函数J(θ)θ²初始θ10梯度g2θ20。用Adam的默认参数学习率为了方便观察取lr0.1β10.9β20.999ε1e-8。第一轮计算t1 m 0.9*0 0.1*20 2 v 0.999*0 0.001*400 0.4 m_hat 2 / (1 - 0.9^1) 20 v_hat 0.4 / (1 - 0.999^1) 400 更新量 0.1 * 20 / (sqrt(400) 1e-8) ≈ 0.1 * 20 / 20 0.1 θ 10 - 0.1 9.9如果同一时刻用朴素SGD同样是lr0.1更新量是0.1*202θ一下子会从10跳到8。Adam看起来走得很慢但它其实在做一件很重要的事把更新量从“梯度大小”中解放出来。因为m_hat大约和sqrt(v_hat)同阶两者的比值通常落在有限范围内所以Adam每一步的有效步长基本由学习率主导而不是被梯度的大小支配。这在梯度经常突变的任务里很关键可以防止单次大梯度把参数撞飞。第二步之后m和v进入稳定状态更新量会保持在一个相对均匀的水平因此Adam收敛曲线通常比SGD更平滑。2.4 PyTorch实现里的隐藏细节eps、amsgrad与状态恢复PyTorch里创建Adam是一行代码的事但它背后有不少细节值得注意。torch.optim.Adam的默认参数是lr0.001, betas(0.9, 0.999), eps1e-8, weight_decay0。这里的weight_decay默认走的是L2正则路线它会把λθ加到梯度里这和后面要讲的AdamW有本质区别。先说eps。默认1e-8在float32精度下通常够用但在混合精度训练时较小的梯度平方统计量很容易被精度截断我习惯把eps调到1e-6或1e-7能减少很多莫名其妙的数值问题。另一个常见参数是amsgrad开启后算法会保留历史二阶矩的最大值避免某些情况下v变小后步长重新变大。这个变体对部分任务有稳定效果但会额外占用一份和模型参数同尺寸的内存不是默认开启也无妨。还有一个容易吃亏的点保存checkpoint时一定要把优化器的state_dict一起存下来。优化器state里保存着每个参数的m和v如果只存模型权重恢复训练后优化器状态全部清零学习率调度虽然从上次继续但动量历史全部丢失前期效果会出现明显回退。3. Adam超参数调试四种参数各自管什么3.1 一张表看懂lr、β1、β2、ε的角色Adam一共有4个主要超参数很多人只看过默认值却不清楚每个到底在控制什么。我把他们整理成一张表超参数默认值控制内容常见调整方向lr0.001全局步长基准任务差异最大优先调整β10.9一阶动量记忆长度GAN/对抗任务可调小到0.5β20.999二阶矩统计的记忆长度训练后期不稳定时调小到0.99或0.98ε1e-8防除零同时限制步长上界混合精度下可调到1e-6~1e-7β1控制的是“方向惯性”。0.9意味着模型会参考过去约10步的梯度方向再决定当前怎么走。如果任务里梯度方向变化很快比如GAN的对抗训练太长的记忆会让更新显得迟钝所以很多人会把β1调到0.5。β2控制的是“步长自适应的反应速度”。0.999会让二阶矩统计对当前梯度的变化非常不敏感适合梯度相对稳定的任务如果训练中经常出现突然的loss尖峰说明二阶矩响应太慢这时把β2降到0.99甚至0.98往往能立刻缓解。3.2 学习率和warmup我实际项目里怎么定大多数深度学习框架里Adam默认lr0.001是论文作者在多个任务上试出来的比较折中的值。但折中意味着在很多任务上不够好小batch时梯度噪声大lr往往要调小一点大batch时可以在早期用较大lr。你直接把lr从0.001改成0.003常常会发现loss先掉得很快然后开始震荡甚至发散这就是步长超过曲面允许范围了。我自己训练Transformer类模型时有个习惯先用warmup把lr线性增加到目标值再用cosine衰减到接近0。warmup在Adam早期特别重要因为这时候二阶统计量还没形成稳定估计偏差校正虽然缓解了冷启动问题但一上来就把lr拉满风险依旧不小。NLP微调场景我通常用2e-5到5e-5之间的lrCV大batch预训练则可能用到1e-3。一个实用经验是在正式训练前先做一个小规模试跑观察前500步的loss变化如果在warmup阶段就出现抖动说明lr偏高需要降一个量级再试。3.3 调参最容易踩的几个坑不看梯度norm等于盲调以下几个坑都是我亲眼见过或者自己踩过的写出来给大家避雷。第一一上来就调β而不是调lr。很多同学训练效果不好第一反应是“把β1改成0.8β2改成0.95”。实际上β对整体收敛速度的影响远不如lr直接除非你已经确定学习率合适否则先调lr保持β默认才是最高效的路线。第二把weight_decay和lr一起盲目放大。weight decay的合适范围通常很小在PyTorch的Adam里默认是0如果你从AdamW切回Adam一定要知道它俩的weight_decay语义不同后面第4节会详细讲。第三不看梯度norm只看loss曲线。loss曲线是宏观指标梯度norm才是更早暴露问题的信号。我习惯每N步打印一次梯度norm如果它超过1e2甚至1e3大概率是数据里有异常样本或lr太大。这样能在loss还没完全崩掉之前就发现异常。4. 从Adam到AdamW泛化差距和权重衰减的解耦4.1 为什么SGDMomentum在CV上经常反超AdamAdam并不是万能的最典型的争议就是泛化差距。在ImageNet分类这类任务上同样迭代次数和训练技巧下用SGDMomentum训练出来的ResNet误差经常比Adam低零点几个点。一个比较流行的解释是Adam的自适应步长让它在损失曲面中更容易找到尖锐的极小值而SGD的权重更新轨迹更偏向于找到平坦的极小值尖锐极小值的泛化性一般更差。这个解释在文献里有支持也有反驳你不一定要全信但“SGD最终精度更高”这个经验规律在很多CV任务里能复现。但这里要强调这不代表Adam“不行”。在一个新任务上Adam/AdamW通常能在更短的训练时间内得到不错的结果这对快速验证idea非常友好。如果项目最终目标就是刷榜再把优化器换成SGDMomentum做一轮精调往往是最划算的组合。我自己做比赛时的常规操作是先用AdamW跑通baseline后面有时间再补一组SGD的对比实验。4.2 Adam与AdamW差别只有一行效果却差很多先说清一个概念weight decay权重衰减和L2正则在SGD里几乎是一回事。SGD更新公式若写成θ - θ - lr*(g λθ)展开后就是θ - (1-lrλ)θ - lr*g可以看到它等价于每次先按系数(1-lrλ)缩一下权重。但在Adam里如果把λθ加到梯度g上这个λθ还会进入m和v的统计量进而影响每个参数的自适应步长。于是正则惩罚被扭成了对梯度历史的干扰不同参数受到的惩罚变得不均衡。AdamW的修改非常直接把weight decay从梯度的统计过程中解耦出来在最后一步更新时单独减去lr*λθ。对比一下两种更新Adam: g_t ∇L(θ_{t-1}) λ θ_{t-1} m_t β1 m_{t-1} (1-β1) g_t v_t β2 v_{t-1} (1-β2) g_t^2 θ_t θ_{t-1} - lr * m̂_t / (√v̂_t ε) AdamW: g_t ∇L(θ_{t-1}) m_t β1 m_{t-1} (1-β1) g_t v_t β2 v_{t-1} (1-β2) g_t^2 θ_t θ_{t-1} - lr * m̂_t / (√v̂_t ε) - lr * λ * θ_{t-1}差别确实只有一行但这一行避免了对自适应统计量的污染。公式里lr * λ * θ的衰减项不受到Adam步长归一化影响每个参数都能得到一致且可控的正则强度这在Transformer、BERT这类大模型上尤其重要。这也是为什么“Adam和AdamW的区别”会被大家反复拿出来问因为从代码上看只是torch.optim.Adam换成torch.optim.AdamW实际效果却可能天差地别。如果你现在要开新项目不要犹豫直接用AdamWweight_decay从0.01开始试。4.3 动量法、Adam、AdamW之外LAMB、Lion等现代优化器除了动量法、Adam、AdamW现代优化器家族里还有几个值得一提的成员。LAMB在AdamW基础上加了一个“trust ratio”让不同层可以使用不同大小的更新量主要解决大batch训练时每个参数更新尺度差异过大的问题BERT大规模预训练经常用它batch size可以堆到几万。Adafactor把二阶统计量做分解显著节省显存适合超长序列或超大模型。Lion则完全换了一种思路用一种符号函数来控制更新方向在部分任务上收敛极快。这些优化器各有各的适用场景但都有一个共同点它们都在围绕“动量、二阶矩、权重衰减、更新尺度”这几件事做文章。把Adam和AdamW理解透之后再看这些变体会轻松很多因为它们本质上是在回答同一个问题——如何让每一步参数更新既稳、又快、又不牺牲泛化能力。5. 工程项目里的优化器选型与排查速查表5.1 面对新任务我按这个顺序选优化器如果现在你手上有一个全新的模型和数据集我的建议是从AdamW开始而不是纠结“哪个优化器更好”。AdamW默认参数在大多数场景下都能稳定收敛先用它跑通pipeline确认数据、模型、loss都没有问题后再根据情况做替换。下面是我常用的几类场景推荐表任务类型推荐优化器默认配置备注NLP / TransformerAdamWlr2e-5~5e-5, wd0.01warmup linear/cosine scheduleGANAdamlr2e-4, β10.5判别器和生成器可分别设lr扩散模型AdamWlr1e-4, wd0.01长时间训练更稳定CV分类/检测SGDMomentum或AdamWlr0.01~0.1 / 1e-3追求精度时SGD往往更稳大batch预训练LAMBlr按batch线性缩放需要配合长warmup注意这张表只是参考起点不代表最优。真正负责任的选型方式是在小规模数据上快速跑几组对比实验用同样的epoch数看验证集表现而不是听别人说“某种优化器好”就直接搬。5.2 稳定训练的工程细节裁剪、混合精度、EMA与checkpoint优化器选对了工程细节跟不上一样会翻车。第一个细节是梯度裁剪NLP训练里几乎必备通常设max_grad_norm1.0左右。注意Adam和AdamW对梯度裁剪的敏感度不同因为裁剪发生在进入优化器之前如果你把裁剪阈值设得太小自适应统计量会变得失真设太大又起不到防尖峰的作用需要观察实际梯度的分布再定。第二个细节是混合精度。AMP训练时梯度有小概率向下溢出eps如果还是1e-8对二阶矩统计影响不大但对浮点精度的余量是个隐患。我一般会把eps提高到1e-6损失几乎不变但NaN概率明显下降。第三个容易被忽视的细节是EMA。指数移动平均模型往往比当前训练权重更平滑、泛化更好。这个和优化器没有直接关系但在同样配置下通常能带来0.1~0.3个点的提升属于无痛涨点技巧。最后再强调一次checkpointoptimizer.state_dict一定要和model.state_dict一起保存恢复训练时也一起加载。5.3 常见问题速查表不收敛、NaN、验证集掉点怎么查我把实际工程中经常遇到的优化器相关问题整理成一张速查表方便大家排查现象可能原因排查方向loss完全不动lr太小或梯度为0打印梯度norm调大lr一个量级试试loss震荡剧烈lr太大或batch噪声大降低lr增加batch size开启warmup训练初期loss直接NaNeps太小或梯度爆炸增大eps到1e-6检查输入数据是否有NaN中后期loss突然飙升二阶矩统计对梯度变化响应慢把β2调到0.99或0.98配合梯度裁剪验证集精度不如预期正则太弱或过拟合增大weight_decay加入EMA延长训练从SGD换Adam后效果变差weight decay语义不同或lr不匹配改用AdamW重设lr别沿用SGD的lr恢复训练后loss升高optimizer state未加载或schedule错位检查checkpoint是否包含optimizer state_dict这张表只能覆盖一部分问题实际项目里遇到怪问题时我的通用排查路线是先看数据是否正常再看梯度norm是否合理最后才怀疑优化器参数。顺序反了会浪费很多时间。最后说点个人体会。接触深度学习这几年我见过太多人把优化器当成一个可以随意更换的“黑盒旋钮”效果不好就换一个却从来不关心它内部更新量的数量级。实际上只要在训练第一步把梯度norm、更新量的最大值、一阶动量和二阶动量的数值打出来看一次绝大多数训练崩溃的原因都能立刻查到。我会在每次新任务开跑前先用很小的模型、很小的数据量跑通流程确认每个模块数值正常后再放大规模和训练时间。这个过程看起来多花了时间实际省下的返工时间远不止这些。如果你现在正被某个训练问题卡住我的建议是先别急着换优化器把当前优化器的状态打印出来看看m、v、更新量的大小是否在一个合理区间再决定动哪个超参数。Adam也好AdamW也好它们不是魔法只是一套设计得相当精密的更新规则你理解得越细它在你手里就越听话。
返回列表