网络加深后信号为何消失?Xavier 与 He 初始化如何守住方差
从深层网络的方差连乘出发,手算前向与反向信号尺度,推导 Xavier/He 初始化,并用 PyTorch 2.13 逐层诊断激活和梯度。
上一篇把损失沿计算图反传到了每一层,也看到深度会让局部 Jacobian 连乘。反向传播算法本身没有错,网络却可能在第一步更新前就已经“失声”:前向激活逐层缩成接近 0,或放大到溢出;反向梯度也会发生同样的指数级变化。
参数初始化(Parameter Initialization)不是随便给权重一点随机噪声。它要解决一个具体问题:在训练刚开始时,让信号和梯度穿过很多层后仍保持可用尺度,同时打破神经元之间的对称。 本文只追踪独立同分布近似下的方差,推导 Xavier 与 He 初始化,并把推导变成可执行的逐层体检。
01 全部设成 0 为什么不是最稳妥?#
考虑含两个隐单元的全连接层。若两个神经元的权重和偏置都相同,它们接收同一输入、输出同一激活、反传得到同一梯度,之后每一步也仍然相同。宽度为 2 的层实际只学出一个重复特征,这叫对称性(Symmetry)没有被打破。
随机初始化可以打破对称,但“随机”仍缺少尺度。设每层权重标准差都为 0.01,深层信号可能不断缩小;设为 1,又可能不断放大。初始化真正需要回答的是:权重方差应该怎样随输入连接数变化?
02 一层线性变换怎样改变方差?#
先看第 层某个神经元的预激活:
其中:
- 是上一层激活;
- ;
- ;
- mini-batch 版本中, 与
W.T[n_in,n_out]相乘,得到 。
在初始化分析的近似里,假设权重与输入相互独立、均值为 0,各分量方差相同,偏置先设为 0。不同求和项的协方差于是近似为 0:
这条式子揭示了危险:若 ,线性信号每层约减半;若等于 2,则每层约翻倍。连续 20 层后,倍率分别是 与 。
A⁽ˡ⁻¹⁾ [N,n_in]
│ 每个输出累加 n_in 项
▼
Z⁽ˡ⁾ = A⁽ˡ⁻¹⁾ @ W⁽ˡ⁾ᵀ + b⁽ˡ⁾ [N,n_out]
│
│ 非线性还会截断或压缩分布
▼
A⁽ˡ⁾ = φ(Z⁽ˡ⁾) [N,n_out]
目标:Var(A⁽ˡ⁾) 不随 l 指数消失或爆炸text03 用四个输入手算“尺度错误”#
设四个独立输入均值为 0、方差为 1,即 。
若权重方差为 :
一层后标准差从 1 变为 2。十个相同线性层后,标准差近似变为 。
若权重方差为 :
线性层前后的方差保持不变。于是最直接的前向原则是:
但神经网络在线性层后还有激活函数。上一篇使用的 ReLU 会把负值截成 0,不能继续沿用纯线性结论。
04 ReLU 为什么把 He 方差变成 2/fan_in?#
若 的分布关于 0 对称,ReLU 输出 。大约一半样本被置 0;在忽略非零均值带来的细微差别时,其二阶矩约保留一半:
要让经过“线性层 + ReLU”后的二阶矩仍与输入接近,线性层应先放大约 2 倍:
这就是 He 初始化(He Initialization),在 PyTorch API 中也称 Kaiming 初始化(Kaiming Initialization)。若从均匀分布 采样,因为其方差为 ,需要:
若从正态分布采样,则标准差为:
05 Xavier 为什么同时看 fan_in 与 fan_out?#
对 tanh 或近似线性的激活,前向保持方差希望:
反向时,设流入本层的梯度为 ,传向上一层的梯度为:
每个输入方向累加 项,因此反向保持梯度方差希望:
当扇入(Fan-in)与扇出(Fan-out)不同,两者无法同时精确满足。Xavier/Glorot 初始化取折中:
正态版标准差为 ;均匀版边界为 。它通常适合 tanh、sigmoid 的近线性区域或无激活线性层;ReLU 隐层更常使用 He。
| 初始化 | 权重方差(典型) | 主要匹配对象 | 直觉 |
|---|---|---|---|
| 固定小高斯 | 与宽度无关 | 无 | 层越宽,累计方差越大 |
| Xavier | tanh / 近线性 | 折中前向与反向 | |
| He/Kaiming | ReLU 前向 | 补偿约一半信号被截断 | |
| 正交初始化 | 由矩阵奇异值控制 | 深线性/循环结构等 | 尽量保持方向长度 |
06 完整初始化伪代码#
输入:层列表、每层激活类型、随机种子
设置随机数生成器
对每个可训练层:
若是 ReLU / Leaky ReLU 前的 Linear 或 Conv:
按 fan_in 与负半轴斜率选择 He/Kaiming 尺度
否则若是 tanh 或近线性隐藏层:
按 fan_in、fan_out 选择 Xavier 尺度与 gain
偏置初始化为 0(除非架构给出明确理由)
用一批真实尺度的输入只做前向:
记录每层激活均值、标准差、零值比例、最大绝对值
构造标量损失并反向一次:
记录每层权重梯度标准差与范数
找到第一个突然衰减、爆炸或非有限的层text初始化必须与“这个权重后面接什么激活”匹配,而不是按层类名机械选择。输出层的尺度还取决于损失:二分类 logits、回归值和 softmax 前分数不应盲目套用同一 gain。
07 不依赖框架,验证方差公式#
下面用 NumPy 构造 20 层 ReLU 网络,不训练,只比较固定小方差、Xavier 与 He。代码显式写出 与 x @ W.T:
import numpy as np
rng = np.random.default_rng(7)
batch_size, width, depth = 4096, 256, 20
x0 = rng.standard_normal((batch_size, width)) # [N,D]
def sample_weight(kind, fan_in, fan_out):
if kind == "small":
std = 0.01
elif kind == "xavier":
std = np.sqrt(2.0 / (fan_in + fan_out))
elif kind == "he":
std = np.sqrt(2.0 / fan_in)
else:
raise ValueError(kind)
return rng.normal(0.0, std, size=(fan_out, fan_in))
def forward_stats(kind):
x = x0.copy()
stats = []
for layer_idx in range(depth):
weight = sample_weight(kind, width, width) # [out,in]
z = x @ weight.T # [N,out]
x = np.maximum(z, 0.0) # ReLU
stats.append((layer_idx + 1, x.std(), (x == 0).mean()))
return stats
for kind in ("small", "xavier", "he"):
stats = forward_stats(kind)
print(kind, "layer 1/20 std:", stats[0][1], stats[-1][1])python预期不是每层标准差精确等于 1,而是看数量级:small 很快接近 0;Xavier 在 ReLU 链中通常逐层衰减;He 在有限宽度的随机波动下更能维持可用尺度。若把激活换成 tanh,结论和合适 gain 也会变化。
08 用 PyTorch 2.13 正确初始化#
PyTorch 2.13 官方 torch.nn.init ↗ 提供 xavier_uniform_、xavier_normal_、kaiming_uniform_、kaiming_normal_ 和 calculate_gain。这些函数会原地修改参数,并在 torch.no_grad() 语义下执行。
import torch
from torch import nn
class DeepMLP(nn.Module):
def __init__(self, input_dim=64, width=128, depth=8, output_dim=1):
super().__init__()
layers = []
in_features = input_dim
for _ in range(depth):
layers.extend([nn.Linear(in_features, width), nn.ReLU()])
in_features = width
layers.append(nn.Linear(width, output_dim))
self.network = nn.Sequential(*layers)
self.reset_parameters()
def reset_parameters(self):
for module in self.modules():
if isinstance(module, nn.Linear):
# 隐层先按 ReLU 初始化;最后一层随后单独覆盖
nn.init.kaiming_normal_(
module.weight,
mode="fan_in",
nonlinearity="relu",
)
nn.init.zeros_(module.bias)
output_layer = self.network[-1]
nn.init.xavier_uniform_(output_layer.weight, gain=1.0)
def forward(self, x):
return self.network(x)
model = DeepMLP()
x = torch.randn(32, 64) # [N,D]
logits = model(x) # [N,1]
assert logits.shape == (32, 1)python官方文档对 Kaiming 的一个关键提醒是:函数假设线性层按 x @ weight.T 使用,weight.shape=[fan_out,fan_in],这正是 nn.Linear 的约定。若你自己保存的是 [fan_in,fan_out] 且计算 x @ weight,应把 weight.T 传给初始化函数,否则 fan 方向会算反。
mode="fan_in" 更关注前向方差,mode="fan_out" 更关注反向方差。普通前馈网络常从 fan_in 开始;不要仅因为“梯度也重要”就给每层混用不同模式。
09 一次前向加反向,怎样定位第一层异常?#
import torch
from torch import nn
def inspect_signal_flow(model, x, target):
activations = {}
handles = []
for name, module in model.named_modules():
if isinstance(module, (nn.Linear, nn.ReLU)):
def save_output(_module, _inputs, output, layer_name=name):
value = output.detach()
activations[layer_name] = {
"shape": tuple(value.shape),
"mean": value.mean().item(),
"std": value.std().item(),
"zero_frac": (value == 0).float().mean().item(),
"finite": torch.isfinite(value).all().item(),
}
handles.append(module.register_forward_hook(save_output))
try:
logits = model(x).squeeze(-1) # [N]
loss = nn.BCEWithLogitsLoss()(logits, target) # []
model.zero_grad(set_to_none=True)
loss.backward()
finally:
for handle in handles:
handle.remove()
gradients = {}
for name, parameter in model.named_parameters():
if parameter.grad is not None:
gradients[name] = {
"shape": tuple(parameter.grad.shape),
"std": parameter.grad.std().item(),
"norm": parameter.grad.norm().item(),
"finite": torch.isfinite(parameter.grad).all().item(),
}
return loss.item(), activations, gradients
model = DeepMLP(input_dim=64, width=128, depth=8)
x = torch.randn(256, 64)
target = torch.randint(0, 2, (256,), dtype=torch.float32)
loss, activation_stats, gradient_stats = inspect_signal_flow(model, x, target)python前向钩子(Forward Hook)适合短期诊断,不应无界保存带计算图的输出;这里立即 detach() 并在结束时移除 handle。检查时重点找相邻层之间的突变,而不是迷信“std 必须等于 1”。
10 卷积层的 fan 怎样数?#
对二维卷积权重 W[out_channels,in_channels,k_h,k_w],每个输出位置累加的输入数是:
每个输入方向影响的典型输出连接数是:
因此不能只拿通道数计算初始化尺度。PyTorch 的初始化函数会根据张量形状处理常见线性与卷积权重;分组卷积、转置卷积或自定义张量布局则应额外核对 fan 的语义和真实乘法方向。
11 常见错误与最短调试路径#
- 每层复制同一组随机权重。 数值不是 0 也可能保留神经元对称;每个参数需要独立采样。
- ReLU 隐层使用过小固定标准差。 先打印每层激活 std,找到首次跌落一个数量级的层。
- 把
fan_in当成权重第 0 维。nn.Linear.weight是[fan_out,fan_in],先写出实际矩阵乘法。 - Leaky ReLU 忘记负斜率
a。kaiming_*的a应与激活的negative_slope一致。 - 初始化后又被构造器或 checkpoint 覆盖。 在加载权重之后检查参数统计;恢复训练通常不应重新初始化。
- 只看参数分布,不看真实输入。 输入尺度、偏置、残差相加与归一化都会改变信号;至少用一批经过真实预处理的数据体检。
- 把随机波动误判为错误。 固定种子,多次初始化比较分布区间;宽度很小时理论近似本来就噪声更大。
一个实用的排查顺序是:输入均值/标准差 → 每层预激活 → 激活零比例 → loss 是否有限 → 每层梯度 → 一步更新比。第一个异常层比最后的 NaN 更有信息。
12 初始化解决不了什么?#
- 极深网络中的相关性和 Jacobian 奇异值分布,不由逐层标量方差完全描述;
- tanh/sigmoid 若被大偏置或异常输入推入饱和区,合适权重方差也救不回局部导数;
- ReLU 神经元训练中可能长期落在负区,He 初始化只能降低起步时大面积死亡的风险;
- 注意力、门控循环、残差分支和输出头常有架构特定初始化规则;
- 混合精度下的溢出、错误损失缩放与过大学习率属于训练数值问题;
- 初始化不能替代归一化、残差连接、合理优化器和数据标准化。
Xavier/He 关注的是分量方差。正交初始化进一步控制线性变换的奇异值,但经过非线性、宽度变化和训练后也不再精确等距。归一化则在运行时根据激活统计调整尺度,与只在训练开始执行一次的初始化完全不同。
13 今天真正需要记住什么?#
- 随机初始化同时承担打破对称与控制尺度;“不是 0”远远不够。
- 线性层近似满足 。
- Xavier 用 与 折中前向和反向,He 用 补偿 ReLU 截断。
- 选择初始化时必须同时写出权重布局、真实矩阵乘法和后续激活。
- 最可靠的验收是一批真实输入上的逐层激活与梯度体检,而不是只打印首层权重标准差。
14 思考题与小练习#
- 一个
Linear(200, 50)后接 ReLU。分别计算 He 正态初始化的标准差、He 均匀初始化的边界,以及 Xavier 正态初始化的标准差。为什么三者不同? - 把 NumPy 实验中的 ReLU 改成纯线性和 tanh,分别比较三种初始化在 20 层后的标准差。tanh 的均值、饱和比例还应怎样记录?
- 构造一个宽度依次为
[64, 512, 32, 256]的 MLP,故意把自定义权重保存为[fan_in,fan_out]。先错误初始化,再按官方说明传入转置张量,比较逐层激活统计。
相关工作#
- LeCun et al. (1998), Efficient BackProp ↗:从输入中心化、曲率与权重尺度讨论高效训练的经典章节。
- Glorot & Bengio (2010), Understanding the Difficulty of Training Deep Feedforward Neural Networks ↗:系统分析深层前馈网络信号传播并提出 Xavier/Glorot 初始化。
- Saxe, McClelland & Ganguli (2014), Exact Solutions to the Nonlinear Dynamics of Learning in Deep Linear Neural Networks ↗:讨论深线性网络的学习动力学与正交初始化。
- He et al. (2015), Delving Deep into Rectifiers ↗:针对 ReLU/PReLU 推导尺度感知初始化并训练更深网络。
- Poole et al. (2016), Exponential Expressivity in Deep Neural Networks Through Transient Chaos ↗:从信号传播与临界性研究随机深网的有序/混沌区间。
15 下一篇预告#
健康初始化让第一批信号能够穿过网络,但每一步仍可能沿狭长损失谷底左右振荡,或让不同参数因梯度尺度差异而进度悬殊。下一篇将把反传得到的梯度变成更新轨迹,手算 Momentum 的速度累积与 Adam 的一、二阶矩和偏差修正。