论文地址:https://arxiv.org/pdf/1512.03385.pdf

1、引言-深度网络的退化问题

在深度神经网络训练中,从经验来看,随着网络深度的增加,模型理论上可以取得更好的结果。但是实验却发现,深度神经网络中存在着退化问题(Degradation problem)。可以看到,在下图中56层的网络比20层网络效果还要差。




cnn网络中加入残差 resnet残差网络原理_2d


上面的现象与过拟合不同,过拟合的表现是训练误差小而测试误差大,而上面的图片显示训练误差和测试误差都是56层的网络较大。

深度网络的退化问题至少说明深度网络不容易训练。我们假设这样一种情况,56层的网络的前20层和20层网络参数一模一样,而后36层是一个恒等映射( identity mapping),即输入x输出也是x,那么56层的网络的效果也至少会和20层的网络效果一样,可是为什么出现了退化问题呢?因此我们在训练深层网络时,训练方法肯定存在的一定的缺陷。

正是上面的这个有趣的假设,何凯明博士发明了残差网络ResNet来解决退化问题!让我们来一探究竟!

2、ResNet网络结构

ResNet中最重要的是残差学习单元:


cnn网络中加入残差 resnet残差网络原理_机器学习_02


对于一个堆积层结构(几层堆积而成)当输入为x时其学习到的特征记为H(x),现在我们希望其可以学习到残差F(x)=H(x)-x,这样其实原始的学习特征是F(x)+x 。当残差为0时,此时堆积层仅仅做了恒等映射,至少网络性能不会下降,实际上残差不会为0,这也会使得堆积层在输入特征基础上学习到新的特征,从而拥有更好的性能。一个残差单元的公式如下:


cnn网络中加入残差 resnet残差网络原理_机器学习_03


后面的x前面也需要经过参数Ws变换,从而使得和前面部分的输出形状相同,可以进行加法运算。

在堆叠了多个残差单元后,我们的ResNet网络结构如下图所示:


cnn网络中加入残差 resnet残差网络原理_人工智能_04


3、ResNet代码实战

我们来实现一个mnist手写数字识别的程序。代码中主要使用的是tensorflow.contrib.slim中定义的函数,slim作为一种轻量级的tensorflow库,使得模型的构建,训练,测试都变得更加简单。卷积层、池化层以及全联接层都可以进行快速的定义,非常方便。这里为了方便使用,我们直接导入slim。

import tensorflow.contrib.slim as slim

我们主要来看一下我们的网络结构。首先定义两个残差结构,第一个是输入和输出形状一样的残差结构,一个是输入和输出形状不一样的残差结构。

下面是输入和输出形状相同的残差块,这里slim.conv2d函数的输入有三个,分别是输入数据、卷积核数量,卷积核的大小,默认的话padding为SAME,即卷积后形状不变,由于输入和输出形状相同,因此我们可以在计算outputs时直接将两部分相加。

def res_identity(input_tensor,conv_depth,kernel_shape,layer_name):
    with tf.variable_scope(layer_name):
        relu = tf.nn.relu(slim.conv2d(input_tensor,conv_depth,kernel_shape))
        outputs = tf.nn.relu(slim.conv2d(relu,conv_depth,kernel_shape) + input_tensor)
    return outputs

下面是输入和输出形状不同的残差块,由于输入和输出形状不同,因此我们需要对输入也进行一个卷积变化,使二者形状相同。ResNet作者建议可以用1*1的卷积层,stride=2,来进行变换:

def res_change(input_tensor,conv_depth,kernel_shape,layer_name):
    with tf.variable_scope(layer_name):
        relu = tf.nn.relu(slim.conv2d(input_tensor,conv_depth,kernel_shape,stride=2))
        input_tensor_reshape = slim.conv2d(input_tensor,conv_depth,[1,1],stride=2)
        outputs = tf.nn.relu(slim.conv2d(relu,conv_depth,kernel_shape) + input_tensor_reshape)
    return outputs

最后是整个网络结构,对于x的输入,我们先进行一次卷积和池化操作,然后接入四个残差块,最后接两层全联接层得到网络的输出。

def inference(inputs):
    x = tf.reshape(inputs,[-1,28,28,1])
    conv_1 = tf.nn.relu(slim.conv2d(x,32,[3,3])) #28 * 28 * 32
    pool_1 = slim.max_pool2d(conv_1,[2,2]) # 14 * 14 * 32
    block_1 = res_identity(pool_1,32,[3,3],'layer_2')
    block_2 = res_change(block_1,64,[3,3],'layer_3')
    block_3 = res_identity(block_2,64,[3,3],'layer_4')
    block_4 = res_change(block_3,32,[3,3],'layer_5')
    net_flatten = slim.flatten(block_4,scope='flatten')
    fc_1 = slim.fully_connected(slim.dropout(net_flatten,0.8),200,activation_fn=tf.nn.tanh,scope='fc_1')
    output = slim.fully_connected(slim.dropout(fc_1,0.8),10,activation_fn=None,scope='output_layer')
    return output

完整的代码地址在:https://github.com/princewen/tensorflow_practice/tree/master/CV/ResNet

参考文献:

1、论文:https://arxiv.org/pdf/1512.03385.pdf