介绍

\quad ResNet(Residual Neural Network)由微软亚洲研究院的Kaiming He等4名华人提出,通过使用Residual Unit成功训练152层深的神经网络,在ILLSVRC 2015比赛中获得了冠军,取得了3.57%的top-5错误率,同时参数量却比VGGNet低,效果非常突出。ResNet的结构可以极快的加速超深神经网络的训练,模型的准确率也会有非常大的提升。
\quad 在ResNet之前,瑞士教授Schmidhuber提出了Highway Network,原理与ResNet很相似。这位Schmidhuber教授同时也是LSTM网络的发明者,而且是最早在1997年发明的,可谓是神经网络领域元老级的学者。通常认为神经网络的深度对其性能非常重要,但是网络越深其训练难度越大,Highway Network的目标就是解决极深的神经网络难以训练的问题。Highway Network相当于修改了每一层的激活函数,此前的激活函数只是对输入做一个非线性变换 y = H ( x W h ) y=H(x,W_h) y=H(xWh),Highway Network则允许保留一定比例的原始输入x,即 y = H ( x , W H ) T ( x , W T ) + x C ( x , W c ) y=H(x,W_H)*T(x,W_T)+x*C(x,W_c) y=H(x,WH)T(x,WT)+xC(x,Wc),其中T为变换系数,C为保留系数,论文中令C=1-T。这样前一层的信息,有一定比例可以不经过矩阵乘法和非线性变换,直接传输到下一层,仿佛一条信息高速公路,因此得名为Highway Network。Highway Network主要通过gating units学习如何控制网络中的信息流,即学习原始信息应保留的比例。这个可学习的gating机制,正式借鉴自Schmidhuber教授早些年的LSTM循环卷积神经网络的gating。几百至千层深的Highway Network可以直接使用梯度下降算法训练,并可以配合多种非线性激活函数,学习极深的神经网络现在变得可行了。事实上,Highway Network的设计在理论上允许其训练任意深的网络,其优化方法基本上与网络的深度独立,而传统的神经网络结构则对深度非常敏感,训练复杂度随深度的增加而急剧增加。
\quad ResNet和HighWay Network非常相似,也是允许原始输入信息直接传输到后面的层中。ResNet最初的灵感出自于这个问题:在不断增加神经网络的深度时,会出现一个Degradation的问题,即准确率会先上升然后达到饱和,再继续增加深度则会导致准确率下降。这并不是过拟合的问题,因为不光在测试集上误差增大,训练集本身误差也会增大。假设有一个比较浅的网络达到了饱和的准确率,那么后面再加上几个y=x的全等映射层,起码误差不会增加,即更深的网络不应该带来训练集上误差上升。而这里提到的使用全等映射直接将前一层输出传到后面的思想,就是ResNet的灵感来源。假定某段神经网络输入是x,期望输出是H(x),我们如果直接把输入x传到输出作为初始结果,那么此时我们要学习的目标就是F(x)=H(x)-x。如下图,
这就是一个ResNet的残差学习单元,ResNet相当于把学习目标改变了,不再是学习一个完整的输出H(x),只是输出和输入的差别H(x)-x,即残差。
KaTeX parse error: Expected 'EOF', got '\qaud' at position 1: \̲q̲a̲u̲d̲在使用了ResNet的结构后,可以发现层数不断加深导致的训练集上误差增大的现象被消除了,ResNet网络的训练误差会随着层数增加而逐渐减少,并且在测试集上的表现也会变好。在ResNet推出后不久,Google就是借鉴了ResNet的精髓,提出了Inception V4和Inception-ResNet-V2,并通过融合这两个模型,在ILSVRC数据集取得了惊人的3.08%的错误率。可见,ResNet及其思想对卷积神经网络的贡献确实非常显著,具有很强的推广性。在ResNet的作者的第二篇相关论文Identity Mappings in Deep Residual Networks中,ResNet V2被提出。ResNet V2和ResNet V1的主要区别在于,作者研究ResNet残差学习单元的传播公式,发现前馈和反馈信号可以直接传输,因此skip connection的非线性激活函数替换为Identity Mappings(y=x)。同时,ResNet V2在每一层中都使用了Batch Normalization。这样处理之后,新的残差学习单元将比以前更容易训练且泛化性更强。
\quad 根据Schmidhuber教授的观点,ResNet类似于一个没有gates的LSTM网络,即将输入x传递到后面层的过程是一直发生的,而不是学习出来的。同时,最近也有两篇论文表示,ResNet基本等价于RNN且ResNet的效果类似于在多层网络间的集成方法。ResNet在加深网络层数上做出了重大贡献,而另外一篇论文The Power of Depth for Feedforward Neural Networks则从理论上证明了加深网络比加宽网络更有效,算是给ResNet提供了***,也是给深度学习为什么要深提供了合理的解释。
\quad 下面来实现ResNet-V2。

