ResNet

import collections
import tensorflow as tf 
slim = tf.contrib.slim

#定义一个典型的Block,需要输入三个参数,分别是scope, unit_fn, args.
#以Block('block1',bottleneck, [(256,64,1)]*2+ [(256,64,2)])这一行代码为例,它可以定义一个典型的Block,其中block1就是我们这个Block的名称(或scope);bottleneck是ResNet V2中的残差学习单元,而最后一个参数[(256,64,1)]*2+ [(256,64,2)]则是这个Block的args,
# args是一个列表,其中每个元素都对应一个bottleneck残差学习单元,前面两个元素都是(256,64,1),最后一个是(256,64,2)。每个元素都是一个三元tuple,即(depth,depth_bottleneck,stride)。比如(256,64,3),代表构建的bottleneck残差学习单元(包含三个卷积层)中
# 第三层输出通道数depth为256,前两层输出通道数depth_bottleneck为64,且中间那层的步长stride为3,这个残差学习单元结构即为(1*1/s1,64),(3*3/s2,64),(1*1/s1,256)。而在这个Block中,一共有3个bottleneck残差学习单元,除了最后一个的步长由3变为了2,其余都一致。
class Block(collections.namedtuple('Block',['scope','unit_fn','args'])):   #我们使用collection.namedtuple设计ResNet基本Block模块组的 named tuple,并用它创建Block的类,但只包含数据结构,不包含具体方法。
    'A named tuple describing a ResNet block.'
def subsample(inputs, factor, scope=None):  #降采样,参数包括输入,采样因子和scope
    if factor == 1:
        return inputs
    else:                               #如果factor不为1,则使用slim.max_pool2d最大池化来实现
        return slim.max_pool2d(inputs,[1,1], stride= factor, scope=scope)     #通过1*1的池化尺寸,stride作步长,即可实现降采样
def conv2d_same(inputs, num_outputs, kernel_size, stride, scope=None):        #定义一个conv2d_same函数创建卷积层,判断stride是否为1,如果为1,则直接使用slim.onv2d并令padding模式为SAME。
    if stride ==1:
        return slim.conv2d(inputs, num_outputs, kernel_size, stride=1,
                             padding='SAME', scope=scope)
    else:                                                                      #如果stride不为1,则显式的pad zero,
        pad_total = kernel_size -1                                               #要pad zero 的总数为kernel_size -1  
        pad_beg = pad_total // 2                                          #Python 有两种除法操作符,一种是单斜杠:用于传统除法,另一种双斜杠:用于浮点数除法,其结果进行四舍五入。
        pad_end = pad_total - pad_beg                                      #  pad_end 为余下的部分
        inputs = tf.pad(inputs,[0,0], [pad_beg, pad_end],                 #接下来用tf.pad对输入变量进行补零操作
                                [pad_beg,pad_end,[0,0]])
        return  slim.conv2d(inputs, num_outputs, kernel_size, stride=stride,     #最后,因为已经进行了zero padding,所以只需再使用一个padding模式为VALID的slim.conv2d创建这个卷积层。
                               padding= 'VALID', scope=scope) 

#接下来定义堆叠Blocks的函数,net为输入, blocks为之前定义的Blocks的class(类)的列表,而outputs_collections,则是用来收集各个end_points的collections.
# 下面使用两层循环,逐个Block,逐个Residual Unit地堆叠,先使用两个tf.variable.scope将残差学习单元命名为block1/unit1地形式。
# 在第二层循环中,我们拿到每个Block中每个Residual Unit 的args,并展开为depth,depth_bottleneck和 stride
# 然后使用unit_fn函数,(即残差学习单元的生成函数)顺序地创建并连接所有的残差学习单元。
# 最后,我们使用slim.utils.collect_named_outputs函数将输出net添加到collection中
# 最后,当所有Block中的所有Residual Unit都堆叠完之后,我们再返回最后的net作为stack_blocks_dense函数的结果

