ResNet 论文地址
ResNet (Residual Net) 是在2015年提出,当时在ImageNet检测、ImageNet定位、COCO检测以及COCO分割上均获得了第一名的成绩。并且它也被很多其他计算机视觉网络作为主干网来做特征提取。

简介

当年,其实也就是2015年,深度卷积神经网络在计算机视觉上取得了突破,大家为了进一步取得更好的成绩,就逐步加深网络的深度,理论上,这可以提高网络精度。但是实际上呢,却出现了退化问题:随着网络深度的增加,网络的准确率不升反降。有图为证:

左边的图是20层和56层网络的训练误差,右边是20层和56层的测试误差。

这就让人想不通了,新增加的层你对模型没有帮助但是也别使坏啊,于是作者提出了一种恒等映射(identity mapping)来构建新增的层。

这个思想就是让前层的输入跳过几个中间层,直接和后边层在激励函数之前进行相加,然后在进入激励函数(这里用的Relu)这样就方便网络就更容易学到一个恒等映射,那就是让上图中的F(x)=0即可。如果没有这种跳跃连接,新增的层很难在有激励函数这种非线性映射的情况下学到一个恒等映射。恒等映射保证了新增的层很容易可以做到不影响模型精度,不会让模型变得更差。还有一个好处就是在网络训练的反向传播的时候,可以让loss快点传到前层网络,对前层weight值进行修正。当然新增的层也不是啥事不干,它在不影响现有网络的情况下多少也能学到一些东西,所以总体上整个网络的精度会提高。

网络结构


左边这个是VGG-19模型,它的计算量是196亿浮点计算。中间的模型是一个普通的34层的网络,计算量为36亿浮点计算。右边是一个34层的残差网络,也是36亿浮点计算。虚线表示维度增加。表1有更多细节和变种。

普通网络

实验是为普通网络的表现做为基准,这个普通网络的思想主要是受VGG网络的启发,卷积网络的fitler都是3×3的,并且:
1. 对于输出相同大小的feature map的层,filter个数是相同的。
2. 如果feature map大小减半,则filter size大小加倍。

不同的是我们直接在卷积层使用步长为2的卷积来进行下采样。而不是用pooling。网络是以global average pooling加上一个1000个全连接softmax结尾。这个普通网络结构简单,计算量只有VGG16的18%。

残差网络

上图最右边是残差网络,他是基于普通网络改造的。我们增加了shortcut 连接。对于shortcut连接的不同层,如果维度一样,可以直接相加,对于维度不一样的,有两种办法:
1. 用0填充不足的维度。
2. 通过线性映射来匹配维度,通过1×1的卷积实现。
{y}= \mathcal{F}({x}, \{W_{i}\}) + W_{s}{x}
当shoutcut连接两个不同维度时,我们都用的是步长为2的卷积。

对于表1,我们可以看到对于ResNet,不如论是ResNet-18,ResNet-50,ResNet-101都有5个conv block,分别是conv1到conv5.这个block的划分是根据feature map size的不同。其中conv3_1,conv4_1,conv5_1都用了步长为2的卷积进行了下采样从而改变了feature map的size。每个conv block里又有多个残差块,比如conv2里在ResNet-18里就有2个残差块,而在ResNet-50里就有3个残差块。

经过实验表明普通网络34层的网络精度反而比18层的低。但是残差网络34层的精度比18层的高。而且训练时网络收敛速度更快。

错误率对比:

普通网络 残差网络
18层 27.94 27.88
34层 28.54 25.03

shortcuts的映射

对于shortcut,如果连接的两边维度一致,那么直接恒等映射就可以,这也被证明对网络是有效的,但是如果两边连接维度的不一样,我们有3种办法:
A. 对增加的维度用0填充,所有的shortcuts连接是无参数的。
B. 对增加的维度使用有weight的线性映射,其他使用恒等映射。
C. 所有维度都使用映射。
实验证明,网络精度A,B,C依次提高,但是提高精度不大,但是它们依次增加的参数和复杂度却很大。所以映射对于解决网络退化问题不是必须的。

