2015年,微软亚洲研究院何凯明等人发表了基于Skip Connection的深度残差网络(Residual Nerual Network,简称ResNet)算法[1],并提出了18层、34层、50层、101层、152层的ResNet-18、ResNet-34、ResNet-50、ResNet-101和ResNet-152等模型,甚至成功训练出层数达到1202层的极深神经网络。ResNet在ILSVRC2015挑战赛ImageNet数据集上的分类、检测等任务上面均获得了最好性能,ResNet论文至今已经获得超25000的引用量,可见ResNet在人工智能行业的影响力。
[1] K. He, X. Zhang, S. Ren 和 J. Sun, “Deep Residual Learning for Image Recognition,” CoRR, 卷 abs/1512.03385, 2015.
ResNet通过在卷积层的输入和输出之间添加Skip Connec-tion实现层数回退机制,如下图所示,输入 x \boldsymbol x x通过两个卷积层,得到特征变换后的输出 F ( x ) \mathcal F(\boldsymbol x) F(x),与输入 x \boldsymbol x x进行对应元素的相加运算,得到最终输出 H ( x ) \mathcal H(\boldsymbol x) H(x):
H ( x ) = x + F ( x ) \mathcal H(\boldsymbol x)=\boldsymbol x+\mathcal F(\boldsymbol x) H(x)=x+F(x)
H ( x ) \mathcal H(\boldsymbol x) H(x)叫做残差模块(Residual Block,简称ResBlock)。由于被Skip Connection包围的卷积神经网络需要学习映射 F ( x ) = H ( x ) − x \mathcal F(\boldsymbol x)=\mathcal H(\boldsymbol x)-\boldsymbol x F(x)=H(x)−x,故称为残差网络。
为了能够满足输入 x \boldsymbol x x与卷积层的输出 F ( x ) \mathcal F(\boldsymbol x) F(x)能够相加运算,需要输入 x \boldsymbol x x的shape与 F ( x ) \mathcal F(\boldsymbol x) F(x)的shape完全一致。当出现shape不一致时,一般通过在Skip Connection上添加额外的卷积运算缓解将输入 x \boldsymbol x x变换到与 F ( x ) \mathcal F(\boldsymbol x) F(x)相同的shape,如下图中 identity ( x ) \text{identity}(\boldsymbol x) identity(x)函数所示,其中 identity ( x ) \text{identity}(\boldsymbol x) identity(x)以 1 × 1 1×1 1×1的卷积运算居多,主要英语调整输入的通道数。
如下图所示,对比了34层的深度残差网络、34层的普通深度网络以及19层的VGG网络结构。可以看到,深度残差网络通过堆叠残差模块,达到了较深的网络层数,从而获得了训练稳定、性能优越的深层网络模型。
深度残差网络并没有增加新的网络层类型,只是通过在输入和输出之间添加一条Skip Connection,因此并没有针对ResNet的底层实现。在TensorFlow中通过调用普通卷积层即可实现残差模块。
首先创建一个新类,在初始阶段创建残差块中需要的卷积层、激活函数层等,首先新建 F ( x ) \mathcal F(\boldsymbol x) F(x)卷积层,代码如下:
class BasicBlock(layers.Layer):
# 残差模块
def __init__(self, filter_num, stride=1):
super(BasicBlock, self).__init__()
# 第一个卷积单元
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=1, padding='same')
self.bn2 = layers.BatchNormalization()
其中:
__init__(self, filter_num, stride=1)
函数为初始化函数,filter_num
为卷积核的数量,stride=1
表示不对输入进行下采样;strides=stride, padding='same'
表示可以直接得到输入、输出同大小的卷积层;layers.BatchNormalization()
表示标准化层;当 F ( x ) \mathcal F(\boldsymbol x) F(x)的形状与 x \boldsymbol x x不同时,无法直接相加,我们需要新建 identity ( x ) \text{identity}(\boldsymbol x) identity(x)卷积层,来完成 x \boldsymbol x x的形状转换。紧跟上面代码,实现如下:
if stride != 1:# 通过1x1卷积完成shape匹配
self.downsample = Sequential()
self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
else:# shape匹配,直接短接
self.downsample = lambda x:x
上述代码表示如果stride(即步长)如果不为1的话,那么输出与输入的shape不相同,那么此时我们就需要新建 identity ( x ) \text{identity}(\boldsymbol x) identity(x)卷积层,来完成 x \boldsymbol x x的形状转换,使之与输入的shape相同。
在向前传播时,只需要将 F ( x ) \mathcal F(\boldsymbol x) F(x)与 identity ( x ) \text{identity}(\boldsymbol x) identity(x)相加,并添加ReLU激活函数即可。向前计算函数代码如下:
def call(self, inputs, training=None):
# [b, h, w, c],通过第一个卷积单元
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)
# 2条路径输出直接相加
output = layers.add([out, identity])
output = tf.nn.relu(output) # 激活函数
return output