目录:
论文阅读
代码解析
小结
论文阅读
论文地址:Deep Residual Learning for Image Recognition
1.介绍
最近研究表明,增加网络的深度是至关重要的。于是增加网络的深度成了大家努力的方向,但是是否堆叠更多的层就一定能够训练出更好的结果呢?事实上并不一定,我们遇到的阻碍之一就是梯度消失和梯度爆炸。如果遇到了梯度消失和梯度爆炸,网络会很难收敛。不过使用normalization可以解决这个问题。但是新的问题来了,实验发现如果训练很深的网络,准确率会在陷入饱和后陡然下降,而这个准确率下降不是由过拟合引起的。并且给一个深度合适的网络增加更多的层也会导致更高的训练误差。这就是深度网络在训练中的退化问题,但是网络越深确实能够带来更多的表达力,每一层网络的学习调整可以适应更多的场景,所以我们还是要追求更深的网络结构。
于是作者提出一种深度残差框架来解决上面提到的退化问题,使用了一种shortcut connection的连接方式。
上图就是残差网络的基本组成单元,叫做Residual block。
A2=relu(A0+Z2)就是residual block的计算方式。
论文中提到这种连接方式既没有增加参数,也没有增加计算量,但是我觉得计算量还是有少量的增加,在A0+Z2处,不过相比卷积运算确实可以忽略不计。这个网络依然可以使用SGD进行端到端的训练。作者在ImageNet上进行了系统的实验,实验显示
- 使用残差网络,网络很深的时候也容易优化。如果普通堆叠的网络在网络深度增加的时候,错误率会增加。
- 我们的残差网络能够从网络深度的增加带来可观的准确率的提升,比以往的网络有更高的准确率。
作者已经成功训练出了超过100层的网络,也对超过1000层的网络做出了探索。并且残差网络在其他识别任务上也有不错的效果,说明残差学习原则是通用的。
梯度消失和梯度爆炸
比如一个三层的神经网络,如果简化忽略b,则,对求导通过链式法则可以表示为
其中,和就是对激活函数的求导
其中
如果网络非常深,则链式求导中会重复很多次对激活函数的求导,而如果这个导数大于1,那么梯度将以指数级增大。如果这个导数小于1,那么梯度将以指数级缩减。这就是梯度爆炸和梯度消失。
2.shortcut的实现方式
假设是残差模块的输入,表示要学习的残差网络,那么可以有如下表示:
(公式1)
其中和必须维度相同,否则不可相加,那如果遇到维度不同如何解决呢?作者想到用来做线性投射,公式如下:
(公式2)
但是作者实验发现,只在维度匹配时使用残差网络效果已经不错了,可以在维度不匹配的时候不使用。另外残差模块至少需要有2层,如果只有1层效果并不好。
3.网络结构
作者从上图显示的plain和residual模型作为例子来说明网络模型。
Plain Network:plain network是上图中间显示的模型,主要受VGG网络(上图左)的启发。卷积层主要是3x3的filter,并遵守两个简单的原则:
- 对于输出的feature map尺寸一样的层,有相同数量的filter,在上图中用不同的颜色块来表示,他们都有相同的filter个数。
- 如果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),由于尺寸不匹配而不能直接相加,有两种做法:
- shortcut仍然执行,用零来填充增加的维度。这中方式不会引入额外的参数。
- 可以使用公式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层以上网络的尝试,残差连接的方式是非常重要的基础。