PyTorch优化器全解析:从SGD到AdamW的原理、参数与实战调参指南
发布时间:2026/9/8 6:00:07 锦皓数字建站

优化器是 PyTorch 训练流程里最容易被“抄作业”抄错的部分。很多人跑模型的时候直接照搬别人代码里的optim.Adam(model.parameters(), lr3e-4)换了个任务就发现 loss 不降、梯度爆炸、收敛慢半拍问题往往就出在优化器选型和参数配置上。这几个月我在各种实验里反复折腾torch.optim从最基础的 SGD 到 AdamW、从学习率调度到梯度裁剪踩了不少坑也把相关论文翻了底朝天。这篇就从一个实战者的角度把 PyTorch 优化器的底细、论文出处、参数含义和实操踩坑经验一次性讲清楚。无论你是刚把 PyTorch 装好、准备跑第一个模型的入门选手还是已经在调参路上挣扎了一段时间的炼丹学徒这篇文章应该都能帮你少走很多弯路。1. torch.optim 到底是什么先搞清楚优化器在训练中的位置1.1 优化器解决的是哪一步问题很多人一上来就学各种网络的代码反而忽略了最基础的问题优化器在整个训练流程里到底干了什么。一句话概括优化器负责回答“参数往哪个方向走、走多远”这个问题。神经网络训练的本质是让损失函数的值尽可能小而损失函数是模型参数的函数。我们通过反向传播算出每个参数对损失的梯度梯度告诉你的是“在这个点上往上走还是往下走才能让损失变小”。优化器拿到这个梯度之后按照某种策略更新模型参数让损失一步步下降。这个行为特别像下山损失函数是连绵起伏的山地模型参数是你的坐标梯度是脚下山坡的倾斜方向。你要做的就是一步步往低处走。但具体每一步迈多大、怎么避开坑、怎么从局部小坑里爬出来这就是优化器策略的学问。PyTorch 把这一整套逻辑封装在torch.optim里。你不需要手写参数更新公式只需要告诉它“你要优化哪些参数、用什么算法、学习率设多少”。剩下的事情——梯度更新、动量累积、自适应学习率计算——它全部帮你做了。1.2 torch.optim 的设计哲学把优化策略做成可插拔模块我最早用 PyTorch 的时候有一个感受特别深换优化器太方便了。从 SGD 切到 Adam只需要改一行代码训练逻辑、反向传播、模型结构完全不用动。这个便利性不是巧合而是 PyTorch 把优化器规范化成了统一的接口。torch.optim的核心抽象是Optimizer基类。任何一个优化器做的事情都可以拆成两步根据当前梯度计算出参数更新量然后把更新量作用到参数上。PyTorch 把这个过程封装成optimizer.step()训练循环里每轮迭代调用一次即可。这个设计带来的好处非常实际实验管理变得极其轻松。我在做对比实验的时候经常在同一份代码里用argparse传一个--optim参数直接切换 SGD、Adam、AdamW、RMSprop完全不需要改动模型代码。对于需要大规模调参的研究项目或工业项目来说这种可替换性节省的时间非常可观。1.3 一个优化器只有三样东西状态、参数、更新规则别看torch.optim里面优化器种类很多拆开来看每个优化器其实就包含三个部分。第一是参数组。优化器存的不是模型本身而是模型参数的引用。你可以选择优化全部参数也可以只优化部分参数比如冻结backbone只微调分类头。PyTorch 用param_groups这个概念来管理每个参数组可以有不同的学习率、动量等超参数。第二是状态。像 SGD 的动量项、Adam 的一阶矩和二阶矩估计这些东西是跨 step 累积的中间量PyTorch 把它们存在优化器的state字典里。这也是为什么你保存模型权重之后还要单独保存优化器状态才能精确恢复训练现场。第三是更新规则。不同的优化器本质上就是梯度更新公式不同。SGD 直接用梯度乘学习率Adam 在梯度基础上做了指数滑动平均和自适应缩放。这部分就是各个优化器源码里step()方法的核心逻辑。理解这三样东西你再看优化器源码就不会觉得头晕。我之前为了搞清楚 Adam 的state里到底存了什么直接打印过optimizer.state_dict()看到里面每个参数的exp_avg和exp_avg_sq就全明白了。2. 主流优化器逐一拆解从 SGD 到 AdamW2.1 SGD最朴素的基线但永远不要小看它随机梯度下降Stochastic Gradient DescentSGD是深度学习的基石。它的更新规则极其简单参数减去学习率乘以梯度。用 PyTorch 实现就是一行optimizer torch.optim.SGD(model.parameters(), lr0.01)朴素 SGD 的优点是内存占用极低、计算开销极小、每步更新只依赖当前梯度。但它有一个致命弱点收敛慢而且容易在 loss 地形中“震荡”。这个震荡怎么理解想象你在山谷里往下走如果这个山谷的横截面是个狭长的陡沟那么下山的最优路径应该顺着沟的长轴方向走。但朴素 SGD 只看当前点的梯度方向这个方向在沟壁两侧来回摆动导致你不断横跳前进速度极慢。这就是病态曲率问题。所以实际工程里大家用的 SGD 几乎都是带动量Momentum的 SGD。动量项相当于给更新过程加了“惯性”让参数更新方向不只由当前梯度决定还参考历史梯度的累计方向这样在沟槽地形里就能有效抑制震荡、加速收敛。optimizer torch.optim.SGD(model.parameters(), lr0.01, momentum0.9)momentum0.9是经验上最常用的默认值。它表示新的更新方向有 90% 的比例来自历史动量、10% 来自当前梯度。这个参数调得越高更新越平滑但可能反应越迟钝。PyTorch 里还支持nesterovTrue开启 Nesterov 动量这是动量的改进版在计算梯度时先“往前看一步”收敛速度通常更快一些。2.2 Adam自适应学习率的集大成者如果说 SGD 是手动挡那 Adam 就是自动挡。Adam 全称 Adaptive Moment Estimation它在 2014 年由 Kingma 和 Ba 提出可以说是深度学习训练里使用频率最高的优化器。Adam 核心的创新在于为每个参数单独维护一个自适应学习率。具体来说它维护了两个量一阶矩估计梯度的均值相当于动量和二阶矩估计梯度平方的均值反映了梯度的振幅。二阶矩大的参数说明这个方向梯度波动剧烈更新步子就小一点二阶矩小的参数更新步子可以大一点。PyTorch 里最常用的写法是optimizer torch.optim.Adam(model.parameters(), lr1e-3, betas(0.9, 0.999), eps1e-8)这套默认参数是原论文里给出的经受住了大量实验验证。lr1e-3是我见过的绝大多数任务里 Adam 收敛效果不错的起点。betas是两个滑动平均的衰减系数0.9对应动量0.999对应梯度平方的滑动平均。eps是一个极小的数用来防止分母除零。Adam 最大的优点是不太需要精细调学习率。同样是lr1e-3在 ResNet、Transformer、GAN 等不同架构上通常都能得到一个还能接受的训练结果。这对初学者极其友好也解释了为什么网上大量代码里默认就是Adam(lr1e-3)。但 Adam 也有自己的毛病。其中最常见的是“极端不收敛”或后期泛化能力差的问题。一些研究表明Adam 使用指数滑动平均来估计梯度统计量在训练后期这些估计值可能存在偏差导致模型的泛化性能不如精细调过的 SGD带动量。这在图像分类等任务上表现特别明显SGD 最终的测试精度往往比 Adam 高一点。2.3 AdamW权重衰减的正确打开方式AdamW 是我这几年来最常使用的优化器尤其是做 Transformer、BERT 这类模型的时候。它的全称是 Adam with Decoupled Weight Decay由 Ilya Loshchilov 和 Frank Hutter 在 2019 年提出。要理解 AdamW先要理解什么是权重衰减。L2 正则化是给损失函数加上所有权重的平方和让权重不要长得太大。传统的 Adam 在实现 L2 正则化时是把权重衰减项直接加到梯度里然后再做自适应学习率计算。但这样有一个被证实的缺陷自适应学习率会“扭曲”权重衰减的效果使得不同权重被衰减的程度不一致从而削弱正则化的效果。AdamW 的做法是把权重衰减从梯度计算中解耦出来。在 Adam 计算出更新量之后直接对参数做一次额外的小幅缩减这个缩减不受自适应学习率影响。听起来只是一个小改动但实际效果非常明显。我在训练一些大规模模型时从 Adam 换成 AdamW验证集表现经常有可见提升而且更容易训练出泛化性能更好的模型。用法同样很简单optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay0.01)weight_decay0.01是我在 HuggingFace 生态里看到的最常见配置。如果是 fine-tune 预训练模型很多人也会用weight_decay0.05或0.1。这个值不是越大越好太大会让模型欠拟合太小则起不到正则化作用。需要根据任务规模做实验。2.4 RMSprop、Adagrad、Adadelta各有各的看家本领除了 SGD 和 AdamPyTorch 还提供了好几个优化器虽然日常使用频率不高但在特定场景里依然有一席之地。Adagrad 是最早的自适应学习率方法之一。它的特点是对低频出现的参数给予更大的更新步长很适合处理稀疏特征比如推荐系统里的用户/物品 ID 嵌入。但它的缺点是累积梯度平方会越来越大导致学习率被压得过小最终训练提前停滞。RMSprop 是 Hinton 在课程中提出的一种优化方法它通过指数滑动平均来估计梯度平方解决了 Adagrad 学习率单调递减的问题。在强化学习里RMSprop 曾经是标配优化器之一。PyTorch 中的实现提供alpha参数控制滑动平均的衰减默认0.99。Adadelta 可以看作是 RMSprop 的一个变体它不需要手动设置学习率更新量会自适应地缩放。不过实际使用中它的通用性不如 Adam我很少在主流框架里看到它作为首选。这几个优化器的 PyTorch 用法都是同一套optimizer torch.optim.Adagrad(model.parameters(), lr0.01) optimizer torch.optim.RMSprop(model.parameters(), lr0.01, alpha0.99) optimizer torch.optim.Adadelta(model.parameters(), rho0.9)2.5 优化器对比一张表看透区别我一直觉得选优化器之前先看一张对比表比自己瞎试高效得多。这里把我常用的几个优化器的关键特性整理出来优化器核心机制适用场景默认学习率主要缺点SGD直接沿梯度方向更新CNN图像分类、大规模数据0.01~0.1收敛慢、需精细调参SGDMomentum梯度历史动量大部分CNN任务0.01~0.1仍依赖学习率调度Adam一阶矩二阶矩自适应大多数场景通用3e-4~1e-3泛化性能有时不如SGDAdamWAdam解耦权重衰减Transformer、预训练模型1e-4~1e-3需要设置weight_decayRMSprop梯度平方滑动平均强化学习、RNN1e-3通用性不如AdamAdagrad梯度平方累积稀疏特征场景1e-2学习率衰减过快我的经验是如果做图像分类这种比较“正统”的监督学习我会优先考虑带动量的 SGD配合余弦学习率衰减最终精度通常不错如果做 NLP、Transformer、GAN 这类对调参敏感的任务直接用 AdamW省心且效果好快速验证某个想法能不能跑通就上 Adam拿默认参数先跑通再说。3. 优化器参数精细解读别再乱调了3.1 learning rate整个训练里最重要的超参数学习率是优化器里最核心、也最需要谨慎调整的参数。它直接决定每一步更新跨多大步。学习率设得太大参数会在最优解附近来回震荡甚至发散设得太小训练过程极其缓慢且容易陷入局部最优出不来。一个我常用的经验判断训练开始时如果 loss 在下降但非常慢可以试着增大学习率如果 loss 出现剧烈波动甚至变成 NaN立刻减小学习率。更系统的做法是用学习率预热和扫描比如在训练最初几十个 step 里让学习率从 0 线性增长到预设值观察 loss 的下降曲线找到能稳定训练的最大学习率。PyTorch 里设置学习率最常见的方式就是优化器初始化时传入optimizer torch.optim.SGD(model.parameters(), lr0.1)后续想动态调整学习率可以直接修改optimizer.param_groups[0][lr]不过更标准的方式是挂一个学习率调度器scheduler后面会详细讲。3.2 betasAdam 家族的“记忆长度”betas是 Adam 和 AdamW 里特有的参数是一个二元组。第一个值控制一阶矩即动量的滑动平均衰减速度默认 0.9意思是每步更新时历史动量的占比是 90%。第二个值控制二阶矩的滑动平均衰减速度默认 0.999。你可以把 betas 理解为“记忆的窗口长度”。一阶矩的窗口大约在1/(1-beta1)步即1/(1-0.9)10步二阶矩的窗口大约是1/(1-0.999)1000步。这意味着 Adam 在估计梯度振幅时看的是过去大约 1000 步的梯度平方情况。什么情况下需要调 betas常见的一种是训练 GAN 时有些人会把beta1调低到 0.5因为 GAN 的对抗训练需要更短的历史记忆让更新更激进一些。而beta2很少需要动除非你发现训练后期数值不稳定可以尝试把beta2调低到 0.99 或 0.995让二阶矩估计对近期梯度变化更敏感。optimizer torch.optim.Adam(model.parameters(), lr1e-3, betas(0.5, 0.999))3.3 weight_decay正则化与过拟合的博弈weight_decay在 PyTorch 里几乎是所有优化器都有的参数。它的作用是在每次参数更新后对所有权重乘一个略微小于 1 的系数从而抑制权重过大。这个操作等价于在损失函数中加入 L2 正则项但实现方式更直接。在 SGD 中weight_decay 与 L2 正则化是等价的。但在 Adam 里正如前面讲的直接把 weight_decay 加进梯度会有问题所以 AdamW 才把它解耦出来。这也是我推荐大家用 AdamW 的原因之一。具体的 weight_decay 数值要看任务。常见范围是0.0001到0.1之间。做图像分类时1e-4是常见的起点做预训练语言模型微调时0.01甚至0.1都有使用。我在做 Transformer 模型时通常会从0.01起步如果验证集过拟合明显就往上调。3.4 eps一个容易被忽略却保命的参数eps的作用是在计算分母时加一个极小值避免除零或极端数值导致的 NaN。它看似无关紧要但我确实碰到过因为eps问题导致训练崩溃的情况。默认eps1e-8对大部分模型都没问题但在使用混合精度训练AMP时梯度幅值可能会被压缩到很小1e-8这个量级的分母相对过高会影响自适应学习率的精度。一些混合精度训练框架会把eps调整到1e-6或1e-7。如果你在 AMP 训练中碰到 loss 突然变成 NaN又排除了学习率太大的原因可以检查一下eps是否合适。3.5 其他值得了解的参数amsgrad是 Adam 的一个小变体它保存了历史上最大的二阶矩估计从而避免学习率在某些情况下被人为增大。PyTorch 里通过amsgradTrue开启。我在训练不稳定的小批量任务时用过它有时能带来稳定性的提升但大多数情况下默认关闭即可。foreach是 PyTorch 2.x 新引入的性能优化参数。设置为True时优化器会使用更高效的遍历方式更新参数在参数数量多、GPU 支持的情况下可以显著提升速度。PyTorch 新版本里往往已经默认开启或自动选择不需要特意管它。maximize参数用于做梯度上升而不是下降。某些对抗攻击场景或者强化学习里需要最大化某个奖励时可以把maximizeTrue这样优化器自动执行梯度上升而不是下降。这个参数很多人不知道但用到的时候能省很多事。4. 优化器论文里的干货原理、缺陷与改进动机4.1 必读的几篇原文从 SGD 到 AdamW优化器的论文大多是 2010 年代中期的经典之作篇幅不长、公式清晰花一个下午读原文比看十篇博客都有用。SGD 加动量的思想可以追溯到 Polyak 在 1964 年的工作但深度学习语境下更常引用的是 Sutskever 等人在 2013 年的论文《On the importance of initialization and momentum in deep learning》。这篇论文详细分析了动量项如何加速收敛以及 Nesterov 动量的优势。Adam 的原文是 Kingma 和 Ba 的《Adam: A Method for Stochastic Optimization》2015 年发表。这篇论文里不仅有算法伪代码还解答了一个关键问题为什么需要对一阶矩和二阶矩做偏差校正。因为指数滑动平均在初始时刻会偏向零偏差校正让前几步的估计更准确。PyTorch 的 Adam 实现里就包含了这个偏差校正逻辑。AdamW 对应的论文是 Loshchilov 和 Hutter 的《Decoupled Weight Decay Regularization》。这篇论文理清了 L2 正则化与权重衰减在自适应优化器中的差别并通过大量实验证明解耦权重衰减能取得更好的泛化性能。如果你想知道为什么现在大家都在用 AdamW这篇论文是最好的答案。还有一篇值得读的是《On the Convergence of Adam and Beyond》它就是amsgrad的来源。这篇论文指出了 Adam 在某些情况下可能不收敛的问题并提出了一种修正策略。虽然 Amsgrad 在实际使用中提升有限但论文里对 Adam 收敛性的分析很能帮助你理解 Adam 的工作原理。4.2 论文里怎么报优化器参数复现实验必看我经常需要复现别人论文里的实验最头疼的事情之一就是论文里优化器配置交代得不清不楚。有些论文只写了“我们使用 Adam学习率 1e-3”完全没有提及 batch size、weight decay、学习率调度策略。而实际上这些细节往往严重影响结果。那些做得好的论文一般会在实验设置部分明确写出优化器名称、初始学习率、weight decay、总训练轮数和学习率衰减策略。比如经典的 SwAV 自监督学习论文里就会写清楚使用了 LARS 优化器、学习率怎么随 batch size 线性缩放、cosine schedule 的细节。复现时要特别注意一个坑不同代码库对weight_decay的默认值不同。PyTorch 里SGD默认weight_decay0但有些论文的代码实现里默认weight_decay5e-4。如果你照着论文里写的超参数复现却没有注意到代码库的默认值差异训练结果可能天差地别。我一般复现时都会先打印一遍优化器的param_groups把所有参数显式写出来避免默认值陷阱。4.3 从论文年代看优化器的发展脉络把优化器论文按时间线排一下能看出深度学习训练方法论的演进。早期2012-2015是 SGD 和动量的天下。AlexNet、VGG、ResNet 这些经典 CNN 模型的训练都是靠 SGD 加动量配合手动设计的学习率衰减策略。那个时代训练一个模型需要非常细致的调参经验学习率什么时候衰减、衰减多少倍都靠人对 loss 曲线的观察判断。中期2015-2018是自适应方法爆发。Adagrad、RMSprop、Adam 相继出现尤其是 Adam 以“开箱即用”的特性迅速成为主流。但很快研究者发现 Adam 在泛化性能上不如 SGD于是开始深入分析原因推动了 AdamW 的诞生。近五年2019 至今的趋势是“大模型 AdamW 固定学习率调度”。BERT、GPT、ViT 这些大规模 Transformer 模型的训练几乎都是由 AdamW 主导的。究其原因Transformer 结构对学习率极其敏感自适应优化器能够更好地处理不同层之间的梯度尺度差异而解耦权重衰减又提供了更好的正则化效果。加上 warmup cosine decay 的调度策略形成了一整套相对成熟的训练范式。5. 实操PyTorch 优化器的完整使用姿势5.1 基础训练循环从定义到 step这里给一个标准的 PyTorch 训练循环模板涵盖优化器、损失函数、反向传播和参数更新。import torch import torch.nn as nn model nn.Linear(10, 2) optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay0.01) criterion nn.CrossEntropyLoss() for epoch in range(num_epochs): for batch_x, batch_y in dataloader: optimizer.zero_grad() output model(batch_x) loss criterion(output, batch_y) loss.backward() optimizer.step()有几个细节值得强调。optimizer.zero_grad()必须放在每次backward()之前因为 PyTorch 默认是累积梯度的。如果忘记清零上一个 batch 的梯度会累加到当前 batch导致参数更新方向完全错误。很多人第一次写训练循环就栽在这里。loss.backward()计算梯度optimizer.step()用梯度更新参数。这个顺序千万别反。有些新手会把optimizer.step()放在loss.backward()前梯度都还没算出来就更新了参数自然一动不动。还有个习惯我要特别推荐在训练早期打印一次optimizer.state_dict()。这一下就能看到优化器到底接收了哪些参数、每个参数组的学习率是多少、状态是否为空。排查配置问题的时候非常高效。5.2 参数分组冻结与差异化学习率实际项目中很少会无脑优化所有参数。最常见的情况是迁移学习用预训练模型做 backbone微调后面的分类头。此时我们希望 backbone 参数变化幅度小分类头参数从头训起。PyTorch 的参数分组就是用param_groups实现差异化配置optimizer torch.optim.SGD([ {params: backbone.parameters(), lr: 1e-4}, {params: head.parameters(), lr: 1e-3}, ], lr1e-3, momentum0.9)注意这里的用法传入的是一个字典列表每个字典表示一个参数组。默认lr写在第二个参数位置被第一个参数组里的lr: 1e-4覆盖。也就是说backbone 参数实际学习率是 1e-4head 参数使用默认的 1e-3其他未在字典里指定的参数组使用全局默认值。另一个常见需求是冻结某些层的参数。最简单的办法是设置param.requires_grad False但注意如果这些参数已经被传入优化器优化器里仍然会维护它们只是梯度为零、不更新。更高效的做法是把冻结参数从优化器中剔除只传入需要训练的参数。trainable_params [p for p in model.parameters() if p.requires_grad] optimizer torch.optim.Adam(trainable_params, lr1e-3)这样参数少了很多优化器每次 step 的计算开销也会降低。5.3 学习率调度别让学习率一成不变固定学习率训练到底层模型通常不是最优选择。经验上训练初期学习率要大一些快速下降后期学习率要小一些精细微调。PyTorch 的torch.optim.lr_scheduler提供了多种调度策略。最常用的是StepLR和CosineAnnealingLR。StepLR是每隔固定轮数把学习率乘一个衰减系数CosineAnnealingLR则让学习率按照余弦曲线从初始值降到最小值。我现在做视觉模型偏爱 cosine因为它的下降曲线更平滑不需要手动设定衰减节点。scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_maxnum_epochs, eta_min1e-6 ) for epoch in range(num_epochs): train_one_epoch() scheduler.step()还有一个极其重要的策略是 warmup尤其在训练 Transformer 类模型时几乎是必需项。warmup 的思想是训练最开始让学习率从极小值线性增长到预设值避免模型初始化不稳定时直接用大学习率导致震荡。PyTorch 里可以用LambdaLR手写def warmup_lambda(step): if step warmup_steps: return step / warmup_steps return 1.0 scheduler torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambdawarmup_lambda)5.4 梯度裁剪、AMP 与优化器的配合梯度裁剪是防止梯度爆炸的常用手段在 RNN、Transformer、GAN 训练里尤其重要。PyTorch 提供了torch.nn.utils.clip_grad_norm_和clip_grad_value_两个函数。区别是前者把梯度的范数限制在给定阈值内后者把每个梯度值裁剪到给定范围。loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step()顺序有讲究裁剪必须在backward()之后、optimizer.step()之前。因为这个操作修改的是参数梯度而 step 恰好要用到它。混合精度训练AMP如今在大模型训练中几乎是标配。PyTorch 1.6 之后提供torch.cuda.amp接口。使用 AMP 时优化器本身不需要大改但需要特别注意如果某些参数是float64或整数类型AMP 可能会报类型不匹配的错误。通常浮点参数用float32就没问题。scaler torch.cuda.amp.GradScaler() for batch_x, batch_y in dataloader: optimizer.zero_grad() with torch.cuda.amp.autocast(): output model(batch_x) loss criterion(output, batch_y) scaler.scale(loss).backward() scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) scaler.step(optimizer) scaler.update()注意 AMP 训练时 grad clip 存在一个顺序问题scaler.unscale_(optimizer)之前梯度是被放大过的直接用 clip 会导致阈值失真。上面的写法先 unscale 再 clip 才是正确的。5.5 状态保存与恢复断点续训的完整姿势训练大模型耗时动辄几十个小时一旦中途中断就得从头开始这是不能接受的。好在 PyTorch 提供了优化器状态保存机制。torch.save({ model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), epoch: epoch, best_acc: best_acc, }, checkpoint.pt) # 恢复 checkpoint torch.load(checkpoint.pt) model.load_state_dict(checkpoint[model_state_dict]) optimizer.load_state_dict(checkpoint[optimizer_state_dict]) epoch checkpoint[epoch]恢复优化器状态时有一点要特别注意必须在加载模型权重、并且在优化器初始化之后才能调load_state_dict。而且如果模型结构变了比如改了分类头的维度优化器里对应的参数状态会不匹配此时不能直接恢复。我一般会在结构改动后重建优化器不恢复优化器状态只是从断点轮次重训。6. 常见问题与排查技巧实录6.1 loss 不降反升到底是谁的锅训练时最让人崩溃的现象就是 loss 不但不降反而一路上涨。遇到这种情况我有一套固定的排查顺序。先检查学习率是不是太大。最简单的验证方法把学习率直接减小 10 倍如果 loss 曲线变得稳定下降那问题基本就是学习率过高。再检查输入数据有没有归一化。不少新手直接拿原始像素 0-255 的图片喂给模型梯度幅值巨大优化器再强也很难稳住。这时候损失函数干瞪眼不一定是优化器的错。最后检查梯度是否出现 NaN。可以在optimizer.step()前打印torch.isnan(model.weight.grad).any()。如果有 NaN说明反向传播里出了问题跟优化器关系不大了。6.2 模型不收敛试试这些调参顺序我调优化器参数有个固定的优先级顺序。第一优先看学习率因为它的影响最大。从论文或类似任务的默认值出发如果 loss 不降就降低学习率如果下降过慢就试着调高。第二是检查 batch size 与学习率的匹配关系。batch size 翻倍学习率通常也该翻倍线性缩放原则。第三才是调 weight_decay、momentum 这些细节。还有一个我特别推荐的做法先跑几十个 step画 loss 曲线观察它是下降、震荡还是发散。很多问题在早期就暴露了。没必要一开始就跑完整训练流程。调参有一个反向直觉如果模型一直不收敛有时候不是优化器调得不够好而是模型结构或数据预处理有问题。优化器只能让模型在损失面上运动如果损失面本身设计得不好再好的优化器也白搭。6.3 优化器相关报错速查表报错信息或现象常见原因解决方法ValueError: could not broadcast input array优化器参数与模型参数维度不匹配检查模型是否有参数被修改或新增重建优化器RuntimeError: element 0 of tensors does not require grad传入优化器的参数没有梯度设置requires_gradTrue或检查模型是否在torch.no_grad内Optimizer got an empty parameter list参数序列里没有任何requires_gradTrue的参数检查requires_grad设置或trainable_params筛选逻辑AMP 训练时GradScaler报错优化器与梯度缩放器配合次序错误按官方文档顺序scale(loss) - backward - unscale - stepload_state_dict时 size mismatch模型结构改动导致优化器状态无法对齐不恢复优化器状态重新创建优化器训练中途 loss 变成 NaN学习率过大、eps 过小、梯度爆炸先降学习率再检查梯度范数必要时开 grad clip6.4 我的一个排查实例AdamW 微调模型验证集掉点最后分享一个真实案例。我前段时间在拿一个预训练模型做下游任务微调一开始用 Adam、学习率 1e-4、无 weight decay验证集分数一直上不去。后来我把优化器换成 AdamW、weight_decay 设为 0.01、学习率调到 5e-5再配合 warmup验证集分数明显上升。整个过程优化器参数只改了几个数模型结构完全没动。这个例子很有代表性。它说明一个常被忽略的道理当你觉得模型效果不好时不要只盯着网络结构改先回过头检查优化器和学习率调度是否合理。很多时候优化器配置的改变带来的提升比换一个更大模型还要明显。尤其是微调预训练模型学习率太大容易灾难性遗忘太小又学不到任务特有信息这个平衡点全靠优化器配合调度策略来把握。在 PyTorch 里优化器不是一个需要花很多时间去“学习”的模块但绝对是一个值得花时间去“理解”的模块。把torch.optim里每个常用优化器的原理、参数、适用场景都摸清你在训练任何模型时都会更有底气。剩下的就是不断试错、积累自己的调参感觉这本身就是深度学习实践里最有趣的修炼过程。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。