AlexNet、 VGG、 GoogLeNet 等网络模型的出现将神经网络的发展带入了几十层的阶段,网络的层数越深,越有可能获得更好的泛化能力。但是当模型加深以后,网络变得越来越难训练,这主要是由于梯度弥散和梯度爆炸现象造成的。在较深层数的神经网络中,梯度信息由网络的末层逐层传向网络的首层时, 传递的过程中会出现梯度接近于 0 或梯度值非常大的现象
怎么解决深层神经网络的梯度弥散和梯度爆炸现象呢?既然浅层神经网络不容易出现梯度现象,那么可以尝试给深层神经网络添加一种回退到浅层神经网络的机制。当深层神经网络可以轻松地回退到浅层神经网络时,深层神经网络可以获得与浅层神经网络相当的模型性能
通过在输入和输出之间添加一条直接连接的 Skip Connection 可以让神经网络具有回退的能力
以 VGG13 深度神经网络为例, 假设观察到 VGG13 模型出现梯度弥散现象,而10 层的网络模型并没有观测到梯度弥散现象,那么可以考虑在最后的两个卷积层添加 SkipConnection,通过这种方式, 网络模型可以自动选择是否经由这两个卷积层完成特征变换,还是直接跳过这两个卷积层而选择 Skip Connection,亦或结合两个卷积层和 Skip Connection 的输出
基于 Skip Connection 的深度残差网络(Residual Neural Network,简称 ResNet)算法,并提出了 18 层、 34 层、 50 层、 101层、 152 层的 ResNet-18、 ResNet-34、 ResNet-50、 ResNet-101 和 ResNet-152 等模型
ResNet 通过在卷积层的输入和输出之间添加 Skip Connection 实现层数回退机制,输入通过两个卷积层,得到特征变换后的输出ℱ(),与输入进行对应元素的相加运算,得到最终输出ℋ():
ℋ() = + ℱ(),ℋ()叫作残差模块(Residual Block,简称 ResBlock)。由于被 Skip Connection 包围的卷积神经网络需要学习映射ℱ() = ℋ() - ,故称为残差网络
为了能够满足输入与卷积层的输出ℱ()能够相加运算,需要输入的 shape 与ℱ()的shape 完全一致。当出现 shape 不一致时,一般通过在 Skip Connection 上添加额外的卷积运算环节将输入变换到与ℱ()相同的 shape,如图identity()函数所示,其中identity()以 1×1 的卷积运算居多,主要用于调整输入的通道数
深度残差网络通过堆叠残差模块,达到了较深的网络层数,从而获得了训练稳定、性能优越的深层网络模型
深度残差网络没有增加新的网络层类型,只是通过在输入和输出之间添加一条 Skip Connection, 并没有针对 ResNet 的底层实现。 在 TensorFlow 中通过调用普通卷积层即可实现残差模块。
首先创建一个新类,在初始化阶段创建残差块中需要的卷积层、 激活函数层等
# 残差模块类
class BasicBlock(layers.Layer):
def __init__(self, filter_num, stride=1):
super(BasicBlock, self).__init__()
# f(x)包含两个普通卷积层
self.conv1 = layers.Conv2D(filter_num, (3, 3), strides=stride, padding='same')
self.bn1 = layers.BatchNormalization()
self.relu = layers.Activation('relu')
self.conv2 = layers.Conv2D(filter_num, (3, 3), strides=stride, padding='same')
self.bn2 = layers.BatchNormalization()
# f(x) 与 x 形状不同,无法相加
if stride != 1: # 不相等,插入identity层
self.downsample = Sequential()
self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
else: # 直接相加
self.downsample = lambda x: x
# 向前传播函数
def call(self, inputs, training=None):
out = self.conv1(inputs)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
# 输入通过identity()转换
identity = self.downsample(inputs)
# f(x) + x
output = layers.add([out, identity])
# 再通过激活函数,放在前面也行
output = tf.nn.relu(output)
return output
首先新建ℱ()卷积层,当ℱ()的形状与不同时,无法直接相加,我们需要新建identity()卷积层,来完成的形状转换。在前向传播时,只需要将ℱ()与identity()相加,并添加 ReLU 激活函数即可
RseNet 通过堆叠多个 ResBlock 可以构成复杂的深层神经网络,如ResNet18,ResNet34......
DenseNet 将前面所有层的特征图信息通过 Skip Connection 与当前层输出进行聚合,与 ResNet 的对应位置相加方式不同, DenseNet 采用在通道轴维度进行拼接操作, 聚合特征信息
输入0 通过H1卷积层得到输出1, 1与0 在通道轴上进行拼接,得到聚合后的特征张量,送入H2卷积层,得到输出2,同样的方法, 2与前面所有层的特征信息 1与0 进行聚合,再送入下一层。如此循环,直至最后一层的输出4和前面所有层的特征信息: {}=0, 1, 2, 3进行聚合得到模块的最终输出, 这样一种基于 Skip Connection 稠密连接的模块叫做 Dense Block
DenseNet 通过堆叠多个 Dense Block 可以构成复杂的深层神经网络