ResNet
深层次网络训练瓶颈:梯度消失,网络退化
深度卷积网络自然的整合了低中高不同层次的特征,特征的层次可以靠加深网络的层次来丰富。从而,在构建卷积网络时,网络的深度越高,可抽取的特征层次就越丰富。所以一般我们会倾向于使用更深层次的网络结构,以便取得更高层次的特征。但是在使用深层次的网络结构时我们会遇到两个问题,梯度消失,梯度爆炸问题和网络退化的问题。
但是当使用更深层的网络时,会发生梯度消失、爆炸问题,这个问题很大程度通过标准的初始化和正则化层来基本解决,这样可以确保几十层的网络能够收敛,但是随着网络层数的增加,梯度消失或者爆炸的问题仍然存在。
还有一个问题就是网络的退化,举个例子,假设已经有了一个最优化的网络结构,是18层。当我们设计网络结构的时候,我们并不知道具体多少层次的网络时最优化的网络结构,假设设计了34层网络结构。那么多出来的16层其实是冗余的,我们希望训练网络的过程中,模型能够自己训练这五层为恒等映射,也就是经过这层时的输入与输出完全一样。但是往往模型很难将这16层恒等映射的参数学习正确,那么就一定会不比最优化的18层网络结构性能好,这就是随着网络深度增加,模型会产生退化现象。它不是由过拟合产生的,而是由冗余的网络层学习了不是恒等映射的参数造成的。
ResNet简介
ResNet(Residual Neural Network)在2015年被提出,在ImageNet比赛classification任务上获得第一名。ResNet的深度远远高于VGGNet,但是参数量却比VGGNet要低,效果也更好。
ResNet中最具创新的一点就是残差学习单元(Residual Unit)的引入,而Residual Unit又是参考的Highway Network。Highway Network的出现就是为了解决较深的神经网络难以训练的问题。
ResNet的主要思想是在网络中增加了直连通道。此前的网络结构是性能输入做一个非线性变换,而Highway Network则允许保留之前网络层的一定比例的输出。允许原始输入信息直接传到后面的层中。类似LSTM中的门控单元(Gating Units)来学习原始信息保留下来的比例。
ResNet使用了一个新的思想,ResNet的思想是假设我们涉及一个网络层,存在最优化的网络层次,那么往往我们设计的深层次网络是有很多网络层为冗余层的。那么我们希望这些冗余层能够完成恒等映射,保证经过该恒等层的输入和输出完全相同。具体哪些层是恒等层,这个会有网络训练的时候自己判断出来。将原网络的几层改成一个残差块,残差块的具体构造如下图所示:
ResNet解决深度网络瓶颈的魔力
网络退化问题的解决
我们发现,假设该层是冗余的,在引入ResNet之前,我们想让该层学习到的参数能够满足h(x)=x,即输入是x,经过该冗余层后,输出仍然为x。但是可以看见,要想学习h(x)=x恒等映射时的这层参数时比较困难的。ResNet想到避免去学习该层恒等映射的参数,使用了如上图的结构,让h(x)=F(x)+x;这里的F(x)我们称作残差项,我们发现,要想让该冗余层能够恒等映射,我们只需要学习F(x)=0。学习F(x)=0比学习h(x)=x要简单,因为一般每层网络中的参数初始化偏向于0,这样在相比于更新该网络层的参数来学习h(x)=x,该冗余层学习F(x)=0的更新参数能够更快收敛,如图所示:
假设该曾网络只经过线性变换,没有bias也没有激活函数。我们发现因为随机初始化权重一般偏向于0,那么经过该网络的输出值为[0.6 0.6],很明显会更接近与[0 0],而不是[2 1],相比与学习h(x)=x,模型要更快到学习F(x)=0。
并且ReLU能够将负数激活为0,过滤了负数的线性变化,也能够更快的使得F(x)=0。这样当网络自己决定哪些网络层为冗余层时,使用ResNet的网络很大程度上解决了学习恒等映射的问题,用学习残差F(x)=0更新该冗余层的参数来代替学习h(x)=x更新冗余层的参数。
这样当网络自行决定了哪些层为冗余层后,通过学习残差F(x)=0来让该层网络恒等映射上一层的输入,使得有了这些冗余层的网络效果与没有这些冗余层的网络效果相同,这样很大程度上解决了网络的退化问题。
梯度消失或梯度爆炸问题的解决
我们发现很深的网络层,由于参数初始化一般更靠近0,这样在训练的过程中更新浅层网络的参数时,很容易随着网络的深入而导致梯度消失,浅层的参数无法更新。
可以看到,假设现在需要更新b1,w2,w3,w4参数因为随机初始化偏向于0,通过链式求导我们会发现,w1w2w3相乘会得到更加接近于0的数,那么所求的这个b1的梯度就接近于0,也就产生了梯度消失的现象。
ResNet最终更新某一个节点的参数时,由于h(x)=F(x)+x,由于链式求导后的结果如图所示,不管括号内右边部分的求导参数有多小,因为左边的1的存在,并且将原来的链式求导中的连乘变成了连加状态,都能保证该节点参数更新不会发生梯度消失或梯度爆炸现象。
ResNet使用的小技巧
这里是应用了ResNet的网络图,这里如果遇到了h(x)=F(x)+x中x的维度与F(x)不同的维度时,我们需要对identity加入Ws来保持Ws*x的维度与F(x)的维度一致。
如果F(x)和x的channel个数不同,分成两种情况考虑:
(1)channel个数一致,采用计算方式:
y
=
F
(
x
)
+
x
y=F(x)+x
y=F(x)+x;
(2)channel个数不同(64和128),采用计算方式:
y
=
F
(
x
)
+
W
x
y=F(x)+Wx
y=F(x)+Wx
其中
W
W
W是卷积操作,用来调整
x
x
x的channel维度的。
使用1*1卷积减少参数和计算量:
如果用了更深层次的网络时,考虑到计算量,会先用11的卷积将输入的256维降到64维,然后通过11恢复。这样做的目的是减少参数量和计算量。
左图是ResNet34,右图是ResNet50/101/152。这一个模块称作building block,右图称之为bottleneck design。在面对50,101,152层的深层次网络,意味着有很大的计算量,因此这里使用11卷积先将输入进行降维,然后再经过33卷积后再用11卷积进行升维。使用11卷积的好处是大大降低参数量计算量。
网络结构
在增加网络深度的同时,我们还要考虑到梯度消失的问题。因为梯度反向传播到前层,重复的乘法可能使梯度无穷小。
为了解决梯度消失问题,ResNet引入了全等映射层。假设某一神经网络的输入是
x
x
x,经过这一段网络的处理之后期望输出为
H
(
x
)
H(x)
H(x),现在将输入
x
x
x传到输出作为输出的一部分,那么此时我们的学习目标不再是一个完整的输出
H
(
x
)
H(x)
H(x),而是
H
(
x
)
−
x
H(x)-x
H(x)−x。
其中ResNet提出了两种mapping:一种是identity mapping,指的就是图中”弯弯的曲线”,另一种residual mapping,指的就是除了”弯弯的曲线“那部分,所以最后的输出是
y
=
F
(
x
)
+
x
y=F(x)+x
y=F(x)+x 。identity mapping顾名思义,就是指本身,也就是公式中的
x
x
x,而residual mapping指的是“差”,也就是
y
−
x
y−x
y−x,所以残差指的就是
F
(
x
)
F(x)
F(x)部分。
ResNet V2与ResNet V1相比:
(1)各个残差学习单元Skip Connection连接时使用的非线性激活函数,如ReLU被更换为Identity Mapping(y=x);
(2)ResNet V2的残差学习单元在每层中都使用了Batch Normalization归一化处理。
这两个方面的改进使得残差学习单元的训练容易度和泛化性都得到了增强。
BN可以看作是一种非常有效的正则化方法。在对神经网络的某层使用BN方法时,每一个mini-batch数据的内部都会进行标准化处理,使输出规范化到
N
(
0
,
1
)
N(0,1)
N(0,1)的正态分布。
Tensorflow实现
整个样例分成了两个文件:
(1)ResNet_struct.py:用于网络的搭建;
(2)ResNet_run.py:用于网络的运行。
ResNet_struct.py:
首先创建一个Block类,这个类用来配置残差学习模型的大小。初始化一个Block类需要传入的3个参数:(1)name:残差学习模块的名称;
(2)residual_unit:创建残差学习模块需要用到的函数;
(3)args:残差学习模块的大小信息。
例如:Block(“block1”,residual_unit,[(256,64,1),(256,64,1),(256,64,2)])
(256,64,1)分别表示第三层的输出通道数,前两层的输出通道数,中间层的步长。
'''ResNet_struct.py'''
import tensorflow as tf
import collections
import time
from datetime import datetime
import math
# 使用collections.namedtuple设计ResNet基本Block模块组的named tuple,并用它创建Block类
class Block(collections.namedtuple('Block', ['scope', 'unit_fn', 'args'])):
'a named tuple decribing a ResNet block.'
# 降采样的方法
def subsample(inputs, stride, scope=None):
# 如果步长为1,之间返回输出
if stride == 1:
return inputs
# 否则,最大池化处理
else:
return slim.max_pool2d(inputs, [1, 1], stride=stride, scope=scope)
# 创建卷积层
def conv2d_same(inputs, num_outputs, kernel_size, stride, scope=None):
if stride == 1:
return slim.conv2d(inputs, num_outputs, kernel_size, stride=1, padding='SAME', scope=scope)
else:
# 步长不为1,则显示的补0
pad_total = kernel_size - 1
pad_beg = pad_total // 2
pad_end = pad_total - pad_beg
# 补零操作,在第二和第三个维度上进行补零操作
inputs = tf.pad(inputs, [[0, 0], [pad_beg, pad_end], [pad_beg, pad_end], [0, 0]])
return slim.conv2d(inputs, num_outputs, kernel_size, stride=stride, padding='VALID', scope=scope)
# 定义残差学习单元的创建过程
def residual_unit(inputs,depth,depth_residual,stride,output_collections=None,scope=None):
with tf.variable_scope(scope,"residual_v2",[inputs]) as sc:
# 输入通道数,取inputs的最后一个元素
depth_input = slim.utils.last_dimension(inputs.get_shape(),min_rank=4)
# 使用slim.batch_norm()函数进行BN操作
preact = slim.batch_norm(inputs,activation_fn=tf.nn.relu,scope="preact")
# 如果输出通道相同,则考虑进行下采样操作。如果stride=1,不进行下采样。如果stride不等于1,使用max_pool2d进行下采样(池化核为[1,1])
if depth == depth_input:
if stride == 1:
identity = inputs
else:
identity = slim.max_pool2d(inputs,[1,1],stride=stride,scope="shortcut")
else:
identity = slim.conv2d(preact,depth,[1,1],stride=stride,scope="shortcut")
# 一个残差学习模块中的3个卷积层
residual = slim.conv2d(preact,depth_residual,[1,1],stride=1,scope="conv1")
residual = conv2d_same(residual,depth_residual,3,stride,scope="conv2")
residual = slim.conv2d(residual,depth,[1,1],stride=1,scope="conv3")
# 将identity的结果和residual的结果相加
output = identity + residual
result = slim.utils.collect_named_outputs(outputs_collections,sc.name,output)
以152层深度的Resnet为例,整个网络的4个残差学习模块的残差学习单元数量分别为3、8、36、3,总层数为152层
(
(
3
+
8
+
36
+
3
)
∗
3
+
2
)
((3+8+36+3)*3+2)
((3+8+36+3)∗3+2)。如下图所示:
残差学习之前的卷积、池化已经将尺寸缩小了4倍,前3个残差学习模块又包含步长为2的层,使得输入图片由224
∗
*
∗ 224缩小为7
∗
*
∗ 7,并且每个残差模块的输出通道数在逐步增加。
def resnet_v2_152(inputs,num_classes,reuse=None,scope="resnet_v2_152"):
blocks = [
Block(
'block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),
Block(
'block2', bottleneck, [(512, 128, 1)] * 7 + [(512, 128, 2)]),
Block(
'block3', bottleneck, [(1024, 256, 1)] * 35 + [(1024, 256, 2)]),
Block(
'block4', bottleneck, [(2048, 512, 1)] * 3)]
return resnet_v2(inputs, blocks, num_classes, global_pool,
include_root_block=True, reuse=reuse, scope=scope)
在resnet_v2_152()函数的返回值中,调用了生成ResNet V2的主函数resnet_v2()。使用resnet_v2()函数就可以生成对应的完整的ResNet。
def resnet_v2(inputs,blocks,num_classes,reuse=None,scope=None):
with tf.variable_scope(scope,"resnet_v2",[inputs],reuse=reuse) as sc:
end_points_collection = sc.original_name_scope + "_end_points"
# 对函数residual_unit()的outputs_collection参数使用参数空间
with slim.arg_scope([residual_unit],outputs_collection=end_points_collection):
# 创建ResNet的第一个卷积层和池化层,卷积核大小为7,深度为64,池化核大小为3
with slim.arg_scope([slim.conv2d],activation_fn=None,normalizer_fn=None):
net = conv2d_same(inputs,64,7,stride=2,scope="conv1")
net = slim.max_pool2d(net,[3,3],stride=2,scope="pool1")
# 两个for循环完成ResNet
for block in blocks:
with tf.variable_scope(block.name, 'block', [net]) as sc:
for i, unit in enumerate(block.args):
with tf.variable_scope('unit_%d' % (i + 1), values=[net]):
unit_depth, unit_depth_bottleneck, unit_stride = unit
net = block.residual_unit(net,
depth=unit_depth,
depth_residual=unit_depth_bottleneck,
stride=unit_stride)
net = slim.utils.collect_named_outputs(outputs_collections, sc.name, net)
# 对net进行BN操作
net = slim.batch_norm(net,activation_fn=tf.nn.relu,scope="postnorm")
# 创建全局平均池化层
net = tf.reduce_mean(net,[1,2],name="pool5",keep_dims=True)
# 如果定义了num_classes,则通过1*1卷积方式获取数目为num_classes的输出
if num_classes is not None:
net = slim.conv2d(net,num_classes,[1,1],activation_fn=None,normalizer_fn=None,scope="logits")
return net
ResNet_run.py:实现网络的测评,在此不再列出。
总结
通过上述的学习,你应该知道了,现如今大家普遍认为更好的网络是建立在更宽更深的网络基础上,当你需要设计一个深度网络结构时,你永远不知道最优的网络层次结构是多少层,一旦你设计的很深入了,那势必会有很多冗余层,这些冗余层一旦没有成功学习恒等变换h(x)=x,那就会影响网络的预测性能,不会比浅层的网络学习效果好从而产生退化问题。
ResNet的过人之处,是他很大程度上解决了当今深度网络头疼的网络退化问题和梯度消失问题。使用残差网络结构h(x)=F(x)+x代替原来的没有shortcut连接的h(x)=x,这样更新冗余层的参数时需要学习F(x)=0比学习h(x)=x要容易得多。而shortcut连接的结构也保证了反向传播更新参数时,很难有梯度为0的现象发生,不会导致梯度消失。
这样,ResNet的构建,使我们更朝着符合我们的直觉走下去,即越深的网络对于高级抽象特征的提取和网络性能更好,不用在担心随着网络的加深发生退化问题了。
参考资料:
[1] Tensorflow实战:ResNet原理及实现(多注释)
[2] ResNet解析
[3] ResNetV2:ResNet深度解析
[4] 十分钟一起学会ResNet残差网络