DL学习笔记-ResNet论文阅读

ResNet

ResNet简介

ResNet(Residual Nerual Network)由微软研究院的何明凯等4名华人提出,通过使用Residuak Unit 成功训练152层深的神经网络, 在ILSCRC2015年比赛中获得3.75%的top-5错误率,获得冠军。ResNet的参数量少,且新增的Residual Unit单元可以极快地加速超深神经网络的训练,同时模型的准确率也有非常大的提升。

相关内容

在ResNet之前,瑞士教授Schmidhuber提出了Highway Network,原理和ResNet很像,Schmidhuber教授有着一个更出名的发明–LSTM网络。

1、 Highway Network解决的问题?
通常认为神经网络的深度对其性能非常重要,而在增加网络深度的同时随之而来的是网络训练难度增大,Highway Network的目标就是解决极深的神经网络难以训练的问题。
2、 Highway Network的原理?
Highway Network相当于修改了每一层激活函数,此前的激活函数是对输入信号做了非线性变换y=H(x,W),Highway Network则允许保留一定比例的原始输入x,即y=H(x,W1) * T(x,W2) + x * C(x,W3),这里T为变换系数,C为保留系数。令C = 1 - T,这样前面一层的信息,有一定比例的可以直接传输到下一层(不经过矩阵乘法和激活函数变换),如同网络传输中的一条高速公路,因此得名Highway Network。Highway Network主要通过gating units学习如何控制网络中的信息流,即学习原始信息应保留的比例。这个可学习的gatting机制,正是借鉴Schmidhuber教授早年的的LSTM网络中的gatting。这正是Highway Network的引入,使得几百层乃至上千次的网络可以训练了。
3、ResNet网络和Highway Network有啥关系?
ResNet和Highway Network非常相似,都是针对网络随着深度的变化而引发的问题,而Highway Network的解决思路给ResNet提供了解决办法.(下面详解ResNet的问题引出)
4、ResNet网络和Highway Network的区别

highway network 通过gating function控制输出,这些gating是有参数的(T,C),但是ResNet的shortcut没有额外的参数。而且当一个gating function 闭合(C接近零)的时候,highway network的层就不会再表示残差了。相反的,ResNet的公式总是学习剩余函数,ResNet的shortcut永远不会关闭(消失),并且所有的信息会一直传递下去,并且会有新的残差加入进来继续学习。

论文分析

问题引出

深度网络以端到端的多层方式集成了低/中/高级特征和分类器,并且特征的水平可以通过叠加层数(深度)来丰富。

那么出现了一个问题:是否叠加更多的层数就能得到更好的网络吗?

这个问题很难回答,因为在网络训练的过程中可能存在梯度消失/爆炸,现在这个问题(梯度)可以通过归一初始化和中间层的归一化来解决,它能使几十层的网络的反向传播以随机梯度下降的方式收敛。

但是当网络深度增加的时候,又暴露了另一个问题:

随着网络的深度增加,网络的错误率却在上升。

在不断加深网络深度时,会出现一个degradation问题,即准确率会先上升然后达到饱和,再持续增加网络深度则会导致准确率下降。这并不是过拟合的问题,因为不光在测试集上误差增大,在训练集(左边图)上误差也会增大。

思考

假设有一个比较浅的网络达到了饱和的准确率。那么后面再加上几个新的网络层,学习到的也是y=x的 全等映射层,起码误差不会增加,即更深的网络不应该带来训练集上误差上升。

解决方法

前面提到了使用 全等映射直接将前一层输出传到后面的思想,这就是ResNet的灵感。假定某段神级网络的输入是x, 期望输出是H(x),如果我们直接把输入x传到输出作为初始结果,那么此时我们需要学习的新目标就是F(x) = H(x) - x, 这就是一个ResNet的残差学习单元。原来的输出期望H(x)就变成了F(x) + x。通过将输入信息绕道直接传到输出,保护信息的完整性,整个网络则只需要学习输入、输出差别的那一部分,简化了学习目标和难度。理论上,如果得到的特征映射是最优的,我们把残差优化到0,比优化一堆非线性的特征映射更容易。
让我们来看看怎么实现它:

我们把building block 默认为 y = F(x, {w}) + x, x 和 y 分别是输入和输出,函数F(x, {w})就是我们想要学习的残差映射。对于上图的两层网络, ,其中σ是ReLU激活函数,为了简化符号省略了偏差。F(x) + x通过shortcut方式直接相加,这种方式在计算中没有引入额外的参数和计算复杂度。我们的输入和输出必须维度相同(因为y=F(x)+x, 因此输入和输出的维度必须保持一致)。
如果输入和输出不一致,可以对X做一个线性映射(1x1卷积)变换维度,再连接到后面的层 y = F(x, {w}) + w*x。

实验

