深度学习这条路上我见过太多人把参数初始化当成一个随便抄两行代码的小步骤要么直接用框架默认值要么干脆全设成零。结果呢同一个网络架构别人跑得好好的自己一跑loss像心电图、甚至直接NaN。问题往往不在学习率、也不在数据增强就出在权重一开始就没放对位置上。参数初始化是什么简单说就是给神经网络的每一层权重还有偏置设定一个起始值。别小看这一步它直接决定了梯度能不能在层与层之间顺畅传下去决定了网络会不会陷入永远训不动的死局。这篇文章我会先讲清楚初始化背后的原理再把五种主流初始化方法的PyTorch代码逐段拆开最后用一个真实的对比实验告诉你换一种初始化训练曲线能差出好几个数量级。适合谁看刚学完反向传播、正打算动手写第一个网络的同学以及已经跑通模型但总在怀疑为什么我的结果这么差的初级从业者。看完你至少能秒答一个经典面试题ReLU网络到底该用哪种初始化为什么1. 先把最重要的概念钉死初始化到底在影响什么1.1 训练的本质从一个随机起点开始的梯度下降神经网络训练的本质是在一个极高维度的损失函数曲面上做梯度下降。你现在搭好的模型结构已经定死了权重空间里那一个个点对应着不同水平的模型表现。初始化干的事情就是在这片地形里给你选一个出发位置。位置选不好后面每一步都受罪。这里有个特别关键的直觉反向传播是通过链式法则把误差从最后一层往前传的。传的时候每经过一层梯度就要乘一次该层的激活值导数、再乘一次权重矩阵。如果权重设得太小经过多层连乘后梯度会指数级衰减前面几层几乎收不到更新信号——这叫梯度消失。反过来权重设得太大梯度又会指数级膨胀参数直接飞出取值范围——这叫梯度爆炸。两个本质上是同一个问题信号在深层网络里无法稳定传递。你可能觉得那我把所有权重都设成 0.01 这样的小值总可以吧理论上能缓解梯度爆炸但实际会让每一层输出的信号几乎为零网络根本学不到东西。一个合格的初始化是要让信号在层间传递时保持稳定前向传播时每一层的输出方差尽量不变反向传播时每一层的梯度方差也尽量不变。后面要讲的Xavier和He初始化思路全都是围绕这个目标展开的。1.2 坏初始化引发的三种事故现场我自己刚学深度学习的时候就踩过一个非常典型的坑用全零初始化训练一个三层MLP跑20轮loss纹丝不动。当时我怀疑网络写错了、数据加载错了、学习率设错了查了个遍最后才发现问题出在初始化上。坏初始化的症状通常有三种你可以对号入座第一种是训练初期loss完全不动。全零初始化的网络每层神经元接收到的都是同样的输入信号反向传播时梯度也完全一样所有神经元在数学上就成了同卵多胞胎——永远学不出不同特征。网上管这叫对称性问题听着玄乎说白了就是所有节点都在复制彼此整个网络的表达力约等于一个线性分类器。第二种是loss直接NaN或一开始就爆炸到天文数字。通常是把权重初始化得过大。矩阵乘法一层层堆叠数值指数增长很快溢出浮点数范围。如果你的输入数据本身范围比较大比如没做归一化的像素值0-255再遇到一个大方差的初始化爆炸概率会更高。第三种比较隐蔽loss在下降但效果一直不理想。这往往是激活值饱和导致的。比如用sigmoid/tanh做激活函数权重一大神经元输出直接挤到饱和区那里的导数接近零。梯度在被激活函数吃掉之前就已经接近于零了网络看似在训练实际上后面每隔一两轮梯度就没影了。注意这三种症状都不是孤立出现的。一个网络在训练的不同阶段可能连续踩好几个坑。排查时先看loss曲线形态再针对性地怀疑数据预处理、初始化、学习率这三个最容易被甩锅的因素。1.3 好初始化的两个验收标准判断一个初始化方案好不好不需要等训练结束有两个中间指标可以在前向传播阶段就检查激活值分布不发散把一批真实数据喂给网络观察每一层的输出。理想情况下各层输出的均值和方差不会出现数量级上的跳跃。如果第一层方差是0.1到第10层变成0.001或者100这就是在向你报警。反向梯度能传到第一层这个在训练早期最明显。跑一个batch的backward之后打印第一层权重的梯度。如果梯度是0、或者是正常梯度的千分之一以下那基本可以确定初始化有问题。这两个标准你可以直接写进自己的实验pipeline里每次换网络结构后先跑一次前向反向的冒烟测试再做正式训练。成本极低却能帮你省下大量排查时间。下面从原理出发看看各种初始化方案是怎么围绕这两个目标设计的。2. 五种主流初始化方法原理、公式与适用场景2.1 零初始化与常数初始化为什么它是陷阱先说最省事的清零。零初始化在逻辑回归这类简单模型上没问题甚至在线性模型中还是一种标准做法。但在神经网络里一旦所有权重相同每一层的所有神经元接收到的输入就完全相同。反向传播后梯度也相同权重更新后依然相同。无论网络多深、每层多宽实际上只有一个神经元在工作。这就叫神经网络退化成单神经元模型。常数初始化同理。给所有权重赋同一个不为零的值同样有对称性问题只是比全零好一点。它唯一的用处在于某些特殊场景比如ResNet中的残差分支经常需要初始化为零来保证初始阶段网络退化为恒等映射这是刻意设计的不对称。一般的做法是大部分层用非对称的随机初始化少量特定层手动置零。如果你不清楚自己为什么要全零初始化那就不要用全零初始化。2.2 朴素随机初始化从均匀分布到正态分布既然全零不行很自然的想法是给权重一个随机的小值打破对称性。比如从标准正态分布采样再乘以某个缩放系数。这种朴素随机初始化比全零好但仍需要小心两个问题第一是方差大小。如果直接用标准正态分布方差为1经过一层线性映射后输出方差会扩大到输入维度那么大量级。举个简单例子输入是512维权重方差为1相加之后输出的方差大约是512。到下一层再乘一层方差直接变成512的平方。几层下来必爆炸。第二是分布形态。均匀分布还是正态分布在实践中差别不大关键是方差要匹配网络层结构。如果你只是图省事从randn里采样后直接赋值多半会输在起跑线上。这也是为什么会有Xavier和He这些经过数学推导的专业初始化方案。2.3 Xavier/Glorot让信号在前向和反向都稳住的均衡解Xavier初始化也叫Glorot初始化是深度学习早期最有影响力的初始化方法之一。它的核心思路为了让前向传播时信号方差保持不变同时又让反向传播时梯度方差保持不变权重方差应当取输入维度和输出维度之间的一个调和点。具体落地是从均匀分布U(-a, a)采样其中a sqrt(6 / (fan_in fan_out))或者从正态分布N(0, σ²)采样其中σ sqrt(2 / (fan_in fan_out))这里的fan_in是当前层的输入维度fan_out是输出维度。取两者之和而不是只取输入维度正是为了兼顾前向和反向两条路的信号传递。为啥这个式子长这样核心推导过程不复杂假设前一层的输出独立同分布且方差为已知值经过一层线性变换后输出的方差就变成fan_in * 权重方差近似。如果想让输出方差等于输入方差就需要权重方差 1/fan_in。反过来考虑反向传播梯度反向经过同一层时要乘fan_out次所以又要求权重方差 1/fan_out。两个要求不可能同时满足Xavier的做法是取一个折中2/(fan_in fan_out)。但是注意Xavier有一个隐含假设激活函数在零点附近的导数接近1。它主要适用于tanh和sigmoid这类在0附近近似线性的激活函数。对ReLU来说Xavier初始化会导致输出方差在每一层衰减一半左右——你回想一下ReLU的负半轴会直接截断一半信号。这就是He初始化的出发点。2.4 He/KaimingReLU时代的官方答案He初始化也叫Kaiming初始化是专门针对ReLU及其变体设计的。思路简单粗暴既然ReLU在负半轴把所有负数置零相当于砍掉了一半的信号那我们就主动把方差调大一倍来补偿。从正态分布N(0, σ²)采样σ sqrt(2 / fan_in)从均匀分布U(-b, b)采样b sqrt(6 / fan_in)有时取sqrt(6 / (fan_in fan_out))两种都有实践中建议用sqrt(6 / fan_in)配合ReLU注意这里分母只用了fan_in因为He初始化优先保证前向传播的信号方差。你可以理解为它把上节公式里的2/(fan_infan_out)调整成2/fan_in恰好给ReLU补上那半条命。实际使用中He初始化配合ReLU家族包括LeakyReLU、PReLU是PyTorch里CNN任务的默认选择。它简单、稳定、几乎不需要调参。你在torch.nn.Conv2d里看到的默认初始化底层实际就是在用类似He的方法做随机采样。2.5 正交初始化与其他高阶方案正交初始化是另一个有意思的方向。它让权重矩阵的行向量相互正交即初始化后的权重矩阵满足W·Wᵀ ≈ I的性质。这样做的好处是层间变换几乎是等距变换可以在很大程度上保持输入信号的范数不变从而避免信号在深层网络中膨胀或消失。对RNN这类需要长期记忆的模型特别有用因为RNN在时间维度上的展开本质上也要求信号稳定传递。LSUVLayer-wise Sequential Unit Variance则是一种后处理式的初始化先用正交初始化或标准随机初始化然后逐层做一个类似BN的方差归一化让每一层的输出方差精确等于1。这个方法思路很直白效果也稳定但需要额外的前向传播过程来统计方差训练前做一次就行。我自己只在一些比较难收敛的网络比如超深网络或GAN里用过效果不错但日常项目完全可以用He初始化搞定。说实话如果你用ReLU系网络默认选He就够了。正交初始化和LSUV更多是用在RNN、Transformer这类对信号流动极其敏感的结构里属于进阶调参手的选择新手先不用太纠结。3. PyTorch实战手写五种初始化并跑一组对比实验3.1 实验准备数据集、模型与评估标准这部分我们来真的。我用一个小实验说明不同初始化的差异建议你也动手跑一遍全程用CPU都能在几分钟内完成。实验配置如下数据集CIFAR-1060张图片的原始类别数据集处理到32×32彩色用它比MNIST更刺激一点能放大初始化差异模型一个6层全连接网络784→512→256→128→64→10故意不加BatchNorm、不加Dropout让初始化问题完全暴露出来训练配置SGD momentum0.9学习率0.01batch_size128跑10轮评估标准训练集loss曲线、验证集准确率、各层激活值方差全部代码我放到下面你直接复制就能跑。先说结论在同样的训练配置下He初始化最终验证准确率比Xavier高约2到3个百分点比零初始化高30个百分点以上零初始化的loss在前30个step几乎是一条直线。import torch import torch.nn as nn import torchvision import torchvision.transforms as transforms device torch.device(cuda if torch.cuda.is_available() else cpu) # 数据准备 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ]) trainset torchvision.datasets.CIFAR10( root./data, trainTrue, downloadTrue, transformtransform) trainloader torch.utils.data.DataLoader( trainset, batch_size128, shuffleTrue, num_workers2)这个数据预处理里有一个细节值得注意我用了Normalize把像素从[0,1]范围平移到[-1,1]。如果不做这个平移输入范围会整体偏正会让第一层权重更新对中位数附近的样本敏感而不是对冒险偏离的样本敏感训练稳定性会差一些。3.2 用apply和init模块实现五种初始化PyTorch中做自定义初始化的标准姿势是model.apply(...)。它会把传入的初始化函数应用到模型的每一个子模块上。下面这段代码把五种初始化全写上了注释里写了各自的要点def init_zeros(m): if isinstance(m, nn.Linear) or isinstance(m, nn.Conv2d): nn.init.constant_(m.weight, 0) nn.init.constant_(m.bias, 0) def init_random_normal(m): if isinstance(m, nn.Linear) or isinstance(m, nn.Conv2d): nn.init.normal_(m.weight, mean0.0, std0.05) nn.init.constant_(m.bias, 0) def init_xavier(m): if isinstance(m, nn.Linear) or isinstance(m, nn.Conv2d): nn.init.xavier_uniform_(m.weight) nn.init.constant_(m.bias, 0) def init_he(m): if isinstance(m, nn.Linear) or isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, modefan_in, nonlinearityrelu) nn.init.constant_(m.bias, 0) def init_orthogonal(m): if isinstance(m, nn.Linear) or isinstance(m, nn.Conv2d): nn.init.orthogonal_(m.weight) nn.init.constant_(m.bias, 0)这里有几个容易踩的坑第一bias不要也随机初始化。实践中bias固定为0通常就是最优选择。如果你把bias也随机设一个非零值就相当于给每个神经元叠加了一个随机偏移量会破坏初始阶段的对称性控制。第二kaiming_normal_里有两个参数要分清。modefan_in表示以输入维度为基数调整方差适合正向传播信号稳定modefan_out则会考虑输出维度在某些场景下梯度回传更稳定。一般用fan_in就好PyTorch源码里对Conv2d的默认实现也是fan_in。第三apply会作用到所有子模块。如果你的模型里有BatchNorm用上面的函数会直接报错因为BatchNorm层的weight即gamma维度跟Linear层不一样。所以要么在初始化函数里用isinstance做类型过滤我上面就是这么写的要么先跑一次model.apply再手动跳过BN层。定义模型、加载数据、对比训练三大流程的完整代码class MLP(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(3 * 32 * 32, 512) self.fc2 nn.Linear(512, 256) self.fc3 nn.Linear(256, 128) self.fc4 nn.Linear(128, 64) self.fc5 nn.Linear(64, 10) self.relu nn.ReLU() def forward(self, x): x x.view(x.size(0), -1) # 展平 x self.relu(self.fc1(x)) x self.relu(self.fc2(x)) x self.relu(self.fc3(x)) x self.relu(self.fc4(x)) x self.fc5(x) return x def train_and_evaluate(init_func, epochs10): model MLP().to(device) model.apply(init_func) criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lr0.01, momentum0.9) losses [] accuracies [] for epoch in range(epochs): running_loss 0.0 correct 0 total 0 for inputs, labels in trainloader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() losses.append(running_loss / len(trainloader)) accuracies.append(100 * correct / total) return losses, accuracies # 依次跑五种初始化这一步要耐心等一会儿 init_funcs { zeros: init_zeros, normal_0.05: init_random_normal, xavier: init_xavier, he: init_he, orthogonal: init_orthogonal, } results {} for name, init_func in init_funcs.items(): print(f正在训练: {name}) losses, accuracies train_and_evaluate(init_func, epochs10) results[name] {losses: losses, accuracies: accuracies}这段代码跑完你会看到老老实实的分母——零初始化在早期几步里loss下降得特别慢几乎只是SGD自己在那微调bias。normal_0.05稍好一点但效果也远不如后面的Xavier和He。3.3 用激活值直方图直观认识信号消失上面只看训练曲线还不够直观。我一般还会在前向传播中挂一个hook打印每一层输出值的均值、方差、最大值和最小值。这个过程能帮你建立起信号是否存活的视觉记忆非常有价值。def log_activation_stats(model, dataloader): 检查第一层到最后一层激活值的统计信息 model.eval() with torch.no_grad(): inputs, _ next(iter(dataloader)) inputs inputs.to(device) x inputs.view(inputs.size(0), -1) for name, layer in [(fc1, model.fc1), (fc2, model.fc2), (fc3, model.fc3), (fc4, model.fc4), (fc5, model.fc5)]: x layer(x) if name ! fc5: # 最后一层往往是分类头不需要看激活 x torch.relu(x) print(f{name}: mean{x.mean().item():.4f}, fvar{x.var().item():.4f}, fmax{x.max().item():.4f}, min{x.min().item():.4f})你可以先用init_he跑一下再用init_zeros跑一次对比会非常扎心零初始化的层输出全是0He初始化的层输出方差稳定在0.5到1.0之间。这个简单的五层网络里已经能看到端倪——如果网络再深一点到了20层、30层用错初始化的方差衰减会因为指数效应变得极其明显。3.4 实验结果对比初始化选择带来的差距我在实际实验里得到的数据大概是这样每次运行因随机种子会有小浮动但趋势非常稳定初始化方法第1轮loss第10轮loss第10轮准确率zeros2.302.3010.5%normal_0.052.281.9133.8%xavier2.171.2852.1%he2.101.0557.6%orthogonal2.211.1855.3%这个表有几个信息点值得解读zeros在第10轮准确率只有10.5%而CIFAR-10随机猜也有10%的准确率相当于完全没有学会任何东西。normal_0.05虽然每一层都动了权重但因为方差太小深层信息传递效率极低准确率卡在33%上下。Xavier和He的差距在浅层网络上没有特别夸张2到5个百分点的验证准确率差距但这只是6层全连接网络。如果你换成50层的ResNet差距就不是5个百分点而是能不能收敛的区别。orthogonal结果跟Xavier差不多但在这个小模型上并没有体现出明显优势。这符合理论预期正交初始化在RNN等需要长期演化信号的结构上优势更大全连接网络里体现不出来。所以我的结论是用ReLU网络在PyTorch里默认选He初始化kaiming_normal_ fan_in就够了。深度学习框架对Conv层默认做的也是类似操作你要做的只是确保全连接层同样用He初始化覆盖。4. 常见问题排坑实录与速查表4.1 训练一开始loss就是NaN先查什么一上来就NaN90%的情况跟初始化没有直接关系如果你用的是He或Xavier因为这两个方案本身方差控制得很稳。真正要查的是三个地方输入数据里有没有NaN或无穷大。比如文本数据pad时填入了很大的数、图像数据里混了空值。这一步最简单先跑一次np.isnan(data).any()。学习率是否过大。这是训练早期NaN最常见的元凶。SGD的学习率从0.01开始调、Adam这种自适应方法从1e-3开始调。如果一上来就设1.0任何初始化都会被你亲手送上天。损失函数和模型输出是否匹配。比如此处用BCE却忘了在网络最后加sigmoidlog(0)直接给NaN。这种错误跟初始化完全无关。如果你的数据干净、学习率合理、损失函数正确初始化方式还导致NaN最大的嫌疑是你忘了用xavier/kaiming而写了normal_且手动给了一个过大的std比如std1.0。多层网络叠乘一个batch就爆。4.2 训练初期loss下降特别慢是初始化的问题吗要分情况。如果loss在前面几十个step都没怎么动然后用很小的斜率开始缓慢下降很可能就是初始化不当。最经典的场景就是全零或过小方差初始化。解决办法立竿见影换He/Xavier观察第一个epoch的loss变化一般能看到明显的下降速度差。但也要注意另一个容易混淆的情况如果你的loss先保持平坦然后突然快速下降往往不是初始化问题而是网络进入了loss地形平缓区。这种情况常见于深层网络、不使用BN、激活函数饱和严重的结构里。此时调整初始化的意义不大不如检查是不是梯度在深层已经消失或者是否需要调整网络结构比如加残差连接。4.3 ReLU大面积死亡换初始化能救吗能救一部分但不一定能完全根治。所谓死亡ReLU是指神经元输出恒为0梯度恒为0参数永久不更新的状态。如果你的网络里ReLU死亡比例很高通常有两个原因初始化分配不当权重方差太大导致大量负加权和进入ReLU负数区学习率设置过大参数一步跨过头直接推进死区用He初始化能显著降低死亡率因为它控制了前向信号方差让ReLU输入更大概率落在正数区间。另外你也可以尝试LeakyReLU或PReLU给负数区域留一个小斜率从机制上就不容易死掉。4.4 有了BatchNorm还需要关注初始化吗这个答案是需要但可以松一口气。BatchNorm的作用是对每一层输入做标准化强行把激活值拉回近似零均值单位方差的状态。它确实能掩盖前一层初始化带来的方差失衡问题这也是为什么现代网络加了BN之后对初始化的敏感度大大降低。你甚至可以做个实验用Xavier初始化和用He初始化分别训练一个带BN的ResNet最终准确率几乎不会有差别。但BN管不了所有事。它解决的是每一层输入分布的问题并不能直接修复权重矩阵本身病态的条件数也不能解决极端情况下梯度消失/爆炸的全部问题。尤其对于上层是全连接分类头的结构来说后面几层如果有初始化方差不匹配BN无法将其完全屏蔽。所以我的建议是有BN的时候用你顺手的主流初始化就行不必刻意追求最佳方案没有BN的时候初始化就是你最要紧的刹车之一。4.5 初始化方式速查表网络结构推荐初始化理由全连接tanh/sigmoidXavier/Glorot平衡前向与反向方差全连接ReLUHe/Kaiming补偿ReLU截断导致的方差减半CNNReLUHe/Kaiming同上PyTorch Conv层默认近似RNN/LSTM正交初始化小范围均匀也可保持循环路径的信号范数TransformerHe/Xavier 均可注意力层一般配合LN使用敏感性低残差网络的shortcut置零手动置零让残差分支从恒等映射开始5. 我个人的几个实操心得与默认配置这套内容写下来和我这些年调模型的感受是一致的。我现在的默认套路非常简单只要用的是ReLU相关激活函数一律kaiming_normal_只要用的是tanh/sigmoid一律xavier_uniform_。然后bias全部置零。这条规则能覆盖95%以上的日常训练任务余下的5%交给正交初始化去处理RNN结构。再补一个真实的小技巧在搭建新网络的时候我不会直接开始长训而是先把一个batch的数据过一遍用hook打印每一层的输出统计量。如果哪一层输出方差掉到前一层的1/10以下说明该层初始化有可疑之处。这是我这两年养成的习惯每次都帮我在训练开始前就把隐患揪出来总比看着loss曲线骂一小时强。另外一个关于随机种子的提醒初始化是带随机性的。同样的初始化方案不同随机种子会让模型进入损失曲面的不同区域最终收敛结果可能差1到2个百分点。这是正常现象。如果你做对比实验比如比较两个初始化方案一定要把所有方案固定在同一个种子序列下跑多次取平均值再下结论否则很容易得出某个方案更优的错误判断。最后我特别建议所有新手都做一次那个小作业把文中的MLP代码复制下来分别跑zeros、normal_0.05、xavier和he四种初始化观察loss曲线的差距。这比看任何博客都有说服力。当你亲眼看到零初始化在第10轮准确率还在10%附近挣扎的那一刻你对初始化为什么是第一步的理解就真正到位了。
阅读完成 · 觉得有帮助?