特别是对于深度网络的Bottleneck架构,恒等很有用。

深度Bottleneck架构

处于对网络训练时间的考虑,我们引入了bottleneck的设计。我们把残差块由两层结构改成3层:

左边是我们之前的设计,右边是我们bootleneck的设计,我们用过1×1的卷积核来先降维,后来再升维。中间的卷积就是基于一个低维度的输入输出进行处理的。减少了运算量。
之前说无参数的恒等映射对于bottlenect架构很有用,因为如果要做映射的话,在bottlenect架构下的shortcut连接的是两个高纬度的输入输出。计算量会非常大。而无参数的恒等映射就避免了这个计算。

ResNet-50

把ResNet-34 的每个block里的两层结构换成3层的bottlenect结构。就成了ResNet-50。在论文里,作者对于维度的增多用了B方案:对增加的维度使用有weight的线性映射,其他使用恒等映射。这个模型的浮点运算是38亿次。但是在TensorFlow里的ResNet-50和论文里的不一样,后边我们会看到。

TensorFlow ResNet-50实现

TensorFlow默认有一个基于Imagenet训练好的ResNet-50的网络。它的源代码在:


\tensorflow\python\keras\_impl\keras\applications\resnet50.py

网络的架构定义在ResNet50这个方法里,这里我们只看核心部分,可以对照着论文里的这张表的50-layer列来看:

Conv1 block


  x = Conv2D(
      64, (7, 7), strides=(2, 2), padding='same', name='conv1')(
          img_input)
  x = BatchNormalization(axis=bn_axis, name='bn_conv1')(x)
  x = Activation('relu')(x)
  x = MaxPooling2D((3, 3), strides=(2, 2))(x)

img_input是输入的224x224x3图片
第一个卷积有64个7×7的卷积核,步长为2,padding是same,所以,输出是112x112x64
然后是进行一个batch normal,接着是激活函数Relu,维度都没有改变。
最后是一个max pooling,因为步长为(2,2),所以输出维度为56x56x64

Conv2 block


  x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1))
  x = identity_block(x, 3, [64, 64, 256], stage=2, block='b')
  x = identity_block(x, 3, [64, 64, 256], stage=2, block='c')

其中有两个不同类型的残差块,一个conv_block,一个是identity_block。conv2 block有3个残差块,一个卷积残差块,两个恒等残差块。这里的conv和identity是针对残差块shortcut的连接方式来定义的。conv_block里shortcut连接的两端维度不一样,输入端要经过一个卷积才能和输出端相加。而identity_block里shortcut连接的两端维度一致,可以直接相加。

卷积残差块

conv_block实现了卷积shortcut,它的主要代码如下:


  x = Conv2D(
      filters1, (1, 1), strides=strides, name=conv_name_base + '2a')(
          input_tensor)
  x = BatchNormalization(axis=bn_axis, name=bn_name_base + '2a')(x)
  x = Activation('relu')(x)

  x = Conv2D(
      filters2, kernel_size, padding='same', name=conv_name_base + '2b')(
          x)
  x = BatchNormalization(axis=bn_axis, name=bn_name_base + '2b')(x)
  x = Activation('relu')(x)

  x = Conv2D(filters3, (1, 1), name=conv_name_base + '2c')(x)
  x = BatchNormalization(axis=bn_axis, name=bn_name_base + '2c')(x)

  shortcut = Conv2D(
      filters3, (1, 1), strides=strides, name=conv_name_base + '1')(
          input_tensor)
  shortcut = BatchNormalization(axis=bn_axis, name=bn_name_base + '1')(shortcut)

  x = layers.add([x, shortcut])
  x = Activation('relu')(x)
  return x

