news 2026/9/8 16:47:01

优化器选型与调参实战:从AdamW到Lion的避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
优化器选型与调参实战:从AdamW到Lion的避坑指南

上个月我把一个视觉模型的训练任务从 AdamW 切到另一个优化器,loss 曲线肉眼可见地往下掉,可 validation 指标却纹丝不动。同事看了一眼训练日志,随口说了一句:“这 Optimizer 挺有意思,不过你是不是没搞清楚它到底在优化什么?” 这句话让我重新把优化器这件事从头捋了一遍。今天这篇,就用 “A Big Beautiful Optimizer?” 这个标题当引子,聊聊我在选型、调参和排坑过程中真正踩过、也真正想明白的东西。

这篇文章适合谁看?正在训练自己第一个像样模型的工程师,或者在 PyTorch、TensorFlow 里调了半天 loss 就是不降、准备换优化器碰碰运气的朋友。我会从优化器在训练流程中的真实角色说起,讲清楚它的参数怎么改、为什么这么改,再给出一套可以直接抄走的配置流程,最后把我这两年排查过的优化器相关 bug 列成一份问题速查表。不会有数学推导堆成山,但也别指望我一句“调 Adam 就完事了”打发你。

1. 优化器的作用比你想的更底层

1.1 从“往山下走”到“凭什么这么走”

大多数教程解释优化器,都会说它是在做梯度下降:算完 loss,把梯度往回传,参数沿着负梯度方向更新一步。这句话没错,但它忽略了一个关键问题——梯度只告诉你哪个方向下降最快,却没说这一步该迈多大,也没说连续几步方向互相矛盾的时候,到底该信哪一步。SGD 在处理这个问题时非常“憨厚”:梯度指向哪,我就往哪走,步长由学习率统一决定。这种做法在 loss 曲面比较平滑时没问题,一旦遇到狭长峡谷、鞍点或者陡峭与平坦交替的区域,SGD 就会来回震荡,像一个人闭着眼在野外走夜路,前面是个坑也照样迈步。

这时候就需要优化器在“方向”和“步长”之外,再引入一些更聪明的判断。Momentum 的诞生就是解决“震荡”问题:把历史的梯度方向累积起来,如果连续几步方向一致就加速,如果方向反复横跳就抵消掉一部分,相当于给下山的人加了一个惯性。RMSProp 和 Adagrad 则是从“每个参数该有自己的步长”入手,对更新频繁的参数减小步长,对更新稀疏的参数放大步长。到了 Adam,直接用一阶矩估计接住 Momentum 的活,用二阶矩估计接住自适应步长的活,两者一组合,就成了过去十年深度学习训练里的默认选项。

但注意,Adam 不是没有代价的。它引入了额外的状态变量——每个参数要保存一阶矩和二阶矩两份中间量,显存开销直接翻倍。很多人在小模型上感觉不到,等模型大到几百亿参数,优化器状态反而成了显存瓶颈。这也是为什么后面出现了 Adafactor、Lion 这类“轻量”优化器:它们要么降低状态存储,要么干脆用符号函数代替部分计算,本质上都是在“Adam 的效果”和“显存.占用”之间找平衡。

1.2 “大”和“美”的真实含义

标题里的 “A Big Beautiful Optimizer”,我理解至少有三种解读。第一种是字面上的“大”:参数规模很大的模型需要什么样的优化器?这个问题从 GPT 系列训练开始就变得非常现实,几十亿参数的模型,Adam 状态就要吃掉几十 GB 显存,这比模型本身还占地方,显然不美。第二种是“大而全”的功能:一个优化器最好能同时处理稀疏特征、控制权重衰减、支持梯度裁剪、自动调整学习率,省得工程师自己拼装一堆组件。第三种解读还带点审美意味——一个好的优化器,它的算法逻辑应该简洁优雅,实现不该超过几百行代码,却在各种任务上都有稳定的表现。

