目录
ResNet的成绩
我们先看一下ResNet在ILSVRC和COCO 2015上的战绩:
ResNet取得了5项第一,并又一次刷新了CNN模型在ImageNet上的历史:
ResNet的作者何恺明也因此摘得CVPR2016最佳论文奖.
1.背景提要:
在2012年的ImageNet Large Scale Visual Recognition Challenge (ILSVRC)中,一种名为AlexNet的深度学习模型取得了突破性的成就,不仅赢得了冠军,而且其性能远远超过了排名第二的模型。这一成就引起了人工智能界的广泛关注和研究,尤其是对AlexNet的深层网络结构。随后,人们开始普遍相信“网络越深,准确率越高”的观点。
这个观点随着后续模型如VGGNet、Inception v1、Inception v2和Inception v3的成功而得到了进一步的验证和强化,使得这一信念在学术界和工业界得到了广泛的认可。确实,更深的网络结构因为其能够捕捉更复杂的特征和模式,理论上具有更高的性能潜力。
但,事实真的如此吗?
2.深度网络的退化问题
实验发现深度网络出现了退化问题(Degradation problem):网络深度增加时,网络准确度出现饱和,甚至出现下降。
如上图所示,56层的网络比20层网络的结果要差 ,有些人就会马上想到过拟合。但并非如此,因为过拟合的训练集会出现误差特别低的情况,但如图所示,56层的结果误差也很高。
梯度消失/爆炸
我们知道深层网络是存在梯度消失或者梯度爆炸。是因为在反向传播中,将误差从末层往前传递的过程需要链式法则)的帮助,因此反向传播算法可以说是梯度下降在链式法则中的应用。
而链式法则是一个连乘的形式,所以当层数越深的时候,梯度将以指数形式传播。梯度消失问题和梯度爆炸问题一般随着网络层数的增加会变得越来越明显。
那么如果连乘的因子大部分小于1,最后乘积的结果可能趋于0,也就是梯度消失,后面的网络层的参数不发生变化。
那么如果连乘的因子大部分大于1,最后乘积可能趋于无穷,这就是梯度爆炸。
3.什么是ResNet
Resnet 分为 Res 和 Net 来理解,Res 指的是残差结构Residual。Net 当然就是神经网络的意思.
3.1残差结构的提出
现在想象以下场景。
你拥有一个浅层的神经网络,并计划通过添加更多层来转换它成为一个深层网络时,存在一种极端的情况:这些新增加的层可能不会学习到任何新的或有意义的特征,而只是简单地复制已有的浅层网络中的特征。这意味着这些新层实际上扮演着恒等映射(Identity mapping)的角色,即它们不对输入数据做任何实质性的改变。
在这种情况下,理论上,深层网络的性能至少应该与原始的浅层网络相同。因为即使新层没有学习到任何有用的新信息,它们也不会削弱或损害网络已经学到的特征和知识。因此,不应该出现所谓的“性能退化”现象——即网络性能因为增加层数而下降。
那要怎样做才能让这种情况下深层网络的性能不比浅层网络差呢?
如果深层网络的后面那些层是恒等映射,那么模型就退化为一个浅层网络。那现在要解决的就是学习恒等映射函数了。 但是直接让一些层在输入为x时去拟合一个潜在的恒等映射函数H(x) = x,比较困难,这可能就是深层网络难以训练的原因。但是,如果把网络设计为H(x) = F(x) + x,如下图。我们可以转换为学习一个残差函数F(x) = H(x) - x. 只要F(x)=0,就构成了一个恒等映射H(x) = x实际上残差不会为0,这也会使得堆积层在输入特征基础上学习到新的特征,从而拥有更好的性能。
3.2残差结构的数学理解
首先残差单元可以表示为:
其中,和分别表示第的 个残差单元的输入和输出,注意每个残差单元一般包含多层结构。 是残差函数,表示学习到的残差,而 表示恒等映射, 是ReLU激活函数。基于上式,我们求得从浅层 到深层 的学习特征为:
利用链式法则推导出反向传播的梯度
小括号中的1表明短路机制可以无损地传播梯度,而另外一项残差梯度则需要经过带有weights的层,梯度不是直接传递过来的。残差梯度不会那么巧全为-1,而且就算其比较小,有1的存在也不会导致梯度消失。所以残差学习会更容易。
3.3残差单元的设计
按照这个思路,ResNet团队分别构建了带有“快捷连接(Shortcut Connection)”的ResNet构建块( BasicBlock)、以及降采样的ResNet构建块( Bottleneck),区降采样构建块的主杆分支上增加了一个1×1的卷积操作。
图中右侧的曲线叫做跳接(shortcut connection),通过跳接在激活函数前,将上一层(或几层)之前的输出与本层计算的输出相加,将求和的结果输入到激活函数中做为本层的输出。
Bottleneck的优势: Bottleneck中 第一个1x1的卷积把256维channel降到64维,然后在最后通过1x1卷积恢复,整体上用的参数数目:1x1x256x64 + 3x3x64x64 + 1x1x64x256 = 69632,而不使用bottleneck的话就是两个3x3x256的卷积,参数数目: 3x3x256x256x2 = 1179648,差了16.94倍。当然层数也会加多,所以其使用要综合考虑。
下图是原论文给出的不同深度的ResNet网络结构配置,注意表中的残差结构给出了主分支上卷积核的大小与卷积核个数,表中 残差块×N 表示将该残差结构重复N次。
- 从50-layer之后,conv2——conv5都是采取三层块结构以减小计算量和参数数量
- 说明50-layer以后开始采用BottleBlock
- 从50-layer之后,层数的加深仅仅体现在conv4_x这一层中,也就是output size为14×14的图像
4.ResNet的网络结构
ResNet网络是参考了VGG19网络,在其基础上进行了修改,并通过短路机制加入了残差单元,如图5所示。变化主要体现在ResNet直接使用stride=2的卷积做下采样,并且用global average pool层替换了全连接层。ResNet的一个重要设计原则是:当feature map大小降低一半时,feature map的数量增加一倍,这保持了网络层的复杂度。从图中可以看到,ResNet相比普通网络每两层间增加了短路机制,这就形成了残差学习,当网络更深时,其进行的是三层间的残差学习,其中虚线表示feature map数量发生了改变。一个值得注意的是隐含层的feature map数量是比较小的,并且是输出feature map数量的1/4。
附上论文中的 结果,作者对比18-layer和34-layer的网络效果,如图7所示。可以看到普通的网络出现退化现象,但是ResNet很好的解决了退化问题。
5.ResNet的实现
这里给出ResNet50的TensorFlow实现,模型的实现参考了Caffe版本的实现,核心代码如下:
class ResNet50(object):
def __init__(self, inputs, num_classes=1000, is_training=True,
scope="resnet50"):
self.inputs =inputs
self.is_training = is_training
self.num_classes = num_classes
with tf.variable_scope(scope):
# construct the model
net = conv2d(inputs, 64, 7, 2, scope="conv1") # -> [batch, 112, 112, 64]
net = tf.nn.relu(batch_norm(net, is_training=self.is_training, scope="bn1"))
net = max_pool(net, 3, 2, scope="maxpool1") # -> [batch, 56, 56, 64]
net = self._block(net, 256, 3, init_stride=1, is_training=self.is_training,
scope="block2") # -> [batch, 56, 56, 256]
net = self._block(net, 512, 4, is_training=self.is_training, scope="block3")
# -> [batch, 28, 28, 512]
net = self._block(net, 1024, 6, is_training=self.is_training, scope="block4")
# -> [batch, 14, 14, 1024]
net = self._block(net, 2048, 3, is_training=self.is_training, scope="block5")
# -> [batch, 7, 7, 2048]
net = avg_pool(net, 7, scope="avgpool5") # -> [batch, 1, 1, 2048]
net = tf.squeeze(net, [1, 2], name="SpatialSqueeze") # -> [batch, 2048]
self.logits = fc(net, self.num_classes, "fc6") # -> [batch, num_classes]
self.predictions = tf.nn.softmax(self.logits)
def _block(self, x, n_out, n, init_stride=2, is_training=True, scope="block"):
with tf.variable_scope(scope):
h_out = n_out // 4
out = self._bottleneck(x, h_out, n_out, stride=init_stride,
is_training=is_training, scope="bottlencek1")
for i in range(1, n):
out = self._bottleneck(out, h_out, n_out, is_training=is_training,
scope=("bottlencek%s" % (i + 1)))
return out
def _bottleneck(self, x, h_out, n_out, stride=None, is_training=True, scope="bottleneck"):
""" A residual bottleneck unit"""
n_in = x.get_shape()[-1]
if stride is None:
stride = 1 if n_in == n_out else 2
with tf.variable_scope(scope):
h = conv2d(x, h_out, 1, stride=stride, scope="conv_1")
h = batch_norm(h, is_training=is_training, scope="bn_1")
h = tf.nn.relu(h)
h = conv2d(h, h_out, 3, stride=1, scope="conv_2")
h = batch_norm(h, is_training=is_training, scope="bn_2")
h = tf.nn.relu(h)
h = conv2d(h, n_out, 1, stride=1, scope="conv_3")
h = batch_norm(h, is_training=is_training, scope="bn_3")
if n_in != n_out:
shortcut = conv2d(x, n_out, 1, stride=stride, scope="conv_4")
shortcut = batch_norm(shortcut, is_training=is_training, scope="bn_4")
else:
shortcut = x
return tf.nn.relu(shortcut + h)
参考博客: