前文「深度学习|误差逆传播:梯度速解」中我们以 AddLayer 和 MulLayer 为例,引入了神经网络标准层
封装的实现方式。神经网络中每一层的运算都可以以相同的接口封装成标准层的形式,每一层通过一个具体实现类实现,这样可以通过调用同一接口的不同实现,只需要经过简单的组装或者切换,就可以完成“任意”新网络结构的构建、学习、推理等复杂的工作。
常见运算的层
我们之前的演示大多是为了走通简单的学习与推理流程,并没有都遵守标准层的实现。后文的演示将以标准层的接口为基准,对神经网络中涉及的计算过程进行统一封装,达到一个通用的 DeepLearning 框架的效果。
参照 AddLayer 和 MulLayer,我们发现可以将神经网络的标准层抽象成包含了如下三个方法的抽象类:
方法 | 说明 |
---|
__init__ (self, args) | 初始化层参数; |
forward(self, args) | 正向传播的计算,用于计算推理结果; |
backward(self, dout) | 逆传播的计算,用于计算梯度; |
我们只需要根据不同层的具体计算逻辑是指这三个方法的不同实现,就可以实现各种网络层次的封装,从而实现一个通用的神经网络框架。
神经网络中常见的层如:
- 全连接层(Fully Connected Layer):如
Affine 层
,每个输入与每个输出节点相连。
- 激活层(Activation Layer):如
ReLU 层
、Sigmoid 层
、Tanh 层
等,用于神经元输出,增加非线性表达能力。
Softmax-with-Loss 层
:用于输出层的正规化和损失计算。
- 卷积层(Convolutional Layer):如
Conv2D
、Conv2DTranspose
、SeparableConv2D
等,用于以卷积操作 提取特征,常用于图像数据处理。
- 池化层(Pooling Layer):如
Max Pooling
、Average Pooling
等,在池化窗口内采样,用于减少特征维度,增强模型的局部不变形。
- 归一化层(Normalization Layer):如
Batch Normalization
、Layer Normalization
、Instance Normalization
等,用于加速训练,提高模型的稳定性与泛化能力。
- 丢弃层(Dropout Layer):用于防止过拟合,随机丢弃一部分神经元。
- 嵌入层(Embedding Layer):用于将稀疏的输入转换为稠密的向量表示。
- 递归层(Recurrent Layer):如
SimpleRNN
、LSTM
、GRU
等,用于处理存在时间依赖关系的序列数据。
- 自注意力层(Self-Attention Layer):用于处理序列数据,提取序列中的重要信息。
只要以标准层的方式提供了所有计算逻辑的标准层封装,我们在应对具体繁杂的业务场景时,搭建不同的网络模型就可以不去太过关注这些计算逻辑的实现,只需摘取我们想要的层进行简单的组合,轻易就能够组装成自己想要的复杂网络。接下来我们以标准层的方式来做一些演示。
Affine 层
在神经网络中,相邻的所有神经元之间都有连接的层,被称为全连接层。全连接层的正向传播中进行的矩阵的乘积运算,在几何领域被称为仿射变换(Affine),这里将进行仿射变换的层称为Affine 层
。
Affine 层的数学式为:
Y=X⋅W+B(1)
其中 X 是矩阵表示的输入(上一层的输出),W 是当前层的权重,B 是当前层的偏置,Y 是输出。
根据链式法则,损失函数 L 关于 Y 的梯度为:∂Y∂L。
那么,损失函数关于 X、W、B 的梯度分别为:
∂X∂L=∂Y∂L⋅∂X∂Y=∂Y∂L⋅WT(2)
∂W∂L=∂Y∂L⋅∂W∂Y=XT⋅∂Y∂L(3)
∂B∂L=∂Y∂L⋅∂B∂Y=∂Y∂L(4)
Affine 层的数学式
给定 Affine 层的输出公式:
Y=X⋅W+B
其中:
- X 是输入矩阵(或上一层的输出)。
- W 是当前层的权重矩阵。
- B 是当前层的偏置向量。
- Y 是当前层的输出。
梯度计算过程
在反向传播过程中,我们需要计算损失函数 L 关于输入 X、权重 W 和偏置 B 的梯度。这可以通过链式法则完成。
1. 梯度关于 X 的推导
根据链式法则,损失函数 L 关于 Y 的梯度为 ∂Y∂L。我们需要计算:
∂X∂L=∂Y∂L⋅∂X∂Y
首先计算 Y 关于 X 的导数:
Y=X⋅W+B
在这个公式中, B 是常数项,所以它对 X 的导数为 0。我们只关心 X⋅W。
对 Y 求导:
∂X∂Y=W
因此,
∂X∂L=∂Y∂L⋅WT
(右乘 WT 是因为 X 和 Y 常常是多维矩阵的情况下需要转置。)
2. 梯度关于 W 的推导
同样,我们需要计算:
∂W∂L=∂Y∂L⋅∂W∂Y
计算 Y 关于 W 的导数:
Y=X⋅W+B
因此,Y 对 W 的导数:
∂W∂Y=X
将其代入公式中,我们得到:
∂W∂L=XT⋅∂Y∂L
3. 梯度关于 B 的推导
最后,我们需要计算:
∂B∂L=∂Y∂L⋅∂B∂Y
计算 Y 关于 B 的导数:
Y=X⋅W+B
这样,∂B∂Y 将是一个单位矩阵或 1(因为偏置 B 对 Y 的影响是直接的):
∂B∂Y=1
因此,
∂B∂L=∂Y∂L
最终的梯度推导公式为:
-
关于 X 的梯度:
∂X∂L=∂Y∂L⋅WT
-
关于 W 的梯度:
∂W∂L=XT⋅∂Y∂L
-
关于 B 的梯度:
∂B∂L=∂Y∂L
这些公式是反向传播过程中用于更新权重和偏置的重要基础,确保了网络能够有效地学习。
式 2.3.5.4 与 2.3.5.5 实质上是乘法的逆传播的批量化,式 2.3.5.6 则是加法逆传播的批量化,其中:
X=(x0,x1,...,xn)
∂X∂L=(∂x0∂L,∂x1∂L,...,∂xn∂L)
最终我们得出,在 Affine 层的逆传播中,损失函数关于 X 的梯度是 ∂Y∂L 点乘 W 的转置;损失函数关于 W 的梯度是 X 的转置点乘 ∂Y∂L;损失函数关于 B 的梯度就是 ∂Y∂L 本身。
Affine 层的代码实现参见 Notebook 1.3.4 - 3。
ReLU 层
ReLU 激活函数的定义如式 2.2.3.2,y 关于 x 的导数为:
∂x∂y={10(x>0)(x≤0)(2.3.6.1)
ReLU 层的逆传播中,输入 x 小于等于 0 时,导数乘以 0,即为 0;大于 0 时,导数乘以 1,即原样输出。
ReLU 层的代码实现参见 Notebook 1.3.4 - 1。
Sigmoid 层
sigmoid 激活函数的定义如式 2.2.3.1,拆解 Sigmoid 公式:
y=x31
x3=1+x2
x2=ex1
x1=−x
根据链式法则,y 关于 x 的导数为:
∂x∂y=∂x3∂y∂x2∂x3∂x1∂x2∂x∂x1=−x3−2×1×ex1×−1=y2×e−x=y(1−y)(2.3.6.2)
Sigmoid 层的逆传播中,导数乘以 y(1-y) 后输出。
Sigmoid 层的代码实现参见 Notebook 1.3.4 - 2。
Softmax-with-Loss 层
学习阶段的输出层经常以 softmax 函数作为激活函数,用于正规化输出结果(推理阶段的输出层通常是不需要进行 softmax 计算的)。
学习阶段的损失逆传播计算也是输出层开始的,因此可以将 Softmax 层与 Loss 层合并为学习阶段专属的一层来实现。
这里以 softmax 函数(式 2.2.3.3)和交叉熵误差(式 2.3.1.3)组合为例。
损失函数 L 关于 A 的梯度为:
∂A∂L=Y−T(2.3.6.7)
其中 Y 是 softmax 函数的输出,T 是监督数据(one-hot-vector 表示)。
这里省略推导过程,感兴趣的同学可以自行在互联网上搜索,或者让 GPT 直接告诉你!
在 Softmax-with-Loss 层的逆传播中,损失函数关于 A 的梯度是 softmax 函数的输出减去监督数据的值。
Softmax-with-Loss 层的代码实现参见 Notebook 1.3.4 - 4。
使用标准层实现方式组装新的神经网络的代码参见 runtime/standard-framework/simple_net.py。其中 trainer 模块实现了用标准层组装的 SimpleNet 进行手写数字识别任务的学习、推理、效果评估的过程。