@slim.add_args_scope
def stack_blocks_dense( net, blocks, outputs_collections=None):

    for block in blocks:
        with tf.variable_scope(block.scope, 'blocks', [net]) as sc:
            for i, unit in enumerate( block.args):          #enumerate:枚举
                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(arg_scope是用来定义某些函数的参数默认值,先设置好BN的各项参数,
# 然后再使用slim.arg_scope将slim.conv2d的几个默认参数设置好,权重正则器设置为L2正则,权重初始化器设为slim.variance_scaling_initializer(),标准化器设为BN)
#最后,将几层嵌套的arg_scope作为结果返回
def resnet_arg_scope(is_training=True,
                      weight_decay= 0.0001,   #权重衰减速率
                      batch_norm_decay=0.997,   #BN衰减速率
                      batch_norm_epsilon=1e-5,
                      batch_norm_scale=True):    #BN的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:
            reurn arg_sc

#接下来定义核心的bottleneck残差学习单元,它是ResNet V2的论文中提到的Full Preactivation Residual Unit 的一个变种。它和ResNet V1中的残差学习单元的主要区别有两点,一是在每一层都用了Batch Normalization,二是对输入进行preactivation,而不是在卷积进行激活函数处理
#然后定义shotcut与residual,shotcut:如果残差单元的输入通道数与输出通道数depth一致,那么使用subsample按步长为stride对inpupts进行空间上的降采样, 如果通道数不一样,我们用步长为stride的1*1卷积改变其通道数。

@slim.add_args_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')        #对输入进行preactivation
        if depth == depth_in:
            shortcut = subsample( inputs, stride, 'shortcut')    #def subsample(inputs, factor, scope=None):  
        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的主函数,我们只要预先定义好网络的残差学习模块组blocks,它就可以生成对应的完整的ResNet.
#global_pool标志是否加上最后的一层全局平均池化(tf.reduce_mean,效率比直接用avg_pool高),include_root_block标志是否加上ResNet网络最前面通常使用的7*7卷积和最大池化,
#通过slim.arg_scope将(slim.conv2d,bottleneck,stack_block_dense)这三个函数的参数outputs_collections默认设置为end_points_collection。
#同时使用slim.utils.convert_collection_to_dict将collection转换为python的dict,最后返回net和end_points.
def resnet_v2(inputs,
               blocks,
               num_classes=None,
               global_pool=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:      #是否加上ResNet网络最前面通常使用的7*7卷积和最大池化
            with slim.arg_scope([slim.conv2d], activation_fn=None,
                               normalizer_fn = None):
                net = conv2d_same(net, 64, 7, stride=2, scope='conv1')     #创建ResNet最前面的64输出通道的步长为2的7*7卷积,然后再接一个步长为2的3*3最大池化,图片尺寸已经被缩小为1/4
            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的生成函数定义好了,不同深度的ResNet网络配置,来设计层数分别为50,101,152,和200的ResNet。我们先来看50层的ResNet。
#4个残差学习Blocks的units数量分别为3,4,6和3,总层数即为(3+4+6+3)*3+2=50(7*7和一个最大池化层),总尺寸缩小了4*2*2*2*2=4*8=32,输入图片尺寸最变为224/32=7
#和Inception V3很像,ResNet不断使用步长为2的层来缩减尺寸,同时输出通道数也在持续增加,最后达到了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 )

#101层的ResNet和50层相比,主要变化就是把4个Blocks的units数量从3,4,6,3提升到了3,4,23,3.即将第三个残差学习Block的units数增加到接近4倍。

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 )  

                  
#然后152层的ResNet,把4个Blocks的units数量从3,4,6,3提升到了3,8,36,3,Units数量提升的主要场所依然是第三个Block.
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 )  



#最后200层的ResNet,把4个Blocks的units数量从3,4,6,3提升到了3,24,36,3,Units
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 )  



#使用评测函数time_tensorflow_run,来测试152层深的ResNet(即获得ILSVRC 2015冠军的版本)的forward性能,图片尺寸回归到AlexNet,VGGNet的224*224,batch size为32
#我们将is_training这个FLAGS置为Flase
batch_size = 32
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)

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

















                

     
                   



阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页