资讯动态

PyTorch优化器实战:SGD、Adam、AdamW原理与调参指南

发布时间:2026/9/8 7:09:23 来源:尧图企业网站定制
做深度学习这几年我跟 PyTorch 打交道的每个项目几乎都绕不开torch.optim这个模块。说实话很多人从一开始就把optimizer torch.optim.Adam(model.parameters(), lr1e-3)当成一段“照着抄就行”的样板代码但真正跑起来你会发现损失不下降、训练震荡、收敛慢、泛化差这些头疼的问题里有一大半都出在这个看似不起眼的优化器配置上。我最早是在小土堆的 PyTorch 入门笔记里看torch.optim的当时只觉得它是训练循环里的一个固定环节先zero_grad()清零梯度再loss.backward()回传梯度最后step()更新参数。后来读原作论文才发现优化器承的东西远不止“更新一下模型参数”这么简单——它要负责在庞大的参数空间中决定每一步走多快、走哪个方向、怎么避免来回震荡还要在收敛速度和最终精度之间做权衡。这篇文章我打算从torch.optim的整体设计讲起把 SGD、Adam、RMSprop、AdamW 这几位“常驻选手”背后的论文原理、数学公式和 PyTorch 参数含义拆开揉碎再结合我自己实际调参的经验和踩过的坑做一次完整总结。适合刚配好 PyTorch 环境、跟着教程跑通模型但想深入理解训练机制的初学者也适合被训练不收敛、损失爆炸折磨过打算系统过一遍优化器知识的从业者。1. torch.optim 的整体设计思路与论文根基1.1 训练循环里优化器到底在干什么先看一段最常见的 PyTorch 训练代码for batch in dataloader: optimizer.zero_grad() output model(batch) loss loss_fn(output, target) loss.backward() optimizer.step()很多初学者会以为优化器是在 “调整模型”其实它干的事更精确优化器负责读取已经计算好的梯度按照某种更新规则修改模型的parameters。梯度本身是loss.backward()算出来的它告诉我们可以把每个参数往哪个方向挪可以让损失下降。但具体挪多少、要不要结合历史梯度、要不要对不同的参数区别对待这些都由优化器来决定。用生活化的类比来解释模型参数是一张地图上的坐标损失函数是地图上每个位置的海拔梯度就是你脚下山坡最陡的朝向。这时候如果每一步都顺着最陡方向往山下走容易在山谷两侧来回震荡如果步子太大可能一步跨过山沟冲到对面的山坡步子太小又可能走到半夜还困在半山腰。优化器要做的就是研究出一套靠谱的“走路策略”——这恰恰是大量论文的作者们在研究的事情。所以torch.optim表面上是几个现成优化器的集合本质上是把论文里的数学更新规则做成了可复用、可插拔的工程模块。1.2 Optimizer 基类state、param_groups 与 defaults要理解torch.optim得先从它的核心抽象开始。torch.optim.Optimizer是基类所有优化器都是它的子类。打开源码你会发现它维护了三个关键结构optimizer.state字典保存优化器在训练过程中积累的“状态”比如动量项、梯度滑动平均、步数计数等。optimizer.param_groups列表每个元素是一个参数组组合了模型参数列表和这组参数各自的超参数lr、weight_decay、momentum等。optimizer.defaults字典保存所有参数组的默认超参数。为什么设计成这样因为实际训练经常需要“分组配置”。比如 BERT 这类模型做迁移学习时我们通常希望对 embedding 层和最后分类头用不同的学习率或者做对抗训练时对判别器和生成器分别调整优化策略。param_groups的存在就是为了在不写多个优化器的前提下对不同参数施加不同规则。实际代码是这么用的optimizer torch.optim.Adam([ {params: model.backbone.parameters(), lr: 1e-4}, {params: model.head.parameters(), lr: 1e-3}, ], lr1e-4)这里外层lr1e-4是默认值两个参数组可以分别覆盖。后面调参时的很多骚操作都要靠这个机制这也是torch.optim比某些别的框架优化器更灵活的地方。1.3 从论文公式到工程实现优化器是数学规则的高性能落地翻开源码或论文会看到每个优化器其实都在反复执行一个并不复杂的更新公式比如SGD的θ ← θ - lr * gAdam则是多套了几层滑动平均和偏差修正。真正让优化器变成工程难点的是数值稳定性、内存开销和极大规模参数的适配。现在主流模型的参数量动辄几亿甚至上千亿优化器为了“记住”历史信息占用的显存往往跟模型参数本身一个量级。比如 Adam 要为每个参数额外维护一阶矩和二阶矩两个状态显存开销直接翻倍。这也是为什么后来出现Adafactor、8-bit Adam这类省内存优化器它们解决的就是工程落地中的真实痛点。理解这一层后你再看torch.optim就不会觉得它仅仅是个 API 集合了。它内部那一堆状态字典、参数分组、回调钩子都是在为“大规模训练 灵活配置”这两个要求服务的。2. 主流优化器逐个拆解论文原理、公式与 PyTorch 参数2.1 SGD 与动量最老牌也最考验功力的基线随机梯度下降Stochastic Gradient Descent是所有优化器的祖师爷核心公式特别简单θ ← θ - lr * g其中g是当前 batch 的梯度。它来自 1951 年 Robbins-Monro 算法提出的随机近似思想后来在深度学习领域被 Yann LeCun 等人的经典论文大量使用。但单纯 SGD 在实践中太容易震荡了于是有了动量Momentum可以理解成给“下山过程”加了一个惯性。实现上优化器会额外维护一个速度项vv ← μ * v gθ ← θ - lr * v这里μ就是momentum参数通常取 0.9 或 0.99。带动量之后如果当前位置前后两步梯度方向一致速度会不断叠加学习加速如果方向相反速度会被抵消抑制震荡。这个技术最早可以追溯到 Polyak 在 1964 年的工作工程上则在 1986 年 Rumelhart、Hinton 和 Williams 的经典论文中被推广。PyTorch 的torch.optim.SGD还支持 Nesterov 动量nesterovTrue它与标准动量的区别在于计算梯度时会“往前多看一步”收敛路径更平稳很多经典花书中都专门提到过它的优势。参数常见取值作用lr0.01 ~ 0.1学习率控制步长momentum0.9 ~ 0.99动量系数抑制震荡weight_decay1e-4 ~ 5e-4权重衰减L2 正则nesterovFalse / True是否启用 Nesterov 动量实践下来SGD 调好了往往能得到比 Adam 更好的泛化性能尤其在 ImageNet 分类这类经典任务上很多 CV 模型刷榜单用的就是带动量的 SGD。代价是它对学习率和动量的配合有要求新手直接上手比较容易在“不收敛”和“震荡”之间反复横跳。2.2 Adam当前最热门的默认选择AdamAdaptive Moment Estimation是如今提到adam优化器时最常被想起的名字论文是 Kingma 和 Ba 在 ICLR 2015 发表的Adam: A Method for Stochastic Optimization。它把动量思想和自适应学习率结合在了一起。Adam 维护了两个状态m梯度的一阶矩也就是动量项相当于对历史梯度的指数移动平均v梯度的二阶矩相当于对历史梯度平方的指数移动平均。更新时先用β1和β2两个系数更新这两个状态在 PyTorch 里对应参数就是betas(0.9, 0.999)。因为初始阶段m和v都是从 0 开始作者又引入了偏差修正避免早期更新被“冷启动”带偏。PyTorch 中并没有直接把偏差修正做成可配参数而是自动在内部完成你只要知道它存在就好。Adam 最大的优点是“省心”它对学习率的敏感性比 SGD 低不少很多任务直接用默认的lr1e-3就能跑出不错的效果。这就是为什么现在普通项目里几乎全民默认 Adam尤其是 NLP、强化学习这类结构复杂、梯度尺度差异大的场景Adam 的适应性优势非常明显。但它也有一个众所周知的问题泛化性能有时候不如调好的 SGD。不少研究认为这和 Adam 的二阶矩更新、隐含正则之间微妙的关系有关。为了应对这个问题学界后来提出了一系列改进型最成功的当属 AdamW。2.3 AdamW 与解耦权重衰减为什么它成了新标配AdamW 的论文是 Loshchilov 和 Hutter 在 ICLR 2019 发表的Decoupled Weight Decay Regularization。这篇文章的核心观点很尖锐标准的 Adam 里如果加上 L2 正则weight_decay参数权重衰减会跟一阶矩、二阶矩的归一化步骤耦合在一起使得正则效果被动态缩放不是真正意义上的“权重衰减”。简单解释一下普通 SGD 里加weight_decay相当于往损失函数里加了一项1/2 * λ * ||θ||²梯度里多出一项λ * θ更新时参数会按比例缩小。但在 Adam 里这个梯度的衰减项会被二阶矩v归一化导致每一步实际衰减量跟v的大小有关不稳定。AdamW 的做法是依然用 Adam 本体来更新梯度方向但把权重衰减这一项单独拿出来在更新时直接对参数乘上一个衰减系数不再参与 Adam 的归一化。PyTorch 里的实现是torch.optim.AdamWoptimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay0.01)注意这里的weight_decay建议取值和 Adam 不同。Adam 里通常用1e-4或5e-4而 AdamW 里常用0.01左右因为它本质上是解耦后的直接衰减率。大模型时代AdamW 几乎成了 Transformers 类模型训练的默认选择Hugging Face 的Trainer默认优化器就是 AdamW。我在实际项目里的体感是AdamW 通常比 Adam 训练更稳最终准确率也略高这也是为什么现在论文、开源代码、预训练脚本里到处都是它的原因。2.4 RMSprop、Adagrad、Adadelta不得不提的过渡与特化选手在 Adam 之前学术界还经历过一个“自适应学习率”的探索期。Adagrad 来自 Duchi 等人在 2010 年发表的论文它的思路是将学习率按参数历史梯度平方和进行缩放让出现频率低的参数获得更大的更新步长适合稀疏特征场景。但问题也很明显分母累积的梯度平方会不断增大学习率会迅速衰减到接近 0训练后期基本挪不动。RMSprop 是 Hinton 在他的 Coursera 课程中提出的方法没有正式期刊论文但在强化学习领域应用极广。它的核心区别是分母用指数移动平均而不是历史累加和这样学习率不会单调衰减到死。RMSprop 在 PyTorch 中的参数很简洁lr、alpha滑动平均系数默认 0.99、eps避免除零默认 1e-8。Adadelta 是 Zeiler 在 2012 年提出的核心是去掉了全局学习率只考虑参数更新的“比例”整体相对小众。这三个优化器现在不太常见但如果你用的代码库是基于老论文复现的可能还会碰到。我的建议是了解原理即可新项目优先考虑 AdamW。优化器核心论文/来源适用场景默认学习率SGDMomentumPolyak 1964 / Rumelhart 1986图像分类、经典 CV 任务0.01 ~ 0.1AdagradDuchi et al. 2010稀疏特征、在线学习0.01RMSpropHinton 课程讲义强化学习、RNN0.001AdamKingma Ba, ICLR 2015NLP、各类任务默认0.001AdamWLoshchilov Hutter, ICLR 2019Transformer、大模型0.0013. 实操全流程从环境准备到优化器接入3.1 环境准备与基础框架搭建要玩转torch.optim前提是先把 PyTorch 装好。如果你还没配环境我个人建议直接用 Anaconda 创建虚拟环境然后去 PyTorch 官网选择跟本机 CUDA 版本匹配的安装命令。比如常见的组合是 Python 3.10 搭配 PyTorch 2.x 加 CUDA 12.1安装命令类似conda create -n torch_env python3.10 conda activate torch_env pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121CPU 版本就把--index-url后面改成 CPU 的链接即可。安装完成后记得验证一下python -c import torch; print(torch.__version__); print(torch.cuda.is_available())能正常输出版本号和True就说明环境没问题。这一步网民们搜“PyTorch 安装教程 GPU”“Anaconda 配置 PyTorch 环境”等等搜得最多其实套路都一样最重要的是版本匹配。显卡驱动版本太老、CUDA 版本不匹配都会导致torch.cuda.is_available()返回False甚至直接报错。3.2 一个完整的小实验对比 SGD、Adam、AdamW 的收敛差异环境配好后直接说实操。下面我用一个简单的二分类模型在合成数据集上对比SGD(momentum0.9)、Adam、AdamW三种优化器的收敛表现。这段代码非常基础适合你直接建个脚本跑起来。import torch import torch.nn as nn from torch.utils.data import TensorDataset, DataLoader from sklearn.datasets import make_moons from sklearn.model_selection import train_test_split import matplotlib.pyplot as plt # 生成数据 X, y make_moons(n_samples1000, noise0.2, random_state42) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) X_tr torch.tensor(X_train, dtypetorch.float32) y_tr torch.tensor(y_train, dtypetorch.long) X_te torch.tensor(X_test, dtypetorch.float32) y_te torch.tensor(y_test, dtypetorch.long) train_ds TensorDataset(X_tr, y_tr) train_dl DataLoader(train_ds, batch_size64, shuffleTrue) # 一个简单的 MLP model nn.Sequential( nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 2) ) opt_configs { SGDMomentum: lambda p: torch.optim.SGD(p, lr0.05, momentum0.9), Adam: lambda p: torch.optim.Adam(p, lr0.001), AdamW: lambda p: torch.optim.AdamW(p, lr0.001, weight_decay0.01), } def train_epoch(optimizer, model, dataloader, loss_fn): model.train() total_loss 0.0 for xb, yb in dataloader: optimizer.zero_grad() out model(xb) loss loss_fn(out, yb) loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(dataloader) for name, build_opt in opt_configs.items(): torch.manual_seed(42) model nn.Sequential( nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 2) ) loss_fn nn.CrossEntropyLoss() optimizer build_opt(model.parameters()) losses [] for epoch in range(100): l train_epoch(optimizer, model, train_dl, loss_fn) losses.append(l) if epoch % 20 0: print(f{name} epoch {epoch}: loss {l:.4f}) plt.plot(losses, labelname) plt.legend() plt.xlabel(epoch) plt.ylabel(train loss) plt.show()我实际跑下来三条曲线的典型特征是SGDMomentum 前期下得慢但到后期比较稳Adam 前期飞快下降很容易让人产生“稳了”的错觉AdamW 前期稍慢于 Adam但整体平滑度最好。如果你把测试集准确率也统计出来大概率会发现 AdamW 的泛化误差并不比 Adam 差甚至更好。3.3 超参数到底怎么选学习率、betas、weight_decay 的实战经验光知道公式还不够调参才是真正磨人的地方。说说我自己的经验。学习率。SGD 通常从0.01或0.1起步尤其带上 momentum 后0.1配合0.9在很多 CV 任务上是经典组合。Adam 系列默认0.001大部分情况不用改但如果你发现损失在前期下降很慢可以试试调到3e-4或1e-3的区间也就是“既要让它动起来又不能动太大”。betas。第一个β1控制动量默认0.9基本不动第二个β2控制二阶矩的窗口大小默认0.999。对训练不太稳定的场景把β2调小到0.98或0.95有时能让更新更平稳因为二阶矩窗口变短对近期梯度的响应更敏感。但千万别一下调太低否则容易破坏自适应学习率的优势。weight_decay。区分 Adam 和 AdamW 的关键就在这里。用 Adam 时weight_decay一般就是1e-4或5e-4相当于一个温和的 L2 正则用 AdamW 时我一般直接给0.01或0.1因为它的行为已经脱钩了不能再拿原来的量级去套。动量的微妙作用。如果你用的是torch.optim.SGDmomentum0.9是最常用值0.95和0.99更适合任务对长程惯性有更高要求的场景但要配合更小的学习率。3.4 调度器才是真正的“隐藏神器”torch.optim.lr_scheduler说到torch.optim很多人会忽略torch.optim.lr_scheduler这个子模块。其实只用固定学习率训练就像在高速上一直用同一个油门车速跑弯道很容易翻车。调度器负责在训练过程中动态调整学习率。最常用的几个StepLR每隔固定 epoch 数把学习率乘以一个gamma衰减因子适合任务阶段明显变化的训练MultiStepLR在指定的里程碑比如 epoch 30、60、90衰减经典 ResNet 训练就爱用这个ReduceLROnPlateau检测某个指标停滞时自动降低学习率特别适合懒得自己观察损失曲线、想让训练器“自适应”的场景CosineAnnealingLR按余弦曲线把学习率从初始值慢慢降到接近 0现在很多大模型训练脚本里的标配。optimizer torch.optim.AdamW(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) for epoch in range(50): train_one_epoch() scheduler.step()CosineAnnealingLR的T_max是周期的一半意思是学习率在T_max个 epoch 内从初始值降到接近 0。实践里还有个更进阶的玩法是OneCycleLR它模拟了先热身后退火的过程经常能帮你在同样 epoch 数下拿到更优结果。4. 我在 torch.optim 上踩过的坑与排查技巧4.1 最容易出问题的几个细节忘记zero_grad()。新手最常见的致命错误。PyTorch 的梯度会默认累积如果漏掉optimizer.zero_grad()每个 batch 的梯度会不断叠加更新步长逐步失控损失曲线会变成“脉冲式”上涨。代码里务必在loss.backward()之前清一次梯度。优化器状态没有和模型一起搬运。做 checkpoint 保存和恢复时很多人只保存model.state_dict()不保存optimizer.state_dict()。这样恢复训练后Adam 的m、v动量状态全丢了相当于重新冷启动学习率曲线和动量累积全部错乱。标准做法是torch.save({ model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), epoch: epoch, }, checkpoint.pt)修改模型后没有重建优化器。如果你在训练中途冻结了某些层或者给模型加了新模块继续用旧的优化器直接step()时参数组的引用列表跟实际模型参数会不一致不仅更新不到新参数还可能直接报错。我的建议是只要模型结构变化就重新创建优化器。4.2 与混合精度、梯度裁剪、分布式训练配合时的坑PyTorch 2.x 里用 AMP 做混合精度训练时GradScaler和优化器的配合有讲究scaler torch.amp.GradScaler(cuda) optimizer.zero_grad() with torch.amp.autocast(cuda): loss loss_fn(model(x), y) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()这里scaler.step(optimizer)会自动判断梯度是否为有限值如果出现 NaN/Inf 它会跳过这步更新。但要注意AMP 下如果手动做了梯度裁剪必须先把scaler.scale出来的梯度反缩回去再裁剪否则梯度值会被压缩成一个小数裁了个寂寞。scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) scaler.step(optimizer) scaler.update()分布式训练DDP里优化器使用的每个进程都要有各自的优化器状态但torch.cuda.synchronize()之前的梯度是异步的跨卡之间的梯度聚合靠 DDP 的梯度同步来完成。这里最容易踩的坑是只把model包进了DistributedDataParallel却忘了在保存 checkpoint 时处理module.前缀导致本地没有optimizer.state_dict()对应参数名。4.3 损失不降、震荡剧烈、收敛太慢的排查清单我从自己的项目里总结了一张速查表平时遇到训练问题先按这个过一遍现象可能原因排查方向loss 完全不动学习率太小 / 梯度消失打印梯度范数逐层检查梯度loss 震荡剧烈学习率太大 / 批次太小 / 数据噪声大降低学习率增大 batch检查数据loss 先降后升过拟合 / 学习率调度过早衰减看 val loss调整 scheduler收敛后泛化差优化器选择不当 / 正则过弱换 AdamW增大 weight_decay出现 NaN/Inf学习率过大 / 数据有 NaN / AMP 精度问题检查输入开启梯度裁剪降低学习率排查时最有效的工具其实是“可视化”。把每个参数的梯度 L2 范数、权重 L2 范数、损失值、学习率变化曲线都记录下来。多数问题一眼就能看出来梯度范数如果突然爆炸要么是学习率太大要么是数据里混入了异常值梯度范数如果一直是 0那就要怀疑网络结构本身了。5. 一些经验向的补充建议还有几个跟优化器直接相关的点单独拎出来说说。第一不同任务的最优优化器往往不同。图像分类、目标检测这类 CV 任务经典做法是 SGDMomentum 配上多阶段学习率衰减NLP 任务比如微调 BERTAdamW 几乎是唯一选择强化学习里 RMSprop 和 Adam 都很常见。不要迷信“最强优化器”这种提法最好在任务上做一组小实验再定。第二别把学习率和优化器分开调。优化器的每一步更新都是以学习率为前提的学习率调不好换什么优化器都白搭。我的习惯是先固定一个合理的优化器花时间把学习率扫描一遍再回来微调优化器参数。所谓“学习率热身 余弦退火 AdamW”这个组合能覆盖掉大部分 Transformer 类模型的训练需求。第三论文复现时注意看作者的优化器细节。很多论文的最后附录里会写一行小字优化器用什么、学习率多少、warmup 多少步、scheduler 怎么设、seed 是多少。这些细节对结果的影响往往不比模型结构本身小。我复现别人工作时第一步就是照着这些设置还原训练环境而不是拿默认参数硬跑。第四新优化器层出不穷但核心思想就那几样。动量、自适应学习率、权重衰减、梯度裁剪、学习率调度几乎所有优化器都是在这些要素上做变形。你把这些要素吃透了遇到再新的优化器论文也能快速看懂它是动了哪个部件而不是觉得又冒出来一个完全陌生的东西。最后再分享一个我实际使用中的小技巧如果你拿不准到底选哪个优化器可以直接跑一个“短平快”实验用固定 epoch 数比如 30 个和固定种子把 SGDMomentum、Adam、AdamW、RMSprop 分别跑一遍看验证集指标和 loss 曲线。这个小实验通常几个小时就能出结果能省下后面好几天盲目调参的时间。优化器是训练代码里看起来最短小、却最值得花心思研究的一块。它承载的不只是“更新参数”这个动作而是你对模型如何学习的整体判断。这篇文章写到的每一个坑都是我实际踩过之后才真正理解的——希望你读完后下一次打开torch.optim时不再是“照着抄一行配置”而是能看懂它在做什么、为什么这么做。

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

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

免费获取报价