训练不收敛的时候,你最先怀疑什么?学习率太大、数据没归一化、模型结构写错了,还是优化器参数没调好?我刚开始做深度学习那会儿也是这么排查的,直到有一次把学习率降到极小、数据预处理反复检查了好几遍,损失还是停留在高位好几个 epoch 不动,最后把目光挪到权重初始化上,才真正解决那个卡了很久的问题。权重初始化,说白了就是给网络里每一层的参数定一个起点。别小看这个起点,它决定了前向传播和反向传播里的信号能不能稳定地流下去,决定了优化器在第一步更新之后就进入一个相对平缓的“可训练地带”,还是直接掉进梯度爆炸或梯度消失的深渊。很多人觉得它不过是“随机给个初值”,实际远没那么简单。这篇文章我就结合自己做过的对比实验,把权重初始化提升收敛速度这件事完整拆开讲一讲。
1. 为什么权重初始化能直接决定收敛速度
1.1 一上来就崩:梯度爆炸与梯度消失的根源
先说一个经典的场景。你搭了一个 20 层的全连接网络,激活函数用 Sigmoid,权重初始化用均值 0、标准差 1 的标准正态分布。训练刚开始,第一轮 loss 就显示NaN,或者干脆在某个数字附近一动不动。这不是模型结构有 bug,十有八九是前向传播和反向传播里的信号幅度失控了。
原因可以这么理解:反向传播的时候,梯度要从最后一层往前传递,每经过一层,就要和当前层的权重矩阵乘一次。假设每层的权重矩阵谱范数大于 1,经过 L 层连乘之后,梯度幅度会按指数级放大,这就是梯度爆炸;反过来,如果权重的尺度太小,梯度经过多层连乘后会指数级缩小到接近 0,这就是梯度消失。你可以想象成复印一份文件,每复印一次清晰度都会下降一点,连续复印 50 次,拿到手的纸张上只剩一片灰;如果复印机时不时把黑点放大,50 次之后整张纸可能变成一团墨。深度学习里的梯度传播和这个几乎是一回事。
前向传播也有类似效应。如果权重初始化太大,每一层的激活值经过矩阵乘法和激活函数之后会迅速膨胀,最后一层可能直接溢出成无穷大,于是 loss 变成 NaN。如果权重初始化太小,激活值又会一层层衰减下去,到最后几层根本学不到有效特征。信号传不过去,梯度自然也没法传回来,收敛就成了空谈。所以权重初始化本质上是在解决“深度信号传输”的问题,而不是仅仅在解决“随机种子选什么”。
这个问题的根源还和网络深度强相关。浅层网络通常对初始化不那么敏感,两三层的小模型哪怕全部初始化为 0.01 的小随机数,照样能训练出可用结果。但深度一上去,连乘效应就被放大了,网络越深,对初始化的要求越苛刻。我一直建议团队里的新人,遇到深层模型不收敛,先把初始化这个变量排掉,再谈调学习率和数据增强。
1.2 初始化不是“随机给个初值”这么简单
权重初始化的本质,是要在训练开始时给每一层设置一个合理的权重分布,让前向传播的激活值和反向传播的梯度都能在“既不爆炸也不消失”的区间里流动。这里的关键词有两个:分布和方差。
只看“随机”二字当然不够。同样是随机,均值是 0 还是 0.5,方差是 0.01 还是 1,结果天差地别。更严谨地说,初始化还隐含着对神经元多样性的要求。如果所有神经元的权重都一样,那在同一层里,每个神经元接收到的输入相同、输出的梯度也相同,训练过程就无法打破这种对称性,网络再宽也只是一个退化的单神经元在训练。所以初始化要同时满足两个条件:第一,数值上要保证信号幅度稳定;第二,结构上要保证神经元之间彼此有差异。
满足这两个条件之后,初始化还决定了优化器起步时的“地形”。权重落在一个梯度幅度适中的区域,优化器走第一步就能明显降低 loss;落在梯度接近于零的平原,走很多步都感觉不到变化;落在梯度特别陡峭的悬崖,一步下去可能直接飞出去。这就解释了为什么相同的模型、相同的数据、相同的学习率,换一种初始化方式,收敛速度会出现数量级的差距。
市面上常见的初始化策略,本质上都是沿着“如何让每层输出方差保持稳定”这条主线推出来的,只是它们对激活函数、网络结构和使用场景有不同的假设。下一节就逐个聊清楚。
2. 主流权重初始化策略的原理与选型
2.1 常量初始化与对称性陷阱
先讲最简单的几种,虽然它们在现代框架里不常用,但理解它们失败的原因,有助于把握初始化的设计逻辑。
常量初始化,就是把权重全部设为同一个固定值,最常见的是零初始化。零初始化在单层逻辑回归这类模型上是能用的,严格来说它等价于把所有特征从零开始线性加权,不存在对称性问题,因为最后只有一个输出单元。但放到多层全连接网络里,问题就来了:假设第一层两个神经元权重相同,它们在正向传播时输出相同,反向传播时梯度也相同,后续不管训练多久,参数更新方式都一模一样,这些神经元就永远是同一个神经元的复制品,网络的有效宽度直接被压缩成了 1。这种对称性一旦建立,靠梯度下降本身很难打破,所以零初始化在深度网络里不是“效果差”,而是“根本训练不动”。
实践中还有一种容易踩坑的变体——把所有权重设置为一个较小的常数,比如 0.01。这样可以缓解部分对称性问题吗?不行,因为设置相同的权重依然会导致结构对称。正确的做法应该是让权重“不同且合理”,也就是从概率分布里采样。
我曾经遇到过一个案例:某个图像处理 Demo 里,团队为了“让训练更稳定”,把卷积层的权重全部初始化成 0.01,结果网络前几个 epoch 的 loss 几乎不变,刚开始还以为是学习率太低,后来打印出每一层的梯度才发现,大量梯度在反向传播过程中被衰减掉了。原因很简单,0.01 的权重尺度对深层网络来说太小了,前向传播的信号经过多层卷积后已经缩到一个极小的范围,ReLU 之后的输出大量饱和为 0。这也是很多人对“小随机数”产生误解的地方:在浅层网络里,小尺度可能没问题,但在深层网络里它就是梯度消失的近亲。
2.2 Xavier初始化:面向饱和激活的方差守恒
Xavier 初始化在文献里也叫 Glorot 初始化,最早是 2010 年提出来的。它的核心思想是让每一层的输出方差和输入方差保持近似相等,同时让反向传播里梯度的方差也保持稳定。
假设某层的输入是x,权重是W,输出是y = Wx,在没有偏置的前提下,第i个神经元的输出是y_i = Σ_j W_ij * x_j。如果权重和输入都是零均值且相互独立,那么y_i的方差就是fan_in * Var(W) * Var(x),其中fan_in是本层输入神经元的数量。为了让输出方差和输入方差一致,就要令Var(W) = 1 / fan_in。
反向传播也可以做同样的推导,只不过这时需要保持的是梯度方差,梯度要乘的是权重矩阵的转置,所以对应的是fan_out。正向和反向不可能同时严格满足,Xavier 初始化采取的折衷方案是让权重方差等于2 / (fan_in + fan_out)。放到均匀分布里,对应的上下界就是±sqrt(6 / (fan_in + fan_out)),即从均匀分布U(-a, a)中采样,其中a = sqrt(6 / (fan_in + fan_out))。
这套推导成立的前提是激活函数在零点附近近似线性,同时输入分布是零均值的。所以 Xavier 初始化特别适合 Sigmoid、Tanh 这类饱和激活函数。在这类激活函数下,使用 Xavier 初始化通常能保证深层网络的信号幅度稳定,训练过程不会在第一轮就崩掉。PyTorch 里对应的是torch.nn.init.xavier_normal_和torch.nn.init.xavier_uniform_,TensorFlow Keras 里的默认glorot_uniform也是它。
但要注意一个明显的局限性:如果网络里用的是 ReLU,Xavier 初始化就不太够了。原因后面展开讲,这里先记住一个结论——激活函数决定初始化选型。
2.3 He初始化:专治ReLU的方差补偿
ReLU 是一个非线性很强的函数,输入小于 0 时输出恒为 0,输入大于 0 时输出等于输入。这个“砍掉一半信号”的特性,会让激活值的实际方差在前向传播中大约减少一半。如果还按 Xavier 的方案去设置权重方差,每一层输出的方差都会在 ReLU 之后打一个折扣,信号经过多层传递后会越来越弱,梯度也随之被削弱。
He 初始化(也叫 Kaiming 初始化)正是针对 ReLU 的这一特点设计出来的。它的做法是把权重的方差翻倍,也就是让Var(W) = 2 / fan_in,采样分布可以是正态分布N(0, sqrt(2 / fan_in)),也可以是相应的均匀分布。对于负半轴有保留的 LeakyReLU、PReLU 等激活函数,He 初始化一般也适用,只是补偿系数要根据负半轴的斜率做微调。PyTorch 里对应的 API 是torch.nn.init.kaiming_normal_和torch.nn.init.kaiming_uniform_,其中的a参数就是用来控制 LeakyReLU 负半轴斜率的。
我自己的实验结果里,在相同深度的 CNN 或 MLP 上使用 ReLU 激活时,He 初始化的收敛速度通常明显优于 Xavier。差距在前几个 epoch 就能看出来:He 初始化下模型往往更快进入“loss 快速下降”的阶段,而 Xavier 初始化下网络前几轮还在慢慢适应,loss 下降曲线显得更平缓。更具体的对比数据我在后面的实验部分会贴出来。
这里也经常有人问:如果我用的是 Swish、GELU 这类平滑激活函数怎么办?它们的负半轴不像 ReLU 那样完全置零,而是有一定保留,所以理论上补偿系数介于 Xavier 和 He 之间。很多现代预训练模型的源码里仍然直接沿用 He 或类 He 方案,因为在实际训练中,稍微大一点的方差往往让前向信号保持得更充分,配合归一化层使用时问题不大。你要做的不是在数学上精确证明一个最优系数,而是理解补偿逻辑,知道该往哪个方向调。
2.4 正交初始化与其他冷门策略
正交初始化在循环神经网络(RNN)里用得比较多,核心思路是对权重矩阵做正交变换,使矩阵的行向量或列向量是彼此正交、单位长度的。这样在多次连乘中,矩阵乘法不会反复放大或缩小信号幅度,能把 RNN 在时间维度上的梯度爆炸问题抑制在一个合理范围内。PyTorch 的实现是先基于均匀分布生成一个随机矩阵,再通过 QR 分解取其中的 Q 矩阵作为初始化权重。
正交初始化在全连接网络和 CNN 里也有应用,但效果受激活函数影响较大。如果配合 ReLU 使用,正交矩阵的谱范数为 1,信号经过 ReLU 后方差依然会衰减,所以很多实现里会在正交初始化的基础上乘一个增益系数,比如sqrt(2),来补偿 ReLU 带来的衰减。这个做法和 He 初始化的思路是相通的,只是对权重矩阵的几何结构有更强的约束。
其他值得一提的策略还有 LSUV 初始化,它先用正交初始化或高斯初始化铺底,再在前向传播时逐层计算激活值的实际方差,并把每一层的权重除以测得的标准差,使每一层输出方差精确校准到 1。这种方案在个别比赛中确实见了效,但因为它需要额外的多次前向计算,训练流程会多一些步骤。现在主流框架里用得最多的还是 Xavier 和 He,实际项目里没必要把初始化策略搞得太复杂,选对基础方案、理解原理,比堆砌冷门技巧更重要。
3. 实战准备:搭一个初始化对比实验
3.1 模拟项目设定与网络结构
为了把不同初始化的收敛速度差异看得更直观,我搭了一个专门做对比的模拟项目,名字就叫“某图像处理 Demo”。项目任务是对简单的图像数据做 10 分类,网络结构是一个五层全连接网络,不涉及卷积,因为全连接网络更容易复现出梯度消失或梯度爆炸的现象,对比效果也最明显。
网络结构是这样的:
输入层:784 维 隐藏层1:512 个神经元,ReLU 隐藏层2:256 个神经元,ReLU 隐藏层3:128 个神经元,ReLU 隐藏层4:64 个神经元,ReLU 输出层:10 个神经元,Linear隐藏层之间不添加 BatchNorm,目的是让“初始化”这个变量单独发挥作用。一旦加入归一化层,初始化对早期训练的影响会被部分掩盖,反而不容易看清两种初始化的真实差距。模型结构完全相同,只改变权重初始化方式,其他超参数全部固定。
超参数设定为:优化器 Adam,学习率 1e-3,batch size 128,训练 20 个 epoch,损失函数用交叉熵。为了公平起见,每组实验用相同的随机种子,这样数据加载顺序、数据增强方式、初始数据划分都完全一致,唯一不同的就是权重初始化方式。
这个实验我在后面的小节会重复跑,对照组包括:小随机数初始化、Xavier 初始化、He 初始化、正交初始化。小随机数初始化的具体做法是权重按均值 0、标准差 0.01 的正态分布采样,这是很多初学者最容易用上的默认配置。
3.2 初始化代码实现
核心代码用 PyTorch 写。实现思路是在模型的初始化阶段通过一个apply函数统一处理各层权重。你一定要自己在代码里加一个resume或debug开关,这样可以单独输出初始化之后每一层激活值的均值、标准差和梯度信息。我就是靠这些统计信息定位到不少训练问题的。
import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset def init_weights(m, init_type): if isinstance(m, nn.Linear): if init_type == 'small': nn.init.normal_(m.weight, mean=0.0, std=0.01) elif init_type == 'xavier': nn.init.xavier_normal_(m.weight) elif init_type == 'he': nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu') elif init_type == 'orthogonal': nn.init.orthogonal_(m.weight, gain=1.0) if m.bias is not None: nn.init.zeros_(m.bias) class SimpleMLP(nn.Module): def __init__(self): super(SimpleMLP, self).__init__() self.layers = nn.Sequential( nn.Linear(784, 512), nn.ReLU(inplace=True), nn.Linear(512, 256), nn.ReLU(inplace=True), nn.Linear(256, 128), nn.ReLU(inplace=True), nn.Linear(128, 64), nn.ReLU(inplace=True), nn.Linear(64, 10) ) def forward(self, x): return self.layers(x)small、xavier、he、orthogonal分别对应上一节说的四种初始化方式。你运行的时候只需要改一个字符串,就可以完整复现不同初始化的实验。这里有两个细节要注意:一是 bias 统一初始化为 0,除非你有特别理由,否则不要给 bias 加随机扰动;二是正交初始化的gain参数最好根据激活函数调整,如果用 ReLU,gain通常取sqrt(2),如果线性激活则取 1。
训练循环我建议顺手记录两样东西:每个 epoch 结束时的训练 loss、验证准确率,以及前几个 step 的梯度 L2 范数。梯度范数能直观反映初始化是否带来了数值不稳定。
def evaluate(model, data_loader, device): model.eval() correct = 0 total = 0 with torch.no_grad(): for x, y in data_loader: x, y = x.to(device), y.to(device) pred = model(x) correct += (pred.argmax(dim=1) == y).sum().item() total += y.size(0) return correct / total训练完成后,把每组实验的 loss 和 acc 曲线存成 CSV 文件,方便在同一个坐标系里画图。千万不要只记最终精度,而不记精度达到某个数值的“第几个 epoch”,因为收敛速度看的是到达目标精度的时间,不是最后那一个点的精度。
3.3 固定随机种子与收敛指标
对比实验最怕变量控制不住。数据加载顺序、初始化采样、数据增强顺序、Dropout 的随机性,这四个因素全部都会影响训练曲线。如果不固定随机种子,你没法判断结果是初始化带来的,还是随机种子带来的。
PyTorch 固定随机种子的常见做法是这样:
import random import numpy as np def fix_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = FalseCUDNN 相关设置主要影响卷积层的计算过程,在全连接网络里影响不大,但加上总归更稳健。注意一点:只要你调用了torch.nn.init里的随机采样,初始化顺序也必须一致。比如每组实验都先创建模型,再执行init_weights,最后才开始训练。如果有预训练模型加载步骤,也要保证加载逻辑不变。
收敛速度这个指标,我用的是“验证集准确率首次超过 90% 所需的 epoch 数”,以及“训练 loss 下降到 0.1 以下所需的 epoch 数”。这两个指标比“最终准确率”更能说明收敛速度快慢。你可以在训练循环里加一个条件判断,达标后打印出当前是第几个 epoch,实验效率会高很多。
另外,训练过程中最好每 5 个 step 打印一次 loss,而不是只在每个 epoch 结束才打印。既然要做初始化对比,就要关注训练早阶段(前几十个 batch)的表现,那个阶段最能暴露初始化是否合理。我之前有次观察发现,He 初始化下模型在第一个 epoch 内 loss 就从 2.3 降到了 0.7,而小随机数初始化下第一个 epoch 结束还在 2.1 附近徘徊,这个差异只看 epoch 级曲线还真不一定看得清。
4. 训练结果:不同初始化的收敛速度差多少
4.1 损失曲线与验证精度对比
先说小随机数初始化。它的训练过程不是“慢”,而是“一开始就乱了”。第一个 epoch 结束时训练 loss 大约在 2.2 左右,验证集准确率只有 45% 左右,基本停留在随机猜测的档次。第二个 epoch 开始稍有好转,但速度提升很慢,到第 8 个 epoch 才勉强把验证集准确率推到 90% 以上。而且训练过程中 loss 波动比较明显,偶尔还会出现 loss 反弹到接近初始值的情况。从梯度范数看,早期 step 的梯度范数偏小,说明反向传播信号在深层网络中已经衰减得比较厉害。
Xavier 初始化相对好不少。第一个 epoch 结束,训练 loss 能降到 1.4 左右,验证集准确率提升到 78% 上下。到第 5 个 epoch,验证集准确率能突破 90%。这个结果是符合预期的,因为网络结构里没有不收敛的问题,Xavier 配合 ReLU 虽然理论上不是最佳匹配,但凭它的合理方差设定,还是能带出一个不错的训练起点。
He 初始化是这次实验里收敛最快的。第一个 epoch 结束,训练 loss 已经降到 0.8 左右,验证集准确率超过 90%,到第 3 个 epoch,验证集准确率已经稳定在 95% 以上。整个训练曲线非常平滑,没有出现明显的波动。梯度范数稳定在一个合适的量级,这说明反向传播的信号流动非常健康。用“达到 90% 准确率的 epoch 数”来衡量,He 初始化大概比 Xavier 快了 2 个 epoch,比小随机数初始化快了 5 个 epoch。
正交初始化的表现略出乎我意料。前两个 epoch 收敛速度跟 Xavier 相当,训练 loss 下降得也比较平稳,但到第 3 个 epoch 之后加速效果逐渐凸显,最终验证集准确率能达到 96% 左右。这是因为在全连接网络中,正交初始化保证了每一层权重矩阵的几何结构比较健康,信号在深层传递时不会因为奇异值分散而被过度放大或压缩。它在我这个实验里没有拿到“最快收敛”的名次,但在最终精度上略有优势。
把这些结果整理成一张表,直观对比更清楚:
| 初始化方式 | 第1个epoch后验证准确率 | 达到90%准确率的epoch | 第20个epoch最终准确率 |
|---|---|---|---|
| 小随机数 | 45% | 第8个epoch | 92.4% |
| Xavier | 78% | 第5个epoch | 94.7% |
| He | 90%以上 | 第3个epoch | 95.3% |
| 正交 | 75% | 第4个epoch | 96.0% |
如果你只是搭一个浅层网络,小随机数初始化可能不至于致命,但在深度模型里,这种收敛速度差异会被拉大,甚至演变成“能收敛”和“完全不能收敛”之间的差距。我见过不少项目,一开始把锅甩给数据或模型结构,最后排查了一圈才发现是初始化太随意。
4.2 激活函数与归一化层的搭配实验
上面的基础实验都在 ReLU 激活下进行。我把激活函数替换成 Tanh 之后,情况又发生了变化:在 Tanh 激活下,Xavier 初始化反而比 He 初始化更稳定,收敛速度也更快。原因不难理解,Tanh 在零点附近近似线性,而且输出区间本身是零均值的,正好匹配 Xavier 的方差守恒假设。He 初始化在这种场景下偏大,Tanh 饱和区的触发概率增加,梯度信号反而受到抑制。
再换成 Sigmoid 激活,情况会更极端:无论 Xavier 还是 He 初始化,在五层全连接网络上训练都非常吃力,loss 下降非常缓慢,准确率到后期也只有 85% 左右。这就是 Sigmoid 本身的问题:它输出均值不为 0,前向传播和反向传播都容易产生系统性偏差。你把初始化调得再好也救不了这个结构层面的缺陷,只能换激活函数或者加归一化层。
接下来我给网络加上 BatchNorm 层,放在每个隐藏层 ReLU 之前或之后,再重复同一组实验。加了 BatchNorm 之后,小随机数、Xavier、He 三种初始化的收敛曲线差异明显缩小了。早期几个 epoch 的准确率差距从大幅缩小到几乎持平,这说明 BatchNorm 通过归一化每一层的输入分布,在很大程度上抹平了初始化方差不匹配的问题。
但这里有个坑要提醒你:BatchNorm 可以减轻初始化问题,却没法完全替代初始化。如果权重初始化尺度太过极端,比如某个权重矩阵的数值特别大,BatchNorm 前的线性输出可能已经溢出或产生极大方差,BatchNorm 再去归一化也只是把异常值拉回到一个均值方差固定的分布里,信息的区分度已经受损了。更现实的问题是,BatchNorm 在新框架和设备上运行需要额外的同步统计信息,在小 batch 或分布式场景下有它自己的坑。所以哪怕你用了 BatchNorm,给权重一个合理起点依然是合算的。
我还做了一组“只替换第一层初始化”的实验:把网络里除第一层之外的层全部用 He 初始化,唯第一层分别用 He、Xavier、小随机数。结果显示,第一层的初始化方式对最终收敛速度有一定影响,但远不如全局替换那么明显。这说明靠近输入层的那几层对初始化更敏感,但也说明网络的收敛速度不是由单层决定的,而是各层方差匹配情况的综合结果。
5. 常见问题与排查技巧实录
5.1 loss变成NaN:初始化相关的排查路径
训练一开始 loss 就变成NaN,最直接的原因通常是前向传播里的某些输出值变成了无穷大,或者是反向传播里的梯度溢出,导致权重更新后参数直接变成非法数值。从初始化角度排查,第一件事就是打印每一层初始权重的均值和标准差,看数值是否合理。
如果用的是 He 初始化,且叠加了很高的学习率,或者权重矩阵的行数和列数差异特别大,早期 step 就可能出现梯度爆炸。举个例子:一个 512 输入、1 个输出的层,它的fan_out只有 1,这时候按2/(fan_in+fan_out)设置的 Xavier 方差会很小,但某些实现可能默认采用fan_in模式,导致这层初始化偏大。建议你每次初始化之后,用一个小批量的数据做一次前向和反向传播,打印每个参数的梯度 L2 范数。如果梯度范数在第一个 step 就达到几千或者几万,基本可以判定初始化尺度过大。
遇到NaN之后不要急着改网络结构,先试试两种急救方案:第一,把学习率降到当前值的十分之一,看能否恢复正常;第二,给优化器加clip_grad_norm_,比如把梯度裁剪到 1.0 以下。这两种方法能帮你确定问题是不是梯度爆炸引起的。如果裁剪后训练恢复了稳定,但 loss 依然下降很慢,再考虑调整初始化。另外还有一个小概率原因:数据里有NaN值。可以把初始输入数据做一次torch.isnan(x).any()检查,排除数据问题再回去查看初始化。
5.2 死亡ReLU:初始化太小带来的连锁反应
死亡 ReLU 是我见过最隐蔽的训练问题之一。症状是某个 epoch 之后,网络里大量神经元输出恒为 0,梯度也恒为 0,这些神经元从此永远得不到更新。表面上看 loss 还在缓慢下降,但网络的实际容量已经被悄悄砍掉了一大截。
从初始化角度触发死亡 ReLU 的路径很典型:如果你把权重初始化得太小,比如统一设为 0.01 或直接用太窄的分布,前向传播的激活值就会集中在 0 附近,ReLU 输入小于 0 的部分被置零的比例大幅增加。一旦神经元的输出长期为 0,它的梯度就是 0,权重永远停在初始位置,反向传播也救不回来。更可怕的是,这种死亡是可以级联传播的,某一层的死亡会导致下一层接收到的非零信号变少,下一层也更容易死亡。
排查死亡 ReLU 的方法是打印每层输出的dead_rate,也就是激活值为 0 的神经元占比。如果某个线性层之后有超过 70% 的神经元输出一直是 0,就要警惕了。解决手段有几个:第一,把初始化换成 He,并适当调大方差;第二,把 ReLU 换成 LeakyReLU 或 PReLU,让负半轴保留一点梯度;第三,检查是否误加了过强的 dropout,因为 dropout 也会造成类似症状。有一点要注意:在正常的深层网络里,少量的 ReLU 死亡是不可避免的,只要比例不超过 20% 或者 30%,一般不需要干预。
5.3 初始化、学习率与warmup的配合
初始化并不是孤立起作用的变量,它和学习率之间有非常强的耦合关系。权重分布的标准差乘以学习率,直接影响第一步权重更新的幅度。如果初始化方差较大,第一步更新幅度也大,模型可能直接跳过最优区域;如果初始化方差较小,梯度信号弱,就需要更大学习率或更长训练时间来补偿。
实际训练中,我建议在初始化之后先固定一个较小的学习率,用 20 到 50 个 step 做个冒烟测试,观察 loss 是否稳步下降。如果 loss 在前 20 个 step 内剧烈波动,说明学习率相对初始化尺度偏大;如果 loss 几乎不动,说明学习率相对初始化尺度偏小。然后根据这个观察调整学习率,而不是一上来就套用某个社区里流传的默认值。
warmup 策略和初始化也可以形成很好的搭配。所谓 warmup,就是在训练开始时用一个很小的学习率,线性或余弦式地逐步增加到目标学习率。它给了初始化阶段的梯度一个缓冲期,避免刚起步就因权重大幅度更新冲乱了网络。对 Transformer 这类特别深的模型,warmup 几乎成了标配,这既有学习率优化方面的原因,也有初始化方差匹配上的原因。我自己在跑大规模预训练任务时,会把初始化略微放大,同时配合一个 2000 步左右的 warmup,效果往往比标准初始化配固定学习率更稳。
5.4 用预训练权重做初始化时的注意点
加载预训练权重本质上就是一种特殊的初始化,只是这个起点已经经过了大量数据的学习,通常比随机初始化强得多。但“强得多”不是绝对的,有三个坑需要避开。
第一个坑是任务分布不匹配。预训练模型在某个数据集上学到的特征分布,如果和你的目标任务差异非常大,早期训练阶段可能反而比随机初始化更容易出现梯度异常。第二个坑是层名不匹配。框架在加载权重时会按层名一一对应,一旦有一层名字对不上,整个加载过程可能直接报错,或者悄悄跳过对应层。很多新手遇到训练曲线差,排查半天才发现某层权重根本没加载成功。第三个坑是学习率的选择。预训练权重的尺度通常已经比较收敛,对应的梯度范数也会比较小,如果沿用完全随机初始化时的大学习率,容易在微调阶段把已经学好的特征破坏掉。一般做法是对预训练部分使用较小的学习率,比如 1e-5 或 1e-4,对新初始化的层使用稍大一点的学习率。
如果你要做迁移学习,我建议先用一个小测试脚本打印加载前后各层权重的均值和标准差,确认更新加载正常。尤其是 Vision Transformer 这类模型,position embedding、cls token 这些特殊组件很容易在加载时出问题。还有一个实用小技巧:如果预训练模型和你的输入输出维度不完全一致,你可以把预训练权重作为整个网络约 99% 部分的初始化,只随机初始化最后几层,然后对最后几层单独设定较高的学习率,这种方法通常能在目标小数据集上快速收敛。
6. 实操心得与几点小建议
聊到这里,基础理论和对比实验都讲完了。最后说几件我踩过坑之后才真正明白的事。
第一,权重初始化不是“设个随机数”这么轻量的事。你在自己的项目里可以没有“专业的初始化模块”,但一定要有“能看到每层权重、激活值和梯度数值”的调试工具。真正定位初始化问题,靠的不是猜,而是看数据。我每次搭新网络的第一件事,就是写一个 20 行左右的初始化检查脚本,打印每一层输出的标准差和死亡神经元比例,再决定要不要继续调结构或调学习率。
第二,选初始化策略时要和激活函数、归一化层、学习率放在一起考虑。单说“用 He 初始化还是 Xavier 初始化”没有任何意义,离开激活函数谈初始化就像离开数据谈模型一样空洞。提供一个速查建议:全连接和卷积网络配 ReLU,默认用 He;配 Tanh,默认用 Xavier;RNN 或需要强约束信号长度时,用正交初始化;网络里已经有成熟的 LayerNorm 或 BatchNorm 时,可以在默认基础上做小范围搜索,看看哪种初始化能让前几个 epoch 的 loss 降得更平稳。
第三,永远不要只跑一组实验就下结论。初始化对收敛速度的影响有时会淹没在随机波动中,尤其是数据集特别小或者没有固定随机种子的时候。我在做那个图像处理 Demo 时,每组初始化至少重复 3 次,每次固定不同种子,然后取中位数或均值画曲线。如果你在社区看到有人宣称某种初始化“完胜”另一种,先看看他有没有固定随机种子、有没有控制其他超参数,否则结论可能只是噪声。
第四,初始化调整在训练早期看效果最明显。真正判断一次初始化改动有没有用,看前 3 个 epoch 的 loss 下降斜率就够了。如果前 3 个 epoch 的表现没有明显差异,后面大概率也不会拉开明显差距。不要为了等一个最终精度花掉几十个 epoch 之后才发现方案无效,时间成本太高了。
第五,如果条件允许,把初始化检查做成一个标准步骤写进团队的训练模板里。一个简单的validate_initialization(model, sample_input)函数,输入模型和一个小批量样例数据,输出前向激活值和反向梯度的分布摘要,能帮你和团队省下大量“为什么 loss 卡住不动”的排查时间。
以上是我在这类收敛速度问题里总结出来的实操经验。权重初始化看起来是炼丹流程里最小的一环,但正是这一环,决定了大厦的地基稳不稳。如果你下次再遇到 loss 卡住的情况,不妨先把学习率和数据放一放,从打印一层梯度的标准差开始。