我个人的观点是,“美”比“大”更难。越是大规模的训练,越要求优化器稳定、可预期。Adam 家族之所以能长期占据主流,就是因为它在一套统一框架里同时解决了方向、步长、稀疏处理等问题,工程实现干净,而且 ResNet、Transformer、扩散模型这些主流架构直接用默认参数就能有不错效果。相比之下,Lion 的公式非常简洁,效果在不少任务上比 AdamW 更好,但它的超参数对学习率的敏感度更高,调起来更需要手感,没那么“随手可用”。所以如果你问我有没有一个又大又美的优化器,我的回答是:现阶段还没有完全通用的答案,只能根据你的任务目标去选。

1.3 一次“更换优化器”决策的真实代价

很多朋友把换优化器想得太简单,以为改一行optimizer = AdamW(...)就完事。实际上,这个决策会连锁影响后面一整条训练链路。首先是学习率,SGD 的默认学习率是 0.01 到 0.1 这个量级,AdamW 则是 1e-4 到 3e-4 左右,Lion 通常还要更小。其次是权重衰减,AdamW 把权重衰减从梯度更新里解耦出来,如果从 SGD 切过来时不调整 weight_decay 的值,模型可能直接发散。再者是调度器,Cosine、OneCycle、线性 warmup 这些调度策略,对不同优化器的收益差别很大。

我见过一个最典型的案例:某团队用 SGD baseline 训练一套检测模型,效果一直卡在 30.5 mAP,后来听人建议换成 AdamW,学习率没改,weight_decay 也没改,结果训练直接 NaN。排查了两天,最后发现是 AdamW 在默认 1e-2 的 weight_decay 作用下,配合偏大的学习率,把参数正则过头了。这说明换优化器不是“换个类名”这么简单,它要求你理解这个优化器内部是怎么处理梯度、怎么应用权重衰减的,否则你只是在碰运气。

2. 优化器参数不是玄学,关键看你怎么拆

2.1 学习率与权重衰减是一对耦合项

很多人把学习率、权重衰减、momentum、beta 这些参数当成互相独立的旋钮,一个一个试。但实际上,学习率和权重衰减是强耦合的。尤其是在 AdamW 这类解耦权重衰减的优化器里,权重衰减项是直接加到参数更新公式上的,它的大小要和学习率一起看。如果学习率是 1e-4,weight_decay 设 0.01,那么一次更新里权重衰减对参数的缩放比例大约是 1e-4 * 0.01 = 1e-6,这个量级在训练初期基本无感。但如果把学习率调到 1e-3,同样的 weight_decay 就会产生 1e-5 的缩放,积累几千步之后,参数范数会被明显压低,模型表现反而退化。

这也是很多“调参经验帖”让人困惑的地方:有人说 weight_decay 设 0.01 效果好,有人设 0.1 效果好,你们到底谁对?其实两个人都没错,关键在于他们的学习率、batch size、训练步数完全不同。正确的做法,是把“学习率和权重衰减的比值”当成一个整体来调。我在实际项目里会先固定一个合理的权重衰减范围(比如 0.01 到 0.1),测量模型参数范数的变化趋势,再反推学习率合不合适。如果参数范数在训练中不断膨胀,说明权重衰减太弱;如果 loss 还没降到位参数就已经缩成一团,说明权重衰减或学习率匹配出了问题。

2.2 beta 和 epsilon 的默认值到底能不能动

AdamW 里有两个很常见的默认值:betas=(0.9, 0.999)eps=1e-8。很多框架代码里写明“使用默认参数”,但你真的理解这两个数字在干什么吗?第一个 beta 控制一阶矩的指数滑动平均时间窗,0.9 大约对应最近 10 步的梯度加权;第二个 beta 控制二阶矩,0.999 对应最近 1000 步的梯度平方加权。二阶矩的滑动窗口越长,对梯度大小变化的反应就越迟钝,适合梯度噪声比较大的场景。当你把 batch size 从 256 减到 32,梯度噪声变大,如果还保持默认的 0.999,二阶矩估计就会偏滞后,导致步长偏大、训练震荡。这时候把 beta2 调到 0.95 甚至 0.9,反而更稳。

