权重初始化:Xavier、He初始化等——给模型一个好起点

说实话,刚入行那会儿,我觉得权重初始化就是随便填个随机数。直到有一次,我训练一个20层的CNN, loss直接炸到NaN,折腾了两天才发现是初始化搞的鬼。嗯,从那以后,我彻底明白了——初始化没做好,后面再怎么调参都是白搭。

权重初始化,说白了就是给模型的参数定个初始值。你想想看,神经网络训练就像在爬山,起点选不对,要么半天爬不到山顶,要么直接掉悬崖里。今天我就把几种主流的初始化方法掰开揉碎了讲清楚。

核心观点:好的初始化能让梯度在正向传播和反向传播中保持稳定。坏的初始化,轻则收敛慢,重则梯度爆炸或消失。

爆炸与消失的根源

为什么会梯度爆炸或消失?我用一个简单场景说明白。假如你有一个100层的网络,每层的权重都是一些小随机数。

正向传播时,每经过一层,输出方差会逐层缩小。到了最后几层,激活值几乎全是0。反向传播也一样,梯度往回传,每层都乘一个小于1的系数,传到前面几层时,梯度已经小到几乎为0了——这就是梯度消失。

反过来,如果权重都偏大,输出方差会逐层放大。几层之后,激活值变成天文数字,梯度也跟着爆炸。

所以,核心问题只有一个:如何让每层的输入输出方差保持稳定?

Xavier初始化——适合饱和激活函数

Xavier初始化(也叫Glorot初始化)是2010年提出的。它的思路很直接:让每层的输出方差等于输入方差。

基于这个假设,Xavier推导出权重的方差应该满足:

Var(W) = 2 / (n_in + n_out)

其中 n_in 是输入神经元数,n_out 是输出神经元数

实际使用时,从均匀分布或正态分布中采样:

# Xavier均匀分布
W ~ U[-sqrt(6/(n_in+n_out)), sqrt(6/(n_in+n_out))]

# Xavier正态分布
W ~ N(0, sqrt(2/(n_in+n_out)))

我个人习惯在Tanh或Sigmoid激活函数上用Xavier。之前做过一个文本分类模型,用Sigmoid做激活,试了随机初始化,结果loss前50轮几乎没降。换Xavier之后,第10轮就开始收敛了。差别就这么大。

适用场景:Tanh、Sigmoid等饱和激活函数。不适用:ReLU及其变体。因为ReLU会把一半的神经元输出置0,破坏了Xavier的方差假设。

He初始化——ReLU的好搭档

ReLU出现后,Xavier就不太好使了。为什么呢?ReLU把负半轴的输出全部置0,实际输出的方差只有输入的一半。

2015年,何恺明团队提出了He初始化(也叫Kaiming初始化)。它针对ReLU重新推导了方差公式:

Var(W) = 2 / n_in

# He正态分布
W ~ N(0, sqrt(2/n_in))

# He均匀分布
W ~ U[-sqrt(6/n_in), sqrt(6/n_in)]

你注意看,He比Xavier的分母少了n_out,并且系数从1变成了2。这正好补偿了ReLU带来的方差减半问题。

我曾经在ResNet-50的训练中踩过坑。一开始用了Xavier初始化,模型在ImageNet上怎么都训不到70%的Top-1准确率。后来换成He初始化,同样的学习率,同样的数据增强,直接蹦到了75%以上。嗯,初始化就是这么关键。

重要提醒:如果你在ReLU网络上用了Xavier,梯度会随层数加深逐渐消失。层数越多,问题越明显。残差网络也只是缓解了路径上的梯度问题,并没有解决初始化本身的缺陷。

其他初始化方法

除了Xavier和He,还有一些常用的初始化,我列个表格给你对比:

方法 分布形式 适用激活函数 特点
全零初始化 常数0 无 所有神经元对称,无法打破平衡,网络不学习
随机小值 N(0, 0.01) 任意 浅层勉强可用,深层容易梯度消失
Xavier N(0, sqrt(2/(n_in+n_out))) Tanh, Sigmoid 保持前后方差一致,适合饱和函数
He N(0, sqrt(2/n_in)) ReLU, LeakyReLU, PReLU 专为ReLU设计,深层网络首选
正交初始化 正交矩阵 RNN, LSTM 保持输入输出范数一致,缓解RNN梯度问题

