资讯动态

Adam优化器深度解析:原理、偏差修正与工程实践

发布时间:2026/9/17 1:15:25 来源:尧图企业网站定制
《ADAM: A METHOD FOR STOCHASTIC OPTIMIZATION》这篇论文刚出来那阵子我身边不少人觉得它不过是把 Momentum 和 RMSProp 拼在一起没什么新意。但真在自己项目里 Trials and errors 之后才发现这个“拼凑”背后其实藏着非常精巧的动机和设计远不是简单缝合那么简单。这篇论文是 Adam 优化器的原始出处由 Diederik P. Kingma 和 Jimmy Ba 在 2014 年底提交、2015 年正式发表到现在依然是深度学习训练里被用得最广泛的优化算法之一。不管你是刚入门的炼丹新手还是已经被 loss 曲线折磨到麻木的老手把这篇原始文献啃透对你理解深度学习训练底层逻辑、排查训练不收敛的问题、甚至是在论文里复现别人 baseline 的超参数都会有实打实的帮助。这篇文章我会从一个“踩过坑、填过坑”的实践者视角把论文里的数学原理掰开揉碎讲清楚。除了公式推导我会重点聊几个平时大家最容易忽略但影响巨大的细节偏置修正到底修正了什么、Adam 和 AdamW 的本质差异在哪里、学习率应该怎么配、epsilon 设多少合适以及在实际训练中我踩过的那些与 Adam 相关的坑。内容会偏长但基本都是干货你可以随手收藏起来对照着看。1. 为什么 Adam 能成为“默认选择”问题背景与设计动机1.1 从 SGD 的痛点说起在 Adam 出现之前主流的随机优化算法是 SGD随机梯度下降以及它的两个改进分支带 Momentum 的 SGD以及自适应学习率的 AdaGrad、RMSProp。SGD 的思路最简单直接每次迭代沿着当前 mini-batch 的负梯度方向走一步。步子迈大了容易震荡步子迈小了训练慢得让人抓狂。更麻烦的是当 loss 曲面在不同方向上“陡峭程度”差异很大时SGD 会剧烈振荡收敛极慢。就好比你从一个狭长的山谷往下走梯度在某个方向上来回打转实际前进的速度却慢得可怜。Momentum 的思路是想办法“记住”历史梯度方向让更新过程带上惯性从而抑制振荡。RMSProp / AdaGrad 则是根据每个参数的历史梯度大小给每个参数单独适配学习率历史梯度大的参数学习率调小一点历史梯度小的参数学习率调大一点。但这两种路线各有短板。Momentum 虽然能缓解振荡但在不同参数之间学习率的尺度差异依然存在RMSProp 对学习率的自适应做得不错但它的更新方向在很大程度上只考虑梯度大小忽略了梯度的“方向惯性”。我当时做图像分类任务尝试用 RMSProp 训练一个比较深的卷积网络经常遇到 loss 前期下降还行、后期训练集准确率上不去、但 val loss 在某个区间内来回横跳的情况调起参数来非常痛苦。1.2 Adam 的“缝合”并不简单Adam 的全称是 Adaptive Moment Estimation直译过来就是“自适应矩估计”。从名字就能看出论文的起点是“估计梯度的一阶矩均值和二阶矩未中心化的方差”然后用这两个估计量来指导更新。读到论文前几页时我被它的设计动机吸引了它不只是简单地把 Momentum 和 RMSProp 揉在一起而是从“我们需要什么样的梯度统计信息”这个问题出发推导出了一套统一的更新规则。具体来说Adam 维护两个状态变量一阶动量 m_t对历史梯度的指数加权平均相当于带遗忘因子的 Momentum记录的是梯度的“平均方向”。二阶动量 v_t对历史梯度平方的指数加权平均相当于 RMSProp 里的自适应学习率分母记录的是梯度“大小的平均量级”。每一轮迭代先用当前 mini-batch 的梯度更新这两个动量再用它们计算参数的更新量。论文里最核心的公式如下我把每一步都拆开了首先是梯度的一阶矩估计m_t beta1 * m_{t-1} (1 - beta1) * g_t这里的 beta1 一般取 0.9g_t 是当前时刻的梯度。这个式子可以理解成“对过去一段时间的梯度方向做平滑”让参数更新方向不轻易被单个 batch 的噪声带偏。然后是梯度的二阶矩估计论文里用的是未中心化的二阶矩而不是方差v_t beta2 * v_{t-1} (1 - beta2) * g_t^2这里的 beta2 一般取 0.999g_t^2 是梯度各元素的平方。这个式子度量的是“过去一段时间梯度的大小量级”它决定了每个参数各自的学习率缩放系数。如果一个参数的历史梯度一直很大v_t 就大它的有效学习率就会被压小反之如果某个参数一直很“平缓”v_t 就小有效学习率就会放大。很多文章到这里就结束了但真正让 Adam 区别于普通 Momentum RMSProp 组合的是接下来的一步偏差修正。1.3 偏差修正Adam 的灵魂细节你可能会问既然 m_t 和 v_t 都是指数移动平均初始值设为 0刚开始的时候它们会明显偏向 0尤其是 beta2 取 0.999 时早期 v_t 会非常小导致初始步长被异常放大。如果不做修正前几步更新可能大得离谱直接让训练发散。论文给出的修正方式是m_hat_t m_t / (1 - beta1^t)v_hat_t v_t / (1 - beta2^t)这里的 t 是当前迭代步数。之所以这样修正是因为当 t 比较小时分母 1 - beta^t 明显小于 1能把 m_t 和 v_t 放大回“正常量级”。当 t 越来越大时beta^t 趋近于 0修正系数趋近于 1偏差修正的影响就会越来越小。这个细节看起来只是数学上的“小补丁”但实际影响巨大。比如你训练一个 Transformer 或大卷积网络如果不做偏差修正而只是简单地把 Momentum 和 RMSProp 的机制结合前几百步几乎必然会出现 loss 剧烈抖动甚至直接变成 NaN。我在第一次自己手写 Adam 优化器时偷懒没加修正结果训练 GPT-2 规模的模型时loss 前 100 步就冲上了天文数字那个场景至今难忘。所以如果你日后阅读其他优化器论文看到类似的“偏差修正”步骤不要急着跳过——这往往是算法能落地的关键。2. 核心细节与实操要点从公式到工程的每一步2.1 完整更新规则速览为了更好地理解 Adam 的全貌我把论文里 Algorithm 1 的核心步骤整理成一张速查表。假设第 t 步的 mini-batch 梯度为 g_t网络参数为 theta_tAdam 的更新流程如下步骤操作公式备注1计算梯度g_t ∇_theta f_t(theta_{t-1})当前 mini-batch 的损失对参数的梯度2更新有偏一阶矩m_t beta1 * m_{t-1} (1 - beta1) * g_t方向平滑3更新有偏二阶矩v_t beta2 * v_{t-1} (1 - beta2) * g_t^2梯度量级估计4修正一阶矩偏差m_hat_t m_t / (1 - beta1^t)早期修正5修正二阶矩偏差v_hat_t v_t / (1 - beta2^t)早期修正6更新参数theta_t theta_{t-1} - lr * m_hat_t / (sqrt(v_hat_t) eps)有效学习率逐参数自适应注意第 6 步里实际是用“修正后的 m_hat_t 除以修正后的 v_hat_t 的平方根”作为更新方向。这等价于先用二阶矩把梯度归一化到近似单位尺度再用一阶矩决定方向。当某一个梯度分量历史波动很大时分母会让该方向上的更新步长变小当某个分量一直平缓时分母会让该方向更新步长放宽。epsilon通常写作 eps在公式里的位置也很值得琢磨。它加在分母上主要作用是防止除零。论文建议的默认值是 1e-8但在很多实际框架里比如 PyTorch 的 Adam 实现默认 eps 是 1e-8但如果你用的是混合精度训练建议调大到 1e-6 或 1e-7否则某些矩阵乘法的梯度极小值会引起数值不稳定。后面我会单独讲这一点。2.2 超参数的含义与选择逻辑论文给了一套默认超参数这组参数至今仍是大批模型训练的起点步长 lr默认 0.001。beta1默认 0.9控制一阶动量衰减。相当于“最近多少步的梯度被纳入方向平均”0.9 约等于看最近 10 步的梯度。beta2默认 0.999控制二阶动量衰减。相当于“最近 1000 步左右的梯度量级被纳入平均”。eps默认 1e-8。离开具体任务谈超参数都是耍流氓但我们至少要理解这些默认值背后的逻辑。beta2 设成 0.999 意味着 v_t 对历史梯度的记忆非常长它会非常平滑地跟踪梯度量级的变化。如果 beta2 设得过小比如 0.9那么 v_t 只反映最近 10 步的梯度大小这会让自适应学习率对局部变化过于敏感更新过程容易抖动。beta1 设成 0.9 是一阶矩的“惯性窗口”。通常在训练周期较长的大型模型时beta1 调大一点比如 0.95 甚至 0.99能让更新方向更平滑在训练周期较短或数据噪声较大的场景beta1 默认值往往更稳。但这些都不是绝对的我在实际项目中试过为了追求极快的收敛把 beta1 调到 0.99结果前期 loss 下降缓慢后来才意识到是惯性太大、方向调整跟不上数据分布变化。关于初始学习率 0.001这个值是被大量实验验证过的“安全值”。对于 CV 分类任务、NLP 的 Transformer 类模型从 0.001 开始通常都能正常收敛。如果你的模型比较大、训练数据比较复杂可以试试 lr 从 1e-4 起跳如果模型很小可以试试 1e-2但要配合 warmup 和梯度裁剪否则前几步可能直接飞掉。我个人的经验是除非有强理由否则永远先用论文默认参数跑一版再根据 loss 曲线做调整。2.3 学习率与权重的交互实际操作中的尺度陷阱Adam 自带逐参数自适应学习率那你可能会想是不是学习率随便设都行我在实际训练中踩过不少坑这里统一说一下。Adam 的更新量约等于 lr 乘以“归一化后的方向”因此它不像 SGD 那样直接依赖梯度的绝对尺度。这意味着不同层、不同规模的梯度分布差异会被自适应归一化“拉平”不少。但你如果让某些层的权重初始化尺度过大或过小仍然会间接影响学习效果。最典型的就是 Transformer 里的 attention 层如果初始化不当即便用 Adam训练前期也可能出现 attention 分数饱和、梯度消失的问题。解决方式之一是采用论文原版里提到的“参数初始化 warmup”的组合策略这点在 Transformer 原始论文里也有类似实践。另外要特别注意Adam 的“自适应”不等于“无脑”。如果你把 lr 从 1e-3 调到 1e-1指望靠自适应来兜底大概率会训练发散。因为一阶矩 m_hat_t 经过修正后大致保持在与梯度同量级的水平而 v_hat_t 的平方根也大致保持在与梯度同量级的水平两者相除后得到的更新方向是 O(1) 量级最后的更新步长主要由 lr 控制。lr 一旦设得过大就会直接把参数推离最优区域。3. 实操过程与核心环节实现手写一个 Adam 优化器3.1 从零实现更容易理解内部机制纸上得来终觉浅我建议你至少手写一次 Adam 优化器。这比直接调用 PyTorch/TensorFlow 里的现成类更能帮你理解内部机制。我用 PyTorch 风格给出一个最简实现方便你对照论文里的算法流程。import torch def adam_update(params, grads, exp_avg, exp_avg_sq, step, beta10.9, beta20.999, lr1e-3, eps1e-8): for p, g, m, v in zip(params, grads, exp_avg, exp_avg_sq): # 更新有偏一阶矩 m.mul_(beta1).add_(g, alpha1 - beta1) # 更新有偏二阶矩 v.mul_(beta2).addcmul_(g, g, value1 - beta2) # 偏差修正 m_hat m / (1 - beta1 ** step) v_hat v / (1 - beta2 ** step) # 参数更新 p.data.addcdiv_(m_hat, v_hat.sqrt().add_(eps), value-lr)这个实现的顺序和论文 Algorithm 1 基本一致。实际工程里为了效率通常会一次性对整个参数张量做操作而不是 for 循环逐个参数。但 for 循环版更贴近论文语义适合用来做单元测试和验证。3.2 与 PyTorch 官方实现的差异检查PyTorch 的官方实现里torch.optim.Adam在默认参数下并不做“分子 m_hat”的偏置修正而是将分母上除以1 - beta2^t再把修正系数作为整体乘到更新量上。更准确的表述是PyTorch 把修正分别应用到了 exp_avg 和 exp_avg_sq 上但为了性能优化它会同时对偏置修正做组合运算。如果你自己手写 optimizer 并和官方结果做数值对比要注意 step 的计算顺序——是先更新 step 再修正还是先修正再更新 step可能导致微小差异。大多数框架都是在迭代开始时 step 自增然后基于新的 step 计算偏差修正系数。这一点在复现论文公式时容易踩坑建议以你所用框架的具体实现为准。另外PyTorch 的torch.optim.Adam有一个默认行为对参数做权重衰减weight_decay时默认使用的是 L2 正则化系数直接加在梯度上而不是 AdamW 里的“解耦权重衰减”。一开始接触这两个概念时我也被绕晕过后面单开一节专门讲因为这是 Adam 和 AdamW 最核心的区别之一。3.3 在真实训练任务中替换优化器如果只是调 API替换优化器非常简单import torch model MyModel() optimizer torch.optim.Adam(model.parameters(), lr1e-3, betas(0.9, 0.999), eps1e-8)但这里有一个新手容易忽略的问题Adam 是有内部状态m_t 和 v_t的优化器。如果你在训练过程中换优化器比如先用 Adam 训到一半改成 SGD那么 Adam 维护的动量信息会全部丢失优化器状态不会自动迁移。更隐蔽的问题是当你加载别人发布的 checkpoint 继续训练时除了模型权重还必须同时加载优化器状态字典否则 Adam 的二阶动量 v_t 会被重新初始化为 0这会导致接下来几百步的学习率异常放大效果上表现为 loss 突降后飙升非常坑人。我在一次复现别人实验时只加载了 model.state_dict()没加载 optimizer.state_dict()结果训练到第 50 个 epoch 时 loss 突然出现一个尖峰然后逐渐恢复正常。排查了半天才发现是这个原因。所以实际工程里凡是涉及断点续训或者加载别人的成品模型继续训练务必确认优化器状态是否一并加载。下面是一个标准的续训流程checkpoint torch.load(model.pt) model.load_state_dict(checkpoint[model]) optimizer.load_state_dict(checkpoint[optimizer]) step checkpoint[step]3.4 Adam 的收敛性分析与理论直觉论文的重要贡献之一是从理论上证明了 Adam 的 regret bound遗憾界是 O(sqrt(T))这表示在在线学习框架下它随时间累积的“后悔值”增长速率不超过时间步数的平方根量级。这个概念听起来很硬核但你可以理解为整体上Adam 在凸优化环境下能保证随迭代步数增加而收敛到接近最优解且不会出现灾难性的发散。不过论文里也提到一个重要前提需要保证“梯度存在上界”等条件。实际中非凸的深度学习问题远比凸优化复杂Adam 的收敛性更多靠经验验证。后来很多优化器论文比如 AdamW、LAMB、LARS都在尝试修正 Adam 在某些场景下的泛化短板。理解这一点后你再看各类优化器的改进方案就不会觉得它们是凭空冒出来的了。4. 常见问题与排查技巧实录4.1 loss 震荡或发散排查顺序与解法如果你用 Adam 训练时发现 loss 不降反升、或者上下剧烈震荡我建议按以下顺序排查检查学习率是否过大。0.001 在中小模型上通常安全但如果你已经把网络堆到几十层、或者用大规模预训练模型做微调0.001 可能偏大。常见做法是在微调阶段用 1e-5 到 1e-4 的学习率或配合 warmup。检查 eps 是否过小。尤其在使用混合精度训练AMP时低精度下的梯度平方容易下溢默认的 eps1e-8 太小建议上调到 1e-6。这也是我在训练 LLM 时最常用到的调参手段之一。检查是否需要梯度裁剪。Adam 虽然不像 RNN 那样对梯度爆炸极其敏感但训练 Transformer 时如果 logits 或 loss 极大梯度范数可能会瞬间飙升。用torch.nn.utils.clip_grad_norm_把梯度范数裁剪到 1.0 或 5.0能显著提升稳定性。检查数据预处理和标签是否正常。我见过太多次“优化器背锅”的情况最后发现是数据集里混入了 NaN 样本或者标签范围异常。这类问题通常表现为 loss 突然变成 NaN 并且无法恢复。下面是一个典型的梯度裁剪 Adam 组合示例import torch scaler torch.cuda.amp.GradScaler() for batch in dataloader: optimizer.zero_grad() with torch.cuda.amp.autocast(): loss model(batch) scaler.scale(loss).backward() scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) scaler.step(optimizer) scaler.update()4.2 为什么加了 weight_decay 反而效果变差很多读者一开始分不清 L2 正则化和权重衰减这在 Adam 里尤为重要。传统 SGD 中L2 正则化相当于给每个参数加上一个正比于权重的梯度项也就是“往零方向拉”。而 Adam 在计算更新时会对梯度做自适应归一化这会导致施加在“大梯度参数”和“小梯度参数”上的正则化强度被非线性地改变。换句话说L2 正则化在 Adam 里不再等于“纯粹的权重衰减”。AdamW 的核心区别是将“权重衰减”从梯度计算中解耦先做 Adam 更新再单独对权重乘以(1 - lr * weight_decay)。这种做法避免了 Adam 的自适应机制扭曲正则化强度是预训练和微调 Transformer 类模型时的标准配置。如果你在自己的模型里发现加了 weight_decay 之后 val loss 反而变差先确认你用的是 Adam 还是 AdamW再检查 weight_decay 具体作用的位置。PyTorch 里通过torch.optim.AdamW传入的weight_decay是解耦的而torch.optim.Adam传进去的 weight_decay 等价于 L2 正则项。4.3 Adam 和 AdamW 的区别速查对比维度AdamAdamW权重衰减方式作为 L2 正则加入梯度独立于梯度更新直接对权重衰减对自适应机制的干扰会被二阶矩归一化影响权重衰减强度失真无干扰正则化强度可控典型适用场景通用的小型模型/中规模任务Transformer 类大模型、预训练/微调默认 weight_decay通常是 0常见为 0.01不同框架默认略有差异经验效果在部分任务上泛化略差在多数现代大模型上泛化更好我在实际使用中的感受是如果你训练的是 ResNet、小型 CNN、或者传统的全连接网络用 Adam 通常足够一旦换成 Transformer/BERT/GPT 这类架构直接无脑 AdamW 往往比“Adam L2 正则”更稳、更好调。原因有很多讨论最主流的解释就是权重衰减解耦后正则化强度不再被 Adam 的自适应学习率扭曲模型能更好地拟合训练分布同时保持泛化性能。4.4 关于 “AMSGrad” 和 Adam 后续改进论文作者在后续 ICLR 2018 的论文里提出了 AMSGrad用来修正 Adam 在某些非凸问题上“收敛性证明不严谨”的问题。核心思路是把 v_t 的计算改成取历史最大值而不是指数移动平均从而保证二阶矩不会不断衰减变小、导致学习率不断变大。实际用下来AMSGrad 在部分任务上有效但并非全面超越 Adam。后来又有 RAdam、AdamP、LAMB、LARS 等优化器各自针对大 batch、ViT、长尾分布等场景做改进。我的建议是不要盲目追求用最新的优化器。先把 Adam 和 AdamW 的原理、行为特性吃透看它们在你的任务上表现如何再根据具体需求考虑换更复杂的优化器。很多时候训练效果不好问题根本不在优化器而在数据质量、模型结构、学习率调度策略或者是 logits 的数值范围。优化器只是整个 pipeline 的一环不能迷信。5. 实操总结Adam 的最佳实践建议5.1 上手建议默认值先跑再调关键项新手也好老手也罢第一版训练我建议这样做先用 AdamWlr1e-3小模型或 1e-4大模型beta10.9、beta20.999eps1e-8weight_decay0.01Transformer 类任务常用配合 warmup 和线性衰减看一下 loss 曲线是否平滑下降。只有出现明显的震荡或收敛缓慢才依次调整 lr、batch size、beta1/beta2、eps、梯度裁剪。5.2 注意优化器状态与复现性深度学习的实验可复现性不仅取决于固定随机种子还依赖优化器的确定性。GPU 并行、cudnn 的确定性开关、PyTorch 的torch.use_deterministic_algorithms(True)都可能影响结果。我以前跑过一个项目同样的数据和代码两次训练结果差异很大最后发现是数据读取用了多进程但没设固定 worker 种子。所以如果你做对比实验一定要把“数据加载随机性”也锁死。5.3 我个人的仿真经验最后分享一个我自己的习惯每拿到一个新的优化器我不会直接上大模型而是在两个“代理任务”上做快速验证。第一个是训练一个非常小的 MLP 去拟合一个简单的非凸函数比如 Rosenbrock 函数或 Rastrigin 函数看优化器能否快速收敛到近似最优解第二个是训练一个 2~4 层的 CNN 在 CIFAR-10 上跑几十个 epoch对比不同优化器的收敛速度和 val acc。这个流程成本低、速度快但对优化器的特性把控非常有帮助。Adam 的论文虽然已经有近十年历史但它里面包含的这些思想——动量的指数加权、二阶矩的自适应缩放、偏差修正、对梯度尺度不变性的追求——至今仍是几乎所有主流优化器的底层地基。把这篇论文读透你再去理解 AdamW、LAMB、 Sophia 这些新词会发现一切都顺理成章。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价