下面我们测试了三个网络:

1、左边:VGG-19 Model(19.6billion FLOPs);

2、中间:普通的网络:使用多个3*3的小卷积核(以VGG网络的思想设计),遵循着两个设计原则:

    a、对于相同的输出特征图尺寸,层与滤波器的个数是相同的

    b、如果输出特征图的尺寸减半,那么滤波器的个数加倍,保持时间复杂度

使用步长为2的卷积层来进行下采样。网络用全局平均池化和具有softmax的1000输出的全连接层结束。

3、右边:residual model,这是建立在普通网络的基础上的,可以看到网络的旁边多了很多前馈线,我们把这些前馈线也称之为shortcut或skip connections. 些前馈线代表的是恒等映射。
前馈线应用会遇到两种情况:

a、输入和输出的维度一致,那可以直接连接

b、如果输出的维度增加了,有两种办法 1、全零填充卷积,2、借鉴inception Net 的思想,经过1*1卷积升维

同时配置了不同层数的ResNet,如图:

对于两层到三层的ResNet残差学习模块,设计如下:

在实验中使用了如下数据:

图像缩放比例为【256,480】随机采样,然后随机裁剪224*224,并进行水平翻转,
之后每个通道的像素都减去各自的平均值,
使用了标准的颜色增强??这个是怎么操作的??
在每次卷积之后和激活之前使用BN。
初始化权值并从头训练,
使用SGD,
batch_size为256,
learning-rate=0.1,当误差平稳时除以10,
weight-decay=0.0001,
momentum=0.9,
不使用dropout,
迭代60*104次
左边是普通网络随着迭代次数的增加,训练误差和验证误差的变化, 粗的是验证误差细的是训练误差。可以看到无论是训练误差还是验证误差都随着深度增加而增加,这就是前面提到的degradation问题。

右边是residual net(使用的是零填充),可以看到随着迭代次数的增加 训练集和验证集的误差都继续下降了,说明residual net结构产生的效果确实比较好。
作者还提到:越深的网络,收敛速度以指数级别降低,所以这会影响训练误差的降低。但是从图上可以看到,在误差很低的时候,resnet的误差抖动还能大,这证明了它的优越性。

评估

ResNet在ImageNet上的表现

ResNet-A是使用zero-padding,
ResNet-B是在等维使用恒等映射,否则使用shortcuts projection,
ResNet-C的所有shortcuts都是projection

我们认为B比A好是因为A的零填充维数确实没有残差学习。C比B好,是因为projection shortcut带来了额外的参数。但是ABC之间的差距很少,说明projection对解决退化问题作用不大。
在imagenet上证明了:1.我们的极深的残差网络很容易优化,但是当深度增加时,对应的简单网络(即简单叠加的网络)表现出更高的错误率,2、我们的深度残差网络可以通过增加网络深度来获得准确率的提升,比之前的网络好。在cifar上也获得同样的表现。在一系列的比赛上也取得很好的成绩,证明残差学习原则是通用的。

参考:

TensorFlow实战
https://blog.csdn.net/u011974639/article/details/76737547 

ResNet在TensorFlow上实现

代码如下

import tensorflow as tf
import numpy as np
import collections

slim = tf.contrib.slim

class Block(collections.namedtuple('block', ['scope', 'unit_fn', 'args'])):
    'A named tuple describing a ResNet block'
    #定义降采样函数
def subsample(inputs, factor, scope = None):
    if factor == 1:
        return inputs
    else:
        return slim.max_pool2d(inputs, [1, 1], stride = factor, 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:
        pad_total = kernel_size -1 #总数   7-1=6
        pad_beg = pad_total // 2         # 6//2=3
        pad_end = pad_total - pad_beg    # 6-3 =3
        #显式的对输入补零
        inputs = tf.pad(inputs, [[0, 0], [pad_beg, pad_end], [pad_beg, pad_end], [0, 0]])
        '''
        pad(tensor, paddings, mode="CONSTANT", name=None, constant_values=0)
        t = tf.constant([[1, 2, 3], [4, 5, 6]])
        paddings = tf.constant([[1, 1,], [2, 2]])
        # 'constant_values' is 0.
        # rank of 't' is 2.
        tf.pad(t, paddings, "CONSTANT")  # [[0, 0, 0, 0, 0, 0, 0],
                                         #  [0, 0, 1, 2, 3, 0, 0],
                                         #  [0, 0, 4, 5, 6, 0, 0],
                                         #  [0, 0, 0, 0, 0, 0, 0]
        所以在输入inputs的周围加上3  3 + 224 + 3 = 230 
        '''
        return slim.conv2d(inputs, num_outputs, kernel_size, stride=stride, padding='VALID', scope=scope)


#定义堆叠block的函数
@slim.add_arg_scope
def  stack_blocks_dense(net, blocks, outputs_collections=None):
    '''

    :param net: 输入
    :param blocks:  之前定义的Block的class的列表
    :param outputs_collections:  用来收集各个end_points的collections
    :return:  将所有的residual unit 都堆叠完后,输出最后的net
    '''
    for block in blocks: #遍历block
        with tf.variable_scope(block.scope, 'block', [net]) as sc:
            for i, unit in enumerate(block.args): #遍历block的参数
                with tf.variable_scope('unit_%d' % (i+1), values=[net]):
                    unit_depth, unit_depth_bottleneck, unit_stride = unit
                    #残差学习单元的生成函数,顺序的创建并连接所有的残差学习单元
                    net = block.unit_fn(net,
                                        depth = unit_depth,
                                        depth_bottleneck = unit_depth_bottleneck,
                                        stride = unit_stride)
            # 将输出添加到collection中
            net = slim.utils.collect_named_outputs(outputs_collections, sc.name, net)
    return net

#创建通用的arg_scope,定义某些函数的参数默认值
def resnet_arg_scope(is_training=True,
                     weight_decay = 0.0001, # 权重衰减速率
                     batch_norm_decay = 0.997, # BN衰减速率
                     batch_norm_espilon = 1e-5, # BN的epsilon
                     batch_norm_scale=True): # BN的scale

    batch_norm_params = {
        'is_training': is_training,
        'decay': batch_norm_decay,
        'epsilon': batch_norm_espilon,
        'scale': batch_norm_scale,
        'updates_collections': tf.GraphKeys.UPDATE_OPS

    }

    #设置卷积函数的默认参数
    with slim.arg_scope(
        [slim.conv2d],
        weights_regularizer = slim.l2_regularizer(weight_decay),
        weights_initializer = slim.variance_scaling_initializer(),
        activation_fn = tf.nn.relu,
        normalizer_fn = slim.batch_norm,
        normalizer_params=batch_norm_params
    ):
        # 设置batch_norm函数的默认参数
        with slim.arg_scope([slim.batch_norm], **batch_norm_params):
            # 设置max_pool的默认参数
            with slim.arg_scope([slim.max_pool2d], padding='SAME') as arg_sc:
                #返回定义好默认参数的函数
                return arg_sc

#定义bottleneck残差学习单元
@slim.add_arg_scope
def bottleneck(inputs, depth, depth_bottleneck, stride, outputs_collections=None, scope=None):
    with tf.variable_scope(scope, 'bottleneck_v2', [inputs]) as sc:
        #获得输入的最后一个维度,即输出通道数 参数min_rank限定最少为4个维度
        depth_in = slim.utils.last_dimension(inputs.get_shape(), min_rank=4)
        #对输入进行BN,并使用激活函数进行预激活
        preact = slim.batch_norm(inputs, activation_fn = tf.nn.relu, scope='preact')
        #如果输入通道数depth_in和输出通道数depth一样,那么subsample按步长为stride对inputs进行空间上的降采样
        #确保空间尺寸和残差一致,因为残差中间层的卷积步长为stride,所以降采样的步长也为stride
        if depth == depth_in:
            shortcut = subsample(inputs, stride, 'shortcut')
        # 如果输入和输出通道数不同,就用[1,1],步长为stride的卷积来改变通道数
        else:
            shortcut = slim.conv2d(preact, depth, [1, 1], stride=stride,
                                   normalizer_fn=None,activation_fn=None, scope='shortcut')

        #定义残差 第一层是1*1 步长为1, 中间是3*3, 步长为stride,
        # 第三层还是1*1 步长为1,而且没有激活函数和BN
        residual = slim.conv2d(preact, depth_bottleneck, [1, 1], stride=1, scope='conv1')
        residual = conv2d_same(residual, depth_bottleneck, 3, stride, scope='conv2')
        residual = slim.conv2d(residual, depth, [1, 1], stride=1, normalizer_fn=None, activation_fn=None, scope='conv3')

        #将残差和shortcut相加 得到最终的输出
        output = shortcut + residual

        return slim.utils.collect_named_outputs(outputs_collections, sc.name, output)

# 定义生成ResNet V2的主函数
def resnet_v2(inputs, blocks, num_class=None, global_pool =True,
              include_root_block=True, reuse=None, scope=None):
    '''

    :param inputs:  输入
    :param blocks:  定义好的Block类的列表
    :param num_class:  最后输出的类数
    :param global_pool:  是否加上最后的一层全局平均池化
    :param include_root_block:  是否加上ResNet网络最前面通常使用的7*7卷积核最大池化
    :param reuse:  是否重用
    :param scope:  整个网络的名字
    :return:
    '''
    with tf.variable_scope(scope, 'resnet_v2', [inputs], reuse = reuse) as sc:

        end_points_collection = sc.original_name_scope + '_end_points'
        with slim.arg_scope([slim.conv2d, bottleneck, stack_blocks_dense], outputs_collections = end_points_collection):
            net = inputs
            # 是否加上7*7的64通道的卷积 ,步长为2, 不使用激活函数和BN  输出为
            if include_root_block:
                with slim.arg_scope([slim.conv2d], activation_fn=None, normalizer_fn=None):
                    # 输入为224 进过显式填充 为224+ 3+ 3=230  (230-7)/2 +1 = 112  所以输出为112*112*64
                    net = conv2d_same(net, 64, 7, stride=2, scope='conv1')
                # 再进过一个3*3 步长为2 的最大池化  (112-3)/2+1 = 56*56*64
                net = slim.max_pool2d(net, [3, 3], stride=2, scope='pool1')
            #block堆叠,每个block进行一次步长为2的卷积,尺寸缩小1/2,通道数增加2倍
            net = stack_blocks_dense(net, blocks)
            #对输出添加一个BN和relu
            net = slim.batch_norm(net, activation_fn=tf.nn.relu, scope= 'postnorm')
            #添加全局平均池化
            if global_pool:
                #用reduce_mean实现全局平均池化,效率比直接用avg_pool高
                net = tf.reduce_mean(net, [1, 2], name='pool5', keep_dims=True)
            #是否分类
            if num_class:
                #如果分类 添加一个输出通道为num_class 1*1 (没有卷积和BN), 为了降维
                net = slim.conv2d(net, num_class, [1, 1], activation_fn=None, normalizer_fn=None, scope='logits')
            #将collection 转为python的字典格式
            end_points = slim.utils.convert_collection_to_dict(end_points_collection)

            #添加一个softmax层输出网络结果
            if num_class is not  None:
                end_points['predictions'] = slim.softmax(net, scope='prediction')

            return net, end_points


# 构建深度为50层的残差网络   一共四个block  每个block里面bottleneck的数量分别为(3, 4, 6, 3),
# 每个bottleneck里面有三个卷积层  总层数为(3+4+6+3) * 3 + 2 =50
# 残差学习模块之前的卷积、池化已经将尺寸缩小了4倍,前3个block都有步长为2的层,因此总尺寸缩小了4*8=32倍, 输出图片变为224/32=7
#在减少尺寸的同时,也在增加输出通道,最后达到2048
def resnet_v2_50(inputs, num_classes=None, global_pool=True, reuse=None, scope='resnet_v2_50'):
    blocks = [
        Block('block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),
        Block('block2', bottleneck, [(512, 128, 1)] * 3 + [(512, 128, 2)]),
        Block('block3', bottleneck, [(1024, 256, 1)] * 5 + [(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)


#第三个block的units数量增加到23
def resnet_v2_101(inputs, num_classes=None, global_pool=True, reuse=None, scope='resnet_v2_101'):
    blocks = [
        Block('block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),
        Block('block2', bottleneck, [(512, 128, 1)] * 3 + [(512, 128, 2)]),
        Block('block3', bottleneck, [(1024, 256, 1)] * 22 + [(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)

# 第二个和第三个分别增加到8和36
def resnet_v2_152(inputs, num_classes=None, global_pool=True, 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)

#第二个和第三个分别增加到24 和36
def resnet_v2_200(inputs, num_classes=None, global_pool=True, reuse=None, scope='resnet_v2_200'):
    blocks = [
        Block('block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),
        Block('block2', bottleneck, [(512, 128, 1)] * 23 + [(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)


from datetime import datetime
import math
import time
def time_tensorflow_run(session, target, info_string):
    num_steps_burn_in = 10
    total_duration = 0.0
    total_duration_squared = 0.0
    for i in range(num_batches + num_steps_burn_in):
        start_time = time.time()
        _ = session.run(target)
        duration = time.time() - start_time
        if i >= num_steps_burn_in:
            if not i % 10:
                print ('%s: step %d, duration = %.3f' %
                       (datetime.now(), i - num_steps_burn_in, duration))
            total_duration += duration
            total_duration_squared += duration * duration
    mn = total_duration / num_batches
    vr = total_duration_squared / num_batches - mn * mn
    sd = math.sqrt(vr)
    print ('%s: %s across %d steps, %.3f +/- %.3f sec / batch' %
           (datetime.now(), info_string, num_batches, mn, sd))
#
batch_size = 3
height, width = 224, 224
inputs = tf.random_uniform([batch_size, height, width, 3])
with slim.arg_scope(resnet_arg_scope(is_training=False)):
    net, end_points = resnet_v2_152(inputs, 1000)

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    num_batches = 100
    time_tensorflow_run(sess, net, 'Forward')

猜你喜欢

转载自blog.csdn.net/qq_36387683/article/details/80709346
今日推荐