正交初始化这里多说一句。我做过一个机器翻译的Seq2Seq模型,LSTM层数一多,梯度消失得厉害。换成正交初始化之后,长序列的BLEU分数直接涨了3个点。它在循环网络中确实有奇效。

我的选择原则:

  • 全连接/卷积 + ReLU → He初始化
  • 全连接/卷积 + Tanh/Sigmoid → Xavier初始化
  • RNN/LSTM → 正交初始化
  • Batch Normalization存在时 → 初始化可以放宽要求,但He/Xavier仍有帮助

实操中的坑和经验

纸上谈兵没用,我跟你说说实际项目中容易踩的坑。

第一个坑:忽略激活函数匹配初始化方法。我曾经接手过一个项目,所有层全用Xavier,但激活函数是ReLU。结果模型训练了三天,效果还不如随机猜。后来排查才发现,前面几层的输出已经全部变成0了——梯度消失了。换成He初始化,问题立刻解决。

第二个坑:不验证初始化效果。训练前,我会先跑一个前向传播,打印每层输出的均值和方差:

def check_init(model, x):
    model.eval()
    with torch.no_grad():
        for name, module in model.named_children():
            x = module(x)
            if hasattr(x, 'shape'):
                print(f"{name}: mean={x.mean():.4f}, std={x.std():.4f}")

如果某层的std突然变得极大(>10)或极小(<0.01),那肯定是初始化有问题。我建议你每次换网络结构后,都跑一遍这个检查。

第三个坑:在预训练模型上乱改初始化。用预训练权重时,千万别再随机初始化了。你微调的时候,新加的层可以用He初始化,但预训练的层要冻住或者用很小的学习率。

实用技巧:PyTorch里直接用torch.nn.init模块非常方便。我的标准写法:

import torch.nn.init as init

for m in model.modules():
    if isinstance(m, nn.Conv2d):
        init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
        if m.bias is not None:
            init.constant_(m.bias, 0)
    elif isinstance(m, nn.Linear):
        init.xavier_normal_(m.weight)
        init.constant_(m.bias, 0)

注意卷积层用fan_out模式,这样能直接控制每层的输出方差。

知识结构总览

下面我把本章的核心逻辑画成一张图。你可以看到初始化方法的选择路径:从输入方差出发,根据激活函数类型,走向不同的初始化策略,最终目的是让梯度稳定传播。

权重初始化决策流程 输入方差 Var(X) 激活函数类型? ReLU / LeakyReLU / PReLU Tanh / Sigmoid RNN / LSTM He初始化 Var(W) = 2 / n_in Xavier初始化 Var(W) = 2 / (n_in + n_out) 正交初始化 W^T W = I 稳定梯度传播 避免梯度爆炸/消失,加速收敛

什么时候可以放松对初始化的要求?

如果你用了Batch Normalization(BN),初始化的影响会小很多。BN每层会重新调整均值和方差,确实能容忍一些不太好的初始化。

但注意,这不意味着你可以随便初始化。我做过对比实验:在ResNet-50(带BN)上,用He初始化比用随机小值,收敛速度快了将近30%。BN能兜底,但好的初始化能让你省时间。

另外,现在很多框架(PyTorch、TensorFlow)的默认初始化已经做得很好了。但我还是建议你了解背后的原理——哪天你遇到一个诡异的收敛问题,排查到初始化这一步时,心里有底。

曾经翻过的一个错误:我在一个GAN的训练里,生成器和判别器都用了He初始化。但生成器的输出层是Tanh,我却忘了改初始化。结果生成器死活学不会,生成的都是灰蒙蒙的图片。发现问题后,把输出层换成Xavier,效果立竿见影。一定要记得:逐层检查。

好了,关于权重初始化,核心就这些。总结一下:先用激活函数类型定方向,再用方差公式定参数,最后别忘了前向验证。这四步走下来,初始化基本不会出问题。