kaiming初始化与批量归一化 (BN) 及残差连接详解

文章目录

深度学习中的关键技术:He初始化与批量归一化 (BN) 及残差连接

摘要

深度神经网络的成功训练离不开有效的参数初始化策略和稳定的数据分布控制。本文将详细介绍两种在现代深度学习中广泛应用的关键技术:He初始化和批量归一化(BN)。我们将探讨它们的动机、核心思想、数学原理、公式推导、实际效果以及不依赖库函数的Python实现。通过理解这些技术,无论是初学者还是研究人员,都能更深入地把握深度模型训练的精髓。


💡 思考以下场景

1. 网络已经足够深,浅层已经提取了足够好的特征

假设一个非常深的网络,其前面的层已经学习到了非常有用的特征。那么,对于紧随其后的某些层,可能并不需要对这些特征做太大的改动,保持原样(恒等映射)或者做一些微小的调整就足够了。

2. 解决网络退化问题

ResNet 提出时,一个核心的观察是"网络退化"——更深的网络反而比浅层网络在训练集和测试集上表现更差。一个理论上的解释是,如果一个浅层网络已经达到了某个性能,那么至少可以通过在这个浅层网络后面添加一些学习恒等映射的层来构建一个更深的网络,这个更深的网络不应该比浅层网络差。但实际的优化器很难让这些额外层精确地学习到恒等映射。

🔑 关键点

  • 不是说整个网络的最终目标是学习恒等映射。 整个网络的目标是学习一个从原始输入到最终任务输出(例如类别概率)的复杂映射。

  • 而是说,在深层网络中,某些局部组件(几层堆叠)可能"期望"或"需要"表现得像一个恒等映射,或者非常接近恒等映射。而传统的方法无法做到这一点。

  • 如果这些层能够轻易地学习恒等映射,那么即使增加了网络的深度,性能至少不会下降。


第0章:深度学习入门前置知识

0.1 什么是机器学习与深度学习?

机器学习 (Machine Learning, ML)

  • 定义: 计算机系统利用数据来"学习"并改进其在特定任务上表现的一种人工智能方法,而无需进行显式编程。
  • 核心思想: 从数据中自动发现模式和规律。
  • 例子: 垃圾邮件分类器(学习识别垃圾邮件的特征)、房价预测模型(学习房屋特征与价格的关系)。

深度学习 (Deep Learning, DL)

  • 定义: 机器学习的一个分支,它使用包含多个处理层的计算模型(即深度神经网络)来学习具有多个抽象级别的数据表示。
  • 核心特点: "深度"指的是神经网络中包含许多层(通常指隐藏层)。
  • 优势: 能够自动从原始数据中学习到有用的特征,而传统机器学习通常需要人工设计特征(特征工程)。
  • 例子: 图像识别(识别图片中的物体)、语音识别(将语音转换为文字)、自然语言处理(机器翻译、文本生成)。

0.2 神经网络基础 (Artificial Neural Networks, ANNs)

神经网络是深度学习的核心构建模块,其灵感来源于生物神经系统。

神经元 (Neuron) / 单元 (Unit)

网络的基本计算单元。

工作方式: 接收来自其他神经元或外部数据的输入信号,对这些输入进行加权求和,然后通过一个激活函数 (Activation Function) 产生输出信号。

数学表示(简化):

z = w 1 x 1 + w 2 x 2 + ⋯ + w n x n + b = ∑ i = 1 n w i x i + b z = w_1 x_1 + w_2 x_2 + \dots + w_n x_n + b = \sum_{i=1}^{n} w_i x_i + b z=w1x1+w2x2++wnxn+b=i=1nwixi+b

a = f ( z ) a = f(z) a=f(z)

其中:

  • x 1 , … , x n x_1, \dots, x_n x1,,xn 是输入信号
  • w 1 , … , w n w_1, \dots, w_n w1,,wn 是对应输入的权重 (Weights),表示每个输入的重要性
  • b b b偏置 (Bias),一个可调的常数项
  • z z z 是加权和(也称线性组合或净输入)
  • f ( ⋅ ) f(\cdot) f()激活函数
  • a a a 是神经元的输出(激活值)

网络层 (Layer)

神经元被组织成层:

  • 输入层 (Input Layer): 接收原始数据(例如图片的像素值)
  • 隐藏层 (Hidden Layer): 位于输入层和输出层之间,执行大部分计算和特征提取。一个深度神经网络可以有多个隐藏层
  • 输出层 (Output Layer): 产生最终结果(例如图像的类别概率)

激活函数 (Activation Function)

作用: 为神经网络引入非线性,使得网络能够学习和表示复杂的函数关系。如果没有激活函数(或者只有线性激活函数),无论网络有多少层,其整体效果都等同于一个单层线性模型。

常见的激活函数:

  • Sigmoid: f ( z ) = 1 1 + e − z f(z) = \frac{1}{1 + e^{-z}} f(z)=1+ez1

    • 输出范围 (0, 1),常用于二分类问题的输出层或早期的神经网络
    • 缺点是容易导致梯度消失
  • Tanh (双曲正切): f ( z ) = e z − e − z e z + e − z f(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}} f(z)=ez+ezezez

    • 输出范围 (-1, 1),通常比Sigmoid表现好一些,但仍有梯度饱和问题
  • ReLU (Rectified Linear Unit,修正线性单元): f ( z ) = max ⁡ ( 0 , z ) f(z) = \max(0, z) f(z)=max(0,z)

    • 计算简单,能够有效缓解梯度消失问题,是现代深度学习中最常用的激活函数之一
    • 缺点是可能导致"神经元死亡"(如果输入恒为负,梯度恒为0)
  • Leaky ReLU, PReLU, ELU等: ReLU的变种,试图解决神经元死亡问题

前向传播 (Forward Propagation)

数据从输入层开始,逐层向前传递,每一层的神经元根据前一层的输出和自身的权重、偏置计算其激活值,直到输出层产生最终结果。这是网络进行预测或推断的过程。

0.3 神经网络的训练过程

训练神经网络的目标是找到一组最优的权重 W W W 和偏置 b b b,使得网络对于给定的输入能够产生尽可能接近真实目标(标签)的输出。

1. 损失函数 (Loss Function) / 代价函数 (Cost Function)

作用: 用来衡量模型预测输出与真实目标之间的差异(即"错误"程度)。

目标: 训练过程的目标是最小化损失函数的值。

例子:

  • 均方误差 (Mean Squared Error, MSE): 常用于回归问题
    L = 1 N ∑ i = 1 N ( y true , i − y pred , i ) 2 L = \frac{1}{N} \sum_{i=1}^{N} (y_{\text{true},i} - y_{\text{pred},i})^2 L=N1i=1N(ytrue,iypred,i)2
  • 交叉熵损失 (Cross-Entropy Loss): 常用于分类问题

2. 优化器 (Optimizer)

作用: 根据损失函数计算得到的梯度,来更新网络的权重和偏置,以期逐步减小损失。

核心算法:梯度下降 (Gradient Descent)

思想: 沿着损失函数梯度的反方向(即下降最快的方向)调整参数。

更新规则 (简化):
W new = W old − α ∂ L ∂ W old W_{\text{new}} = W_{\text{old}} - \alpha \frac{\partial L}{\partial W_{\text{old}}} Wnew=WoldαWoldL

其中 α \alpha α学习率 (Learning Rate),控制每次更新的步长。

变种:

  • 批量梯度下降 (Batch Gradient Descent): 使用整个训练集计算梯度。计算开销大,不常用
  • 随机梯度下降 (Stochastic Gradient Descent, SGD): 每次只使用一个样本计算梯度。更新快,但梯度噪声大
  • 小批量随机梯度下降 (Mini-batch SGD): 每次使用一小批样本计算梯度。是实践中最常用的方法,平衡了计算效率和梯度稳定性
  • 高级优化器: Adam, RMSprop, Adagrad等,它们在SGD的基础上引入了动量、自适应学习率等机制,通常能更快更好地收敛