可以看到首先是一个利用1×1卷积核降维的卷积层,对于conv2 block,步长为1。并没有改变维度。64个卷积核,接下来在Relu前进行了BN。
然后进入第二个卷积层,64个3×3的卷积核。同样在Relu前进行了BN。
接着进入最后一个卷积层,256个1×1的卷积核,然后做了BN。需要注意的是,这里我们在这个残差块最后一个卷积层进入激励函数之前,需要接入输入的shortcut。然而,输入的维度为56x56x64,而最后一个卷积层输出的维度为56x56x256。这时我们需要对输入用1×1的filter进行卷积来改变维度。生成一个shortcut。通过这个卷积后,shortcut输出的tensor维度为56x56x256。可以和最后一个卷积层的输出相加。然后通过Relu,作为Conv2的第一个卷积残差块的输出。

恒等残差块

接着我们看Conv2的第二个恒等残差块,它的定义在identity_block方法里,它的核心代码如下:


  x = Conv2D(filters1, (1, 1), name=conv_name_base + '2a')(input_tensor)
  x = BatchNormalization(axis=bn_axis, name=bn_name_base + '2a')(x)
  x = Activation('relu')(x)

  x = Conv2D(
      filters2, kernel_size, padding='same', name=conv_name_base + '2b')(
          x)
  x = BatchNormalization(axis=bn_axis, name=bn_name_base + '2b')(x)
  x = Activation('relu')(x)

  x = Conv2D(filters3, (1, 1), name=conv_name_base + '2c')(x)
  x = BatchNormalization(axis=bn_axis, name=bn_name_base + '2c')(x)

  x = layers.add([x, input_tensor])
  x = Activation('relu')(x)
  return x

可以看到恒等残差块和卷积残差块不同的地方是,它没有用1×1的卷积来改变输入的维度。它的输入为上一个卷积残差块的输出,维度为56x56x256,这个恒等残差块最后一个卷积层的输出也是56x56x256,所以可以直接相加。
Conv2的最后一个残差块也是恒等残差块,和上边这个残差块一样,不再赘述。

Conv3 block


  x = conv_block(x, 3, [128, 128, 512], stage=3, block='a')
  x = identity_block(x, 3, [128, 128, 512], stage=3, block='b')
  x = identity_block(x, 3, [128, 128, 512], stage=3, block='c')
  x = identity_block(x, 3, [128, 128, 512], stage=3, block='d')

Conv3 block看起来和Conv2 block很像,除了它多了一个恒等残差块之外,最大的不同是它的第一个卷积残差块用了步长为2的卷积来进行下采样。
它的卷积残差块的输入维度为:56x56x256。经过下采样后,维度为28x28x512。

Conv4 block


  x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a')
  x = identity_block(x, 3, [256, 256, 1024], stage=4, block='b')
  x = identity_block(x, 3, [256, 256, 1024], stage=4, block='c')
  x = identity_block(x, 3, [256, 256, 1024], stage=4, block='d')
  x = identity_block(x, 3, [256, 256, 1024], stage=4, block='e')
  x = identity_block(x, 3, [256, 256, 1024], stage=4, block='f')

Conv4 block和Conv3 block类似,除了恒等残差块为5个。它的输出维度为14x14x1024

Conv5 block


  x = conv_block(x, 3, [512, 512, 2048], stage=5, block='a')
  x = identity_block(x, 3, [512, 512, 2048], stage=5, block='b')
  x = identity_block(x, 3, [512, 512, 2048], stage=5, block='c')

Conv5 block和Conv4 block类似,也用步长为2的卷积进行了下采样,除了恒等残差块为2个。它的输出维度为7x7x2048

其他

TensorFlow 的ResNet50,在经过了Conv1-5 block后可以选择是Flatten+Dense的方式来为分类做准备,也可以选择用GlobalPooling来为分类做准备,通过include_top来做区分,具体代码如下:


   x = AveragePooling2D((7, 7), name='avg_pool')(x)

  if include_top:
    x = Flatten()(x)
    x = Dense(classes, activation='softmax', name='fc1000')(x)
  else:
    if pooling == 'avg':
      x = GlobalAveragePooling2D()(x)
    elif pooling == 'max':
      x = GlobalMaxPooling2D()(x)

发表评论

电子邮件地址不会被公开。 必填项已用*标注

%d 博主赞过: