
上个月我把一个视觉模型的训练任务从 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-4weight_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)和eps1e-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.1momentum 0.9weight_decay 1e-4batch size 256Cosine 调度训练 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(), lrlr, # 常用 0.01 ~ 0.1 momentum0.9, # 经典取值 weight_decayweight_decay # 常用 1e-4 ~ 5e-4 ) elif name adamw: optimizer AdamW( model.parameters(), lrlr, # 常用 1e-4 ~ 3e-4 betas(0.9, 0.999), # 可以先不动 eps1e-8, weight_decayweight_decay # 常用 0.01 ~ 0.1 ) elif name lion: # torch 标准库没有 Lion需要从 lion_pytorch 安装 from lion_pytorch import Lion optimizer Lion( model.parameters(), lrlr, # 常用 1e-5 ~ 1e-4注意比 AdamW 小 weight_decayweight_decay ) else: raise ValueError(fUnknown optimizer: {name}) return optimizer # 以 AdamW 为例组合学习率调度先线性预热再余弦退火 optimizer build_optimizer(model, adamw, lr2e-4, weight_decay0.05) total_steps 10000 warmup_steps 500 warmup_scheduler LinearLR(optimizer, start_factor0.1, total_iterswarmup_steps) cosine_scheduler CosineAnnealingLR(optimizer, T_maxtotal_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_norm1.0) optimizer.step() scheduler.step()这份代码里有三个点值得展开。第一SequentialLR把 warmup 和 cosine 阶段拼在一起是现在训练 Transformer 类模型的常见做法预热阶段给优化器一个“适应期”避免一开始步子迈太大。第二梯度裁剪不是优化器的一部分但它经常决定模型能不能收敛我会在后面的问题排查里再讲。第三Lion 的学习率不要照抄 AdamW我见过很多人在换成 Lion 后忘了调小学习率导致 loss 直接爆掉。3.3 训练曲线和日志别只盯着 loss 这一个数很多人习惯只记录 train lossloss 降了就开心loss 不动就焦虑。但优化器选没选对光看 loss 是不够的。你至少要同时记录以下几项学习率当前值、gradient norm梯度范数、parameter norm参数范数、train loss、validation loss 或相关指标。gradient norm 尤其重要如果它在训练初期就异常大说明模型结构和优化器配合有问题如果它缓慢增长但 loss 不降可能需要调整学习率或做梯度裁剪。这又引出一个实用性建议一定要有结构化的实验记录。不管是 WB、TensorBoard 还是本地 CSV都比你在终端里瞄几眼可视化强得多。我自己会用一张表来记录每次实验的关键参数组合和结果下面这张表给你参考实验编号优化器学习率weight_decaybatch_size调度器最终指标备注001SGD0.11e-4256Cosine0.892baseline002AdamW2e-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 是否存在我的答案已经变了它不是一个固定的算法而是你对训练目标的理解、对超参数关系的把握以及对训练日志中每个细节的敏感。把这些基本功练好你手上的每一个优化器都可以用得很漂亮。