3. 反向传播算法 (Backpropagation)

作用: 高效地计算损失函数相对于网络中所有权重和偏置的梯度。

核心思想: 链式法则 (Chain Rule)。从输出层开始,将损失逐层向后传播,计算每一层参数对最终损失的贡献(即梯度)。

这是训练神经网络的核心计算步骤。

4. 训练周期 (Epoch)

指整个训练数据集在神经网络中完整地前向传播和反向传播一次的过程。通常需要训练多个周期才能使模型达到较好的性能。

0.4 理解方差 (Variance) 和标准差 (Standard Deviation)

方差 ( Var ( X ) \text{Var}(X) Var(X) σ 2 \sigma^2 σ2)

衡量一组数据离其均值的偏离程度。方差越大,数据点越分散。

计算公式:

  • 总体: σ 2 = ∑ i = 1 N ( x i − μ ) 2 N \sigma^2 = \frac{\sum_{i=1}^{N} (x_i - \mu)^2}{N} σ2=Ni=1N(xiμ)2,其中 μ \mu μ 是总体均值
  • 样本(无偏估计): s 2 = ∑ i = 1 n ( x i − x ˉ ) 2 n − 1 s^2 = \frac{\sum_{i=1}^{n} (x_i - \bar{x})^2}{n-1} s2=n1i=1n(xixˉ)2,其中 x ˉ \bar{x} xˉ 是样本均值

标准差 ( σ \sigma σ s s s)

方差的平方根。它与原始数据具有相同的单位,更容易解释。

σ = Var ( X ) \sigma = \sqrt{\text{Var}(X)} σ=Var(X)

0.5 为什么这些前置知识重要?

  • He初始化 关注的是权重的初始方差,以确保信号(激活值和梯度)在前向传播反向传播中以稳定尺度流动,特别是当使用ReLU激活函数时。

  • 批量归一化 (BN) 通过对每一的输入(在一个小批量数据上)进行归一化(调整均值方差),来解决内部协变量偏移问题,从而稳定和加速神经网络的训练,并对梯度传播有益。

  • 残差连接 解决了深度神经网络退化问题,这与优化器难以学习某些复杂映射(如恒等映射)以及梯度在深层网络中有效传播有关。


第一部分:He 初始化 (He Initialization)

1.1 背景与动机

在深度神经网络中,权重的初始值对训练过程至关重要。不恰当的初始化会导致以下问题:

梯度消失 (Vanishing Gradients)

如果权重过小,信号在逐层前向传播时会衰减,导致反向传播时梯度也变得极小,使得网络浅层参数几乎不更新。

梯度爆炸 (Exploding Gradients)

如果权重过大,信号会逐层放大,导致反向传播时梯度变得极大,使得训练不稳定,甚至发散。

目标是找到一种初始化方法,使得信号(激活值和梯度)在网络中以一个合理的尺度传播。

1.2 核心思想

He初始化的核心思想是,在网络前向传播和反向传播过程中,保持每一层输入和输出的方差大致相同。特别是针对使用ReLU及其变种作为激活函数的网络层。

1.3 数学原理与公式推导 (针对ReLU激活函数)

前向传播方差分析

假设一个全连接层的计算为 y l = W l x l + b l y_l = W_l x_l + b_l yl=Wlxl+bl,其中 x l x_l xl 是第 l l l 层的输入, W l W_l Wl 是权重, b l b_l bl 是偏置。我们通常将偏置初始化为0。激活后的输出为 a l = f ( y l ) a_l = f(y_l) al=f(yl),其中 f f f 是ReLU激活函数。

我们关注方差。假设 x l x_l xl 的元素和 W l W_l Wl 的元素都是独立同分布的,并且均值为0:

E [ x l ] = 0 , E [ W l ] = 0 E[x_l] = 0, \quad E[W_l] = 0 E[xl]=0,E[Wl]=0

那么, y l y_l yl 的一个元素的方差可以表示为:

Var ( y l , i ) = Var ( ∑ k = 1 n l − 1 W l , i k x l − 1 , k ) \text{Var}(y_{l,i}) = \text{Var}\left(\sum_{k=1}^{n_{l-1}} W_{l,ik} x_{l-1,k}\right) Var(yl,i)=Var(k=1nl1Wl,ikxl1,k)

由于 W l , i k W_{l,ik} Wl,ik x l − 1 , k x_{l-1,k} xl1,k 相互独立,且均值为0:

Var ( y l , i ) = ∑ k = 1 n l − 1 Var ( W l , i k x l − 1 , k ) \text{Var}(y_{l,i}) = \sum_{k=1}^{n_{l-1}} \text{Var}(W_{l,ik} x_{l-1,k}) Var(yl,i)=k=1nl1Var(Wl,ikxl1,k)

又因为 E [ W l , i k 2 x l − 1 , k 2 ] = E [ W l , i k 2 ] E [ x l − 1 , k 2 ] = Var ( W l , i k ) Var ( x l − 1 , k ) E[W_{l,ik}^2 x_{l-1,k}^2] = E[W_{l,ik}^2] E[x_{l-1,k}^2] = \text{Var}(W_{l,ik}) \text{Var}(x_{l-1,k}) E[Wl,ik2xl1,k2]=E[Wl,ik2]E[xl1,k2]=Var(Wl,ik)Var(xl1,k)(因为均值为0,方差等于二阶矩)。

所以:
Var ( y l , i ) = n l − 1 Var ( W l ) Var ( x l − 1 ) \text{Var}(y_{l,i}) = n_{l-1} \text{Var}(W_{l}) \text{Var}(x_{l-1}) Var(yl,i)=nl1Var(Wl)Var(xl1)

其中 n l − 1 n_{l-1} nl1 是第 l − 1 l-1 l1 层的神经元数量(即第 l l l 层的输入维度,fan_in)。 Var ( W l ) \text{Var}(W_l) Var(Wl) 是权重 W l W_l Wl 中单个元素的方差。

现在考虑ReLU激活函数 f ( x ) = max ⁡ ( 0 , x ) f(x) = \max(0, x) f(x)=max(0,x)。如果 y l y_l yl 的均值为0且对称分布(例如高斯分布),那么ReLU会将其一半的输入置为0。

可以证明,对于均值为0的输入 y l y_l yl
Var ( f ( y l ) ) = 1 2 Var ( y l ) \text{Var}(f(y_l)) = \frac{1}{2} \text{Var}(y_l) Var(f(yl))=21Var(yl)

如果我们希望每一层的输入方差 Var ( x l ) \text{Var}(x_l) Var(xl) 和输出方差 Var ( x l − 1 ) \text{Var}(x_{l-1}) Var(xl1) 保持一致,即 Var ( x l ) = Var ( x l − 1 ) \text{Var}(x_l) = \text{Var}(x_{l-1}) Var(xl)=Var(xl1),则需要:

1 = 1 2 n l − 1 Var ( W l ) 1 = \frac{1}{2} n_{l-1} \text{Var}(W_{l}) 1=21nl1Var(Wl)

因此,权重方差:
Var ( W l ) = 2 n l − 1 \text{Var}(W_{l}) = \frac{2}{n_{l-1}} Var(Wl)=nl12

反向传播方差分析

在反向传播中,梯度的方差也需要保持稳定。可以类似地推导出,为了保持梯度的方差,权重方差也应满足 Var ( W l ) = 2 n l \text{Var}(W_l) = \frac{2}{n_l} Var(Wl)=nl2,其中 n l n_l nl 是第 l l l 层的输出维度 (fan_out)。

实践中,通常使用 fan_in 或者 (fan_in + fan_out) / 2(Xavier/Glorot 初始化)。He 初始化明确针对ReLU,主要关注 fan_in

He 初始化规则

高斯分布 (Normal Distribution)

权重从均值为 0,标准差为 σ = 2 n in \sigma = \sqrt{\frac{2}{n_{\text{in}}}} σ=nin2 的高斯分布中采样:

W ∼ N ( 0 , Var ( W ) ) , Var ( W ) = 2 n in W \sim N(0, \text{Var}(W)), \quad \text{Var}(W) = \frac{2}{n_{\text{in}}} WN(0,Var(W)),Var(W)=nin2

均匀分布 (Uniform Distribution)

权重从区间 [ − l i m i t , l i m i t ] [-limit, limit] [limit,limit] 的均匀分布中采样,其中 l i m i t = 6 n in limit = \sqrt{\frac{6}{n_{\text{in}}}} limit=nin6

对于均匀分布 U ( − l i m i t , l i m i t ) U(-limit, limit) U(limit,limit),其方差为 l i m i t 2 3 \frac{limit^2}{3} 3limit2。令 l i m i t 2 3 = 2 n in \frac{limit^2}{3} = \frac{2}{n_{\text{in}}} 3limit2=nin2,则 l i m i t = 6 n in limit = \sqrt{\frac{6}{n_{\text{in}}}} limit=nin6

对于卷积层: n in n_{\text{in}} nin 通常是 输入通道数 * 卷积核高度 * 卷积核宽度

1.4 代码实现 (Python, 无库函数)

import numpy as np

def he_initialization_normal(fan_in, fan_out):
    """
    He正态分布初始化权重。

    参数:
    fan_in (int): 输入单元的数量
    fan_out (int): 输出单元的数量 (虽然He主要用fan_in,但通常会传入fan_out以保持接口一致性)

    返回:
    numpy.ndarray: 初始化后的权重矩阵 (fan_out, fan_in)
    """
    stddev = np.sqrt(2.0 / fan_in)
    # 注意:np.random.randn 产生标准正态分布(均值为0,方差为1)
    # 我们需要乘以stddev使其标准差为我们期望的值
    # 形状通常是 (输出维度, 输入维度)
    return np.random.randn(fan_out, fan_in) * stddev

def he_initialization_uniform(fan_in, fan_out):
    """
    He均匀分布初始化权重。

    参数:
    fan_in (int): 输入单元的数量
    fan_out (int): 输出单元的数量

    返回:
    numpy.ndarray: 初始化后的权重矩阵 (fan_out, fan_in)
    """
    limit = np.sqrt(6.0 / fan_in)
    # np.random.uniform(low, high, size)
    return np.random.uniform(-limit, limit, (fan_out, fan_in))

# 示例使用
input_dim = 784  # 例如MNIST图像展平后的维度
hidden_dim = 256
output_dim = 10

# 初始化第一层权重 (输入层 -> 隐藏层)
W1_normal = he_initialization_normal(input_dim, hidden_dim)
W1_uniform = he_initialization_uniform(input_dim, hidden_dim)
b1 = np.zeros((hidden_dim, 1)) # 偏置通常初始化为0

# 初始化第二层权重 (隐藏层 -> 输出层)
# 如果最后一层不是ReLU (例如Softmax),可能不适用He初始化,但这里为演示
W2_normal = he_initialization_normal(hidden_dim, output_dim)
W2_uniform = he_initialization_uniform(hidden_dim, output_dim)
b2 = np.zeros((output_dim, 1))

print("He 正态初始化 W1 (部分):", W1_normal[0, :5])
print("W1_normal 的均值:", np.mean(W1_normal))
print("W1_normal 的方差:", np.var(W1_normal), "期望方差:", 2.0/input_dim)
print("-" * 30)
print("He 均匀初始化 W1 (部分):", W1_uniform[0, :5])
print("W1_uniform 的均值:", np.mean(W1_uniform))
print("W1_uniform 的方差:", np.var(W1_uniform), "期望方差:", 2.0/input_dim)

1.5 效果与讨论

缓解梯度消失/爆炸

He初始化显著改善了使用ReLU激活函数的深层网络的训练稳定性。

加速收敛

通过提供更好的初始权重,网络可以更快地收敛到较好的解。

与激活函数相关

He初始化是专门为ReLU及其变种(如Leaky ReLU, PReLU)设计的。对于sigmoid或tanh等激活函数,Xavier/Glorot初始化通常更合适。


第二部分:批量归一化 (Batch Normalization, BN)

2.1 背景与动机

深度神经网络的训练面临一个挑战,即内部协变量偏移 (Internal Covariate Shift, ICS)

ICS指的是在训练过程中,由于前面网络层的参数发生变化,导致后续网络层输入的分布不断发生变化的现象。

问题:

  • 训练减速: 后续层需要不断适应其输入分布的变化
  • 对初始化敏感: ICS使得网络对参数初始化和学习率的选择更加敏感
  • 饱和激活函数问题: 如果输入数据落入激活函数(如sigmoid)的饱和区域,梯度会变得很小,学习停滞

BN的目标是减少ICS,通过在网络的每一层(通常在激活函数之前)对输入数据进行归一化处理,使其具有固定的均值和方差。

2.2 核心思想

BN通过对每个小批量(mini-batch)的激活值进行归一化,然后通过可学习的缩放和平移参数来恢复其表达能力。

2.3 数学原理与公式推导

对于一个包含 m m m 个样本的小批量数据 B = { x 1 , x 2 , … , x m } B = \{x_1, x_2, \dots, x_m\} B={x1,x2,,xm},针对其中一个激活(或特征维度):

1. 计算小批量均值 (Mini-batch Mean)

μ B = 1 m ∑ i = 1 m x i \mu_B = \frac{1}{m} \sum_{i=1}^{m} x_i μB=m1i=1mxi

2. 计算小批量方差 (Mini-batch Variance)

σ B 2 = 1 m ∑ i = 1 m ( x i − μ B ) 2 \sigma_B^2 = \frac{1}{m} \sum_{i=1}^{m} (x_i - \mu_B)^2 σB2=m1i=1m(xiμB)2

3. 归一化 (Normalize)

x ^ i = x i − μ B σ B 2 + ϵ \hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}} x^i=σB2+ϵ xiμB

其中 ϵ \epsilon ϵ 是一个很小的正数(例如 10 − 5 10^{-5} 105),用于防止除以零并增加数值稳定性。

4. 缩放和平移 (Scale and Shift)

y i = γ x ^ i + β y_i = \gamma \hat{x}_i + \beta yi=γx^i+β

其中 γ \gamma γ (gamma) 和 β \beta β (beta) 是可学习的参数,与该激活(或特征维度)相关联。它们通过反向传播进行学习。

  • γ \gamma γ 允许BN层调整归一化后数据的方差
  • β \beta β 允许BN层调整归一化后数据的均值
  • 如果网络发现原始激活分布更好,它可以学习 γ = σ B 2 + ϵ \gamma = \sqrt{\sigma_B^2 + \epsilon} γ=σB2+ϵ β = μ B \beta = \mu_B β=μB,从而近似地恢复原始激活

训练与推断的区别

训练阶段

  • μ B \mu_B μB σ B 2 \sigma_B^2 σB2 是基于当前小批量计算的
  • γ \gamma γ β \beta β 通过反向传播更新
  • BN层还会维护全局的移动平均均值 (running_mean)移动平均方差 (running_var)。这些值在每次前向传播后,使用当前小批量的均值和方差进行指数移动平均更新:

running_mean ← momentum × running_mean + ( 1 − momentum ) × μ B \text{running\_mean} \leftarrow \text{momentum} \times \text{running\_mean} + (1 - \text{momentum}) \times \mu_B running_meanmomentum×running_mean+(1momentum)×μB

running_var ← momentum × running_var + ( 1 − momentum ) × σ B 2 \text{running\_var} \leftarrow \text{momentum} \times \text{running\_var} + (1 - \text{momentum}) \times \sigma_B^2 running_varmomentum×running_var+(1momentum)×σB2

其中 momentum 通常是一个接近1的数,例如0.9或0.99。

推断(测试)阶段

  • 不再计算小批量的均值和方差(因为单个样本或小批量可能不具有代表性,且希望模型行为确定)
  • 使用训练阶段累积得到的全局 running_meanrunning_var 来进行归一化:

x ^ = x − running_mean running_var + ϵ \hat{x} = \frac{x - \text{running\_mean}}{\sqrt{\text{running\_var} + \epsilon}} x^=running_var+ϵ xrunning_mean

  • γ \gamma γ β \beta β 是训练好的固定参数

y = γ x ^ + β y = \gamma \hat{x} + \beta y=γx^+β

BN的反向传播

BN层的反向传播相对复杂,需要计算损失函数对 γ \gamma γ, β \beta β, 以及BN层输入 x x x 的梯度:

∂ L ∂ γ = ∑ i = 1 m ∂ L ∂ y i x ^ i \frac{\partial L}{\partial \gamma} = \sum_{i=1}^m \frac{\partial L}{\partial y_i} \hat{x}_i γL=i=1myiLx^i

∂ L ∂ β = ∑ i = 1 m ∂ L ∂ y i \frac{\partial L}{\partial \beta} = \sum_{i=1}^m \frac{\partial L}{\partial y_i} βL=i=1myiL

∂ L ∂ x i \frac{\partial L}{\partial x_i} xiL 的计算涉及到 μ B \mu_B μB σ B 2 \sigma_B^2 σB2 x i x_i xi 的依赖关系。

2.4 代码实现 (Python, 无库函数)

import numpy as np

class BatchNorm1d:
    def __init__(self, num_features, eps=1e-5, momentum=0.9):
        """
        一维批量归一化层。

        参数:
        num_features (int): 输入特征的数量 (例如,全连接层后的神经元数量)
        eps (float): 防止除以零的小值
        momentum (float): 用于计算运行均值和方差的动量
        """
        self.num_features = num_features
        self.eps = eps
        self.momentum = momentum

        # 可学习参数 gamma 和 beta
        # 通常初始化 gamma 为 1, beta 为 0
        self.gamma = np.ones((1, num_features)) # 形状 (1, D) 以便广播
        self.beta = np.zeros((1, num_features))  # 形状 (1, D)

        # 运行均值和运行方差 (用于推断)
        self.running_mean = np.zeros((1, num_features))
        self.running_var = np.ones((1, num_features)) # 通常初始化为1而不是0

        # 缓存中间变量以便反向传播
        self.cache = None
        self.training_mode = True # 默认为训练模式

    def train(self):
        self.training_mode = True

    def eval(self):
        self.training_mode = False

    def forward(self, x):
        """
        前向传播。
        x 的形状: (N, D), N是批量大小, D是特征数量
        """
        N, D = x.shape
        if D != self.num_features:
            raise ValueError(f"输入特征维度 {D} 与层定义的特征维度 {self.num_features} 不匹配")

        if self.training_mode:
            # 1. 计算小批量均值和方差
            sample_mean = np.mean(x, axis=0, keepdims=True) # (1, D)
            sample_var = np.var(x, axis=0, keepdims=True)   # (1, D)

            # 2. 归一化
            x_normalized = (x - sample_mean) / np.sqrt(sample_var + self.eps) # (N, D)

            # 3. 缩放和平移
            out = self.gamma * x_normalized + self.beta # (N, D)

            # 4. 更新运行均值和方差
            self.running_mean = self.momentum * self.running_mean + (1 - self.momentum) * sample_mean
            self.running_var = self.momentum * self.running_var + (1 - self.momentum) * sample_var

            # 缓存用于反向传播的值
            self.cache = (x, x_normalized, sample_mean, sample_var, self.gamma, self.eps)
        else: # 推断模式
            # 使用运行均值和方差
            x_normalized = (x - self.running_mean) / np.sqrt(self.running_var + self.eps)
            out = self.gamma * x_normalized + self.beta
        
        return out

    def backward(self, dout):
        """
        反向传播。
        dout 的形状: (N, D), 上游传来的梯度
        """
        if not self.training_mode:
            raise RuntimeError("Backward pass should only be called in training mode.")

        x, x_normalized, sample_mean, sample_var, gamma, eps = self.cache
        N, D = dout.shape

        # 计算 gamma 和 beta 的梯度
        dgamma = np.sum(dout * x_normalized, axis=0, keepdims=True) # (1, D)
        dbeta = np.sum(dout, axis=0, keepdims=True)                 # (1, D)

        # 计算对 x_normalized 的梯度
        dx_normalized = dout * gamma # (N, D)

        # 计算对 sample_var 的梯度
        inv_std = 1.0 / np.sqrt(sample_var + eps) # (1, D)
        dsample_var = np.sum(dx_normalized * (x - sample_mean) * (-0.5) * (inv_std**3), axis=0, keepdims=True) # (1, D)

        # 计算对 sample_mean 的梯度
        dsample_mean_term1 = np.sum(dx_normalized * (-inv_std), axis=0, keepdims=True) # (1,D)
        
        # 梯度反向传播到 x
        # 1. 通过 x_normalized
        dx1 = dx_normalized * inv_std # (N,D)
        # 2. 通过 sample_mean
        dx2 = (1.0/N) * dsample_mean_term1 # (1,D) broadcasted
        # 3. 通过 sample_var
        dx3_term = dsample_var * (2.0/N) * (x - sample_mean) # (N,D)

        dx = dx1 - dx2 - dx3_term # Note the minus signs for dmu and dvar contributions

        return dx, dgamma, dbeta


# 示例使用
N_samples = 4
D_features = 3
bn_layer = BatchNorm1d(D_features)

# 模拟输入数据 (N, D)
x_input = np.random.randn(N_samples, D_features) * 2 + 3 # 均值3,标准差2
print("输入 x:\n", x_input)

# 训练模式前向传播
bn_layer.train()
output_train = bn_layer.forward(x_input)
print("\n训练模式输出 (期望均值接近0, 方差接近1):\n", output_train)
print("训练输出均值 (各特征):", np.mean(output_train, axis=0))
print("训练输出方差 (各特征):", np.var(output_train, axis=0))
print("运行均值:", bn_layer.running_mean)
print("运行方差:", bn_layer.running_var)

# 模拟上游梯度
dout_sim = np.random.randn(N_samples, D_features)

# 训练模式反向传播
dx_grad, dgamma_grad, dbeta_grad = bn_layer.backward(dout_sim)
print("\n反向传播梯度 dx (部分):\n", dx_grad[0])
print("反向传播梯度 dgamma:\n", dgamma_grad)
print("反向传播梯度 dbeta:\n", dbeta_grad)

# 推断模式前向传播
bn_layer.eval()
x_test = np.random.randn(2, D_features) * 2 + 3 # 新的测试数据
print("\n测试输入 x_test:\n", x_test)
output_test = bn_layer.forward(x_test)
print("\n推断模式输出 (使用运行均值/方差归一化):\n", output_test)
# 注意:推断模式输出的均值和方差不一定是0和1,取决于测试数据和运行统计量

注意: BN反向传播的直接手动实现较为复杂且容易出错,上述代码中的backward部分是一个简化版本,可能与库函数的精确实现有差异,尤其是dx的计算。完整的推导需要仔细处理每个变量之间的依赖关系。在实际应用中,强烈建议使用成熟的深度学习框架(如PyTorch, TensorFlow)提供的BN层实现,它们经过了严格测试和优化。

2.5 效果与讨论

✅ 优势

  • 减少内部协变量偏移: 显著稳定了网络各层输入的分布,加速了训练过程
  • 允许更高的学习率: 由于归一化,网络对参数的尺度不那么敏感,可以使用更大的学习率
  • 正则化效果: 小批量统计引入的噪声具有一定的正则化作用,有时可以减少对Dropout的依赖
  • 缓解梯度问题: 通过将激活值约束在较好范围内,有助于梯度的稳定传播

⚠️ 注意事项

  • 对小批量大小敏感: BN的效果依赖于小批量统计量的准确性。如果批量太小(例如1或2),均值和方差的估计可能非常嘈杂,影响BN性能。在这种情况下,可以考虑其他归一化方法,如层归一化(Layer Normalization)或组归一化(Group Normalization)
  • 应用位置: 通常应用于卷积层或全连接层之后,激活函数(如ReLU)之前

第三部分:残差连接 (Residual Connections)

3.1 背景与动机:深度网络的退化问题

在He初始化和批量归一化(BN)等技术的帮助下,研究人员能够开始训练比以往更深的网络。然而,当网络深度达到一定程度(例如数十层)时,出现了一个令人困惑的现象:网络退化 (Degradation Problem)

🔍 退化现象

  • 现象: 随着网络层数的增加,模型的准确率先是达到饱和,然后迅速下降
  • 关键点: 这种性能下降不仅仅是测试集上的过拟合,更严重的是,训练集上的误差也会随之增加
  • 理论与实际的矛盾: 理论上,一个更深的模型至少应该能够达到其较浅对应模型的性能

例如,如果一个浅层网络已经训练好了,我们可以通过在它后面添加一些学习"恒等映射 (Identity Mapping)"的层来构建一个更深的模型,这个更深的模型至少不应该比原来的浅层网络差。但实际的优化器似乎很难找到这种(或更好的)解。

这表明,简单地堆叠网络层使得优化变得异常困难,即使这些额外的层仅仅需要学习一个恒等变换。

3.2 残差学习 (Residual Learning) 的核心思想

为了解决深度网络的退化问题,何恺明等人在其著名的ResNet论文中提出了残差学习框架。其核心思想是:

与其让堆叠的网络层直接学习一个期望的底层映射 H ( x ) H(x) H(x),不如让它们学习一个残差映射 F ( x ) = H ( x ) − x F(x) = H(x) - x F(x)=H(x)x

那么,原始的期望映射就变成了:
H ( x ) = F ( x ) + x H(x) = F(x) + x H(x)=F(x)+x

💡 直观理解

  • 如果某些层需要学习一个接近恒等映射的变换(即 H ( x ) ≈ x H(x) \approx x H(x)x),那么让这些层学习一个接近于零的残差 F ( x ) ≈ 0 F(x) \approx 0 F(x)0 会比直接学习 H ( x ) ≈ x H(x) \approx x H(x)x 容易得多。因为将权重参数驱动到接近零比驱动到精确拟合一个恒等变换(通常需要特定的权重结构)要简单。

  • 即使最优函数不是恒等映射,残差学习也可以被看作是对恒等映射的一种"扰动学习",这可能比从头学习一个全新的复杂映射更容易。

3.3 残差块 (Residual Block) 的结构

残差学习通过引入"快捷连接 (Shortcut Connections)"或"跳跃连接 (Skip Connections)"来实现 F ( x ) + x F(x) + x F(x)+x 的结构。

典型的残差块结构(以两层为例)

      x (输入)
      |
      |---- Shortcut ----|
      |                  |
  权重层 (W1, BN, ReLU)  |
      |                  |
  权重层 (W2, BN)      |  (通常在相加后再接一个ReLU)
      |                  |
    F(x)                 |
      |                  |
      +------------------+
      | (逐元素相加)
    F(x) + x
      |
    ReLU (可选,通常有)
      |
    H(x) (输出)

数学表示

令残差块的输入为 x x x。堆叠的网络层学习的函数为 F ( x , { W i } ) F(x, \{W_i\}) F(x,{Wi}),其中 { W i } \{W_i\} {Wi} 是这些层的权重。

则残差块的输出 y y y(即 H ( x ) H(x) H(x))可以表示为:
y = F ( x , { W i } ) + x y = F(x, \{W_i\}) + x y=F(x,{Wi})+x

在实际应用中,通常在 F ( x , { W i } ) + x F(x, \{W_i\}) + x F(x,{Wi})+x 之后再接一个ReLU激活函数:
y = ReLU ( F ( x , { W i } ) + x ) y = \text{ReLU}(F(x, \{W_i\}) + x) y=ReLU(F(x,{Wi})+x)

3.4 快捷连接的类型

1. 恒等快捷连接 (Identity Shortcut)

  • F ( x ) F(x) F(x) 的输出维度与输入 x x x 的维度相同时,可以直接将 x x x 加到 F ( x ) F(x) F(x)
  • 这种连接不引入任何额外的参数或计算复杂度(除了逐元素加法)
  • 这是ResNet中最常用的类型,也是其成功的关键之一

2. 投影快捷连接 (Projection Shortcut)

  • F ( x ) F(x) F(x) 的输出维度与输入 x x x 的维度不同时(例如,由于下采样或通道数增加),需要对 x x x 进行变换以匹配维度,然后才能相加
  • 常用的变换方法是使用一个1×1的卷积层(称为投影卷积)作用于 x x x

y = F ( x , { W i } ) + W s x y = F(x, \{W_i\}) + W_s x y=F(x,{Wi})+Wsx

其中 W s W_s Ws 是投影卷积的权重。

  • 投影快捷连接会引入额外的参数和计算。ResNet论文中对比了全用投影、仅在维度变化时用投影、以及用零填充,发现仅在维度变化时使用投影(或恒等映射配合零填充)已经足够好,且更经济。

3.5 残差连接的优势

1. 🎯 缓解退化问题,易于优化

如前所述,如果恒等映射是最优的,残差块可以轻易地通过将 F ( x ) F(x) F(x) 的权重驱动到零来实现。这使得非常深的网络更容易优化。

实验表明(例如ResNet论文中的图4),使用残差连接后,更深的网络确实能比浅层网络取得更好的训练和测试效果,解决了退化问题。

2. 🔄 改善梯度传播

考虑反向传播。损失函数 L L L 对输入 x x x 的梯度 ∂ L ∂ x \frac{\partial L}{\partial x} xL 可以通过链式法则从输出 y y y 的梯度 ∂ L ∂ y \frac{\partial L}{\partial y} yL 得到。

对于 y = F ( x ) + x y = F(x) + x y=F(x)+x(暂时忽略ReLU):

∂ L ∂ x = ∂ L ∂ y ∂ y ∂ x = ∂ L ∂ y ( ∂ F ( x ) ∂ x + ∂ x ∂ x ) = ∂ L ∂ y ( ∂ F ( x ) ∂ x + 1 ) \frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} \frac{\partial y}{\partial x} = \frac{\partial L}{\partial y} \left(\frac{\partial F(x)}{\partial x} + \frac{\partial x}{\partial x}\right) = \frac{\partial L}{\partial y} \left(\frac{\partial F(x)}{\partial x} + 1\right) xL=yLxy=yL(xF(x)+xx)=yL(xF(x)+1)

这个 +1 项非常关键! 它意味着即使 ∂ F ( x ) ∂ x \frac{\partial F(x)}{\partial x} xF(x) 非常小(例如接近0,可能导致梯度消失),梯度仍然可以通过这个 +1 的路径直接从 ∂ L ∂ y \frac{\partial L}{\partial y} yL 传递到 ∂ L ∂ x \frac{\partial L}{\partial x} xL

对于一个包含多个残差块的深度网络,梯度可以通过一系列的快捷连接相对无衰减地传播到较浅的层,极大地缓解了梯度消失问题。

3. 🏗️ 允许构建更深的网络

由于优化更容易且梯度传播更顺畅,残差连接使得构建和训练数百层甚至上千层的神经网络成为可能。ResNet论文中就成功训练了152层的网络,并在后续工作中探索了超过1000层的网络。

4. 🧩 模块化设计

残差块提供了一种标准化的构建模块,可以方便地堆叠起来形成非常深的网络。

3.6 残差网络的变体与讨论

瓶颈结构 (Bottleneck Design)

对于非常深的网络(如ResNet-50, 101, 152),为了减少参数量和计算成本,使用了"瓶颈"残差块。这种块通常包含三层卷积:

  1. 1×1卷积: 减少通道数(瓶颈输入)
  2. 3×3卷积: 在较少通道上进行特征提取
  3. 1×1卷积: 恢复(或增加)通道数(瓶颈输出)

快捷连接仍然连接瓶颈块的输入和输出。

预激活 (Pre-activation)

后续研究(如 “Identity Mappings in Deep Residual Networks” by He et al., 2016)发现,将BN和ReLU等操作放在权重层之前(即所谓的"预激活"结构),可以进一步改善梯度流和正则化效果,使得训练更深的网络(如1001层)更加容易,并取得更好的性能。

预激活残差块的典型顺序是: BN → ReLU → 权重层 (卷积) → BN → ReLU → 权重层 (卷积) → 相加。

与稠密连接 (Dense Connections) 的比较

DenseNet是另一种利用快捷连接思想的网络,但它将每一层的输出都连接到后续所有层的输入,形成更"稠密"的连接模式。

3.7 代码示例思路 (概念性)

由于残差块的实现依赖于底层的卷积、BN等操作,这里给出一个概念性的Python类结构,说明如何组织残差块。实际实现需要依赖一个深度学习框架或自己实现这些底层操作。

import numpy as np # 假设已有其他模块实现Conv2D, BatchNorm2d, ReLU

