2020年2月16日我的深度学习笔记写到第八篇。上一篇从经验风险最小化聊到泛化误差基本把“优化目标”讲清楚了但目标归目标真正让模型训练起来的是优化算法本身。这篇笔记把镜头拉到更新公式这一层重点拆两块基本优化算法里的梯度下降与动量以及很多人一开始不太重视、却常常决定训练能否正常起步的参数初始化策略。如果你正在复习深度学习基础、准备面试或者发现自己的loss曲线莫名其妙不收敛这篇应该对你有用。我会跳过复杂的证明尽量把“为什么这么做”讲明白再补一些我实际跑实验时踩过的坑。1. 优化问题不能只盯着“梯度下降”四个字1.1 参数空间里的一次次移动才是训练的本质训练一个神经网络说穿了就是在参数空间里找一个点让损失函数的值足够低同时不把训练集上的表现原封不动搬到测试集上。反向传播给出的梯度只是告诉我“当前位置往哪个方向走能最快降低损失”真正决定怎么走的是优化算法。所以我一直觉得把优化理解成“沿着负梯度走”是不完整的。负梯度只提供了一个方向步长、历史速度、噪声、起点位置共同决定了最终的落点。最近几年大家常用的自适应优化器也是在调整“步长怎么算”这件事但基本算法反而是理解一切后续变体的地基。1.2 基本算法和初始化为什么必须放在一起看这一篇笔记把“初始化策略”和“梯度下降、动量”并列来讲不是图省事。你在训练开始之前做的第一个动作就是给参数赋一个初值这个初值决定了网络一开始输出的数值范围进而影响第一轮梯度的大小影响动量缓冲区的初始状态。如果初始化尺度不对后面的优化算法再先进也很难救回来。尤其是深层网络前向传播经过几十层非线性变换方差要么爆炸要么消失loss在第一次更新时就已经废掉了。所以我在笔记里把这两件事绑在一起看基本算法解决“怎么走”初始化解决“从哪里开始”。2. 梯度下降的三种形态全量、随机、小批量2.1 全量梯度下降理论最干净实战最奢侈全量梯度下降的更新公式写成这样θ ← θ - η * ∇_θ L(θ)这里的 L(θ) 是全体训练样本的平均损失。每一步都会把整个训练集过一遍反向传播计算出来的梯度是整个数据集上损失函数的真实梯度。从数值上看这个方向最稳每走一步都严格往损失下降最快的方向走收敛曲线也很好观察。但它的问题非常现实训练数据一旦有几万、几十万条单次更新就要把整个数据集放进显存或者内存时间成本和存储成本都扛不住。深度学习的训练集规模通常都很大所以全量梯度下降在实际的深度网络训练里很少作为主力使用。它更适合小数据集、凸优化问题或者需要精确衡量梯度的调试场景。2.2 随机梯度下降单个样本的极端噪声随机梯度下降把更新单位缩小到一个样本θ ← θ - η * ∇_θ ℓ(x_i, y_i, θ)每次只拿一个样本来算梯度更新频率非常高。用一个样本的梯度代替全体梯度方向上有很大噪声但也正是这种噪声让随机梯度下降偶尔能跳出损失曲面里的某些局部极小值或鞍点。不过它的缺点也和优点一样明显loss曲线会非常抖像高倍镜下看心电图的杂波。如果学习率调不好训练过程会一直在某个区域内来回横跳甚至越跳越远。我现在很少用它直接训练网络除非是在线学习场景数据一条一条到来了就更新。2.3 小批量梯度下降折中后的工业标准实际训练里用得最多的是小批量梯度下降每次从训练集里随机抽 m 个样本算梯度的平均然后更新一次。θ ← θ - η * (1/m) Σ_{j1}^{m} ∇_θ ℓ(x_j, y_j, θ)这个小批量的大小通常取 32、64、128。为什么是这些数字一方面是因为 GPU 的并行计算粒度喜欢这种对齐的数字另一方面是批量太小梯度噪声大批量太大计算成本和全量梯度接近。我自己的经验是图像分类这类任务从 32 开始批量越大学习率可以适当调大批量越小学习率要谨慎一点。小批量梯度下降让每一轮更新不偏随机、不偏全量梯度带一点噪声但总体方向稳定是后面动量、自适应学习率方法存在的前提。2.4 三种形态对照和学习率的角色形态每次更新使用的样本数梯度噪声收敛稳定性使用场景全量梯度下降全部无高小数据、凸优化、调试随机梯度下降1大低在线学习、极端内存限制小批量梯度下降m如32/64/128适中中高深度网络训练主力学习率 η 是所有梯度下降算法的灵魂。它相当于每走一步的步长太小loss 下降慢到让你怀疑人生太大loss 直接飞出“盆地”表现为开局几个step就出现NaN或者loss先降后疯狂震荡。我在初学者阶段最喜欢做的一件事是固定其他设置只把学习率从 0.1 一路按 10 倍往下降看loss曲线形态的变化。这个操作看起来笨但对直观理解三种梯度下降形态很有帮助。3. 动量给梯度下降装上一个有记忆的速度3.1 动量的物理直觉与指数滑动平均标准梯度下降只依赖当前梯度完全没有“记忆”。而损失曲面常常不是平滑的球面有的方向坡面很陡、很快就能下降有的方向坡面则比较平缓。如果只按当前梯度走参数会在陡峭方向的谷壁两边来回震荡白白浪费很多步。动量方法给参数更新加了一个速度项类比成一个球从山坡滚下来速度会积累不会因为局部坡度的细微变化立刻掉头。用公式表示一般是先更新速度再更新参数v ← μ * v - η * ∇_θ L(θ)θ ← θ v这里的 μ 是动量系数通常取 0.9。v 是历史梯度方向的指数滑动平均当连续几个step的梯度方向一致时v 的模会不断增大当梯度方向在水平方向上反复横跳时v 把正负梯度累加互相抵消。所以动量能加速一致方向的更新抑制振荡方向的更新。3.2 标准动量的更新公式与工程实现工程上实现标准动量时我习惯按下面这段伪代码来理解# 标准动量更新伪代码 v v * momentum - learning_rate * grad param param v这个写法和数学公式唯一需要注意的地方是符号约定。有的框架把速度定义为 v μ * v learning_rate * grad然后参数更新用 param param - v。两种写法数学上等价只是正负号放的位置不一样。我在复盘代码的时候经常被这一步绕晕所以建议你自己维护一个统一符号grad 统一表示损失对参数的梯度速度项统一朝负梯度方向累积。动量和学习率之间还有一个微妙关系加入动量的等效步长在方向一致时比普通梯度下降更大。也就是说如果原来学习率 0.01 已经临界收敛加了动量 0.9 之后可能就会过冲。所以调参时不要同时大改这两个值一次只动一个。3.3 提前量动量先迈半步再看脚下的坡标准动量是“先根据历史速度滑过去到了地方再看路”。带提前量的动量则反过来先用当前速度把位置向前探半步再计算探到那个位置的梯度用这个梯度来更新。v ← μ * v - η * ∇_θ L(θ μ * v)θ ← θ v把这个公式里的 θ μ*v 看成一个临时探路点。你可以理解为一个跑步的人不是等到脚落地才判断方向而是在空中就根据落点的坡面调整下一步。这个变体在很多情况下比标准动量更稳收敛速度也稍有提升尤其在回归或生成模型这类连续损失曲面上。我见过不少资料把“带提前量”当作一个额外的可选技巧实际上很多成熟优化器都默认集成过类似思路。你只需知道它是标准动量的一个“看远处的修正版”不是另一个完全独立的算法。3.4 动量系数的调节经验动量系数 μ 的常见值与效果如下μ 取值效果0.5更像轻微平滑的梯度下降适合动量大改场景0.9最常见起点兼顾速度与稳定0.99平滑更强适合梯度噪声大的小批量场景但容易“刹不住车”我的调参顺序一般是先把 μ 固定为 0.9用一个小实验把所有重点放在学习率上等学习率定下来再小幅调 μ比如从 0.9 试到 0.95。80% 的模型在 0.9 附近就能得到满意结果没有必要一上来就 0.99。4. 参数初始化的底层逻辑起跑点决定了能跑多远4.1 对称性陷阱零初始化为什么不行如果网络所有参数都初始化为 0那么同一层内的所有神经元在前向传播时会得到完全相同的输出在反向传播时也会得到完全相同的梯度。无论训练多久这些神经元都会一直保持着对称状态无法学到不同的特征。这就像一群人长得一模一样、穿着一样、目标一样让他们分工合作一点意义都没有。常数初始化也一样只是把“对称”从 0 变成了同一个常数。真正有意义的是随机初始化让每个参数有独立的随机初始值打破对称性神经元才有机会分化出不同功能。4.2 方差守恒推导出合理的权重尺度随机初始化不能乱来。如果初始化尺度太大经过层层放大激活值直接爆炸成NaN如果尺度太小激活值逐层缩水到深层几乎等于零梯度也跟着消失。以最简单的线性层为例y W * x假设 W 和 x 独立且 W 的每个元素均值为 0x 的各分量均值为 0、方差为 1。输出的一维方差近似为Var(y) ≈ n_in * Var(W)这里的 n_in 是输入维度也就是 fan_in。如果我们希望输出方差和输入方差保持同一个量级就要求Var(W) 1 / n_in这个思路是整个参数初始化策略的核心初始化权重的方差要根据网络层的输入输出维度来缩放。偏置项在做方差推导时通常会额外引入一个常数所以实践上一般直接把偏置初始化为 0。4.3 两套常见的缩放初始化策略与适用激活函数针对不同的激活函数实际常用的初始化尺度有两套。第一套适合 sigmoid、tanh 这类在零点附近近似线性、两端饱和的激活函数。它同时考虑输入维度 fan_in 和输出维度 fan_out把权重初始化为均匀分布 U(-limit, limit)极限取limit sqrt(6 / (fan_in fan_out))这个来自均匀分布方差公式如果 U(-a, a)那么方差是 a² / 3。代入 a² 6 / (fan_in fan_out)得到方差 2 / (fan_in fan_out)。它既不像 1 / fan_in 那样只保前向传播也不像 1 / fan_out 那样只保反向传播而是中间均衡。第二套是为现代深度网络里常见的 ReLU 类激活函数准备的我会在下一小节详细讲。很多框架封装的“方差缩放初始化”底层就是按这两套公式来的你只需要按激活函数选择模式。4.4 ReLU激活函数存在的“方差减半”问题ReLU 会把小于 0 的输入全部截断为 0。假设输入 x 的分布接近对称均值为 0那么经过 ReLU 之后大约一半的信息被直接置零输出的方差差不多减半。为了让下一层的输入方差保持正常初始化权重方差就需要翻倍Var(W) 2 / fan_in对应到均匀分布边界就是limit sqrt(6 / fan_in)如果用正态分布初始化可以用均值为 0、标准差为 sqrt(2 / fan_in) 的截断正态。这里有件事很容易被忽略如果你的前一层已经用了 ReLU 且方差控制得刚刚好那么下一层的输入方差其实是上一层输出被截断后的方差而不是上一层输入层的方差。我在理解这一层推导时画了不少前向传播的示意图才彻底绕明白。简单记法是凡是激活函数里存在“把负半轴清零”的操作初始化方差就要适当放大。4.5 初始化与优化算法的配合一个好的初始化并不会保证训练一定快但它保证训练在第一几步不会“原地去世”。相反如果初始化特别差哪怕后面用动量用自适应学习率也需要花很多额外step先把参数搬到合理区域。另外我发现一个容易踩的心态陷阱觉得模型用上了自适应优化器初始化就可以随便来。其实自适应优化器最多是让不同参数的更新幅度更均衡它无法修复“信息从一开始就消失”的问题。信息一旦在前向传播里归零反向传播的梯度也为零自适应也拿不到信号。从工程角度看初始化还要注意随机种子同样的网络结构不同的随机种子可能带来最终的精度差异。让实验可复现是深度学习实验的基本素养我会在每次训练前固定种子并在关键实验里多换几个种子取中位数。5. 我在实际训练中反复踩过的四个坑5.1 动量太大loss出现“过冲锯齿”有一次做图像分类实验网络不深loss本来下降得挺好。我想加快收敛就把动量从 0.9 调到 0.99同时保持学习率不变。结果训练loss曲线出现一种非常典型的锯齿连续下降两三个step猛弹一下然后再降。验证集指标也在一定区间来回波动迟迟不上涨。我排查了一圈最后把动量调回 0.9锯齿立刻消失。原因很简单动量太大时速度项的惯性会把参数“甩过”最优区域类似开车速度太快冲过弯道。解决方式有两种要么降低动量把速度的刹车距离缩短要么在前几个epoch做warmup让动量从 0 慢慢爬到目标值。我更推荐后一种尤其在大批量、大学习率场景里。5.2 换激活函数后没换初始化尺度有一段时间我把网络的 tanh 激活函数批量替换成 ReLU但初始化策略沿用了之前的均匀缩放没有切换成适配 ReLU 的方差版本。结果训练loss下降变得很慢前几个epoch几乎没有动静。用脚本打印了每一层输出的标准差发现从第二层开始激活值标准差的数值就在逐层下降。换成适配 ReLU 的初始化后深层激活值的尺度恢复了loss在第一个epoch就开始稳步下降。自此以后我每次换激活函数都会顺手检查初始化策略是否需要同步更新。5.3 深层网络前向方差悄悄消失在一次搭建超过二十层的纯卷积结构的模拟项目里我用的是 1 / fan_in 的普通缩放初始化搭配 ReLU。结果前向传播到第十层左右特征图的标准差已经小到接近浮点精度极限反向传播在更早的地方就已经没有了有效梯度。这个问题靠看loss曲线很难第一时间发现因为loss在一开始就保持一个常数你甚至会误以为是学习率太小。最直观的诊断方式就是打印各层前向输出的均值、标准差。后来我们把初始化改为 2 / fan_in 的ReLU适配版本同结构网络在相同训练步数内验证集指标提升了非常明显。这个例子让我相信初始化不是一个可以随便选默认值的细节。5.4 一个快速诊断训练状态的模板我现在每搭一个新网络都会在正式训练前先跑一个五六行伪代码的“体检”# 伪代码逐层打印激活分布 model.forward(input_batch, return_hiddenTrue) for layer_name, hidden_output in hidden_units.items(): print(layer_name, hidden_output.mean(), hidden_output.std())我会关注两个点一是标准差不随网络层数急剧缩小或爆炸二是没有任何一层的激活值出现全零或全相同。形态正常之后再接入优化器用一小批数据跑两三个step观察loss是否肉眼可见地下降。如果loss在第一步就出现NaN优先检查学习率和初始化尺度如果loss一点不动优先检查激活值方差和梯度是否消失。这套流程听起来特别简单但真的能在我手里省下好几个小时的无效训练。优化算法和初始化策略说到底都是为了同一件事让梯度信号稳定地在网络里流动。只要信号还在流动训练就只是时间问题信号一旦断掉再好的优化器也没有用武之地。其实这一篇笔记写到这里我最大的体会倒不是哪个公式更漂亮而是“先诊断再调参”。我遇到的大部分训练问题都不是单一原因造成的而是学习率、动量、初始化尺度、批量大小好几个因素纠缠在一起。能把它们拆开看分步试才算是真正理解了优化这一章。