算法实现详细过程

  1. 导入包
#coding=utf-8
#Resnet V2.py
import collections
import tensorflow as tf
slim = tf.contrib.slim
from datetime import datetime
import math
import time
  1. 我们使用collections.namedtuple设计ResNet基本Block模块组的named_tuple,并用它创建Blcok的类,但只包含数据结构,不包含具体方法。我们要定义一个典型的Block,需要输入3个参数,分别是scope,unit_fn和args。以Block(‘block1’,bottleneck,[(256,64,1)] × \times × 2 +[(256, 64, 2)])这一行代码为例,它可以定义一个典型的Block,其中block1就是我们这个Block的名称(或scope);bottleneck是ResNet V2中的残差学习单元;而最后一个参数[(256,64,1)] × \times × 2 +[(256, 64, 2)]则是这个Block的args,args是一个列表,其中每个元素都对应一个bottleneck残差学习单元,前面两个元素都是(256, 64, 1),最后一个元素是(256,64,2)。每个元素都是一个三元的tuple,即(depth,depth_bottleneck,stride)。比如(256,64,3),代表构建的bottleneck残差学习单元 (每个残差学习单元包含3个卷积层)中,第3层输出通数depth为256,前两层输出通道数depth_bottleneck为64,且中间那层的步长stride为3.这个残差学习单元结构即为[(1 × \times × 1/s1, 64), (3 × \times × 3/s2, 64), (1 × \times × 1/s1,256)]。而在这个Block中,一共有3个bottleneck残差学习单元,除了最后一个的步长由3变为2,其余都一致。
class Block(collections.namedtuple('Block', ['scope', 'unit_fn', 'args'])):
    'A named tuple describing a ResNet block'
  1. 下面定义个降采样subsample的方法,参数包括inputs,factor(采样因子)和sacope。这个函数非常简单,如果factor为1,则不做修改直接返回inputs;如果不为1,则使用slim.max_pool2d最大池化来实现,通过1 × \times × 1的池化尺寸,stride作步长,即可实现降采样。
#定义一个降采样subsample方法
def subsample(inputs, factor, scope=None):
    if factor == 1:
        return inputs
    else:
        return slim.max_pool2d(inputs, [1, 1], stride=factor, scope=scope)
  1. 再定义一个conv2d_same函数创建卷积层。先判断stride是否为1,如果为1,则直接使用slim.conv2d并令padding模式为SAME。如果stride不为1,则显示的pad zero,要pad zero的总数为kernel_size-1,pad_beg为pad/2,pad_end为余下的部分。接下来使用tf.pad对输入变量进行补0操作。最后,因为已经进行了zero padding,所以只需要再使用一个padding模式为VALID的slim.conv2d创建这个卷积层。
#定义一个conv2d_same函数创建卷积层
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
        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)
  1. 接下来定义堆叠Blocks的函数,参数中的net即为输入,blocks是之前定义的Block的class列表,而output_collections则是用来搜集各个end_points的collections。下面使用2层循环,逐个Block,逐个Residual Unit地堆叠,先使用两个tf.variable_scope将残差学习单元命名为block1/unit_1的形式。下面使用2层循环,逐个Block,逐个Residual Unit地堆叠,先使用两个tf.variable_scope将残差学习单元命名为block1/unit_1的形式。在第2层循环中,我们拿到每个Block中每个Residual Unit的args,并展开为depth,depth_bottleneck和stride,其含义在前面定义Blocks类时已经讲解过。然后使用unit_fn函数(即残差学习单元的生成函数)顺序的创建并连接所有所有的残差单元。最后,我们使用slim.utils.collect_named_outputs函数将输出Net添加到collection中。最后,当所有Block中的所有Residual Unit都堆叠完之后,我们再返回最后的net作为stack_blocks_dense函数的结果。
#定义堆叠Blocks的函数,net为输入,blocks是之前定义的Block的calss的列表
@slim.add_arg_scope
def stack_blocks_dense(net, blocks, outputs_collections=None):
    for block in blocks:
        with tf.variable_scope(block.scope, '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.unit_fn(net, depth=unit_depth, depth_bottleneck=unit_depth_bottleneck, stride=unit_stride)
            net = slim.utils.collect_named_outputs(outputs_collections, sc.name, net)
    return net
  1. 创建ResNet通用的arg_scope,关于arg_scope,我们在前面的章节已经介绍过其功能—用来定义某些函数的参数默认值。这里定义训练标记is_training默认为True,权重衰减速率weight_decay默认为0.0001,BN的衰减速率默认为0.997,BN的epsilon默认为1e-5,BN的scale默认为True。和在Inception V3中定义arg_scope一样,先设置好BN的各项参数,然后通过slim.arg_scope将slim.conv2d的几个默认参数设置好;权重正则器设置为L2正则,权重初始化器设为slim.variance_scaling_initializer(),激活函数设为ReLU,标准化器设为BN。并将最大池化的padding模式设为SAME(注意,ResNet原论文中使用的是VALID模式,设为SAME可让特征对其更简单)。最后将基层嵌套的arg_scope作为结果返回。
#创建ResNet通用的arg_scope,用于定义某些函数的参数默认值
def resnet_arg_scope(is_training=True, weight_decay=0.001, batch_norm_decay=0.997, batch_norm_epsilon=1e-5, batch_norm_scale=True):
    batch_norm_params = {
        'is_training': is_training,
        'decay': batch_norm_decay,
        'epsilon': batch_norm_epsilon,
        '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):
        with slim.arg_scope([slim.batch_norm], **batch_norm_params):
            with slim.arg_scope([slim.max_pool2d], padding='SAME') as arg_sc:
                return arg_sc
  1. 接下来定义核心的bottleneck残差学习单元,它是ResNet V2的论文中提到的Full Preactivation Residuanl Unit的一个变种。它和ResNet V1中的残差学习单元的主要区别有两点,一是在每一层前都用了Batch Normalization,二是对输入进行preactivation,而不是在卷积进行激活函数处理。我们来看一下bottleneck的参数,inputs是输入,depth,depth_bottleneck和stride这3个参数前面的Blocks类中的args,outputs_collections是收集end_points的collection,scope是这个unit的名称。下面先使用slim.utils.last_dimension函数获取输入的最后一个维度,即输出通道数,其中的参数min_rank=4可以限定最少为4个维度。接着,使用slim.batch_norm对输入进行Batch Normalization,并使用ReLU函数进行激活Preactivate。然后定义shortcut(即直连的x);如果残差单元的输入通道数depth_in和输出通道数depth一致,那么使用subsample按步长stride对inputs进行空间上的降采样(确保空间尺寸和残差一致,因为残差中间那层的步长为stride);如果输入,输出通道数不一样,我们用步长为stride的1 × \times × 1卷积改变其通道数,使得与输出通道数一致。然后定义residual(残差),residual这里有3层,先是1个1 × \times × 1尺寸,步长为1,输出通道数为depth_bottleneck的卷积,最后一个是一个 1 × \times × 1的卷积,步长为1,输出通道数为depth的卷积,得到最终的residual,这里注意最后一层没有正则项也没有激活函数。然后将residual和shortcur相加,得到最后结果output,再使用slim.utils.collect_named_outputs将结果添加进collection并返回output作为函数结果。
#定义核心的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:
        depth_in = slim.utils.last_dimension(inputs.get_shape(), min_rank=4)
        preact = slim.batch_norm(inputs, activation_fn=tf.nn.relu, scope='preact')
        if depth == depth_in:
            shortcut = subsample(inputs, stride, 'shortcut')
        else:
            shortcut = slim.conv2d(preact, depth, [1, 1], stride=stride, normalizer_fn=None, activation_fn=None, scope='shortcut')

        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')

        output = shortcut + residual

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

8.下面定义生成ResNet V2的主函数,我们只要预先定义好网络的残差学习模块组blocks它就可以生成对应的完整的ResNet。先来看一下这个函数的参数,inputs即输入,blocks为定义好的Block类的列表,num_classes是最后输出的类别数,global_pool标志是否加上最后一层的全局平均池化,include_root_block标志是否加上ResNet网络的最前面通常使用的7 × \times × 7卷积和最大池化,reuse标志是否重用,scope是整个网络的名称。在函数体内,我们先定义好variable_scope及end_points_collection,再通过slim.arg_scope将(slim.conv22, bottleneck, stack_block_dense)这3个函数的参数outputs_collections默认设为end_points_collection。然后根据include_root_block标记,创建ResNet最前面的64输出通道的步长为2的7 × \times × 7卷积。然后根据include_root_block标记,创建ResNet最前面的64输出通道的步长为2的7 × \times × 7卷积,然后再接一个步长为2的3 × \times × 3最大池化。经历两个步长为2的层,图片已经被缩小为1/4.。然后使用前面定义的stack_blocks_dense将残差学习模块生成好,再根据标记添加全局平均池化层,这里用tf.resuce_mena实现全局平均池化,效率比直接用avg_pool高。下面根据是否有分类数增加一个输出通道为num_classes的1 × \times × 1卷积(该卷积层无激活函数和正则项),再添加一个Softmax层输出网络结果。同时使用slim.utils.convert_collection_to_dict将collection转化为Python的dict,最后返回net和end_points。

#定义生成ResNet V2的主函数
def resnet_v2(inputs, blocks, num_classes=None, global_pool=True, include_root_block=True,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'
        with slim.arg_scope([slim.conv2d, bottleneck, stack_blocks_dense], outputs_collections=end_points_collection):
            net = inputs
            if include_root_block:
                with slim.arg_scope([slim.conv2d], activation_fn=None, normalizer_fn=None):
                    net = conv2d_same(net, 64, 7, stride=2, scope='conv1')
                net = slim.max_pool2d(net, [3, 3], stride=2, scope='pool1')
            net = stack_blocks_dense(net, blocks)
            net = slim.batch_norm(net, activation_fn=tf.nn.relu, scope='postnorm')
            if global_pool:
                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')
            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
  1. 自此,我们就将ResNet的生成函数定义好了,下面来设计152层的ResNet。
#定义不同深度的ResNet网络配置
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)
  1. 最后我们使用一直以来的time_tensorflow_run,来测试152层深的ResNet的forward性能。图片尺寸回归到AlexNet,VGGNet的224 × \times × 224,batch_size为32,。我们将is_training这个FLAG设为false,然后使用resnet_v2_152创建网络,再由time_tensorflow_run函数评测其forward性能。
#定义AlexNet的每轮时间评估函数
def time_tensorflow_run(session, target, info_string):
    num_steps_burn_in = 10 #程序预热
    total_durations = 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_durations += duration
            total_duration_squared += duration * duration
    #计算每轮迭代的平均耗时和标准差sd,最后将结果显示出来
    mn = total_durations / 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))

