深入解析 Kaiming 初始化:解决深度神经网络梯度消失的核心技术

引言:为什么我们需要关注权重初始化?

在构建深度神经网络时,你是否曾遇到过这样的情况:模型结构设计得很完美,数据也经过了精心的清洗,但在训练开始时,损失函数却几乎不下降,或者直接变成了 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 时,你知道该怎么做了!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/33215.html
点赞
0.00 平均评分 (0% 分数) - 0