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")
TensorBoard
最新推荐文章于 2020-11-25 20:50:00 发布