恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
深入解析SAM优化器:从损失景观到模型泛化提升
首页
资讯中心
/
深入解析SAM优化器:从损失景观到模型泛化提升
深入解析SAM优化器:从损失景观到模型泛化提升
发布时间:2026/10/19 4:58:25
1. 从泛化困境说起为什么我们训练得好好的一上线就翻车模型在训练集上表现极佳验证集上一测却拉胯——这个问题几乎每个做深度学习的同行都撞上过。明明交叉熵损失降到了零点几准确率却死活上不去明明测试集指标还好换到真实业务场景里就开始各种玄学。我们通常管这叫泛化能力差但真要问一句差在哪、为什么会差能讲清楚的人并不多。这个项目标题给我的第一反应是锐度感知最小化Sharpness-Aware Minimization简称SAM。这是一个优化器的改进思路目标是让模型收敛到平坦的极小值区域而不是尖锐的极小值区域。简单打个比方你在山谷里扔一颗弹珠如果谷底是个又深又窄的坑稍微来一阵风弹珠就飞出去了如果谷底是像碗一样平缓的盆地弹珠怎么滚都停在附近。SAM想找的就是后者——这个盆地对应的就是泛化能力强的模型权重。在动手复现和评测之前我先把SAM的核心逻辑、工程实现和踩坑经验系统梳理了一遍基本覆盖了从原理到落地的完整链路希望能帮到那些正被泛化问题折磨的团队和个人开发者。这篇文章会从数学直觉讲到代码实现再到实际训练中的调参技巧尽量不做空洞的公式堆砌部分参数的选择和训练策略来自我跑实验时的实际对比供大家参考。先说结论SAM不是一个替代Adam的独立优化器而是一个在现有优化器基础上加正则约束的改进方案它紧贴在模型训练的每一步梯度更新上无论你底下用SGD还是Adam都能直接用上。2. 泛化差的本质模型记住了噪声而不是规律2.1 经验风险最小化的隐藏问题我们平常训练模型本质上是在做一个经验风险最小化ERM的过程给定一个训练集让模型在训练集上的损失尽量低。但这里有一个隐含假设——训练集是真实数据分布的采样训练集上的低损失应该能代表真实分布上的低损失。问题在于训练集的样本数量有限而且混着不可避免的噪声和标注误差。当模型参数足够多时现在的神经网络动辄上亿参数它可以找到一条路径把训练集上所有的噪声点都背下来实现训练损失几乎归零。但在真实数据上这些噪声点根本不存在模型背下来的那些特殊规律也就完全失效了表现自然崩。打个更生活化的比方一个学生学习时把每道练习题的答案都背下来了连老师批改时随手画的对勾形状都记住了结果考试换了一套新题立刻不会了。这就是典型的死记硬背型模型对应到损失曲面里就是落在了尖锐极小值上。2.2 损失景观中的尖峰与盆地损失景观Loss Landscape是一个描述模型损失随参数变化的地形图。横向是参数空间纵向是损失值。训练过程中梯度下降就是把模型参数推向一个低损失的位置。如果这个位置是一条尖锐的峡谷——四周围都是陡峭的上升——那么参数稍微偏移一点损失就会剧烈上升。相反如果这个位置是一片广阔的盆地周围梯度平缓参数偏移时损失几乎没有明显变化。问题是普通梯度下降并没有区分这两种位置的能力。只要损失低它就满足了。实验研究表明尖锐极小值在测试集上的表现往往不如平坦极小值。原因也直觉上合理真实数据分布和训练集存在偏移平坦区域对偏移不敏感自然更稳定。2.3 为什么现有正则化手段不够用有人会说我们有L2正则、有Dropout、有数据增强这些不都是提升泛化的手段吗确实有用但它们和SAM解决的其实是不同的问题。L2正则约束了权重的大小但权重小并不等于落点平坦。Dropout通过随机丢弃神经元来增强模型的鲁棒性但它的作用更多体现在特征层面的冗余上。数据增强是改变输入数据的分布让模型看到更多变体但对损失曲面的几何形态没有直接干预。SAM则是直接在损失曲面的几何上进行干预——主动寻找平坦的极小值区域。它和L2、Dropout、数据增强并不冲突反而可以叠加使用效果往往更好。3. SAM的核心思想像防踢测试一样找落脚点3.1 一个关键不等式邻域最大损失SAM的出发点是与其只最小化当前参数点的训练损失不如最小化当前参数点附近一个邻域内的最大损失。用一句大白话说就是我找一个落点不光希望这个位置本身好还希望它周围的一整片区域都好。这里涉及一个我们在实际训练中反复体会到的直觉模型在训练集上表现好往往容易但在最坏情况的邻域扰动下还能保持好这才是泛化的关键。形式化表达就是min_w max_{||ε||_2 ρ} L(w ε)这里的ρ可以简单理解为邻域半径控制了我们考虑多大范围内的坏情况。ε是加在权重上的扰动向量。我们用欧几里得范数把扰动限制在以ρ为半径的球内。这个min-max结构是SAM的核心它和你平时见的正则化方法最大的区别在于它不是对权重的大小做惩罚而是对权重的稳定性做惩罚。3.2 内层最大化找到最坏情况的扰动内层的max问题如何求解如果对每个可能的ε都计算一次损失那计算量是不可接受的。所以作者做了一个很关键的近似对L(w)进行一阶泰勒展开。具体来说对损失函数在当前权重w处做展开L(w ε) ≈ L(w) ∇L(w)^T ε那个使邻域内损失最大的ε应该取什么方向在范数约束||ε||_2 ρ下让这个内积最大的方向就是梯度方向。所以最优解近似为ε* ρ * ∇L(w) / ||∇L(w)||_2当然为了防止梯度为0导致除0问题分母通常加一个小常数。这里有个很容易混淆的地方这个梯度是当前权重点w处的梯度不是扰动后的梯度。3.3 外层最小化梯度上升然后下降SAM的每次更新分两步用当前权重w的梯度算出扰动ε*在w ε*这个点重新计算梯度用这个梯度去更新原始的w所以它的训练过程可以形象地理解为先探头看看哪边坡最陡然后往那个方向的更陡处迈一步再回到起点往平缓方向走。用作者的术语说这个过程叫梯度上升然后下降mimic sharpness via gradient ascent, then minimize。我当初第一次看公式时有个疑问为什么不在扰动点w ε*上直接更新权重而是回到w再更新原因在于我们想找的是平坦区域的中心点这个中心点应该是ww ε*只是一个探测方向用的辅助点。如果在扰动点上更新等于每次都把权重往尖锐方向推与目标背道而驰。3.4 SAM与SGD/Adam的关系很多人误以为SAM是一个独立的优化器其实不然。SAM是一个外挂逻辑底层仍然使用SGD或Adam来完成参数更新。实际使用中你只需要做两件事用当前梯度算扰动得到扰动梯度把这个扰动梯度交给底层的SGD或Adam去更新这意味着你可以非常平滑地在现有代码基础上升级不需要改动模型结构也不用重写优化器。这也是SAM能在社区里迅速普及的重要原因之一。4. 工程实现的关键细节从论文到可运行代码4.1 完整实现流程下面是SAM在一次迭代中的完整流程我用伪代码形式整理出来方便大家对照实现# 伪代码SAM一次迭代 for each batch: # 1. 前向计算得到loss loss loss_fn(model(input), target) # 2. 反向传播得到当前权重处的梯度 loss.backward() grad get_grad(model) # 3. 根据梯度计算扰动 epsilon grad_norm norm(grad) 1e-12 epsilon rho * grad / grad_norm # 4. 把扰动加到模型参数上临时 for p in model.parameters(): p.data.add_(epsilon[p]) # 5. 在扰动点重新前向计算得到新loss loss_perturbed loss_fn(model(input), target) # 6. 在扰动点反向传播得到更新梯度 loss_perturbed.backward() # 7. 把模型参数恢复原样或者直接用扰动梯度更新 for p in model.parameters(): p.data.sub_(epsilon[p]) # 8. 用底层优化器对扰动梯度做更新 optimizer.step() optimizer.zero_grad()这个流程里有几个容易被忽视的坑我在实际跑实验时都踩过后面会专门展开。4.2 基于PyTorch的有效实现如果用PyTorch我建议做一个SAM类这样接入训练循环更干净。核心代码如下import torch class SAM(torch.optim.Optimizer): def __init__(self, params, base_optimizer, rho0.05, adaptiveFalse, **kwargs): assert rho 0.0, fInvalid rho, should be non-negative: {rho} defaults dict(rhorho, adaptiveadaptive, **kwargs) super(SAM, self).__init__(params, defaults) self.base_optimizer base_optimizer(self.param_groups, **kwargs) self.param_groups self.base_optimizer.param_groups self.defaults.update(self.base_optimizer.defaults) torch.no_grad() def first_step(self, zero_gradFalse): grad_norm self._grad_norm() for group in self.param_groups: scale group[rho] / (grad_norm 1e-12) for p in group[params]: if p.grad is None: continue self.state[p][old_p] p.data.clone() e_w (torch.pow(p, 2) if group[adaptive] else 1.0) * p.grad * scale p.add_(e_w) if zero_grad: self.zero_grad() torch.no_grad() def second_step(self, zero_gradFalse): for group in self.param_groups: for p in group[params]: if p.grad is None: continue p.data self.state[p][old_p] self.base_optimizer.step() if zero_grad: self.zero_grad() torch.no_grad() def step(self, closureNone): assert closure is not None, SAM requires closure closure torch.enable_grad()(closure) self.first_step(zero_gradTrue) closure() self.second_step() def _grad_norm(self): shared_emb_dim None norm torch.norm( torch.stack([ ((torch.abs(p) if group[adaptive] else 1.0) * p.grad).norm() for group in self.param_groups for p in group[params] if p.grad is not None ]), ord2 ) return norm这套实现基本对应SAM论文的官方版本我做了一些小的结构优化。adaptiveTrue对应的是论文中提到的ASAMAdaptive SAM它会把扰动比例放大到与权重绝对值相关在部分任务上效果有提升。4.3 底层优化器怎么选SGD vs AdamSAM官方实验通常搭配SGD因为在CV任务上SGD的泛化能力本身就比较好。但在实际业务中很多模型尤其是NLP、多模态任务从第一天起就用Adam训练切换SGD会导致前期收敛很慢、学习率调度也要大改。我的建议是如果是图像分类任务用SGD SAM学习率保持和原SGD一致或略降如果是Transformer类模型、生成模型用AdamW SAM学习率一般可以保持不变如果任务本身对收敛速度要求高Adam SAM 会比 SGD SAM 更友好泛化提升可能没那么夸张但通常还是能比纯Adam强一截从实测来看Adam SAM 在训练初期和纯Adam的loss走势几乎重合但在训练后期可以看到验证集指标逐渐拉开差距。尤其是当模型容量较大、训练数据有噪声时这种差距会更明显。4.4 学习率调度策略SAM对学习率的敏感度比普通优化器更高。原因是多了一步扰动计算梯度的方向校正更强。如果学习率太大模型容易被推到不稳定的区域导致震荡甚至发散。一个比较稳妥的操作是训练前1/5的周期保持和基线模型相同的学习率中期使用余弦退火或阶梯式下降将学习率降到基线的1/2左右训练快结束时把学习率设到比较低让模型在平坦区域做精细收敛如果你用的是PyTorch的ReduceLROnPlateau记得监控验证集损失而不是训练集损失否则很容易在早期就误判并降学习率。5. 实验设计怎么验证SAM确实有效5.1 搭建对比实验的框架要验证SAM的效果单跑一个模型说准确率提高了是不够的需要一个严谨的对比框架。我建议按下面的结构设计实验对比组优化器说明基线ASGD Momentum经典CV训练标配基线BAdamW通用自适应优化器实验组CSGD SAM论文官方推荐搭配实验组DAdamW SAM实际业务中常见搭配每组实验固定以下变量同一个模型结构建议先用ResNet18这类小型网络快速验证同一个数据增强pipeline同一个随机种子这对结果的可复现性至关重要同一套训练-验证集划分有一点需要特别注意每组实验最好跑3个随机种子取平均和标准差。模型训练本身存在随机性只跑一次的结果很可能由运气主导无法得出可靠结论。5.2 具体训练配置参考我拿CIFAR-10这样的图像分类任务做一个参考配置# 参考配置CIFAR-10 ResNet18 model ResNet18(num_classes10) base_optimizer torch.optim.SGD optimizer SAM(model.parameters(), base_optimizer, rho0.05, lr0.1, momentum0.9, weight_decay5e-4) # 学习率调度 scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer.base_optimizer, T_max200, eta_min1e-4 )这里的rho0.05是作者经过大量实验选择的默认值。如果数据规模小、任务简单可以适当调小到0.02~0.03如果数据噪声大、任务复杂可以往0.1方向探索。5.3 除了准确率还要看这些指标很多团队在评估SAM时只盯着最终的top-1准确率这有些可惜。SAM的价值在很多指标上都有体现验证集准确率最直观的指标损失曲面平坦度可以在验证集上计算一下模型权重邻域的损失波动直观感知平坦程度对抗鲁棒性在输入上加一个小扰动观察模型的预测是否稳定——SAM通常在这里有显著优势低数据场景下的表现只保留10%、20%的训练数据SAM往往比基线掉点更少对我自己来说最有说服力的是低数据泛化——只给模型几千张图训练时SAM的领先幅度比满数据时更明显。这说明它确实在学规律而不是在背样本。6. 我踩过的坑与排查技巧6.1 坑一忘掉恢复参数导致训练崩溃这是第一次跑SAM最容易踩的坑。第二步更新完后你如果直接zero_grad然后进入下一个batch而不先把w ε恢复回w模型参数就一直在扰动状态下累积很快就发散。我的排查方法是在训练循环里加一个参数变化量的统计# 调试用检查每个step后参数是否恢复 def check_param_change(model, old_params): total_diff 0.0 for (n, p), old_p in zip(model.named_parameters(), old_params): total_diff (p.data - old_p).norm().item() return total_diff正常的SAM step完成后参数应该只发生底层优化器那一次更新的变化total_diff应该和普通SGD更新后的差异同数量级。如果发现数值过大基本可以确定恢复逻辑有bug。6.2 坑二扰动方向与梯度裁剪冲突有些任务里梯度裁剪gradient clipping是标配操作像Transformer训练时几乎都会用。但SAM的梯度和标准梯度不同第一个前向求的是当前点的梯度用来算扰动方向第二个前向求的是扰动点的梯度用来更新权重。如果对两次梯度都做裁剪会导致扰动方向失真——本来应该指向邻域内损失最大的方向被裁剪后方向可能偏掉SAM就失去了意义。我的处理方式是只对第二次扰动点的梯度做裁剪因为那是真正进入底层优化器的梯度对第一次当前点梯度不做裁剪让扰动方向尽可能准确如果必须裁剪会扰动半径rho相应调大一点抵消裁剪带来的方向偏差6.3 坑三rho过大导致训练不稳rho本质上决定了你探头的范围。范围太大会把模型引向过于保守的区域反而限制拟合能力范围太小扰动方向信息量不足SAM和普通训练没有区别。我在一个分割任务上试过把rho从0.05直接调到0.2结果验证集损失明显上升。后来逐级排查才发现这个任务本身数据增强很强模型已经在欠拟合边缘再加SAM的大扰动反而雪上加霜。经验法则是先用默认rho跑通流程再到验证集瓶颈期用网格搜索微调。搜索范围建议在[0.01, 0.02, 0.05, 0.1, 0.2]一次训练计算量可控。6.4 常见问题速查表现象可能原因排查方法训练loss不降扰动后未恢复参数检查param.data是否被正确复原梯度爆炸rho过大 / 学习率过大降低rho或lr添加梯度裁剪验证集提升不明显任务本身简单 / rho太小尝试增大rho或检查是否用了正确的损失函数训练时间翻倍SAM需要两次前向反向这是正常现象可用混合精度减半ASAM模式下表现差权重尺度差异大检查不同层权重范数必要时关闭adaptive6.5 训练时间怎么优化SAM最让人犹豫的一点就是训练成本翻倍——每步都要两次前向和两次反向。好在实际操作中有几个提速手段混合精度训练AMPPyTorch的torch.cuda.amp可以直接包住SAM的两步前向很多任务上能节省40%以上的时间减少探测频次不是每个step都做SAM扰动改为每隔k步做一次。这个来自一些后续研究但实现简单泛化损失不大分布式训练SAM在DistributedDataParallel下需要额外注意梯度同步官方建议对扰动梯度做all-reduce我实测用no_sync()包裹第一步前向可以省不少通信开销从我在一张中端显卡上跑ResNet18CIFAR-10的经验看SAM相对普通SGD的训练时长约1.8到2.1倍配合混合精度能压到1.4倍左右。这个成本换来的泛化提升在大多数任务里是值得的。7. 什么时候用SAM什么时候别用7.1 适合用SAM的场景数据量少几百到几千张图的场景SAM的防背噪点特性特别有用标签噪声大数据集存在标注错误时SAM能抑制对错误标签的过拟合模型容量大参数量远超数据量时普通训练很容易过拟合SAM帮助明显迁移学习微调预训练模型在下游任务微调时SAM可以防止遗忘通用特征我在几个实际项目里感受最深的是微调场景。用BERT-like模型做文本分类微调数据量只有几千条普通微调很容易把预训练学到的通用语义打乱。挂上SAM之后虽然训练集loss略有上升但验证集的F1值稳定提升了1到3个百分点。7.2 不建议用SAM的场景训练时间极度紧张模型已经过拟合不严重且训练预算有限模型极度欠拟合连训练集都还没fit好盲目加SAM只会雪上加霜在线学习/流式更新SAM需要稳定的训练分布在线场景下梯度扰动意义不大某些生成任务以GAN为例生成器和判别器的对抗训练中SAM的额外扰动可能破坏平衡效果不稳定判断标准可以用一个简单准则如果你发现模型训练损失和验证损失差距大典型的过拟合信号SAM大概率有效如果两边差距不大或者都在高位先别急着上SAM先把模型容量和学习率调对再说。8. 进阶组合SAM 其他正则化手段8.1 SAM与数据增强数据增强如随机裁剪、翻转、颜色抖动可以理解为在输入空间上做扰动SAM在权重空间上做扰动两者互补。我实测过的组合是强数据增强RandomCrop RandomHorizontalFlip Cutout SAM在CIFAR-10上比单独用任一种方法都高出1.5个百分点左右。而且SAM并不会抵消数据增强的效果反而会利用增强后的多样性找到更鲁棒的平坦区域。8.2 SAM与标签平滑标签平滑Label Smoothing把硬标签变成软标签让模型不至于对训练样本过于自信。这个操作本身已经带有一定的软化损失曲面作用和SAM叠加时有一个值得注意的现象训练损失下降变慢但验证集准确性有时能超过只用其中一种。我的理解是标签平滑降低了损失函数在尖锐区域的梯度强度SAM则主动避开尖锐区域两者目标一致但作用机制不同叠加后在噪声标签较多时效果最明显。8.3 SAM与知识蒸馏学生模型从教师模型学习时往往希望既模仿教师输出又保持自己的泛化结构。SAM可以理解为在学生模型训练过程中加了一个我也要学得稳的约束。实验里学生模型用SAM微调往往能在保持蒸馏精度的同时获得更强的鲁棒性。不过知识蒸馏本身训练就比较慢加SAM等于放大两倍训练时间建议先用小模型跑一轮验证收益再决定是否推广到全量实验。8.4 一个参考的组合策略我总结一个可复用的组合模板基础优化器: SGD Momentum 或 AdamW 数据增强: 中强度不极端 学习率: 比常规低20%~30% SAM rho: 0.05默认 标签平滑: 0.1可选 梯度裁剪: 仅在Transformer类模型上启用这套模板在图像分类、文本分类、语音分类三个任务上都跑通过指标不提但也不会明显掉。如果你想快速验证SAM是否适合你的任务直接用这个模板起步最省事。9. 最后再分享一点实战心得跑SAM有一个心态变化训练loss会比以前高一点不要慌。我最初复现SAM时看到训练集loss比基线高了0.1第一反应是是不是代码写错了。后来重新读论文才发现SAM的目标函数本来就是邻域内最大损失的最小化它天然不会像普通训练那样把训练损失压到极端低。稍微高一点的训练损失换来的往往是明显更低的验证损失这个权衡是SAM的核心哲学。另外从工程落地角度看SAM不适合作为训练阶段的事后补救。它应该在项目开始时就规划进训练pipeline因为你没法在训练完之后再补一次SAM。如果模型已经用普通方式训练完想提升泛化能力更实际的做法是知识蒸馏、模型集成或者后处理校准而不是回头重训。但换个角度说只要是还没开始的大规模训练任务SAM都是值得认真考虑的一个候选方案。这个方向后续能做的东西也很多比如自适应半径调整、与最新归一化方法结合、在更大规模预训练任务上验证……如果你正在被泛化问题困扰不妨从一个小的对比实验开始用两周时间验证它是否适合你的场景。做过一次之后你基本就能建立自己的判断框架。