epsilon 的作用是防止分母除以零,但它同时也在影响“步长上限”。epsilon 越小,二阶矩估计对梯度大小的敏感度越高,更新步长会更大;epsilon 越大,对梯度历史信息的依赖越弱,步长越保守。有个有意思的经验是,当 loss 曲面特别陡峭时,把 epsilon 从 1e-8 提到 1e-6 或者 1e-4,能有效抑制早期发散。这个做法在混合精度训练里尤其常见,因为低精度计算下梯度噪声更大,稍微提高 epsilon 相当于给每个参数更新加了一个软上限。需要说明的是,这些调整没有绝对的金标准,关键是你得先理解它们各自影响的是什么,再结合自己的任务设计实验。

2.3 优化器状态也占显存,这不是小事

我遇到过很多“我的卡不够用”的求助,一看代码,模型不算大,瓶颈居然出在优化器状态上。AdamW 为每个参数保存一阶矩和二阶矩两个 float32 张量,意思是模型 1GB 的时候,优化器状态就要占 2GB 显存。如果用混合精度训练,模型权重可以存成 half,但优化器状态通常仍以 float32 保存,显存占用依然很高。这不是“少想一步”的问题,而是很多新手教程压根不讲的部分。

解决思路有三种。第一种是换用 Adafactor,它用按行和按列的统计量近似完整二阶矩,显存占用可以从 2 倍模型参数量降到接近 1 倍,效果在 Transformer 类任务上和 Adam 很接近。第二种是用 8-bit 优化器,比如 bitsandbytes 库里的 8 位 AdamW,把一阶、二阶矩量化存储,显存几乎减半,代价是偶尔需要一些 warmup 来稳定训练。第三种是模型并行或做梯度累积的同时切分优化器状态,但这需要额外的框架支持,工程复杂度高。我自己的做法是:如果模型能放进单卡,直接用 AdamW 配混合精度;如果放不下,先试 Adafactor,不行再上 8-bit。别一上来就搞分布式,那是在给自己挖坑。

3. 实操:从零到一配置一套可复现的优化器方案

3.1 先定 baseline,再用控制变量法找方向

这一步听起来很基础,但我发现绝大多数翻车案例,都是因为一开始没把 baseline 定清楚。所谓 baseline,不只是在某个数据集上跑出一个数字,而是要有一份结构化的训练配置,包括优化器类型、学习率、batch size、调度器、预热步数、权重衰减、梯度裁剪阈值,甚至数据增强策略。这份配置写在代码里或者实验管理平台里,之后每次只改其中一个变量,你才能判断“效果变好”到底是谁的功劳。

举个例子,我在做图像分类任务时,第一步固定用 SGD with Momentum,学习率 0.1,momentum 0.9,weight_decay 1e-4,batch size 256,Cosine 调度,训练 100 epoch。这个 baseline 的效果可能不是最好的,但它非常稳定,而且 SGD 的每个参数对结果的影响路径都相对清晰。然后我再切到 AdamW,学习率从 3e-4 开始扫,weight_decay 从 1e-2 开始扫。你会发现,当你面对一个复杂模型时,SGD baseline 能帮你快速定位到底是不是优化器本身的问题,而不是被某个玄学超参带偏。

这种方法的另一个好处是方便复现。你能精确地告诉同事或者未来的自己:“我在第 x 次实验里把优化器从 A 换成 B,其他条件保持不变,指标变化了 y”。而不是说“我好像调了一下学习率,然后效果好了一点”。实验科学和炼丹的区别,很多时候就在这一份 baseline 的清晰度上。

3.2 用 PyTorch 搭一套三档配置:SGD、AdamW、Lion

