很多人学梯度下降的时候都会经历一个困惑期:明明书上说“沿着梯度的反方向走,就能到达损失函数的最低点”,可一到自己训练神经网络,loss曲线却像心电图一样上蹿下跳,完全没有想象中那种一路下滑的丝滑感。
这份笔记是我们神经网络原理学习笔记系列的第五篇,重点解决的就是这个困惑。这篇我打算把随机梯度下降算法SGD彻底讲透,顺带把经常和它一起出现的两个概念——“离线学习”和“在线学习”——也一并整理清楚。之所以把它们放在一起讲,是因为SGD的很多特性必须放在“数据是提前准备好还是持续到达”这个背景下才看得明白,脱离开数据供给方式讨论优化算法,很多结论都是悬空的。
这篇笔记适合正在学深度学习、被各种梯度下降变体绕晕的初学者,也适合快要面试、需要把优化器原理说清楚的同学参考。我尽量用大白话加手绘图说明的方式来写,争取让每个概念都能在心里长出画面来。
1. 先回答那个困惑:梯度下降为什么需要“随机”
1.1 从最陡下降方向说起
要理解随机梯度下降,得先回到梯度下降本身。神经网络的训练目标说白了就一件事:找到一组参数θ,让损失函数J(θ)的值尽量小。损失函数衡量的是模型预测和真实标签之间的差距,差距越小,模型越准。
那怎么找这组参数呢?最朴素的办法就是:算出当前参数点上的梯度∇J(θ),也就是损失函数上升最快的方向,然后朝它的反方向迈一步。参数更新公式长这样:
θ_new = θ_old − η · ∇J(θ_old)
这里的η是学习率,控制每一步迈多大。这个策略叫“最陡下降法”,因为每一步都选择了局部最陡的下坡方向。把损失函数想象成一片山地地形,梯度就是脚下山坡最陡的朝向,你每一步都朝最陡的方向下山,最终就能走到谷底。
这个思路本身完全没问题,问题出在“这个梯度怎么算”上。对于整个训练集,损失函数是所有样本损失的平均值,所以真正的梯度需要遍历全部N个样本才能算出来。当训练集只有几百个样本时,这倒无所谓;但当训练集有几百万甚至上亿条样本时,每一轮参数更新都要扫一遍全量数据,代价就太大了。
1.2 全量梯度下降的三个硬伤
全量梯度下降,就是每次更新都用全部训练样本计算梯度,它在理论上是干净的、稳定的,但在实际环境里至少有三个硬伤。
第一个是计算开销问题。每一轮梯度更新都相当于完整遍历一遍数据集,训练一个上亿参数的大模型,可能要迭代几十万轮,算力完全扛不住。而且大量样本之间存在高度冗余——比如图像数据里,相邻的图片往往很相似,每一张都算一遍梯度的边际信息量很小,纯属浪费。
第二个是数据结构的问题。很多训练样本彼此相似,全量算完的梯度本质上是个“平均场”,它会把各个样本上的个性化信号互相抵消掉。我们需要的恰恰是那些有差异的、有挑战性的样本带来的梯度信号,而不是一个被平均得毫无性格的总体方向。
第三个是工程上的致命伤:全量梯度下降没法做增量更新。今天模型上线以后,系统里又产生了新的数据,你想让模型学到这些新东西,就得把新旧数据合并起来重新训练一遍。数据永远在增长,这个方案在真实系统里根本跑不通。
1.3 我画的第一张图:损失函数地形
为了把这些抽象的东西变成直觉,我画了这张学习笔记的第一幅手绘图:
左半部分是三维视角的损失函数曲面,看着就像一只倒扣的碗,碗底最低。我在碗壁上标了几个参数点,每个点都画了一条带箭头的虚线,箭头所指的方向就是该点的梯度方向——也就是上坡最陡的方向。右半部分是这个碗的俯视图,我用一圈一圈的椭圆等高线来表示损失值相同的点,越靠中心损失越小,等高线越密的地方表示坡越陡。
这张图我旁边批注了一句特别重要的话:“梯度只告诉你当前位置最陡的方向,不等于全局最优的方向。”初期学到这里,很容易把梯度和全局方向混为一谈。实际上你沿着梯度走,能保证的是每一步都在局部下坡,但整条路径可能绕来绕去,不一定笔直通向全局最低点。这个认知为后面理解SGD的“抖动路径”提前打了底。
2. SGD的核心机制:用单个样本的梯度闯天下
2.1 一个样本的梯度和全量梯度是什么关系
随机梯度下降的思路非常大胆:既然算全量梯度太贵,那我每次只随机抽一个样本,用这个样本的梯度来近似全量梯度,直接更新参数。更新公式变成:
θ_new = θ_old − η · ∇J_i(θ_old)
其中∇J_i是第i个样本的损失对参数的梯度。抽完一个样本就更新一次,更新完了继续抽下一个,直到把这一轮(epoch)的所有样本用完,然后进入下一轮。
乍一看这种做法非常不靠谱——一个样本的梯度方向和全量梯度方向能一样吗?答案是:大概率不一样,甚至可能完全相反。但是注意,这里的“不一样”是有规律的。如果我们反复随机抽取样本,把大量单个样本梯度放在一起看,它们的平均方向会逼近全量梯度的方向。用数学语言说,单个样本的梯度是全量梯度的无偏估计,它的期望值就等于真实梯度。
这个性质太重要了,是整个SGD能够成立的基石。它不是靠某一次更新有多准,而是靠着“估计无偏”和“反复迭代”这两个机制,让噪声在长时间尺度上互相抵消,整体趋势始终指向真正该去的地方。
2.2 噪声不是坏事:跳出局部极小与鞍点
既然单个样本梯度是个带噪声的估计,那SGD的参数更新路径天然就是抖动的。很多人把这个抖动当成缺点,我一开始也这么想,后来学深了才发现,这个抖动恰恰是SGD宝贵的副产品。
为什么?因为全量梯度下降走过的路径太平滑了,它严格沿着“最陡下降”走,遇到局部极小值或者平坦的鞍点区域,梯度变零就直接卡住,没有任何机制帮它逃出来。而SGD因为有噪声,每次更新的方向都带着一点随机偏转。当模型掉进一个浅的局部极小值时,噪声可能会把它“踹”出来;当它走到鞍点附近时,噪声带来的不确定方向也能帮助它横穿过去。
我常跟朋友打一个比方:全量梯度下降像是一个喝了酒却坚持走直线的人,每一步都走得特别稳,但一旦走进死胡同就掉头无望;SGD像个脚步略微摇晃的人,虽然轨迹不好看,但不时歪那么一下,反而有机会从小巷子里绕出来。所谓“噪声驱动的探索”,在优化里不是玄学,是有理论支撑的。
2.3 第二张手绘图:两条完全不同的下山路径
这一章我画了本笔记的第二张手绘图,画的是同一个等高线地形图上,两条不同的优化路径对比。
全量梯度下降的路径我画成一条平滑的曲线,从起始点出来以后,几乎垂直于等高线,一条直线切向碗心,非常优雅。SGD的路径我画成一条粗粗的锯齿状折线,每两步就拐一个方向,有些地方甚至看起来在往回走,但如果拿铅笔沿着轨迹整体描一下,会发现它整体还是在螺旋式地逼近碗心。我在图下面写了一行总结:“路径的混乱程度不等于收敛的失败,要看整体趋势和最终落点。”
这张图还有一个细节:SGD因为抖动,走过的区域覆盖面更广,地形信息被探索得更充分。这在一定程度上降低了对初始位置的敏感度,也让它在复杂的非凸问题上比全量梯度更不容易早早陷进坏区域。
3. 三种梯度下降放在一张表里对比,以及batch size怎么选
3.1 BGD、SGD、Mini-batch的全面对比
学习SGD的过程中,完全可以不绕开另外两个近亲:全量梯度下降(BGD)和小批量梯度下降(Mini-batch Gradient Descent,简称MBGD)。我把它们放进了笔记里的对比表格,这也是每次重读笔记时都会扫一眼的地方。
| 对比维度 | 全量梯度下降 BGD | 随机梯度下降 SGD | 小批量梯度下降 Mini-batch |
|---|---|---|---|
| 每次更新的样本量 | 全部训练样本 | 1个样本 | 一小批样本,通常16~256个 |
| 更新频率 | 每遍历完一遍数据集更新一次,很慢 | 每看一个样本就更新,非常快 | 每处理一小批更新一次,适中 |
| 梯度方向质量 | 准确、稳定 | 噪声大、方向抖动 | 噪声适中、方向较可靠 |
| 收敛过程中的损失曲线 | 平滑下降 | 剧烈震荡,但整体趋势向下 | 既有明显下降趋势又带一定波动 |
| 硬件利用效率 | 内存一次性装下全部数据,不现实 | 无法利用矩阵批量计算,浪费算力 | 充分利用GPU并行计算,效率最高 |
| 能否在线更新 | 不能,来新数据要全量重训 | 能,天然支持流式数据 | 能,也适合增量训练 |
| 典型场景 | 小数据集、教学演示 | 概念原型、在线学习场景 | 工业界主流深度学习训练 |
我自己学完这张表最大的感受是:没有一种方案是全能的。BGD输在工程效率,SGD赢在理论简洁但工程上浪费算力,Mini-batch则是在“梯度可靠度”和“计算效率”之间取了一个让工程界满意的平衡点。
3.2 为什么工业界几乎都在用Mini-batch
如果翻开任何一套深度学习框架的训练代码,你会看到几乎清一色的Mini-batch训练。一个数据加载器每次“喂”给模型的不是一个样本,也不是全部数据,而是一小批。
Mini-batch能成为工业界的事实标准,核心原因有三个。第一个是硬件的适配性。现代GPU擅长大规模并行矩阵运算,单个样本的矩阵乘法和一批样本的矩阵乘法在时间上差不了太多,相当于“捎带手”就把几十个样本一起算了。SGD那种一个样本更新一次的模式,反而把硬件优势全浪费掉了。
第二个是梯度质量的平衡。Mini-batch相当于把若干个随机样本的梯度叠加后取平均,噪声被压低了不少,但又不至于像全量梯度那样把所有个性都抹掉。一般认为batch size在32到256之间时,梯度方向的可靠性和随机探索性都能得到保留。
第三个是工程灵活性。Mini-batch天然支持数据流水线式读取、分布式训练、以及断点续训。一批数据放到显存里算完梯度,更新完参数再读下一批,数据加载和计算可以重叠,吞吐量非常可观。
3.3 batch size和learning rate的联动关系
最近在学里面更深的内容,我发现batch size不是孤立决定的,它和学习率之间有强烈的联动关系。一个很粗糙的直觉是:batch size越大,每个mini-batch的梯度方向越接近真实梯度,噪声越小,那步子就可以迈大一点;反过来batch size越小,梯度越抖,步子就得迈小一点,不然会被噪声带着到处乱跑。
有个经验法则是,如果batch size翻倍,学习率也大致翻倍,这样可以维持相似的更新动态。为什么?因为一个mini-batch的平均梯度,其方差大概是单样本梯度的1/b,而更新公式里的方差大小无形中决定了参数轨迹的散度。想要保持同样的“噪声/信号比”,batch size与学习率按同一比例放大是合理的起手式。
不过这个法则也只是经验,不是绝对真理。尤其是当batch size大到一定程度后,梯度估计已经非常准了,再同步放大学习率反而容易让训练爆炸。所以更稳妥的做法是,改batch size之后先用一个小学习率试跑几步,观察损失曲线的形态,再决定学习率要不要跟着调。
4. 离线学习与在线学习:数据供给方式决定训练策略
4.1 离线学习:数据齐了再动手
离线学习,也叫批量学习,是最传统的训练模式。它的做法是:先把训练数据一次性收集齐全,然后模型在整份数据集上反复迭代训练,直到收敛,训练结束以后模型固定下来再投入使用。
这种模式适合数据相对稳定、采集成本低、对实时性要求不高的场景。比如一个图像分类模型,先攒十万张标注图片,训练好模型,之后拿去识别新图片,这时候模型是静止的,不再因为新数据而变化。离线学习的优点是可以利用全量数据做非常充分的训练,数据分布稳定,验证和评估也容易做,训练流程清晰可控。
但它的缺点也很突出:一旦环境发生变化,数据分布漂移了,就得重新准备数据集并训练。如果你负责的是一个用户行为预测系统,用户偏好在不停变化,离线模型可能刚上线几天就“过时”了。这本质上是把“训练”和“推理”两个阶段完全割裂开,一切更新都要靠重训来解决。
4.2 在线学习:边来数据边更新
在线学习则完全反过来。数据不是一次性准备好的,而是以数据流的形式持续到达。模型每接收到一条新样本,就用这条样本计算梯度并更新一次参数。整个过程是边来数据边训练,模型永远在滚动更新。
在线学习的最大优势是能持续跟踪数据分布的变化。用户的每一次点击、每一次购买、每一个搜索行为,都能在发生的那一刻被模型感知到,并通过SGD式的更新转化为参数调整。这在推荐系统、广告点击率预估这些场景里极为重要。另一个优势是数据存储压力小,模型接收到样本用完就可以丢掉,不需要把海量历史数据全部攒下来。
代价是训练的稳定性更难控制。数据流本身可能有噪声、有异常、有短期波动,模型可能被个别极端样本带偏。所以工程上做在线学习通常需要配合监控机制:跟踪损失均值的变化、检测参数突变、必要时做模型回滚或周期性评估。纯粹的在线学习在工业界往往是“理想形态”,更多落地为“定期用新数据微调”的准在线模式。
4.3 在线学习为什么和SGD天然搭
理解了这两种学习模式以后,再回头看SGD,会发现它们其实是天生一对。SGD每次只用一个样本更新参数,这和在线学习“来一个样本学一个”的节奏完全吻合。
离线学习也可以用SGD,但那是把已经准备好的数据反复打乱重新抽取;在线学习里的SGD则是一种更极端也更纯粹的形态——每次更新只有一个样本,样本用完即弃,永不重复。这种模式对应着随机逼近领域的经典结论:只要学习率满足特定条件,即使每个样本只用一次,模型依然能收敛到最优参数附近的区域。
所以可以这样理解:SGD是连接“离线大规模训练”和“在线流式训练”的一座桥梁。在离线场景下,它是Mini-batch的极端特例;在在线场景下,它就是主力算法。学到这里,SGD、离线学习、在线学习三个概念在我的笔记里就不再是割裂的三个知识点,而是一条逻辑线:数据怎么供给,决定了参数怎么更新,进而决定了选哪种梯度下降变体。
5. 学习率才是SGD的命门:大小、衰减与改进方向
5.1 学习率过大过小分别会发生什么
SGD里所有微妙的平衡,最后都集中在对学习率η的控制上。学习率本质上决定了你对梯度方向的信任程度:η越大,每一步越激进;η越小,每一步越保守。
学习率过大的典型表现是训练loss不降反升,甚至变成NaN。因为步子跨得太大,参数直接越过了峡谷的壁沿,跑到了损失更高的区域,然后下一轮又是更大的一步,最后彻底发散。这就像下山的时候不要命地冲刺,结果滚出山道直接摔下山崖。
学习率过小的表现则相反:loss下降得像蜗牛爬,训练了无数个epoch,模型还在原地徘徊。这种情况下模型最终也许能收敛,但时间成本高到无法接受。很多人训练大模型时发现loss下不去,第一反应是模型结构有问题,其实先检查一下学习率是否太小才是正经事。
比较隐蔽的问题是学习率适中但不够灵活,导致模型在收敛中段就开始来回震荡,始终无法进入最优点附近的小区域。这时候就需要让学习率随着训练进程逐渐减小,让参数能从大步探索平滑过渡到小步精修。
5.2 我画过的学习率三线对比图
这章笔记我也画了图,是一张三条loss下降曲线对比图。
第一条曲线用红笔画的,学习率设得偏大,曲线先是急速下降,然后突然掉头向上,最后直接飞出图外,我标注了“发散”。第二条曲线用蓝笔画的,学习率适中,loss一路波动的下降,前期快、后期慢,但始终没有完全平稳,在尾部能看到明显的高频抖动,我标注了“收敛但震荡”。第三条曲线用绿笔画的,学习率偏小,整条曲线又平又缓,一路慢慢下滑,直到最后一个epoch也没有完全走到底,我标注了“太慢”。
这三条线并排看的时候,理解learning rate的“适中”变得非常直观:它不只是一个具体的数值,而是一条介于“发散”和“蜗牛”之间的狭窄通道。而且这个通道的位置还跟模型结构、数据规模、batch size都有关系,没有任何一套万能数值。
5.3 从固定学习率到自适应方法的演进
既然固定学习率这么难选,工程上自然演化出了各种学习率调度策略。最简单的是分段衰减:每训练若干个epoch,就把学习率缩小一个数量级。进阶一点的用指数衰减或余弦退火,让学习率平滑地持续下降。还有近年很流行的warmup策略:训练开始时用很小的学习率预热,再逐步升到目标学习率,目的是避免模型冷启动时参数更新的方向太离谱。
顺着这个思路继续往前,就发展出了自适应学习率方法。比如动量法不再是单纯放大当前梯度,而是让更新方向带上历史梯度的“惯性”,这样前后方向一致的维度走得越来越快,方向频繁变化的维度被自然压制,SGD的锯齿抖动被大幅削弱。再往后还有按参数维度分开调整学习率的优化器,比如AdaGrad、RMSProp、Adam这些名字。它们本质上都是在回答同一个问题:学习率怎么给,才能让SGD跑得又稳又快。
我在笔记里给自己留了句话:“学SGD不要只盯着公式,要盯着学习率。”SGD面目朴素,真正的学问全在他旁边的那个η上。
6. 学完这一章我踩过的坑和画图心得
6.1 关于“随机”的两个最容易误读的点
反复重读这一章,发现有两个关于“随机”的点特别容易误读,值得专门记下来。
第一个误读是把SGD里的“随机”理解成“优化方向的随机噪音”。准确说,SGD的随机性来自“每次随机抽取一个样本”。如果在训练之前不把数据集打乱,样本的顺序里带有某种结构性规律,SGD会被这个规律牵着走,收敛会变慢甚至卡住。所以每次epoch开始前打乱数据,是SGD正确工作的前提步骤,不是可有可无的优化技巧。
第二个误读是以为SGD每一步都那么“随机”,所以完全不看方向。实际上单个样本梯度是永远朝“让当前样本损失下降”的方向走,它是有明确目标的,只是目标只针对那一个样本。把成千上万个样本的“个人目标”一轮一轮累加起来,最终才形成整个模型对全体数据的正确拟合。理解了这一层,再看“SGD收敛性证明”就不会觉得那是纯玄学了。
另外一个我在代码里踩过的坑,是把SGD写成了“一个样本更新一次,但在epoch里重复使用同一个样本”。这样等价于反复用一小撮样本训模型,其他样本完全没有参与,loss看起来下降得很快,但泛化能力一塌糊涂。后来排查了半天才意识到是数据加载器的shuffle写错了。这个坑很真实,也很离谱,写在这里给自己以后再提个醒。
6.2 手绘图怎么画才有用
很多人觉得手绘图花时间,画出来还不一定比教科书里的配图精致,但我觉得学习笔记里的手绘图有价值的地方恰恰在于“它是你自己提炼过的信息”。
我画图有两个原则。第一个原则是把抽象概念转化为空间关系。比如梯度下降的各种路径,画等高线图就能很直观地展示“为什么SGD会抖动但依然收敛”,“为什么batch size大路径更直”。这类地理地形图一旦画出来,之后遇到相关讨论,脑海里自然会浮现这张图。
第二个原则是一张图只讲一个概念。最开始的版本我试过把损失曲面、梯度向量、等高线、路径对比全都塞进一张图里,结果图面混乱,复习的时候反而要辨认半天。后来拆成三张图,每张图对应一个独立认知环节:地形长什么样、SGD怎么走、学习率如何影响走向。信息密度下降了,但复习效率大大提高了。这个经验其实也适用于所有写技术笔记的场景:追求一张图讲清一个点,远胜于一图多义。
6.3 推荐的验证实验:用小模型把三条路径跑出来
最后给自己留一个非常具体的验证实验方案,也分享给看到这份笔记的人。不要只在纸面上理解BGD、SGD和Mini-batch的区别,找个几十行代码就能搞定的回归问题,手动把三条路径画出来。
最简单的是造一个二维参数模型,让损失函数是一个椭圆碗形。分别实现BGD、纯SGD和Mini-batch版本的参数更新,把每次更新后的参数点记录到列表里,最后用绘图库把参数轨迹画在等高线图上。你很快会看到三种风格完全不同的轨迹:BGD长驱直入,SGD杂乱蜿蜒,Mini-batch则介于两者之间。
这个实验之所以值得做,是因为它能把笔记里的文字认知变成一手经验:你会亲眼看到SGD的路径走很多“回头路”,但最终也没比BGD差到哪里去;你会亲眼看到学习率一旦偏大,轨迹会像脱缰野马一样甩出去,再回头已经救不回来;你还会直观感受到shuffle对SGD轨迹的巨大影响。之后再去学动量、Adam之类的东西,你会发现万变不离其宗,都是在同一条轨迹上做“形状修正”。
我自己做完这个实验以后,对SGD的信任度大幅度提升,也对“深度学习训练是一门实验科学”这句话有了更深的体会。很多时候,别急着背公式,先把一张图画出来、把一条轨迹跑出来,比看十遍推导都管用。