class BasicBlock: # ResNet-18/34 中的基础残差块
    def __init__(self, in_channels, out_channels, stride=1):
        super().__init__()
        self.stride = stride
        self.in_channels = in_channels
        self.out_channels = out_channels

        # F(x) 部分
        self.conv1 = Conv2D(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = BatchNorm2d(out_channels)
        self.relu = ReLU()
        self.conv2 = Conv2D(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = BatchNorm2d(out_channels)

        # Shortcut 部分
        self.shortcut = Sequential() # 一个空的序列,如果不需要投影
        if stride != 1 or in_channels != out_channels:
            # 需要投影来匹配维度
            self.shortcut = Sequential(
                Conv2D(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
                BatchNorm2d(out_channels)
            )

    def forward(self, x):
        identity = x # 保存原始输入用于快捷连接

        # F(x) 的计算
        out = self.conv1.forward(x)
        out = self.bn1.forward(out)
        out = self.relu.forward(out)

        out = self.conv2.forward(out)
        out = self.bn2.forward(out)

        # Shortcut 连接
        identity_transformed = self.shortcut.forward(identity)
        out += identity_transformed # 逐元素相加

        out = self.relu.forward(out) # 通常在相加后再接一个ReLU
        return out

class BottleneckBlock: # ResNet-50/101/152 中的瓶颈残差块
    expansion = 4 # 输出通道数是中间瓶颈通道数的4倍
    def __init__(self, in_channels, bottleneck_channels, stride=1):
        super().__init__()
        out_channels = bottleneck_channels * self.expansion
        
        self.conv1 = Conv2D(in_channels, bottleneck_channels, kernel_size=1, bias=False)
        self.bn1 = BatchNorm2d(bottleneck_channels)
        self.conv2 = Conv2D(bottleneck_channels, bottleneck_channels, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn2 = BatchNorm2d(bottleneck_channels)
        self.conv3 = Conv2D(bottleneck_channels, out_channels, kernel_size=1, bias=False)
        self.bn3 = BatchNorm2d(out_channels)
        self.relu = ReLU()

        self.shortcut = Sequential()
        if stride != 1 or in_channels != out_channels:
            self.shortcut = Sequential(
                Conv2D(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
                BatchNorm2d(out_channels)
            )

    def forward(self, x):
        identity = x

        out = self.relu.forward(self.bn1.forward(self.conv1.forward(x)))
        out = self.relu.forward(self.bn2.forward(self.conv2.forward(out)))
        out = self.bn3.forward(self.conv3.forward(out)) # 注意这里没有ReLU,因为要先加

        identity_transformed = self.shortcut.forward(identity)
        out += identity_transformed
        out = self.relu.forward(out) # 相加后再ReLU
        return out

# 假设 Sequential, Conv2D, BatchNorm2d, ReLU 已经定义
# class Sequential:
#     def __init__(self, *layers): self.layers = layers
#     def forward(self, x):
#         for layer in self.layers: x = layer.forward(x)
#         return x
# (以上只是示意,实际框架实现更复杂)

3.8 效果与讨论

🏆 里程碑式进展

残差连接是深度学习发展史上的一个里程碑,它使得训练数百乃至上千层的超深网络成为现实,并显著提升了各种计算机视觉任务(如图像分类、目标检测、图像分割)的性能。

🌐 广泛应用

残差连接的思想不仅局限于ResNet,它已被广泛应用于各种网络架构中,包括自然语言处理领域的Transformer模型。

🔬 理论解释仍在探索

尽管残差连接在实践中取得了巨大成功,但其背后的完整理论解释仍在被积极探索。有观点认为它使得损失曲面更平滑,或者可以看作是多个浅层网络的隐式集成等。


📖 深入理解:实例分析

场景:一个非常简单的图像处理任务

假设我们有一个任务:输入一张灰度图像 x x x(比如一个数字"7"的图像),我们希望网络输出一张几乎完全一样的图像 H ( x ) H(x) H(x),只是可能稍微锐化了一点点边缘。

  • 输入 x x x 原始的数字"7"的灰度图像
  • 期望的底层映射 H ( x ) H(x) H(x) 一张与 x x x 非常相似,但边缘稍微锐化了一点的"7"的图像

情况一:传统(非残差)网络的两层堆叠

假设我们有两层卷积网络(为了简化,我们忽略激活函数或假设它们是线性作用的),它们的目标是直接学习 H ( x ) H(x) H(x)

  • 第一层卷积核 W 1 W_1 W1
  • 第二层卷积核 W 2 W_2 W2

这两层需要协同工作,使得 H ( x ) ≈ W 2 ∗ ( W 1 ∗ x ) H(x) \approx W_2 * (W_1 * x) H(x)W2(W1x)* 代表卷积操作)。

🚫 挑战

如果 H ( x ) H(x) H(x) x x x 非常非常相似(例如,锐化效果非常微弱,几乎就是恒等变换),那么 W 1 W_1 W1 W 2 W_2 W2 需要被优化器调整成一种非常特殊的状态:

  • W 1 W_1 W1 可能需要接近一个"单位卷积核"(只保留中心像素,其他为0),这样 W 1 ∗ x ≈ x W_1 * x \approx x W1xx
  • 然后 W 2 W_2 W2 也需要接近一个"单位卷积核",或者是一个能产生微弱锐化效果的卷积核,同时又要确保前一层 W 1 W_1 W1 的微小偏差不会被放大

让优化器通过调整大量参数,使得两个(或更多)非线性变换的组合精确地近似于一个恒等变换,是非常困难的。优化器可能会在参数空间中"迷路",找不到这个精确的点,或者收敛到次优解,导致 H ( x ) H(x) H(x) x x x 有较大差异,甚至比不加这两层效果还差(这就是网络退化的表现)。

情况二:残差网络的两层堆叠(一个残差块)

现在,我们使用一个残差块来完成同样的任务。这个残差块也有两层卷积 W 1 ′ W_1' W1 W 2 ′ W_2' W2,但它们学习的是残差 F ( x ) F(x) F(x)

  • 输入 x x x
  • 残差分支:
    • 第一层卷积核 W 1 ′ W_1' W1
    • 第二层卷积核 W 2 ′ W_2' W2
    • 这两层计算出 F ( x ) = W 2 ′ ∗ ( W 1 ′ ∗ x ) F(x) = W_2' * (W_1' * x) F(x)=W2(W1x)
  • 快捷连接: 输入 x x x 直接跳过这两层
  • 输出: O u t p u t = F ( x ) + x Output = F(x) + x Output=F(x)+x

我们的目标仍然是让最终的 O u t p u t ≈ H ( x ) Output \approx H(x) OutputH(x),并且 H ( x ) H(x) H(x) x x x 非常相似(边缘稍微锐化)。

✅ 优势

因为 H ( x ) H(x) H(x) x x x 非常相似,所以 H ( x ) − x H(x) - x H(x)x 的差值将会非常小。这意味着我们期望的残差 F ( x ) = H ( x ) − x F(x) = H(x) - x F(x)=H(x)x 也会非常小。

  • F ( x ) F(x) F(x) 的目标: 学习那个"微弱的锐化效果"。它是一个小的"扰动"或"修正"
  • W 1 ′ W_1' W1 W 2 ′ W_2' W2 的学习目标: 它们不再需要去精确地"复制"输入 x x x,然后再做微小调整。它们只需要学习如何产生那个小的锐化效果 F ( x ) F(x) F(x)
  • 如果锐化效果几乎为零(即 H ( x ) ≈ x H(x) \approx x H(x)x): 那么 F ( x ) ≈ 0 F(x) \approx 0 F(x)0。让 W 1 ′ W_1' W1 W 2 ′ W_2' W2 的输出接近于零是相对容易的。例如,优化器可以通过将 W 1 ′ W_1' W1 W 2 ′ W_2' W2 的权重值都驱动到非常小来实现。当权重很小时,卷积操作的输出也会很小,接近于零
🔄 对比
  • 传统网络: 优化器需要费力地调整 W 1 , W 2 W_1, W_2 W1,W2 去"模仿"一个复杂的恒等变换(如果 H ( x ) ≈ x H(x) \approx x H(x)x
  • 残差网络: 优化器只需要让 W 1 ′ , W 2 ′ W_1', W_2' W1,W2 的输出 F ( x ) F(x) F(x) 接近于零(如果 H ( x ) ≈ x H(x) \approx x H(x)x)。“什么都不做”(输出为零)通常比"精确地复制输入"更容易学习

更具体的锐化例子

假设锐化操作可以通过一个特定的卷积核 K sharpen K_{\text{sharpen}} Ksharpen 实现,这个核与单位核 K identity K_{\text{identity}} Kidentity 的差异很小:

H ( x ) = K sharpen ∗ x H(x) = K_{\text{sharpen}} * x H(x)=Ksharpenx
K sharpen = K identity + K small_difference K_{\text{sharpen}} = K_{\text{identity}} + K_{\text{small\_difference}} Ksharpen=Kidentity+Ksmall_difference

  • 传统网络 W 2 ∗ W 1 W_2 * W_1 W2W1 需要学习近似 K sharpen K_{\text{sharpen}} Ksharpen
  • 残差网络 F ( x ) F(x) F(x) 只需要学习近似 K small_difference ∗ x K_{\text{small\_difference}} * x Ksmall_differencex

如果 K small_difference K_{\text{small\_difference}} Ksmall_difference 的值很小,那么 F ( x ) F(x) F(x) 的值也会很小。

🎯 总结这个例子

在这个锐化图像的例子中,因为期望的输出 H ( x ) H(x) H(x)(轻微锐化的图像)与输入 x x x(原始图像)非常接近,所以它们之间的差值(即残差 F ( x ) F(x) F(x),那个"锐化效果")是一个很小的量。

  • 对于传统网络,它必须直接学习从 x x x H ( x ) H(x) H(x) 的完整变换。即使这个变换只是对 x x x 的微小修改,网络层也需要学习如何先"复制" x x x 的大部分内容,然后再进行修改

  • 对于残差网络,快捷连接已经"免费"提供了 x x x 的大部分内容。残差分支 F ( x ) F(x) F(x) 只需要专注于学习那个"微小的修改部分"(锐化效果)。学习一个小的、局部的修改,通常比学习一个完整的、几乎不变的变换要容易得多

如果理想情况是完全不做任何修改 ( H ( x ) = x H(x)=x H(x)=x),传统网络仍然要努力学习恒等变换,而残差网络只需要让 F ( x ) F(x) F(x) 学习输出零即可,这显然更容易。这就是为什么说"将权重参数驱动到接近零比驱动到精确拟合一个恒等变换要简单"的原因。


🔍 深度分析:为什么传统优化器很难学到恒等映射

场景:在已有网络后添加一个简单的"恒等映射"层

假设我们有一个已经训练好的浅层网络,它在某个任务上表现不错。我们现在想尝试构建一个更深的网络,看看是否能进一步提升性能。最简单的方式就是在原有网络的输出后面再添加一个或多个额外的层。

为了简化,我们只添加一个额外的全连接层,并且我们期望这个额外的层能够学习到恒等映射。也就是说,这个额外层的输出应该和它的输入完全一样。

📐 数学要求

  • 输入到额外层: a prev a_{\text{prev}} aprev (来自前一个网络的输出,假设是一个包含 n n n 个特征的向量)
  • 额外层:
    • 权重矩阵: W extra W_{\text{extra}} Wextra (形状为 n × n n \times n n×n)
    • 偏置向量: b extra b_{\text{extra}} bextra (形状为 n × 1 n \times 1 n×1)
    • 激活函数:为了学习恒等映射,我们通常会使用线性激活函数,或者在理想情况下不使用激活函数,或者使用ReLU并期望输入恒为正。我们这里先假设是线性激活,或者说 z = W extra a prev + b extra z = W_{\text{extra}} a_{\text{prev}} + b_{\text{extra}} z=Wextraaprev+bextra 就是该层的输出
  • 期望输出: H ( a prev ) = a prev H(a_{\text{prev}}) = a_{\text{prev}} H(aprev)=aprev

要让这个额外层学习到恒等映射,需要:

W extra a prev + b extra = a prev W_{\text{extra}} a_{\text{prev}} + b_{\text{extra}} = a_{\text{prev}} Wextraaprev+bextra=aprev

这意味着,对于任何可能的输入 a prev a_{\text{prev}} aprev,上述等式都必须成立。

这强烈地约束了 W extra W_{\text{extra}} Wextra b extra b_{\text{extra}} bextra 的值:

1. 偏置必须是零向量

如果 b extra b_{\text{extra}} bextra 不是零,那么即使 W extra W_{\text{extra}} Wextra 是单位矩阵,输出也会有一个固定的偏移。所以:

b extra = 0 b_{\text{extra}} = \mathbf{0} bextra=0

2. 权重矩阵必须是单位矩阵

b extra = 0 b_{\text{extra}} = \mathbf{0} bextra=0 时,我们需要 W extra a prev = a prev W_{\text{extra}} a_{\text{prev}} = a_{\text{prev}} Wextraaprev=aprev

这要求 W extra W_{\text{extra}} Wextra 是一个单位矩阵(主对角线元素为1,其他元素为0):

W extra = I = ( 1 0 … 0 0 1 … 0 ⋮ ⋮ ⋱ ⋮ 0 0 … 1 ) W_{\text{extra}} = I = \begin{pmatrix} 1 & 0 & \dots & 0 \\ 0 & 1 & \dots & 0 \\ \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & \dots & 1 \end{pmatrix} Wextra=I= 100010001

❌ 为什么优化器很难找到这个精确解?

1. 参数空间的巨大性与解的稀疏性
  • 权重矩阵 W extra W_{\text{extra}} Wextra n × n n \times n n×n 个参数,偏置 b extra b_{\text{extra}} bextra n n n 个参数。总共有 n 2 + n n^2 + n n2+n 个参数需要被优化器调整
  • 在这些参数构成的巨大空间中,只有唯一一个点对应于精确的恒等映射( W extra = I , b extra = 0 W_{\text{extra}} = I, b_{\text{extra}} = \mathbf{0} Wextra=I,bextra=0
  • 对于优化器来说,通过基于梯度的迭代更新,精确地"命中"这个特定的点是非常困难的
2. 梯度的性质

假设我们使用均方误差损失函数:

L = 1 2 ∣ ∣ ( W extra a prev + b extra ) − a prev ∣ ∣ 2 L = \frac{1}{2} || (W_{\text{extra}} a_{\text{prev}} + b_{\text{extra}}) - a_{\text{prev}} ||^2 L=21∣∣(Wextraaprev+bextra)aprev2

来驱动学习。

  • 梯度 ∂ L ∂ W extra \frac{\partial L}{\partial W_{\text{extra}}} WextraL ∂ L ∂ b extra \frac{\partial L}{\partial b_{\text{extra}}} bextraL 会指示参数如何调整以减小误差
  • 然而,当参数值已经接近最优解(例如 W extra W_{\text{extra}} Wextra 接近 I I I b extra b_{\text{extra}} bextra 接近 0 \mathbf{0} 0)时,误差本身可能已经很小了,导致梯度也非常小
  • 小的梯度会导致更新步长非常小,使得优化器在最优解附近"徘徊"或收敛得极其缓慢,可能永远无法精确达到 W extra = I , b extra = 0 W_{\text{extra}} = I, b_{\text{extra}} = \mathbf{0} Wextra=I,bextra=0
3. 非线性激活函数的干扰

如果我们在这个额外层之后添加了非线性激活函数(比如ReLU),情况会更糟:

H ( a prev ) = ReLU ( W extra a prev + b extra ) H(a_{\text{prev}}) = \text{ReLU}(W_{\text{extra}} a_{\text{prev}} + b_{\text{extra}}) H(aprev)=ReLU(Wextraaprev+bextra)

要让 ReLU ( W extra a prev + b extra ) = a prev \text{ReLU}(W_{\text{extra}} a_{\text{prev}} + b_{\text{extra}}) = a_{\text{prev}} ReLU(Wextraaprev+bextra)=aprev,不仅需要 W extra = I , b extra = 0 W_{\text{extra}} = I, b_{\text{extra}} = \mathbf{0} Wextra=I,bextra=0,还需要保证 a prev a_{\text{prev}} aprev 的所有元素都大于等于0(因为ReLU会把负数变成0)。

这个额外的约束使得学习恒等映射几乎不可能,除非我们能保证 a prev a_{\text{prev}} aprev 总是非负的,并且优化器能精确找到 W extra = I , b extra = 0 W_{\text{extra}} = I, b_{\text{extra}} = \mathbf{0} Wextra=I,bextra=0

4. 初始化和随机性
  • 神经网络的权重通常是随机初始化的(例如,从某个均值为0,方差较小的高斯分布中采样)
  • 从一个随机的初始点开始,通过梯度下降逐步调整,要精确地调整 n 2 n^2 n2 个权重元素使得它们完美地形成一个单位矩阵(大部分为0,对角线为1),同时 n n n 个偏置元素都为0,这是一个非常低概率的事件

📊 举个简单的2维例子

假设输入 a prev = ( x 1 x 2 ) a_{\text{prev}} = \begin{pmatrix} x_1 \\ x_2 \end{pmatrix} aprev=(x1x2)

额外层的权重 W extra = ( w 11 w 12 w 21 w 22 ) W_{\text{extra}} = \begin{pmatrix} w_{11} & w_{12} \\ w_{21} & w_{22} \end{pmatrix} Wextra=(w11w21w12w22),偏置 b extra = ( b 1 b 2 ) b_{\text{extra}} = \begin{pmatrix} b_1 \\ b_2 \end{pmatrix} bextra=(b1b2)

要学习恒等映射,我们需要:

( w 11 w 12 w 21 w 22 ) ( x 1 x 2 ) + ( b 1 b 2 ) = ( x 1 x 2 ) \begin{pmatrix} w_{11} & w_{12} \\ w_{21} & w_{22} \end{pmatrix} \begin{pmatrix} x_1 \\ x_2 \end{pmatrix} + \begin{pmatrix} b_1 \\ b_2 \end{pmatrix} = \begin{pmatrix} x_1 \\ x_2 \end{pmatrix} (w11w21w12w22)(x1x2)+(b1b2)=(x1x2)

这要求 w 11 = 1 , w 12 = 0 , w 21 = 0 , w 22 = 1 , b 1 = 0 , b 2 = 0 w_{11}=1, w_{12}=0, w_{21}=0, w_{22}=1, b_1=0, b_2=0 w11=1,w12=0,w21=0,w22=1,b1=0,b2=0

优化器在四维的权重空间和二维的偏置空间中搜索。它可能会找到一个解,比如:

w 11 = 0.99 , w 12 = 0.01 , w 21 = − 0.005 , w 22 = 1.02 , b 1 = 0.003 , b 2 = − 0.001 w_{11}=0.99, w_{12}=0.01, w_{21}=-0.005, w_{22}=1.02, b_1=0.003, b_2=-0.001 w11=0.99,w12=0.01,w21=0.005,w22=1.02,b1=0.003,b2=0.001

这个解产生的输出可能与输入非常接近,损失函数的值也很小。但它并不是精确的恒等映射。当这样的"近似恒等映射"层堆叠很多时,这些微小的偏差会累积,最终导致性能下降,即网络退化。

✅ 对比残差连接

如果使用残差连接,这个额外层学习的是 F ( a prev ) F(a_{\text{prev}}) F(aprev),输出是 F ( a prev ) + a prev F(a_{\text{prev}}) + a_{\text{prev}} F(aprev)+aprev

要实现恒等映射,只需要 F ( a prev ) = 0 F(a_{\text{prev}}) = \mathbf{0} F(aprev)=0

W extra a prev + b extra W_{\text{extra}} a_{\text{prev}} + b_{\text{extra}} Wextraaprev+bextra(在ReLU之前)的输出为负数或零是相对容易的,例如,可以将所有 w i j w_{ij} wij b i b_i bi 都驱动到非常小的值,或者让它们的组合效果产生负输出。这比精确地让 W extra = I , b extra = 0 W_{\text{extra}}=I, b_{\text{extra}}=\mathbf{0} Wextra=I,bextra=0 要容易得多。

🎯 结论

优化器是通过迭代和近似的方式工作的。让它在巨大的参数空间中精确地找到一个点(如单位矩阵和零偏置),使得多层非线性(或线性)变换完全等同于恒等变换,是非常困难的。这些"额外层"很容易学成一个"近似但不完美"的恒等映射,当这些不完美累积时,就会导致更深网络的性能退化。残差连接通过改变学习目标,使得"什么都不做"(近似恒等映射)成为一个更容易达成的状态,从而解决了这个问题。


🏁 总结与展望

He初始化、批量归一化和残差连接是现代深度学习模型设计与训练中相辅相成的三大基石:

🎯 三大技术的协同作用

  • He初始化 为网络提供了一个良好的权重起点,确保信号在深层网络中以合适的尺度传播

  • 批量归一化 在训练过程中稳定了数据分布,加速收敛并允许使用更高的学习率,同时具有一定的正则化效果

  • 残差连接 通过巧妙的快捷方式解决了深度网络的退化问题,使得训练极深的网络成为可能,并显著改善了梯度的传播

🚀 实际应用建议

  1. 选择合适的初始化策略:

    • 对于ReLU及其变种,使用He初始化
    • 对于sigmoid/tanh,考虑Xavier/Glorot初始化
  2. 合理使用批量归一化:

    • 在卷积层或全连接层之后,激活函数之前应用
    • 注意批量大小对BN效果的影响
    • 在小批量情况下考虑Layer Normalization等替代方案
  3. 设计深度网络架构:

    • 对于深层网络(>10层),强烈建议使用残差连接
    • 可以尝试预激活残差块以获得更好的性能
    • 根据任务需求选择基础残差块或瓶颈残差块

🔮 未来发展方向

这些技术为深度学习的发展奠定了坚实基础,后续的许多创新都建立在这些基础之上:

  • 注意力机制和Transformer: 同样采用了残差连接的思想
  • 更高效的归一化方法: Layer Normalization, Group Normalization等
  • 更先进的初始化策略: 针对特定架构的优化初始化方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值