下面我给你一份可以直接改来用的代码框架,覆盖三种主流优化器的配置方式。我用 PyTorch 写,是因为它在工业界和学术界都用得最多。

import torch from torch import nn from torch.optim import SGD, AdamW from torch.optim.lr_scheduler import CosineAnnealingLR, LinearLR, SequentialLR # 假设 model 已经定义好,dataloader 也已经准备好 model = nn.Sequential( nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10), ) def build_optimizer(model, name, lr, weight_decay): if name == "sgd": optimizer = SGD( model.parameters(), lr=lr, # 常用 0.01 ~ 0.1 momentum=0.9, # 经典取值 weight_decay=weight_decay # 常用 1e-4 ~ 5e-4 ) elif name == "adamw": optimizer = AdamW( model.parameters(), lr=lr, # 常用 1e-4 ~ 3e-4 betas=(0.9, 0.999), # 可以先不动 eps=1e-8, weight_decay=weight_decay # 常用 0.01 ~ 0.1 ) elif name == "lion": # torch 标准库没有 Lion,需要从 lion_pytorch 安装 from lion_pytorch import Lion optimizer = Lion( model.parameters(), lr=lr, # 常用 1e-5 ~ 1e-4,注意比 AdamW 小 weight_decay=weight_decay ) else: raise ValueError(f"Unknown optimizer: {name}") return optimizer # 以 AdamW 为例,组合学习率调度:先线性预热,再余弦退火 optimizer = build_optimizer(model, "adamw", lr=2e-4, weight_decay=0.05) total_steps = 10000 warmup_steps = 500 warmup_scheduler = LinearLR(optimizer, start_factor=0.1, total_iters=warmup_steps) cosine_scheduler = CosineAnnealingLR(optimizer, T_max=total_steps - warmup_steps) scheduler = SequentialLR( optimizer, schedulers=[warmup_scheduler, cosine_scheduler], milestones=[warmup_steps] ) # 训练循环中常规调用 for step, (inputs, labels) in enumerate(dataloader): optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() # 可选梯度裁剪,对Transformer等结构尤其重要 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() scheduler.step()

这份代码里有三个点值得展开。第一,SequentialLR把 warmup 和 cosine 阶段拼在一起,是现在训练 Transformer 类模型的常见做法,预热阶段给优化器一个“适应期”,避免一开始步子迈太大。第二,梯度裁剪不是优化器的一部分,但它经常决定模型能不能收敛,我会在后面的问题排查里再讲。第三,Lion 的学习率不要照抄 AdamW,我见过很多人在换成 Lion 后忘了调小学习率,导致 loss 直接爆掉。

3.3 训练曲线和日志:别只盯着 loss 这一个数

很多人习惯只记录 train loss,loss 降了就开心,loss 不动就焦虑。但优化器选没选对,光看 loss 是不够的。你至少要同时记录以下几项:学习率当前值、gradient norm(梯度范数)、parameter norm(参数范数)、train loss、validation loss 或相关指标。gradient norm 尤其重要,如果它在训练初期就异常大,说明模型结构和优化器配合有问题;如果它缓慢增长但 loss 不降,可能需要调整学习率或做梯度裁剪。

这又引出一个实用性建议:一定要有结构化的实验记录。不管是 W&B、TensorBoard 还是本地 CSV,都比你在终端里瞄几眼可视化强得多。我自己会用一张表来记录每次实验的关键参数组合和结果,下面这张表给你参考:

实验编号优化器学习率weight_decaybatch_size调度器最终指标备注
001SGD0.11e-4256Cosine0.892baseline
002AdamW2e-40.05256Cosine0.901比 baseline 更稳
003AdamW2e-40.05128Cosine0.898batch 变小后需要微调
004Lion5e-50.02256Cosine0.905收敛快,但对 lr 更敏感

这种记录方式的另一个好处是,你能在项目结束后复盘:为什么这个 batch size 配合这个优化器效果最好?它的根本原因是梯度噪声水平变了,还是有效学习率变了?带着这些思考,你会慢慢从“调参侠”变成真正理解训练过程的人。

