Resnet V1论文阅读和代码解析

目录:

论文阅读

代码解析

小结

论文阅读

论文地址:Deep Residual Learning for Image Recognition

1.介绍

最近研究表明,增加网络的深度是至关重要的。于是增加网络的深度成了大家努力的方向,但是是否堆叠更多的层就一定能够训练出更好的结果呢?事实上并不一定,我们遇到的阻碍之一就是梯度消失和梯度爆炸。如果遇到了梯度消失和梯度爆炸,网络会很难收敛。不过使用normalization可以解决这个问题。但是新的问题来了,实验发现如果训练很深的网络,准确率会在陷入饱和后陡然下降,而这个准确率下降不是由过拟合引起的。并且给一个深度合适的网络增加更多的层也会导致更高的训练误差。这就是深度网络在训练中的退化问题,但是网络越深确实能够带来更多的表达力,每一层网络的学习调整可以适应更多的场景,所以我们还是要追求更深的网络结构。

于是作者提出一种深度残差框架来解决上面提到的退化问题,使用了一种shortcut connection的连接方式。

上图就是残差网络的基本组成单元,叫做Residual block。

A2=relu(A0+Z2)就是residual block的计算方式。

论文中提到这种连接方式既没有增加参数,也没有增加计算量,但是我觉得计算量还是有少量的增加,在A0+Z2处,不过相比卷积运算确实可以忽略不计。这个网络依然可以使用SGD进行端到端的训练。作者在ImageNet上进行了系统的实验,实验显示

  1. 使用残差网络,网络很深的时候也容易优化。如果普通堆叠的网络在网络深度增加的时候,错误率会增加。
  2. 我们的残差网络能够从网络深度的增加带来可观的准确率的提升,比以往的网络有更高的准确率。

作者已经成功训练出了超过100层的网络,也对超过1000层的网络做出了探索。并且残差网络在其他识别任务上也有不错的效果,说明残差学习原则是通用的。


梯度消失和梯度爆炸

比如一个三层的神经网络,如果简化忽略b,则a_{i+1} = f(a_i * w_{i+1}),对w_1求导通过链式法则可以表示为

\frac{\partial Loss}{\partial w_1} = \frac{\partial Loss}{\partial a_3} * \frac{\partial a_3}{\partial a_2} * \frac{\partial a_2}{\partial a_1} * \frac{\partial a_1}{\partial w_1}        其中\frac{\partial a_1}{\partial w_1} = a_0 = x\frac{\partial a_3}{\partial a_2}\frac{\partial a_2}{\partial a_1}就是对激活函数的求导

\frac{\partial Loss}{\partial w_2} = \frac{\partial Loss}{\partial a_3} * \frac{\partial a_3}{\partial a_2} * \frac{\partial a_2}{\partial w_2}  其中\frac{\partial a_2}{\partial w_2} = a_1 = f(x_0 * w_1)

如果网络非常深,则链式求导中会重复很多次对激活函数的求导,而如果这个导数大于1,那么梯度将以指数级增大。如果这个导数小于1,那么梯度将以指数级缩减。这就是梯度爆炸和梯度消失。


2.shortcut的实现方式

假设x是残差模块的输入,F(x, w_i)表示要学习的残差网络,那么可以有如下表示:

                                                                             y = F(x, w_i) + x(公式1)

其中xF(x, w_i)必须维度相同,否则不可相加,那如果遇到维度不同如何解决呢?作者想到用W_s来做线性投射,公式如下:

                                                                           y = F(x, W_i) + W_sx(公式2)

但是作者实验发现,只在维度匹配时使用残差网络效果已经不错了,可以在维度不匹配的时候不使用。另外残差模块至少需要有2层,如果只有1层效果并不好。

3.网络结构

作者从上图显示的plain和residual模型作为例子来说明网络模型。

Plain Network:plain network是上图中间显示的模型,主要受VGG网络(上图左)的启发。卷积层主要是3x3的filter,并遵守两个简单的原则:

  1. 对于输出的feature map尺寸一样的层,有相同数量的filter,在上图中用不同的颜色块来表示,他们都有相同的filter个数。
  2. 如果feature map的尺寸减半,那么filter的个数就翻倍,为了保证每一层有同样的时间复杂度。

通过stride为2的卷积层来降低feature map的尺寸。网络最后是一个average pooling层加上unit为1000的FC和softmax。把网络中有参数的层加起来一共34层。

虽然这个网络层数比vgg多,但是计算量只有vgg的18%。

Residual Network。基于plain network,插入shortcut connections,就得到了Residual网络(上图右)。当输入和输出维度相同时,shortcuts能够直接使用。当维度增加时(上图中虚线表示的shortcut),由于尺寸不匹配而不能直接相加,有两种做法:

  1. shortcut仍然执行,用零来填充增加的维度。这中方式不会引入额外的参数。
  2. 可以使用公式2的方式来做线性投射。

4.训练

图片预先处理:

  • 将图片的短边压缩成224,然后从图片中随机截取224x224的图片
  • 同样减去所有像素的均值

训练参数:

  • Conv后激活函数前有BN处理
  • batchsize是256
  • 使用SGD进行梯度下降
  • learning rate是0.1,遇到瓶颈就除以10
  • weight decay是0.0001,momentum是0.9
  • 没有使用dropout

5.实验

实验的内容比较多,因为作者要进行各种角度的对比,具体结果可以查看论文中的详细信息。总的来说证明了以下几点:

  • 34层的plain网络比18层的plain网络有更高的误差,证明了plain网络深度增加会发生退化
  • 34层的残差网络比18层的残差网络有更高的精确度,错误率降低,跟34层的plain网络相比更是有明显的提升。另外18层的残差网络虽然跟18层的plain网络精确度类似,但是残差网络收敛的更快。这些都能表明残差网络结构是有效的。
  • 对于上面公式2的连接方式我们叫做projection shortcut,三种方式比较1)尺寸不匹配用0来填充;2)尺寸不匹配时用projection shortcut,其他都是identiy shortcut;3)全部用projection shortcut;实验证明结果虽然3好于2,2好于1,但是带来的精度提升是非常细微的,可能是因为添加了额外参数导致的。所以后面不再使用3
  • 使用三层的残差模块来替代两层的残差模块,实验证明网络深度增加时效果依然很好

代码解析

代码地址:resnet_v1

在看是代码之前先说明一下,resnet v1似乎是内嵌入了tensorflow的安装包,所以我们可以直接使用resnet v1网络,使用方法如下:

from tensorflow.contrib.slim.nets import resnet_v1
import tensorflow as tf

slim = tf.contrib.slim

inputs = tf.placeholder(dtype=float32, shape=[None, 224, 224, 3])
with slim.arg_scope(resnet_v1.resnet_arg_scope()):
  net, end_points = resnet_v1.resnet_v1_101(inputs, 1000, is_training=False)
  print(net)
  print(end_points['predictions'])

代码中实现了resnet_v1_50,resnet_v1_101,resnet_v1_152和resnet_v1_200四种网络结构,其实他们的实现都是以resnet_v1_block的方式来堆叠网络的,所以下面以其中50层的网络作为例子来解读,其他的网络结构都是同样的方式实现的。

  blocks = [
      resnet_v1_block('block1', base_depth=64, num_units=3, stride=2),
      resnet_v1_block('block2', base_depth=128, num_units=4, stride=2),
      resnet_v1_block('block3', base_depth=256, num_units=6, stride=2),
      resnet_v1_block('block4', base_depth=512, num_units=3, stride=1),
  ]

1. resnet_v1_block

def resnet_v1_block(scope, base_depth, num_units, stride):  
  return resnet_utils.Block(scope, bottleneck, [{
      'depth': base_depth * 4,
      'depth_bottleneck': base_depth,
      'stride': 1
  }] * (num_units - 1) + [{
      'depth': base_depth * 4,
      'depth_bottleneck': base_depth,
      'stride': stride
  }])

