1. 为什么SGD是深度学习最核心的引擎
先把结论放在前面:如果你只能记住深度学习优化里的一个算法,那一定是随机梯度下降(SGD)。后面眼花缭乱的Adam、RMSProp、AdaGrad,本质上都是在SGD骨架上做改良。这篇笔记是系列第五篇,前几篇把前向传播、反向传播、损失函数过了一遍,这篇终于轮到优化器了,也就是网络“怎么学”的问题。
先抛一个很多初学者都会有的困惑:我们已经有了损失函数,也知道要最小化它,为什么不能直接令导数为零求出解析解,非得一遍遍迭代?
因为神经网络是个极度复杂的非线性函数,参数动辄百万千万级别,损失函数曲面坑坑洼洼,根本不存在一个能用闭式解直接求出来的全局最小值。即便存在,面对的也是高维空间,计算量大到不现实。所以只能靠迭代逼近:算出梯度,沿负梯度方向迈一小步,让损失逐步下降——这就是梯度下降的基本思想。
SGD在这个思想上的改动只有一点:计算梯度时,不用全部样本,而是一次随机抽一小批,甚至只抽一个样本,用这个局部估计出来的梯度去更新参数。别小看这个改动,它让深度学习在大规模数据集上真正变得可行。
这篇笔记适合什么基础的人读?你只需要知道损失函数是啥、反向传播大致在干什么就能跟上。我会把SGD的数学直觉、代码实现、手绘的示意图、以及离线学习和在线学习的区别一次性讲透,把训练中那些踩过的坑一并交代。
2. 从全量梯度下降到SGD:一个“偷懒”却更聪明的改动
2.1 下山类比:你手头只有一张局部地图
想象你在山里浓雾中,要走到山底。全量梯度下降的做法是:先把整座山每条路都测量一遍,算出最陡的方向,然后走一步,再重新测量。这样走法理论上每一步都很准,但代价是你每走一步都得把整座山勘察一遍——数据量一大,成本高得离谱。
SGD的做法就接地气多了:只看脚下附近几米的方向,觉得那边像是下坡就走一步,走完再重新看。步子不一定每次都朝全局最陡的方向,甚至偶尔还会往上走几步——但没关系,整体趋势是向下的,而且每一步的成本极低,可以走非常多步。最终走到的位置,往往不比全量梯度下降差多少,甚至因为“随机性”反而更容易跳出局部极小点。
2.2 三种梯度下降的对比
设训练集有N个样本,参数为θ,损失函数为L(θ),三种更新方式的差异在“算梯度时用多少数据”:
| 方式 | 每次迭代使用样本数 | 梯度噪声 | 单次计算量 | 收敛速度 | 内存压力 |
|---|---|---|---|---|---|
| 批量梯度下降(Batch GD) | 全部N个 | 无,确定性 | 高 | 慢,每步都贵 | 高 |
| 随机梯度下降(SGD) | 1个 | 很大 | 极低 | 快但震荡大 | 低 |
| 小批量梯度下降(Mini-batch GD) | 常见取32/64/128 | 适中 | 适中 | 稳定又快 | 适中 |
注意一点:在工程实践和大部分深度学习框架中,所谓的“SGD”默认指的是Mini-batch SGD,不是严格意义上的单样本SGD。PyTorch里的torch.optim.SGD,你传入一个batch的数据,它按这个batch的平均梯度去更新。真正每次只拿一个样本的版本,反倒很少直接用。
2.3 为什么Mini-batch是甜点位
全量梯度下降虽然方向准,但有两个硬伤:一是大数据集下算一次梯度要遍历全部样本,算力扛不住;二是确定性梯度容易一头扎进尖锐的局部极小值,很难出来。
严格的单样本SGD反过来,每步噪声太大。想象一个碗状的最低点,单样本的梯度估计方差巨大,走到碗底附近后依然来回乱跳,收敛很慢,甚至不收敛。
Mini-batch恰好平衡了这两者:一批样本的平均梯度方向已经比较靠谱,噪声又保留了足够的“随机扰动”,既能加速收敛,又天然具备一定的逃离局部极小的能力。再加上显卡的并行计算特性,32个样本的前向传播时间远小于32个样本逐个计算的时间,硬件的性价比也最高。
2.4 SGD的数学形式与直观含义
SGD的更新公式写出来很简单:
θ = θ - η * ĝ其中η是学习率,ĝ是用一个小批量样本估计出来的梯度:
ĝ = (1/m) * Σ ∇θ L(x_i, y_i; θ), i从1到mm就是批量大小(batch size)。整个SGD的精髓就是:用一小撮样本的梯度均值,去近似整个数据集的梯度。从期望角度看,这个估计的均值是无偏的;方差则随着batch size增大而降低。
这里补一个经验数值:batch size在32到256之间是多数任务的甜点区。增大batch size通常会带来更稳定的梯度,但过大的batch size会让模型泛化能力变差,原因是梯度噪声变小、更接近全量梯度下降,容易收敛到sharp minima(尖锐极小点)。这是我在实际训练中体会很深的一点:为了省时间去拉大batch size,结果验证集准确率反而掉了,最后老老实实调回64。
3. 手画SGD示意图:一张草图画清楚梯度的本质逻辑
这篇笔记的标题里有“附手绘图”,很多读者私信问我是用什么工具画的。其实我就是拿纸笔和手机拍的,或者用普通画图软件直接勾的。手绘图的优势不是精美,而是强迫你自己把每个细节走一遍脑子,这比看十条别人的精美示意图都管用。
3.1 最经典的二维损失曲面下山图
竖轴是损失值,横轴是参数值,画一条类似山谷形状的曲线。在曲线右侧高处标一个实心圆点,写上“当前参数位置”,然后从该点沿曲线切线往下画一条带箭头的直线段,箭头指向曲线更低的一侧。箭头旁边标注“梯度方向”,然后在终点再画一个空心圆,标注“更新后位置”。最后在旁边写:
θ_new = θ_old - η * ∇L(θ_old)这张图把SGD最关键的信息全画出来了:更新的方向是负梯度方向,更新的幅度由学习率η控制。如果η太大,箭头会直接从山谷的另一侧飞出去,图中可以在对侧画一条虚线加一个叉,标注“η过大导致越过谷底”。
3.2 等高线视角:SGD的折线路径
在二维参数空间里画一组椭圆形的等高线,圆心是极小值点。用一条折线从外侧向圆心绕进去——注意这不是直线,而是一条来回锯齿形、不断向圆心逼近的线。
这张图画的其实是SGD的真面目:由于每次只用一小批样本,梯度方向有噪声,更新方向会带有一定随机偏差,于是一路上都磕磕绊绊、来回震荡,但总体趋势是往中心区域移动的。这正好说明了为什么SGD的loss曲线会有明显的“毛刺感”,而不是平滑下降。
我当时画这张图最大的体会是:了解了路径特征,再去看训练日志里那条抖得厉害的loss曲线,就不再焦虑了。震荡本身就是SGD的工作方式,只要整体趋势往下走就没问题。
3.3 画手绘图的两个实用原则
如果在做学习笔记,建议以下三点:
- 先画带等高线的损失曲面图,这是理解一切优化器行为的基础
- 在图上把公式和图示对应起来,哪怕只是简单地在箭头旁写“-ηg”,这个联立过程本身就是深度理解
- 手绘不必追求精致,画错了重画一遍反而记得更牢
我自己画过不下二十版SGD示意图,但真正理解得最透彻的一次,是某天随手画的一张潦草草稿。那个瞬间忽然想明白了为什么batch size增大时梯度方向会更“准”,因为底层的几何直觉一直在。所以强烈建议你试试手绘,也许画完你就“通”了。
4. 学习率与关键参数:SGD性能的天平
4.1 学习率η:小则慢,大则飞
学习率是最重要的超参数,没有之一。如果把SGD比作开车下山,学习率就是油门。踩小了,车走得慢,训练半天还在半山腰;踩大了,车直接冲出去,翻沟里。神经网络的训练里,最常见的“翻沟”现象就是loss突然变成NaN——好多人第一反应是代码写错了,其实先检查学习率。
常用的学习率参考值:
- 全连接/CNN网络:0.01到0.1之间起步
- 深度ResNet类网络:从0.1起步,配合warmup
- Transformer类模型:通常用1e-4到3e-4,配合Adam优化器
注意,SGD对学习率异常敏感。我跑过不少项目,SGD的学习率从0.1调到0.01,loss直接不降了;调到0.5,两轮就NaN了。这与Adam这种自适应优化器完全不同,后者相对“皮实”一些。
4.2 动量(Momentum):解决震荡的物理直觉
SGD沿着窄长山谷走的时候,会在山谷两侧来回弹跳——因为梯度方向垂直于山谷壁的分量很大,而沿山谷方向的分量很小。一个经典解决方案来自物理直觉:让更新方向和“之前的速度”叠加,像小球从斜坡滚下,越滚越快,方向也趋向于沿山谷方向。
更新公式变为:
v = α * v_old + η * ĝ θ = θ - v其中α是动量系数,通常取0.9。引入动量后,垂直于主要前进方向的分量会互相抵消,而一致的方向不断累积,所以收敛速度和稳定性都有显著提升。
实际体验:同一个CNN任务,纯SGD可能要100个epoch才到90%准确率,加上动量后60个epoch就够了。如果你还在用纯SGD训练,先别急着换优化器,加上动量试试,效果往往比直接换Adam更有惊喜。
4.3 权重衰减(Weight Decay)与SGD的正则化
SGD更新公式里还能附加一项权重衰减,本质是L2正则化:
θ = θ - η * (ĝ + λ * θ)在PyTorch中对应torch.optim.SGD(params, lr, momentum=0.9, weight_decay=5e-4)。对于CNN分类任务来说,5e-4是个非常经典的默认值。weight_decay过大模型会欠拟合;过小则起不到抑制过拟合的作用。建议以1e-4为基准上下搜索。
注意区分:PyTorch实现SGD的weight_decay时,是直接往梯度上加λθ,与一些框架“解耦”的实现略有不同。如果在大规模预训练任务里迁移权重,要先确认优化器的具体实现,否则效果可能有差异。
5. 离线学习与在线学习:数据的两种喂法
5.1 概念辨析:不要让名词吓住你
离线学习和在线学习的区别,核心在于数据到达方式和模型更新方式的差异。
离线学习(Offline Learning/Batch Learning):数据全部准备好放在那里,模型一遍又一遍地在完整数据集上遍历训练。所有深度学习里的标准训练流程都是离线学习——先把训练集准备好,然后对全量数据做多轮epoch。常见的“小样本没有,数据要攒够再训”就是离线学习的典型写照。
在线学习(Online Learning/增量学习):数据是流式到达的,比如实时传感器数据、用户点击日志。模型每接收到一条或一小批新样本,就立即更新一次参数。因为数据量无限且无法存储全部历史,模型的更新方式只能是“来一条学一条、学完就丢”。
用一句话总结:离线学习是“攒够了再学”,在线学习是“边来边学”。
5.2 为什么要专门区分这两个概念
因为两者的核心矛盾完全不同:
- 离线学习的瓶颈是计算资源。数据集固定,目标是在有限计算资源下把模型训练到最优,所以需要设计优化器、学习率调度、数据增强等手段来“榨干”数据信息
- 在线学习的瓶颈是数据流特点。样本分布可能随时间漂移(概念漂移),模型需要持续适应新数据,但又不能忘记太久远的有用信息
SGD在这两种场景中的角色也不一样。离线学习中,SGD是主力优化器,配合epoch式的数据遍历,保证每个样本被反复学习;在线学习中,SGD天然“长”在这类场景里——因为它的每次更新只依赖当前一批样本,天然适配流式数据。甚至可以认为,严格意义上的单样本SGD本身就是在线学习算法。
5.3 在线学习的一个小例子
假设你运营一个推荐系统,用户行为数据每一秒都在进来。如果走离线学习的路线,就得每天晚上把全天日志攒起来统一训练一次,模型第二天上线时用的还是昨天的旧知识,白天新出现的热点完全不知道。这种情况下在线学习就有明显优势:每当一个用户行为样本到达,模型立刻做一次θ = θ - η * ĝ更新,几秒钟内就能让模型“知道”新热点。
当然在线学习也有麻烦事:一个异常样本就可能导致参数剧烈波动,所以通常需要配异常检测、学习率衰减和回滚策略。不过这个领域水很深,这里先指出方向,后续可以单独展开。
5.4 离线/在线与学习率的微妙关系
在线学习中,模型在持续接收数据,学习率不能归零,要保留一定的“适应能力”,否则遇到新数据就学不动了。实践中常给在线学习设置一个较小的恒定学习率,比如1e-4甚至更小,配合数据分布变化的监测机制。
离线学习则恰恰相反,通常会使用学习率调度:训练初期用较大学习率快速探索参数空间,然后按epoch或step逐步衰减,最后阶段用小学习率“精修”。经典的阶梯式衰减、余弦退火、ReduceLROnPlateau都是干这个的。
6. 常见问题与排查技巧实录
6.1 训练中loss变成NaN或突然爆炸
遇到这个问题的优先级最高,按顺序排查:
- 第一步:检查学习率。如果是0.1以上的SGD,试着直接除以10,多数情况下立刻恢复
- 第二步:检查输入数据是否有NaN或Inf。数据预处理没做好,比如日志里偶尔出现异常大值,很可能撑爆梯度
- 第三步:检查是否有除零操作。比如归一化时某个batch的方差为0;Transformer的attention mask设置不当也容易导致异常值
- 第四步:加入梯度裁剪。
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5)这种操作能兜住偶发的梯度爆炸——注意max_norm太小会影响正常训练,从5开始调比较稳
6.2 loss曲线震荡厉害,是出问题了吗
如果整体趋势在下降,只是曲线有毛刺,这是SGD的正常表现,不必过度紧张。梯度噪声带来的震荡是SGD与生俱来的属性。但如果你发现loss完全没有下降趋势,震荡范围越来越大,那就得当心了,大概率是学习率过大。
还有一个高频场景:验证集loss先降后升,训练集loss继续降——这是过拟合信号,优先加weight_decay、加Dropout、做数据增强,而不是去调学习率。
6.3 为什么SGD收敛效果好但训练曲线不如Adam平滑
很多人对比SGD和Adam时会有直观感受:Adam收敛快、曲线平滑,SGD慢吞吞且波动大。但不少实证工作发现,最终泛化性能上SGD往往不落下风,甚至更好。原因比较复杂,多数研究认为SGD持续的梯度噪声帮助模型跳到了更平坦的极小区域,而平坦极小值的泛化性能通常优于尖锐极小值。Adam的自适应性让它更快收敛,但有时也会收敛到更尖锐的地方。
实操建议:小数据集、调试期用Adam快速验证思路;数据集较大、追求最终精度时,不妨切换到SGD+momentum跑较长训练周期。
6.4 学习率调度在SGD训练中的重要性
SGD对学习率非常敏感,几乎是靠着“预定衰减计划表”才实现高精度收敛的。我自己在CIFAR类任务上试过不下五次:固定学习率0.1的SGD,训练到后期loss根本压不下去,就像一个漫无目的散步的人在谷底附近徘徊。而同样的设置只要在60%和80%的epoch处把学习率除以10,最终准确率直接高出三四个点。
常见的调度策略有:
- StepLR:每训练固定步数,学习率乘以一个固定系数
- CosineAnnealingLR:按余弦周期调整,适合大批量训练
- ReduceLROnPlateau:监控验证集指标,停滞时自动调低学习率
这些调度器在PyTorch里都有现成实现,建议优先用CosineAnnealingLR,几乎不需要手动调参,效果通常很好。
6.5 一个关于batch size的实战教训
之前一个图像分类项目,为了缩短训练时间把batch size从64提到256,learning rate也按直觉翻了四倍(线性缩放规则),结果验证集准确率掉了不少。后来把学习率调回原来的1.5倍左右才恢复。这说明大batch size对学习率的“最优值”并不是简单的线性关系,跟数据本身复杂度、模型结构都有交互。如果你也遇到“batch size一拉大,效果就变差”,先别急着怀疑模型,从学习率和epoch数入手查更现实。
7. 写在后面的一段真实体会与延伸建议
这篇笔记断断续续写了几天,中间翻了不少以前的训练日志和手写草图。整理下来最大的感受是:SGD真正难的不是算法本身——公式就那么两行——难的是在面对训练日志里那些奇怪现象时,你能用算法原理去解释、去定位问题。loss不降是学习率小了;loss先降后猛涨是学习率大了或者过拟合了;loss带着毛刺整体下降那是SGD的日常。学会看训练日志里那条曲线的“气质”,训练模型的功力就算真正上了一个台阶。
另外手绘的习惯我真的很推荐。不用画得多精美,随身揣几张纸一支笔,逼着自己把算法画出来、把公式推到能默写的程度,比反复看课本有用得多。我自己现在看一篇新论文,拿到新算法,第一步仍然是画草图。画着画着,很多本来觉得模糊的地方就清晰了。
如果你是按着系列笔记一路读到这里的,下一篇很自然地可以聊聊SGD的“亲戚”——Adam家族的原理与选型对比。但眼下,不妨先把SGD的手绘图画一遍,把离线学习和在线学习的概念差别用自己的话复述一遍,再做一次学习率从固定到衰减的对比实验。这种自己动手验证过的理解,才是真正带得走的。