介绍深度神经网络的经典-ResNet。

禁止转载,侵权必究!

前言

上一章中我们结合iChallenge-PM问题介绍了LeNet,AlexNet,VGG。明显可以看到随着神经网络能力越强,网络越复杂,层数越多。那是不是层数越深的CNN网络就一定效果越好呢?

答案是否定的,直到ResNet出现。

我们来看看ResNet的设计者—何恺明博士是如何解决这个问题的:

假定:

H(x) = F(x) + x

其中H(x)是更深的网络,F(x)是较浅的网络。

这个假设是什么意思呢?何恺明博士发现在众多公开的数据集上,简单加深网络层数,训练loss和测试loss不但不会减小,反而还会增加。那么假定F(x)这个浅层网络之后的每一层都是恒等网络y=x,因此一个更深的网络H(x)就可以表达为上述假定。简而言之,就是假定更深的层学不到新的特征图了,那么之后的层都是恒等层。

简单加深网络层数,训练loss和测试loss不但不会减小,反而还会增加。还反映出普通的CNN网络有什么样的问题呢?

简而言之,更深的卷积层conv连恒等映射y=x都不如。因为在恒等映射下,更深网络H(x)应该和浅层网络F(x)表现相同,也就是说loss应该稳定下来,而不是增加。

那么给卷积层输出y增加输入x会怎么样呢?

何恺明博士依据这个思路,修改了普通CNN网络,对比如下:

图(a)普通CNN 图(b)ResNet

图(b)所示网络就是H(x) = F(x) + x的图形化表示。我们只要把N个图(b)串联起来,就可以得到比普通CNN图a所示更好的神经网络—ResNet了。

何恺明博士把图(b)简称为残差块

残差块编码

1.残差块定义

# 定义残差块
# 每个残差块会对输入图片做三次卷积,然后跟输入图片进行短接
# 如果残差块中第三次卷积输出特征图的形状与输入不一致,则对输入图片做1x1卷积,将其输出形状调整成一致
class BottleneckBlock(fluid.dygraph.Layer):
    def __init__(self,
                 num_channels,
                 num_filters,
                 stride,
                 shortcut=True):
        super(BottleneckBlock, self).__init__()
        # 创建第一个卷积层 1x1
        self.conv0 = ConvBNLayer(
            num_channels=num_channels,
            num_filters=num_filters,
            filter_size=1,
            act='relu')
        # 创建第二个卷积层 3x3
        self.conv1 = ConvBNLayer(
            num_channels=num_filters,
            num_filters=num_filters,
            filter_size=3,
            stride=stride,
            act='relu')
        # 创建第三个卷积 1x1,但输出通道数乘以4
        self.conv2 = ConvBNLayer(
            num_channels=num_filters,
            num_filters=num_filters * 4,
            filter_size=1,
            act=None)

        # 如果conv2的输出跟此残差块的输入数据形状一致,则shortcut=True
        # 否则shortcut = False,添加1个1x1的卷积作用在输入数据上,使其形状变成跟conv2一致
        if not shortcut:
            self.short = ConvBNLayer(
                num_channels=num_channels,
                num_filters=num_filters * 4,
                filter_size=1,
                stride=stride)

        self.shortcut = shortcut

        self._num_channels_out = num_filters * 4

    def forward(self, inputs):
        y = self.conv0(inputs)
        conv1 = self.conv1(y)
        conv2 = self.conv2(conv1)

        # 如果shortcut=True,直接将inputs跟conv2的输出相加
        # 否则需要对inputs进行一次卷积,将形状调整成跟conv2输出一致
        if self.shortcut:
            short = inputs
        else:
            short = self.short(inputs)

        y = fluid.layers.elementwise_add(x=short, y=conv2)
        layer_helper = LayerHelper(self.full_name(), act='relu')
        return layer_helper.append_activation(y)

注意三个卷积层的卷积核的大小分别为:1×1 3×3 1×1

  1. 如果输入输出数组形状相等: x–>conv0–>conv1–>conv2–>+x–>y
  2. 如果输入输出数组形状不等:x–>conv0–>conv1–>conv2–>+short()–>y

2.残差块的每个Layer的代码实现:

# ResNet中使用了BatchNorm层,在卷积层的后面加上BatchNorm以提升数值稳定性
# 定义卷积批归一化块
class ConvBNLayer(fluid.dygraph.Layer):
    def __init__(self,
                 num_channels,
                 num_filters,
                 filter_size,
                 stride=1,
                 groups=1,
                 act=None):
        """
        
        num_channels, 卷积层的输入通道数
        num_filters, 卷积层的输出通道数
        stride, 卷积层的步幅
        groups, 分组卷积的组数,默认groups=1不使用分组卷积
        act, 激活函数类型,默认act=None不使用激活函数
        """
        super(ConvBNLayer, self).__init__()

        # 创建卷积层
        self._conv = Conv2D(
            num_channels=num_channels,
            num_filters=num_filters,
            filter_size=filter_size,
            stride=stride,
            padding=(filter_size - 1) // 2,
            groups=groups,
            act=None,
            bias_attr=False)

        # 创建BatchNorm层
        self._batch_norm = BatchNorm(num_filters, act=act)

    def forward(self, inputs):
        y = self._conv(inputs)
        y = self._batch_norm(y)
        return y

我们通常会把输入数据做归一化,方便模型使用。经过实践,科学家发现在模型的中间层,数据也需要做归一化,这个归一化的方法就是:BatchNorm

深入了解数据形状

W(kernel_size):M-输出特征(通道)数 C-输入通道数 H-高度 W-宽度

