ResNet原理及其在TF-Slim中的实现

摘要

微软的深度残差网络ResNet源于2016年CVPR最佳论文---图像识别中的深度残差学习(Deep Residual Learning for Image Recognition)(https://www.leiphone.com/news/201606/BhcC5LV32tdot6DD.html), 论文来源(https://link.jianshu.com/?t=https://arxiv.org/pdf/1512.03385v1.pdf),翻译地址(https://tower.im/users/sign_in)

这个152层ResNet架构深,除了在层数上面创纪录,ResNet 的错误率也低得惊人,达到了3.6%,人类都大约在5%~10%的水平。这是目前为止最好的深度学习框架。可以看作人工神经网络领域的又一里程碑。

2016年8月31日,Google团队宣布针对TensorFlow开源了最新发布的TF-slim资料库,它是一个可以定义、训练和评估模型的轻量级的软件包,也能对图像分类领域中几个主要有竞争力的网络进行检验和定义模型。这其中,就包括了ResNet网络结构。本文将结合TF-slim库中的ResNet模型的代码,介绍一下ResNet网络的结构和原理。

ResNet的原理

论文中提到,近几年的研究发现网络的深度是使网络性能更优化的一个关键因素,但是随着网络深度的加深,梯度消失&爆炸问题十分明显,网络甚至出现了退化。在论文中通过一个20层和一个56层的普通网络进行了对比,发现56层网络的性能远低于20层网络,如图1所示。

图1

而在ResNet的这篇论文中,通过引入一个深度残差学习框架,解决了这个退化问题。它不期望每一层能直接吻合一个映射,而是明确的让这些层去吻合残差映射。形式上看,就是用H(X)来表示最优解映射,但我们让堆叠的非线性层去拟合另一个映射F(X):=H(X) - X, 此时原最优解映射H(X)就可以改写成F(X)+X,我们假设残差映射跟原映射相比更容易被优化。极端情况下,如果一个映射是可优化的,那也会很容易将残差推至0,把残差推至0和把此映射逼近另一个非线性层相比要容易的多。

F(X)+X的公式可以通过在前馈网络中做一个“快捷连接”来实现(如图2) ,快捷连接跳过一个或多个层。在我们的用例中,快捷连接简单的执行自身映射,它们的输出被添加到叠加层的输出中。自身快捷连接既不会添加额外的参数也不会增加计算复杂度。整个网络依然可以用SGD+反向传播来做端到端的训练。

图2.残差网络:一个结构块

它有二层,如下表达式,其中σ代表非线性函数ReLU

然后通过一个shortcut,和第2个ReLU,获得输出y

而在论文的后续,又提出来深度瓶颈结构,如图3右侧.在文中是这样描述这个结构的:接下来我们描述我们为ImageNet准备的更深的网络。因为太过漫长的训练时间我们负担不起,所以修改了单元块,改为一种瓶颈设计。对于每个残差函数F,我们使用3层来描述,而不是2层。这三层分别是1×1、3×3,和1×1的卷积层,其中1×1层负责先减少后增加(恢复)尺寸的,使3×3层具有较小的输入/输出尺寸瓶颈。

图3.普通shortcut和深度瓶颈结构对比

这个深度瓶颈结构在TF-Slim库中的代码实现如下所示:

def bottleneck(inputs, depth, depth_bottleneck, stride, rate=1,
               outputs_collections=None, scope=None):

  with tf.variable_scope(scope, 'bottleneck_v1', [inputs]) as sc:
    depth_in = slim.utils.last_dimension(inputs.get_shape(), min_rank=4)    if depth == depth_in:
      shortcut = resnet_utils.subsample(inputs, stride, 'shortcut')    else:
      shortcut = slim.conv2d(inputs, depth, [1, 1], stride=stride,
                             activation_fn=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)    return slim.utils.collect_named_outputs(outputs_collections,
                                            sc.original_name_scope,
                                 output)

需要注意的是,在论文中提到的当输入输出尺寸发生增加时(图4中的虚线的快捷连接),会考虑两个策略:(a)快捷连接仍然使用自身映射,对于维度的增加用零来填补空缺。此策略不会引入额外的参数;(b)投影捷径(公式2)被用来匹配尺寸(靠1×1的卷积完成)。对于这两种选项,当快捷连接在两个不同大小的特征图谱上出现时,用stride=2来处理。而在TF-Slim的代码实现中我们可以看到采用了第二种解决方式,即通过通过1X1的卷积核卷积来达成尺寸匹配。(虽然论文中说这样提高不多但需要更多参数所以最后没有使用。)

同时,在代码中对于下采样操作(subsample)是通过1x1的池化来完成的。

ResNet的结构

所以我们可以根据一个普通的神经网络来构造一个ResNet,如图4所示,论文中选择的基础网络是VGG-Net。

图4.普通网络结构和ResNet网络结构对比

而它的具体网络结构如图5的表中所示。

图5.ResNet网络结构

在TF-Slim中的代码实现如下(以ResNet-50为例):

def resnet_v1_50(inputs,
                 num_classes=None,
                 is_training=True,
                 global_pool=True,
                 output_stride=None,
                 reuse=None,
                 scope='resnet_v1_50'):
  """ResNet-50 model of [1]. See resnet_v1() for arg and return description."""
  blocks = [
      resnet_utils.Block(          'block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),
      resnet_utils.Block(          'block2', bottleneck, [(512, 128, 1)] * 3 + [(512, 128, 2)]),
      resnet_utils.Block(          'block3', bottleneck, [(1024, 256, 1)] * 5 + [(1024, 256, 2)]),
      resnet_utils.Block(          'block4', bottleneck, [(2048, 512, 1)] * 3)
  ]  

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

在这段代码中,其实只是声明了一个通过Block组合成的List,Block的声明如下,其中的关键是collections.namedtuple这个函数,它把前面元组的值和后面的命名对应了起来。

class Block(collections.namedtuple('Block', ['scope', 'unit_fn', 'args'])):  
"""
A named tuple describing a ResNet block.  
Its parts are:    
scope: The scope of the `Block`.    
unit_fn: The ResNet unit function which takes as input a `Tensor` and      
returns another `Tensor` with the output of the ResNet unit.    
args: A list of length equal to the number of units in the `Block`. The list      
contains one (depth, depth_bottleneck, stride) tuple for each unit in the     
 block to serve as argument to unit_fn.

 """

而将个元素为block的 LIst转换为一个网络的函数,则是resnet_v1,这个函数是ResNet的核心,而不同层数的ResNet只需要改变上述函数blocks中block的个数就可以了。

class Block(collections.namedtuple('Block', ['scope', 'unit_fn', 'args'])):   
"""
A named tuple describing a ResNet block.    
Its parts are:     
scope: The scope of the `Block`.    
 unit_fn: The ResNet unit function which takes as input a `Tensor` and       
returns another `Tensor` with the output of the ResNet unit.     
args: A list of length equal to the number of units in the `Block`. The list       
contains one (depth, depth_bottleneck, stride) tuple for each unit in the       
block to serve as argument to unit_fn.
  """
def resnet_v1(inputs,               
blocks,               
num_classes=None,               
is_training=True,               
global_pool=True,               
output_stride=None,               
include_root_block=True,               
reuse=None,               
scope=None):    with tf.variable_scope(scope, 'resnet_v1', [inputs], reuse=reuse) as sc:     end_points_collection = sc.name + '_end_points'     
with slim.arg_scope([slim.conv2d, bottleneck,                          
resnet_utils.stack_blocks_dense],                         
outputs_collections=end_points_collection):      
with slim.arg_scope([slim.batch_norm], is_training=is_training):         
net = inputs        
if include_root_block:          
if output_stride is not None:            
if output_stride % 4 != 0:              
raise ValueError('The output_stride needs to be a multiple of 4.')             
output_stride /= 4          
net = resnet_utils.conv2d_same(net, 64, 7, stride=2, scope='conv1')           
net = slim.max_pool2d(net, [3, 3], stride=2, scope='pool1')         
net = resnet_utils.stack_blocks_dense(net, blocks, output_stride)        
if global_pool:          
# Global average pooling.          
 net = tf.reduce_mean(net, [1, 2], name='pool5', keep_dims=True)        
if num_classes is not None:           
net = slim.conv2d(net, num_classes, [1, 1], activation_fn=None,                             normalizer_fn=None, scope='logits')        
# Convert end_points_collection into a dictionary of end_points.         
end_points = slim.utils.convert_collection_to_dict(end_points_collection)        
if num_classes is not None:           
end_points['predictions'] = slim.softmax(net, scope='predictions')      
  return net, 
end_points

在这个函数中,将blocks转换为net的语句是

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

这个函数的具体实现如下,它通过一个循环将list中的每个block读取出来,然后将block中相应的参数代入到前文提到的bottleneck这个函数中,这样就生成了相应的ResNet网络结构。

def stack_blocks_dense(net,                        
blocks,                        
output_stride=None,                        
outputs_collections=None):    
# The current_stride variable keeps track of the effective stride of the   
# activations. This allows us to invoke atrous convolution whenever applying   
# the next residual unit would result in the activations having stride larger   
# than the target output_stride.   current_stride = 1    
# The atrous convolution rate parameter.   rate = 1    for block in blocks:    
with variable_scope.variable_scope(block.scope, 'block', [net]) as sc:      
for i, unit in enumerate(block.args):        
if output_stride is not None and current_stride > output_stride:          
raise ValueError('The target output_stride cannot be reached.')        
with variable_scope.variable_scope('unit_%d' % (i + 1), values=[net]):           
unit_depth, unit_depth_bottleneck, unit_stride = unit          
# If we have reached the target output_stride, then we need to employ          
 # atrous convolution with stride=1 and multiply the atrous rate by the           
# current unit's stride for use in subsequent layers.           
if output_stride is not None and current_stride == output_stride:             
net = block.unit_fn(                 
net,                 
depth=unit_depth,                 
depth_bottleneck=unit_depth_bottleneck,                 
stride=1,                
 rate=rate)             
rate *= unit_stride         
else:             
net = block.unit_fn(                 
net,                 
depth=unit_depth,                 
depth_bottleneck=unit_depth_bottleneck,                 
stride=unit_stride,                 
rate=1)             
current_stride *= unit_stride       
net = utils.collect_named_outputs(outputs_collections, sc.name, net)  
if output_stride is not None and current_stride != output_stride:    
raise ValueError('The target output_stridecannot be reached.')  
return net

在这里,代码中提到了 atrous convolution这个结构,简单来说,它是如图6(b)所示的一个结构,可以起到在使用了步长为1的池化层后扔使得原结构保持相同的感受野。

图6.atrous convolution

参考文献

[1]Deep Residual Learning for Image Recognition [2]http://blog.csdn.net/tiandijun/article/details/52526317 [3]http://blog.csdn.net/mao_feng/article/details/52734438 [4]http://blog.csdn.net/helei001/article/details/52692128 [5]http://blog.csdn.net/u012759136/article/details/52434826#t9

原文发布于微信公众号 - 人工智能LeadAI(atleadai)

原文发表时间:2018-01-10

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏老秦求学

[图片生成]使用VAEs生成新图片

从隐图像空间进行采样以创建全新的图像或编辑现有图像是目前创作AI最受欢迎和最成功的应用方式。

501
来自专栏机器之心

302页吴恩达Deeplearning.ai课程笔记,详记基础知识与作业代码

4008
来自专栏AI研习社

Must Know! 数据科学家们必须知道的 5 种聚类算法

聚类是一种关于数据点分组的机器学习技术。给出一组数据点,我们可以使用聚类算法将每个数据点分类到特定的组中。理论上,同一组中的数据点应具有相似的属性或特征,而不同...

3818
来自专栏专知

【干货】手把手教你Python实现自动贝叶斯调整超参数

【导读】机器学习中,调参是一项繁琐但至关重要的任务,因为它很大程度上影响了算法的性能。手动调参十分耗时,网格和随机搜索不需要人力,但需要很长的运行时间。因此,诞...

2035
来自专栏ATYUN订阅号

用TensorFlow.js进行人体姿态估计:在浏览器中即可实时查看人体姿态

在与谷歌创意实验室的合作,我很高兴地宣布的发行TensorFlow.js版本PoseNet 机器学习模型,它允许在浏览器中实时估计人类姿态。在这里试试现场演示(...

1144
来自专栏机器之心

教程 | 无需反向传播的深度学习:DeepMind的合成梯度

选自GitHub 作者:iamtrask 机器之心编译 参与:王宇欣、Ellen Han 在这篇博文中,我们将从起点(从零开始)学习 DeepMind 最近提...

29210
来自专栏marsggbo

论文笔记系列-Simple And Efficient Architecture Search For Neural Networks

本文提出了一种新方法,可以基于简单的爬山过程自动搜索性能良好的CNN架构,该算法运算符应用网络态射,然后通过余弦退火进行短期优化运行。

641
来自专栏大数据文摘

解决机器学习问题有通法!看这一篇就够了!

1774
来自专栏机器学习算法与Python学习

循环神经网络(RNN)

前言: 前馈神经网络的输入和输出的维数都是固定的,不能任意改变。当处理序列数据时,前馈神经网络就无能力为了。因为序列数据是变长的。为了使得前馈神经网络能处理变长...

2796
来自专栏Spark学习技巧

【深度学习】②--细说卷积神经网络

1. 神经网络与卷积神经网络 先来回忆一下神经网络的结构,如下图,由输入层,输出层,隐藏层组成。每一个节点之间都是全连接,即上一层的节点会链接到下一层的每一个节...

3688

扫码关注云+社区