4. 常见问题与排查技巧实录

4.1 loss 发散或直接 NaN,先别急着换优化器

训练一开始就 NaN,是优化器相关最常见的问题。很多人的第一反应是换个优化器,但我的建议是照着下面这个顺序排查。先看数据里有没有 NaN 或无穷大,再看看模型输出是不是 NaN,如果都不是,再怀疑优化器。我遇到过最讽刺的情况是,一个朋友把优化器从 Adam 换成 SGD,问题立刻消失,他以为是优化器的问题,结果后来发现是 label 里有脏数据,Adam 对异常梯度的响应更剧烈,把问题放大了。

如果确认是优化器和学习率的问题,我的标准操作是:先把学习率下调一个数量级,看训练是否恢复稳定;再把梯度裁剪打开,max_norm从 1.0 开始试。这两个动作都能快速判断问题是不是“步长过大”导致的。如果调完依然发散,检查 weight_decay 是否过大,特别是 AdamW 的解耦权重衰减,在长训练中会持续压缩参数范数,权重衰减过大会导致 loss 降到某个点后反升。最后一步才是换优化器,而且换的时候一定要把学习率调小,没有例外。

4.2 收敛太慢,先看学习率调度再换算法

如果训练能跑但一直不收敛,问题往往不在优化器类型,而在学习率和调度策略。这一步我建议对比两类指标:gradient norm 是否在合理范围,parameter update 的幅度是否和参数范数在一个量级。如果 gradient norm 持续很小,可能是网络输出饱和、梯度消失,也可能是初始化不合适,这时候换优化器也没用。如果 gradient norm 正常但 loss 不降,考虑调高学习率或者换成 OneCycle 这种带热启动的调度策略。

另一个很容易被忽略的问题是 warmup 步数设置。学习率从 0 或一个很小的值线性爬到峰值,是为了避免模型刚初始化时参数还很脆弱、大梯度直接把训练搞崩。但 warmup 过长会浪费大量训练步数,特别是你总共才训练 1000 步,warmup 却占了 300 步,那收敛自然就慢了。经验值是按总步数比例来设,通常建议 1% 到 5%,我大部分任务用的是 2%。如果是微调预训练模型,warmup 可以设得更短。实际项目中,我往往会单独跑一个小步数的实验来观察前几十步的 loss 变化曲线,据此决定 warmup 应该拉长还是缩短。

4.3 显存不够,优化器状态是最常被忽视的元凶

我在前面提过 Adam 的优化器状态占显存是模型参数的两倍,这里再展开讲一下排查路径。当你发现一张卡装不下模型时,先查三件事:模型参数本身占了多少显存、前向激活值占了多少、优化器状态占了多少。如果激活值是主要瓶颈,那就该做梯度检查点(activation checkpointing)或者减小 batch size;如果优化器状态是主要瓶颈,那就该换 Adafactor 或 8-bit 优化器,而不是一味升级显卡。

我在一个文本生成任务里实践过:同样一个 1.2B 参数模型,用 AdamW 需要大约 28GB 显存,换成 Adafactor 只要 18GB 左右,而且训练曲线几乎一致。这个替换在工程上非常值得。当然 Adafactor 也有一些注意点,它的二阶矩近似对某些任务可能不如完整 AdamW 稳定,所以切换后要更频繁地观察梯度范数。这里我想强调一个观点:优化器不是一个“需要时才想起来的组件”,它直接决定了训练能跑多大规模、需要多少卡、持续多久,应该在项目最初设计时就纳入考量。

4.4 断点续训后指标漂移,可能是优化器状态没保存干净

断点续训是一个看起来简单、实际上容易埋坑的操作。很多人只保存了model.state_dict(),再加载模型权重继续训练,却忘记了优化器状态和调度器状态。要知道 Adam 的一阶矩和二阶矩是模型训练信息的一部分,如果它们被清零,相当于优化器“失忆”了,重新训练的收敛轨迹和之前完全不同,最终指标可能漂移。断点续训的正确姿势是同时保存并加载三样东西:model、optimizer、scheduler。PyTorch 的写法大致是:

# 保存 checkpoint = { "model": model.state_dict(), "optimizer": optimizer.state_dict(), "scheduler": scheduler.state_dict(), "epoch": epoch, "step": step, } torch.save(checkpoint, "checkpoint.pt") # 加载 checkpoint = torch.load("checkpoint.pt") model.load_state_dict(checkpoint["model"]) optimizer.load_state_dict(checkpoint["optimizer"]) scheduler.load_state_dict(checkpoint["scheduler"]) epoch = checkpoint["epoch"] step = checkpoint["step"]

这里有个小细节:如果训练过程中改了优化器配置,比如中途把学习率从 1e-4 调成 5e-5,然后直接 load 旧状态,旧的 optimizer 状态里可能残留之前的大步长信息。稳妥的做法是显式重建 optimizer 对象,再选择性地保留或重置优化器状态。这种问题不会每次都触发,但一旦触发,排查成本极高。我建议所有训练脚本从一开始就把三件套的状态保存写进去,不要等项目要交付了才想起来。

写在最后的个人体会

优化器这个领域,理论文章每年一大堆,新的变体层出不穷,有人迷信最新的算法,有人抱着 SGD 不放。但这些年做实际项目下来,我最深的感受是:优化器选型不是找一个“最牛的”,而是找一个“和你任务匹配的”。大规模预训练任务,稳定性和显存效率是第一优先,Adafactor 这类轻量方案很值得试;中小规模的微调任务,AdamW 依然是省心之选;如果你对收敛速度有极致要求,且愿意多花时间调参,Lion 可能带来惊喜。A Big Beautiful Optimizer 是否存在?我的答案已经变了:它不是一个固定的算法,而是你对训练目标的理解、对超参数关系的把握,以及对训练日志中每个细节的敏感。把这些基本功练好,你手上的每一个优化器,都可以用得很漂亮。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 16:46:58

鸿蒙设备街机模拟器完整指南:从选型到ROM配置与手柄调试

前阵子在平板上折腾街机模拟器,发现鸿蒙设备相关的零散资料特别多,但真正能讲清楚"怎么选、去哪下、装完怎么调"的少之又少。很多人一上来就搜"鸿蒙街机模拟器app下载",结果下了一堆来路不明的安装包,要么闪退…

作者头像 李华
网站建设 2026/9/8 16:44:39

AI文本人味化改造:从困惑度到突发性的实战指南

如果你最近一直在捣鼓AI写作,大概率会撞上humanizer这个词。我第一次注意到它,是朋友发来一篇纯AI生成的产品介绍,问我哪里不对劲。通篇读下来语法没毛病、逻辑顺得离谱,但就是有一股说不清的"机器味",让人不…

作者头像 李华
网站建设 2026/9/8 16:44:32

智慧社区物业SaaS平台PRD撰写指南:从状态机到多租户隔离

简介:智慧社区/智慧管家物业SaaS系统平台PRD文档,面向产品经理、UI设计师及SaaS平台研发团队,提供一套覆盖业主端、物业端、平台运营端的完整产品方案,可解决智慧社区场景下人员管理、智能门禁、收费停车、报修工单等核心业务需求…

作者头像 李华
网站建设 2026/9/8 16:43:56

IntelliJ IDEA 社区版:Java 开发场景的入门完整指南

IntelliJ IDEA 社区版:Java 开发场景的入门完整指南 【免费下载链接】intellij-community IntelliJ IDEA & IntelliJ Platform 项目地址: https://gitcode.com/GitHub_Trending/in/intellij-community IntelliJ IDEA 社区版(IntelliJ IDEA Co…

作者头像 李华