1-conv层输出特征图计算公式: (224 + 2x padding – Kh)/2 +1 = (224 + 2 x (7-1)//2 – 7)/2 +1 = (224 + 2 x 3 -7)/2 +1 = 112.5 ≈ 112,其中(7-1)//2是在ConvBNLayer中定义的。

paddle-paddle 卷积向下取整,池化向上取整。

1-conv-- kernel_size MCHW:[64, 3, 7, 7], padding:[3, 3], stride:[2, 2]
conv0-- kernel_size MCHW:[64, 64, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[64, 64, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[256, 64, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_0
conv0-- kernel_size MCHW:[64, 256, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[64, 64, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[256, 64, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_1
conv0-- kernel_size MCHW:[64, 256, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[64, 64, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[256, 64, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_2
conv0-- kernel_size MCHW:[128, 256, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[128, 128, 3, 3], padding:[1, 1], stride:[2, 2]
conv2-- kernel_size MCHW:[512, 128, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_3
conv0-- kernel_size MCHW:[128, 512, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[128, 128, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[512, 128, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_4
conv0-- kernel_size MCHW:[128, 512, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[128, 128, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[512, 128, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_5
conv0-- kernel_size MCHW:[128, 512, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[128, 128, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[512, 128, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_6
conv0-- kernel_size MCHW:[256, 512, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[256, 256, 3, 3], padding:[1, 1], stride:[2, 2]
conv2-- kernel_size MCHW:[1024, 256, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_7
conv0-- kernel_size MCHW:[256, 1024, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[256, 256, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[1024, 256, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_8
conv0-- kernel_size MCHW:[256, 1024, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[256, 256, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[1024, 256, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_9
conv0-- kernel_size MCHW:[256, 1024, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[256, 256, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[1024, 256, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_10
conv0-- kernel_size MCHW:[256, 1024, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[256, 256, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[1024, 256, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_11
conv0-- kernel_size MCHW:[256, 1024, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[256, 256, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[1024, 256, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_12
conv0-- kernel_size MCHW:[512, 1024, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[512, 512, 3, 3], padding:[1, 1], stride:[2, 2]
conv2-- kernel_size MCHW:[2048, 512, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_13
conv0-- kernel_size MCHW:[512, 2048, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[512, 512, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[2048, 512, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_14
conv0-- kernel_size MCHW:[512, 2048, 1, 1], padding:[0, 0], stride:[1, 1]
conv1-- kernel_size MCHW:[512, 512, 3, 3], padding:[1, 1], stride:[1, 1]
conv2-- kernel_size MCHW:[2048, 512, 1, 1], padding:[0, 0], stride:[1, 1]
bottleneck_block-- name:bottleneck_block_15

可以看到ResNet-50有15个ResidualBlock,它的部分层如下:

w形状w参数个数b形状b参数个数输出形状
1-conv[64, 3, 7, 7]9408[3]3[10,64,112,112]
pooling[10,64,56,56]
b0_conv0[64, 64, 1, 1]4096[64]64[10,64,56,56]
b0_conv1[64, 64, 3, 3]36864[64]64[10,64,56,56]
b0_conv2[256, 64, 1, 1]16384[256]256[10,256,56,56]
b1_conv0[64, 256, 1, 1]16384[64]64[10,64,56,56]
b1_conv1[64, 64, 3, 3]36864[64]64[10,64,56,56]
b1_conv2[256, 64, 1, 1]16384[256]256[10,256,56,56]
b2_conv0[64, 256, 1, 1]16384[64]64[10,64,56,56]
b2_conv1[64, 64, 3, 3]36864[64]64[10,64,56,56]
b2_conv2[256, 64, 1, 1]16384[256]256[10,256,56,56]

高亮的数字为每个bottleneck_block的输入通道数。

评估ResNet-50的算力消耗

大部分残差块如下:

W形状输出形状乘法执行次数
conv0[64, 256, 1, 1][10,64,56,56]约5.1亿
conv1[64, 64, 3, 3][10,64,56,56]约11.6亿
conv2[256, 64, 1, 1][10,256,56,56]约5.1亿

总共有48个残差块,约1046亿。残差块之前有一个卷积层,之后有一个全连接层,总共50层。因此本例的一个batch乘法次数约1046亿。同理,加法次数和乘法相当,因此一个batch总算力消耗约为2092亿次。

对比VGG-16的3069亿次,所以何恺明博士说ResNet-50时间消耗优于VGG-16。主要原因是ResNet-50的三层卷积核分别是1×1 3×3 1×1,而VGG-16的卷积核全部是3×3。

示例代码

源码地址

ResNet更多思考

Q1:ResNet的数学表示为y = F(x) + x。那么y = F(x) + 2x行不行?

实践证明,2x和x比提升不大,每个round都白白浪费了一次乘法算力。

Q2:ResNet的形象化理解?

深度神经网络的训练过程:好比盲人摸象,它首先发现了象腿很粗,但是它进一步去了解象腿的其他特征的时候,随着网络越深它逐渐忘记了象腿很粗的知识,这个时候把已学到的知识传递到更深的层,相当于用个笔记下来,让它进一步去学习更多的细节,并继续记录下来,依此类推。

Q3:bottleneck结构的理解?

bottleneck中第一层conv为1×1,第二层conv为3×3,第三层conv为1×1但输出通道乘与4形似玻璃瓶口。和VGG网络设计有很大不同。为什么设定这样的卷积超参数呢?其实它的含义是:对稀疏的特征进行压缩。