如果以resnet_v1_50的block1的参数为例子展开可以变成下面的代码,表示block1有3个args。

  return resnet_utils.Block(scope, bottleneck, [{
      'depth': 64 * 4,
      'depth_bottleneck': 64,
      'stride': 1
  }, {
      'depth': 64 * 4,
      'depth_bottleneck': 64,
      'stride': 1
  }, {
      'depth': 64 * 4,
      'depth_bottleneck': 64,
      'stride': 2
  }]

所以同样的block2有4个args,block3有6个args,block4有3个args。

在resnet_utils.py中定义了Block这个空类,这个空类将数据放入了名为Block的数组中,scope是传入的block1之类的,unit_fn是bottleneck,args就是传入的数组

class Block(collections.namedtuple('Block', ['scope', 'unit_fn', 'args'])):

2. resnet_v1

接着再回到resnet_v1_50函数中,最后调用了resnet_v1函数。

  return resnet_v1(inputs, blocks, num_classes, is_training,
                   global_pool=global_pool, output_stride=output_stride,
                   include_root_block=True, spatial_squeeze=spatial_squeeze,
                   store_non_strided_activations=store_non_strided_activations,
                   reuse=reuse, scope=scope)

在resnet_v1中开始搭建网络,首先是一个convolution计算和一个max pooling

net = resnet_utils.conv2d_same(net, 64, 7, stride=2, scope='conv1')
net = slim.max_pool2d(net, [3, 3], stride=2, scope='pool1')

conv2d_same就是用padding为valid的方式,通过自己计算上下左右的padding行数来实现类似padding为same的计算。为什么要这样实现呢?注释里面说因为这两种方式计算出来的padding行数是有差别的。比如padding为same算下来padding需要上下padding加起来行数为5,而为conv2d_same的方式,上下padding都是3行。但是其实我不明白这个padding行数不一样有什么作用,可能是为了计算的对称性吧。

接着调用了resnet_utils.stack_blocks_dense

net = resnet_utils.stack_blocks_dense(net, blocks, output_stride,
                                      store_non_strided_activations)

在stack_blocks_dense中对之前定义的4个blocks以及block中的args一一遍历,搭建网络。

  for block in blocks:
      ......
      for i, unit in enumerate(block.args):
         ......
            net = block.unit_fn(net, rate=1, **unit)
      ......

unit_fn就是之前传入block的bottleneck函数,unit就是block中的args,其中存放的信息是depth、depth_bottleneck和stride。

最后调用了一个convolution计算和一个softmax,就是一个完整的网络了,下面看看bottleneck的实现。

3. bottleneck

      shortcut = slim.conv2d(
          inputs,
          depth, [1, 1],
          stride=stride,
          activation_fn=tf.nn.relu6 if use_bounded_activations else None,
          scope='shortcut')
    residual = slim.conv2d(inputs, depth_bottleneck, [1, 1], stride=1,
                           scope='conv1')
    residual = resnet_utils.conv2d_same(residual, depth_bottleneck, 3, stride,
                                        rate=rate, scope='conv2')
    residual = slim.conv2d(residual, depth, [1, 1], stride=1,
                           activation_fn=None, scope='conv3')

      output = tf.nn.relu(shortcut + residual)

residul block就是在bottleneck中实现的,根据代码画出下面图可以很清晰的看出residul block的结构。

基本上对代码的梳理到这里就结束了,最后提到一点关于@slim.add_arg_scope的细节。

4. @slim.add_arg_scope

因为我们会用到arg_scope对conv2d、bottleneck和stack_blocks_dense设置默认参数,conv2d本来就可以这样使用,但是bottleneck等新增的函数需要这样使用必须要在函数上方声明@slim.add_arg_scope

    with slim.arg_scope([slim.conv2d, bottleneck,
                         resnet_utils.stack_blocks_dense],
                        outputs_collections=end_points_collection):

5. 网络层数

resnet_v1_50表示50层的resnet网络。我们可以数一下代码实现中的网络层数(只考虑有参数的网络层),在resnet_v1中调用stack_blocks_dense前后各有一层,加起来就是2层。stack_block_dense中调用bottleneck3+6+4+3=16次,每个bottleneck有3层,因为总共的层数是2+(3+6+4+3)*3=50

小结

resnet v1的block是以一种残差连接的方式让网络可以实现的越来越深,现在开始出现了1000层以上网络的尝试,残差连接的方式是非常重要的基础。

猜你喜欢

转载自blog.csdn.net/stesha_chen/article/details/81870591
今日推荐