目录
引言:为什么我们需要关注权重初始化?
在构建深度神经网络时,你是否曾遇到过这样的情况:模型结构设计得很完美,数据也经过了精心的清洗,但在训练开始时,损失函数却几乎不下降,或者直接变成了 NaN?又或者,你会发现网络前面的层学习得很快,而后面的层几乎没有什么变化?
这背后的罪魁祸首,往往是我们容易忽视的一个环节——权重初始化。
在这篇文章中,我们将深入探讨由 Kaiming He 提出的初始化方法,也被称为 He 初始化。我们将一起理解它为何能成为解决深度神经网络(尤其是使用 ReLU 激活函数的网络)中梯度消失或梯度爆炸问题的关键钥匙。我们不仅会剖析其背后的数学原理,还会通过实际的代码示例,展示如何在 PyTorch 和 TensorFlow 中应用它,并分享一些实战中的最佳实践。
问题的根源:ReLU 与方差偏移
在深度学习的发展早期,Xavier 初始化(Glorot 初始化)是非常流行的方案。它假设我们使用的是像 Sigmoid 或 Tanh 这样的线性激活函数(或对称函数)。在这些函数中,如果输入的方差保持稳定,梯度的方差也能保持稳定。
然而,随着 ReLU(线性整流单元)及其变体成为现代卷积神经网络(CNN)的标配,Xavier 初始化逐渐显露出了不足。为什么?
ReLU 的“破坏性”
让我们回想一下 ReLU 函数的定义:
$$f(x) = \max(0, x)$$
这意味着,对于所有小于 0 的输入,ReLU 都会无情地将其置为 0。这就好比一个开关,关闭了一半的信号通道。从统计学的角度来看,如果我们的输入 $x$ 服从均值为 0 的对称分布(如正态分布),那么通过 ReLU 后,输出的均值就不再是 0 了,而且信号的方差会减半。
如果你在深层网络中仍然使用 Xavier 初始化,每一层信号的方差都会逐渐缩小(因为 Xavier 没有考虑到 ReLU 的这种“截断”效应)。当信号传导到深层时,方差趋近于 0,导致梯度消失,网络也就无法学习了。
Kaiming 初始化的核心思想
Kaiming He 等人在论文《Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification》中指出,为了保证信号在正向传播和反向传播中都能保持稳定的方差,我们需要根据 ReLU 的特性来调整初始化的权重方差。
简单来说,既然 ReLU 大约“杀死”了一半的神经元(将其置零),那么为了保持总能量不变,我们在初始化剩余的权重时,就需要加倍它们的方差。这就是 Kaiming 初始化最直观的理解。
数学推导:方差的一致性
让我们通过一段简短的数学旅程来验证这个直觉。不用担心,我们会一步步拆解。
假设我们有第 $l$ 层的一个神经元,其输出 $y_l$ 可以表示为:
$$yl = Wl x{l} + bl$$
其中,$x_{l}$ 是来自上一层的输入(也就是上一层的输出经过 ReLU 后的结果)。为了推导方便,我们暂时忽略偏置 $b$(通常初始化为 0),并假设 $W$ 和 $x$ 是独立的,且均值为 0。
我们需要确保每一层输出 $yl$ 的方差等于输入 $xl$ 的方差,即 $Var(yl) = Var(xl)$。这样,信号的分布就不会在网络中发生畸变。
利用方差的性质 $Var(Wx) = E[W^2]E[x^2] + E[W]^2Var(x) + Var(W)E[x]^2$,在均值为 0 的假设下,公式简化为:
$$Var(yl) = nl \cdot Var(Wl) \cdot E[xl^2]$$
这里 $n_l$ 是该层的输入数量(即 fan-in)。
关键点来了:$E[xl^2]$ 是什么?因为 $xl = ReLU(y_{l-1})$,所以它是上一层数据经过 ReLU 后的二阶矩。
假设 $y_{l-1}$ 对称分布于 0 均值,那么 ReLU 会把一半的负值变成 0,另一半正值保持不变。数学上可以证明:
$$E[xl^2] = \frac{1}{2} E[y{l-1}^2] = \frac{1}{2} Var(y_{l-1})$$
现在,我们将 $E[xl^2]$ 代回 $Var(yl)$ 的公式中,并令 $Var(yl) = Var(y{l-1})$(保持方差一致):
$$Var(y{l-1}) = \frac{1}{2} nl \cdot Var(Wl) \cdot Var(y{l-1})$$
消去两边的 $Var(y_{l-1})$,我们可以解出权重 $W$ 需要满足的方差:
$$Var(Wl) = \frac{2}{nl}$$
看!这里的系数 2 正是为了补偿 ReLU 带来的那一半的方差损失。标准差 $\sigma$ 就是方差的平方根:
$$\sigma = \sqrt{\frac{2}{n_l}}$$
这就是 Kaiming 正态分布初始化(He Normal)公式中 $\sqrt{2/n}$ 的由来。对于均匀分布,推导逻辑类似,但区间边界会调整为 $\sqrt{6/n}$。
代码实战:在框架中应用
理论讲完了,让我们看看如何在代码中实际应用它。现在的深度学习框架(如 PyTorch 和 TensorFlow)都已经内置了这一方法,但了解如何正确调用它至关重要。
示例 1:PyTorch 中的手动设置
在 PyTorch 中,默认的卷积层和线性层通常使用 Xavier 初始化(INLINECODEd39c35be),但为了确保针对 ReLU 的最佳性能,我们通常会显式地指定 INLINECODE8dca4fc3 或 kaiming_uniform。
import torch
import torch.nn as nn
class DeepNet(nn.Module):
def __init__(self):
super(DeepNet, self).__init__()
# 定义一个包含三个全连接层的简单网络
self.fc1 = nn.Linear(784, 4096)
self.relu = nn.ReLU() # 注意:这里使用的是标准 ReLU
self.fc2 = nn.Linear(4096, 2048)
self.fc3 = nn.Linear(2048, 10)
# 初始化权重
self._initialize_weights()
def _initialize_weights(self):
# 遍历网络中的所有模块
for m in self.modules():
if isinstance(m, nn.Linear):
# 应用 Kaiming 正态初始化
# mode=‘fan_in‘ 表示基于输入的数量 n
# nonlinearity=‘relu‘ 告诉框架我们使用的是 ReLU,从而自动应用 sqrt(2/n) 的修正
nn.init.kaiming_normal_(m.weight, mode=‘fan_in‘, nonlinearity=‘relu‘)
# 别忘了偏置,通常初始化为 0
if m.bias is not None:
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = self.relu(self.fc1(x))
x = self.relu(self.fc2(x))
x = self.fc3(x)
return x
# 实例化模型并检查初始权重的分布
model = DeepNet()
# 获取 fc1 层的权重
w = model.fc1.weight.data
print(f"fc1 权重标准差: {w.std().item():.4f}")
print(f"理论标准差 (sqrt(2/784)): {(2/784)**0.5:.4f}")
代码解析:
- INLINECODEee717d8f: 这个参数非常关键。如果你使用的是 Leaky ReLU 或 PReLU,你需要将其修改为 INLINECODE7dc76ddd,否则方差修正系数会不准确。
- INLINECODE6dba20fa: 这是默认设置,意味着 $n$ 取决于输入权重的数量。这对保持正向传播的方差稳定性很有帮助。在某些反向传播特定的场景下,可能会用到 INLINECODE98168e66。
示例 2:处理 Leaky ReLU
如果你的网络架构使用了 Leaky ReLU(即 $f(x) = \max(0.01x, x)$),标准的 ReLU 初始化就不够精确了。因为 Leaky ReLU 并没有完全把负值置零,而是保留了很小的一部分(比如 0.01 倍)。这意味着方差不会减半那么多。
Kaiming 初始化允许你指定这个负斜率。
import torch.nn as nn
import torch
def init_leaky_relu_layer(layer, negative_slope=0.01):
"""
为使用 Leaky ReLU 的层应用 Kaiming 初始化。
参数:
layer: nn.Linear 或 nn.Conv2d
negative_slope: Leaky ReLU 的负斜率,默认为 0.01
"""
# 这里的 gain 参数会根据 negative_slope 自动计算
# 公式大致为: gain = sqrt(2 / (1 + negative_slope^2))
nn.init.kaiming_normal_(layer.weight, mode=‘fan_in‘, nonlinearity=‘leaky_relu‘, a=negative_slope)
if layer.bias is not None:
nn.init.zeros_(layer.bias)
# 使用示例
layer = nn.Linear(100, 100)
init_leaky_relu_layer(layer, negative_slope=0.01)
print("层已针对 Leaky ReLU 完成初始化。")
示例 3:TensorFlow / Keras 实现
在 TensorFlow 中,事情变得简单得多,因为 Keras 的层默认初始化器通常就是 INLINECODE205f65b4(Xavier),我们需要显式地改为 INLINECODE70e7869d。
import tensorflow as tf
from tensorflow.keras import layers, models
def create_cnn_with_he_init():
model = models.Sequential([
# 在 Conv2D 层中,直接指定 kernel_initializer
layers.Conv2D(32, (3, 3), activation=‘relu‘, input_shape=(28, 28, 1),
kernel_initializer=‘he_normal‘),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(64, (3, 3), activation=‘relu‘,
kernel_initializer=‘he_normal‘),
layers.MaxPooling2D((2, 2)),
layers.Flatten(),
layers.Dense(128, activation=‘relu‘,
kernel_initializer=‘he_normal‘),
layers.Dense(10, activation=‘softmax‘)
])
return model
model = create_cnn_with_he_init()
model.summary()
进阶见解:Fan-in vs Fan-out
你可能在使用 INLINECODEcd4b5f22 时看到过 INLINECODE7608833d 参数。除了默认的 INLINECODE84c341b9,还有一个 INLINECODEa0258b33 选项。这两者有什么区别呢?
- Fan-in ($n{in}$): 也就是我们上面推导中使用的 $n$。它是输入连接的数量。使用 INLINECODE1fde2cb4 进行初始化,主要是为了在正向传播时保持信号方差的一致性。
- Fan-out ($n{out}$): 它是输出连接的数量(即该层神经元的数量)。使用 INLINECODE4e4861b2 进行初始化,主要是为了在反向传播时保持梯度方差的一致性。
实战建议:
在大多数卷积神经网络(CNN)和全连接网络(MLP)中,我们主要关注正向传播的稳定性,因此保持默认的 INLINECODEf6b2cd1d 通常是最佳选择。然而,在某些特殊的网络结构(如残差网络 ResNet 的反向传播路径非常复杂)或者特定的循环神经网络(RNN)设置中,尝试使用 INLINECODE574334bd 可能会带来微小的性能提升。但在 99% 的常规深度学习任务中,你不需要改变这个默认值。
最佳实践与常见误区
1. 不要忘记偏置
虽然我们花了大量篇幅讨论权重 $W$,但偏置 $b$ 的初始化同样重要。对于 ReLU 网络,将偏置初始化为很小的正数(如 0.01) 有时是一个有用的技巧。
- 为什么? 如果偏置为 0,ReLU 在初始化阶段有一半的概率处于“死区”(输入为负,输出为 0)。这意味着一开始很多神经元根本不工作。
- 做法:给一个小的正数偏置,可以让 ReLU 在初始化时更有可能处于激活状态(正向导通),从而让梯度更容易流过网络。这在训练非常深的网络时尤为有效。
2. 批归一化改变了游戏规则
如果你的网络中使用了批归一化,对权重初始化的敏感度会大大降低。BN 层会将每一层的输入强行拉回到标准正态分布,这在一定程度上缓解了梯度消失/爆炸的问题。即便如此,使用 Kaiming 初始化依然是一个好的习惯,它能让网络在训练的最初始阶段(第一个 batch 完成统计之前)表现得更稳定。
3. 激活函数的选择决定初始化策略
一定要确保你的初始化方法与激活函数匹配:
- ReLU / Leaky ReLU / PReLU: 使用 Kaiming Initialization (He Initialization)。
- Sigmoid / Tanh / Softmax: 使用 Xavier Initialization (Glorot Initialization)。
- SELU (Scaled Exponential Linear Unit): 需要配合 LeCun Normal Initialization,且必须使用“自归一化”网络架构,不能使用 Batch Norm。
4. 调试技巧:检查激活值分布
如果你不确定自己的网络是否初始化得当,可以在训练开始前,跑一次前向传播,然后打印出每一层激活值的直方图。
- 好的现象:各层的激活值分布比较均匀,没有大量集中在 0(对于 ReLU),也没有出现极端的离群点(NaN 或 Inf)。
- 坏的现象:如果某一层的激活值全部为 0,说明 ReLU “死”了,可能需要调整初始化或降低学习率。如果数值非常大,说明发生了梯度爆炸,应减小初始化的 scale。
总结
Kaiming 初始化并非什么魔法,它是一套基于严谨数学推导的方差控制技术,专门为解决 ReLU 及其变体在深度网络中的传播问题而生。
让我们回顾一下核心要点:
- 问题根源:ReLU 会将一半的输入置零,导致信号方差减半。
- 解决方案:Kaiming 初始化通过将权重方差加倍(系数 2),补偿了 ReLU 造成的方差损失。
- 应用关键:根据你使用的激活函数(INLINECODE6b4ca53f 还是 INLINECODE7dcb3706)正确设置
nonlinearity参数。 - 实用建议:虽然 Batch Norm 能缓解初始化问题,但养成使用正确初始化(Kaiming for ReLU)的习惯,是构建稳定、高效深度学习模型的重要基石。
希望这篇文章不仅帮你理解了“怎么做”,更帮你理解了“为什么”。下次当你搭建一个深层 CNN 时,你知道该怎么做了!