#对ResNet152进行测试
batch_szie = 32
height, width = 224, 224
inputs = tf.random_uniform((batch_szie, height, width, 3))
with slim.arg_scope(resnet_arg_scope(is_training=False)):
    net, end_points = resnet_v2_152(inputs, 1000)

init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)
num_batches = 100
time_tensorflow_run(sess, net, "Forward")

算法实现过程完整代码

#coding=utf-8
#Resnet V2.py
import collections
import tensorflow as tf
slim = tf.contrib.slim
from datetime import datetime
import math
import time
#使用collections.nametuple设计ResNet基本的Block模块组
class Block(collections.namedtuple('Block', ['scope', 'unit_fn', 'args'])):
    'A named tuple describing a ResNet block'

#定义一个降采样subsample方法
def subsample(inputs, factor, scope=None):
    if factor == 1:
        return inputs
    else:
        return slim.max_pool2d(inputs, [1, 1], stride=factor, scope=scope)

#定义一个conv2d_same函数创建卷积层
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
        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)

#定义堆叠Blocks的函数,net为输入,blocks是之前定义的Block的calss的列表
@slim.add_arg_scope
def stack_blocks_dense(net, blocks, outputs_collections=None):
    for block in blocks:
        with tf.variable_scope(block.scope, '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.unit_fn(net, depth=unit_depth, depth_bottleneck=unit_depth_bottleneck, stride=unit_stride)
            net = slim.utils.collect_named_outputs(outputs_collections, sc.name, net)
    return net

#创建ResNet通用的arg_scope,用于定义某些函数的参数默认值
def resnet_arg_scope(is_training=True, weight_decay=0.001, batch_norm_decay=0.997, batch_norm_epsilon=1e-5, batch_norm_scale=True):
    batch_norm_params = {
        'is_training': is_training,
        'decay': batch_norm_decay,
        'epsilon': batch_norm_epsilon,
        '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):
        with slim.arg_scope([slim.batch_norm], **batch_norm_params):
            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:
        depth_in = slim.utils.last_dimension(inputs.get_shape(), min_rank=4)
        preact = slim.batch_norm(inputs, activation_fn=tf.nn.relu, scope='preact')
        if depth == depth_in:
            shortcut = subsample(inputs, stride, 'shortcut')
        else:
            shortcut = slim.conv2d(preact, depth, [1, 1], stride=stride, normalizer_fn=None, activation_fn=None, scope='shortcut')

        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')

        output = shortcut + residual

        return slim.utils.collect_named_outputs(outputs_collections, sc.name, output)
#定义生成ResNet V2的主函数
def resnet_v2(inputs, blocks, num_classes=None, global_pool=True, include_root_block=True,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'
        with slim.arg_scope([slim.conv2d, bottleneck, stack_blocks_dense], outputs_collections=end_points_collection):
            net = inputs
            if include_root_block:
                with slim.arg_scope([slim.conv2d], activation_fn=None, normalizer_fn=None):
                    net = conv2d_same(net, 64, 7, stride=2, scope='conv1')
                net = slim.max_pool2d(net, [3, 3], stride=2, scope='pool1')
            net = stack_blocks_dense(net, blocks)
            net = slim.batch_norm(net, activation_fn=tf.nn.relu, scope='postnorm')
            if global_pool:
                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')
            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
#定义不同深度的ResNet网络配置
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)

#定义AlexNet的每轮时间评估函数
def time_tensorflow_run(session, target, info_string):
    num_steps_burn_in = 10 #程序预热
    total_durations = 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_durations += duration
            total_duration_squared += duration * duration
    #计算每轮迭代的平均耗时和标准差sd,最后将结果显示出来
    mn = total_durations / 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))

#对ResNet152进行测试
batch_szie = 32
height, width = 224, 224
inputs = tf.random_uniform((batch_szie, height, width, 3))
with slim.arg_scope(resnet_arg_scope(is_training=False)):
    net, end_points = resnet_v2_152(inputs, 1000)

init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)
num_batches = 100
time_tensorflow_run(sess, net, "Forward")