『PyTorch × TensorFlow』第十七彈_ResNet快速實現
要點
- 神經網絡逐層加深有Degradiation問題,准確率先上升到飽和,再加深會下降,這不是過擬合,是測試集和訓練集同時下降的
- 提出了殘差結構,這個結構解決了深層網絡訓練誤差反而提升的情況,使得網絡理論上可以無限深
- bottleneck網絡結構,注意Channel維度變化:
,宛如一個中間細兩端粗的瓶頸,所以稱為“bottleneck”。這種結構相比VGG,早已經被證明是非常效的,能夠更好的提取圖像特征。

殘差結構
截取代碼如下,
@slim.add_arg_scope
def bottleneck(inputs,depth,depth_bottleneck,stride,
outputs_collections=None,scope=None):
"""
核心殘差學習單元
輸入tensor給出一個直連部分和殘差部分加和的輸出tensor
:param inputs: 輸入tensor
:param depth: Block類參數,輸出tensor通道
:param depth_bottleneck: Block類參數,中間卷積層通道數
:param stride: Block類參數,降采樣步長
3個卷積只有中間層采用非1步長去降采樣。
:param outputs_collections: 節點容器collection
:return: 輸出tensor
"""
with tf.variable_scope(scope,'bottleneck_v2',[inputs]) as sc:
# 獲取輸入tensor的最后一個維度(通道)
depth_in = slim.utils.last_dimension(inputs.get_shape(),min_rank=4)
# 對輸入正則化處理,並激活
preact = slim.batch_norm(inputs,activation_fn=tf.nn.relu,scope='preact')
'shortcut直連部分'
if depth == depth_in:
# 如果輸入tensor通道數等於輸出tensor通道數
# 降采樣輸入tensor使之寬高等於輸出tensor
shortcut = subsample(inputs,stride,'shortcut')
else:
# 否則,使用尺寸為1*1的卷積核改變其通道數,
# 同時調整寬高匹配輸出tensor
shortcut = slim.conv2d(preact,depth,[1,1],stride,
normalizer_fn=None,activation_fn=None,
scope='shortcut')
'residual殘差部分'
residual = slim.conv2d(preact,depth_bottleneck,[1,1],stride=1,scope='conv1')
residual = slim.conv2d(residual,depth_bottleneck,3,stride=stride,scope='conv2')
residual = slim.conv2d(residual,depth,[1,1],stride=1,scope='conv3')
output = shortcut + residual
return slim.utils.collect_named_outputs(outputs_collections,sc.name,output)
邏輯流程如下,
批正則化數據
shortcut分量處理:調整輸入tensor使之和輸出tensor深度一致,寬高一致
residual分量處理:1*1/1卷積->3*3/自定步長(所以上面需要調整shortcut寬高)卷積->1*1/1卷積
shortcut + residual 作為最終輸出,注意是加,不是concat。
代碼
之前一直對變量作用域的實際功效比較不解(雖然介紹文章看了很多)特別是reuse屬性搭配上后,這節網絡結構較為復雜,圖結構生成方式很混亂,也和自己粗心有關,運行時出了一些變量重復的錯,修改過程中對變量作用域的理解深入了。所謂的reuse或者重名什么的和變量生成函數沒有關系,只要同一個生成函數不同生成次數時指定不同的name就完全沒問題,重點是在計算圖的位置要不一樣。如果是同一個計算圖位置重復生成(初始化)的話要注明reuse=True。
實際例子中作者采用了一些寫起來顯得麻煩但是提高了效率的架構方式,比如conv2d_same的實現,和網絡結束部分的全局平均池化,都為了效率放棄了最簡單的寫法。
網絡結構如下,
# Author : Hellcat
# Time : 2017/12/14
import math
import time
from datetime import datetime
import collections
import tensorflow as tf
slim = tf.contrib.slim
class Block(collections.namedtuple('Block',['scope','unit_fn','args'])):
"""
使用collections.namedtuple設計ResNet基本模塊組的name tuple,並用它創建Block的類
只包含數據結構,不包含具體方法。
定義一個典型的Block,需要輸入三個參數:
scope:Block的名稱
unit_fn:ResNet V2中的殘差學習單元生成函數
args:Block的args(輸出深度,瓶頸深度,瓶頸步長)
"""
def subsample(inputs,factor,scope=None):
"""
如果factor為1,則不做修改直接返回inputs;如果不為1,則使用
slim.max_pool2d最大池化來實現,通過1*1的池化尺寸,factor作步長,實
現降采樣。
:param inputs: A 4-D tensor of size [batch, height_in, width_in, channels]
:param factor: 采樣因子
:param scope: 域名
:return: 采樣結果
"""
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):
"""
卷積層實現,有更簡單的寫法,這樣做其實是為了提高效率
:param inputs: 輸入tensor
:param num_outputs: 輸出通道
:param kernel_size: 卷積核尺寸
:param stride: 卷積步長
:param scope: 節點名稱
:return: 輸出tensor
"""
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)
@slim.add_arg_scope
def stack_block_dense(net,blocks,outputs_collections=None):
"""
示例,Block('block1',bottleneck,[(256,64,1)]*2 + [(256,64,2)])
:param net: A `Tensor` of size [batch, height, width, channels].
:param blocks: 是之前定義的Block的class的列表
:param outputs_collections: 收集各個end_points的collections
:return: Output tensor
"""
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]):
# 示例:(256,64,1)
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)
'''
這個方法會返回本次添加的tensor對象,
意義是為tensor添加一個別名,並收集進collections中
實現如下
if collections:
append_tensor_alias(outputs,alias)
ops.add_to_collections(collections,outputs)
return outputs
據說本方法位置已經被轉移到這里了,
from tensorflow.contrib.layers.python.layers import utils
utils.collect_named_outputs()
'''
return net
@slim.add_arg_scope
def bottleneck(inputs,depth,depth_bottleneck,stride,
outputs_collections=None,scope=None):
"""
核心殘差學習單元
輸入tensor給出一個直連部分和殘差部分加和的輸出tensor
:param inputs: 輸入tensor
:param depth: Block類參數,輸出tensor通道
:param depth_bottleneck: Block類參數,中間卷積層通道數
:param stride: Block類參數,降采樣步長
3個卷積只有中間層采用非1步長去降采樣。
:param outputs_collections: 節點容器collection
:return: 輸出tensor
"""
with tf.variable_scope(scope,'bottleneck_v2',[inputs]) as sc:
# 獲取輸入tensor的最后一個維度(通道)
depth_in = slim.utils.last_dimension(inputs.get_shape(),min_rank=4)
# 對輸入正則化處理,並激活
preact = slim.batch_norm(inputs,activation_fn=tf.nn.relu,scope='preact')
'shortcut直連部分'
if depth == depth_in:
# 如果輸入tensor通道數等於輸出tensor通道數
# 降采樣輸入tensor使之寬高等於輸出tensor
shortcut = subsample(inputs,stride,'shortcut')
else:
# 否則,使用尺寸為1*1的卷積核改變其通道數,
# 同時調整寬高匹配輸出tensor
shortcut = slim.conv2d(preact,depth,[1,1],stride,
normalizer_fn=None,activation_fn=None,
scope='shortcut')
'residual殘差部分'
residual = slim.conv2d(preact,depth_bottleneck,[1,1],stride=1,scope='conv1')
residual = slim.conv2d(residual,depth_bottleneck,3,stride=stride,scope='conv2')
residual = slim.conv2d(residual,depth,[1,1],stride=1,scope='conv3')
output = shortcut + residual
return slim.utils.collect_named_outputs(outputs_collections,sc.name,output)
def resnet_arg_scope(is_training=True,
weight_decay=0.0001, # L2權重衰減速率
batch_norm_decay=0.997, # BN的衰減速率
batch_norm_epsilon=1e-5, # BN的epsilon默認1e-5
batch_norm_scale=True): # BN的scale默認值
batch_norm_params = { # 定義batch normalization(標准化)的參數字典
'is_training': is_training,
# 是否是在訓練模式,如果是在訓練階段,將會使用指數衰減函數(衰減系數為指定的decay),
# 對moving_mean和moving_variance進行統計特性的動量更新,也就是進行使用指數衰減函數對均值和方
# 差進行更新,而如果是在測試階段,均值和方差就是固定不變的,是在訓練階段就求好的,在訓練階段,
# 每個批的均值和方差的更新是加上了一個指數衰減函數,而最后求得的整個訓練樣本的均值和方差就是所
# 有批的均值的均值,和所有批的方差的無偏估計
'zero_debias_moving_mean': True,
# 如果為True,將會創建一個新的變量對 'moving_mean/biased' and 'moving_mean/local_step',
# 默認設置為False,將其設為True可以增加穩定性
'decay': batch_norm_decay, # Decay for the moving averages.
# 該參數能夠衡量使用指數衰減函數更新均值方差時,更新的速度,取值通常在0.999-0.99-0.9之間,值
# 越小,代表更新速度越快,而值太大的話,有可能會導致均值方差更新太慢,而最后變成一個常量1,而
# 這個值會導致模型性能較低很多.另外,如果出現過擬合時,也可以考慮增加均值和方差的更新速度,也
# 就是減小decay
'epsilon': batch_norm_epsilon, # 就是在歸一化時,除以方差時,防止方差為0而加上的一個數
'scale': batch_norm_scale,
'updates_collections': tf.GraphKeys.UPDATE_OPS,
# force in-place updates of mean and variance estimates
# 該參數有一個默認值,ops.GraphKeys.UPDATE_OPS,當取默認值時,slim會在當前批訓練完成后再更新均
# 值和方差,這樣會存在一個問題,就是當前批數據使用的均值和方差總是慢一拍,最后導致訓練出來的模
# 型性能較差。所以,一般需要將該值設為None,這樣slim進行批處理時,會對均值和方差進行即時更新,
# 批處理使用的就是最新的均值和方差。
#
# 另外,不論是即使更新還是一步訓練后再對所有均值方差一起更新,對測試數據是沒有影響的,即測試數
# 據使用的都是保存的模型中的均值方差數據,但是如果你在訓練中需要測試,而忘了將is_training這個值
# 改成false,那么這批測試數據將會綜合當前批數據的均值方差和訓練數據的均值方差。而這樣做應該是不
# 正確的。
}
with slim.arg_scope(
[slim.conv2d],
weights_regularizer=slim.l2_regularizer(weight_decay), # 權重正則器設置為L2正則
weights_initializer=slim.variance_scaling_initializer(),
activation_fn=tf.nn.relu,
normalizer_fn=slim.batch_norm, # 標准化器設置為BN
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
def resnet_v2(inputs,
blocks,
num_classes=None,
global_pool=True,
include_root_block=True,
reuse=None,
scope=None):
"""
網絡結構主函數
:param inputs: 輸入tensor
:param blocks: Block類列表
:param num_classes: 輸出類別數
:param global_pool: 是否最后一層全局平均池化
:param include_root_block: 是否最前方添加7*7卷積和最大池化
:param reuse: 是否重用
:param scope: 整個網絡名稱
:return:
"""
with tf.variable_scope(scope,'resnet_v2',[inputs],reuse=reuse) as sc:
# 字符串,用於命名collection名字
end_points_collecion = sc.original_name_scope + 'end_points'
print(end_points_collecion)
with slim.arg_scope([slim.conv2d,bottleneck,stack_block_dense],
# 為新的收集器取名
outputs_collections=end_points_collecion):
net = inputs
if include_root_block:
with slim.arg_scope([slim.conv2d],
activation_fn=None,
normalizer_fn=None):
# 卷積:2步長,7*7核,64通道
net = conv2d_same(net,64,7,stride=2,scope='conv1')
# 池化:2步長,3*3核
net = slim.max_pool2d(net,[3,3],stride=2,scope='pool1')
# 至此圖片縮小為1/4
# 讀取blocks數據結構,生成殘差結構
net = stack_block_dense(net,blocks)
net = slim.batch_norm(net,
activation_fn=tf.nn.relu,
scope='postnorm')
if global_pool:
# 全局平均池化,效率比avg_pool更高
# 即對每個feature做出平均池化,使每個feature輸出一個值
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')
# 將collection轉化為dict
end_points = slim.utils.convert_collection_to_dict(end_points_collecion)
if num_classes is not None:
# 為dict添加節點
end_points['predictions'] = slim.softmax(net,scope='predictions')
return net, end_points
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)
#-------------------評測函數---------------------------------
# 測試152層深的ResNet的forward性能
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 = 32
height,width = 224,224
inputs = tf.random_uniform([batch_size,height,width,3])
with slim.arg_scope(resnet_arg_scope(is_training=False)):
# 1000分類
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')
# forward計算耗時相比VGGNet和Inception V3大概只增加了50%,是一個實用的卷積神經網絡。
和上節同理,不貼時耗。
