Tensorflow學習:ResNet代碼(詳細剖析)

https://blog.csdn.net/superman_xxx/article/details/65452735

https://blog.csdn.net/qq_29893385/article/details/81207203

https://blog.csdn.net/zzc15806/article/details/83540661

https://blog.csdn.net/chaipp0607/article/details/75577305

https://github.com/chaipangpang/ResNet_cifar

https://blog.csdn.net/Eric2016_Lv/article/details/77528583參考

參考鏈接:

感謝此位博主的工作,本博主只做進一步的剖析,目的爲掌握和具備二次開發能力。 
http://blog.csdn.net/superman_xxx/article/details/65452735

先貼代碼:

先貼代碼:
# -*- coding: utf-8 -*-
"""
Created on Thu Aug 17 16:24:55 2017
Project: Residual Neural Network
E-mail: [email protected]
Reference: 《Tensorflow實戰》P143-P156
@author: DidiLv
"""

"""

Typical use:

   from tensorflow.contrib.slim.nets import resnet_v2

ResNet-101 for image classification into 1000 classes:

   # inputs has shape [batch, 224, 224, 3]
   with slim.arg_scope(resnet_v2.resnet_arg_scope(is_training)):
      net, end_points = resnet_v2.resnet_v2_101(inputs, 1000)

ResNet-101 for semantic segmentation into 21 classes:

   # inputs has shape [batch, 513, 513, 3]
   with slim.arg_scope(resnet_v2.resnet_arg_scope(is_training)):
      net, end_points = resnet_v2.resnet_v2_101(inputs,
                                                21,
                                                global_pool=False,
                                                output_stride=16)
"""
import collections # 原生的collections庫
import tensorflow as tf
slim = tf.contrib.slim # 使用方便的contrib.slim庫來輔助創建ResNet

# 這裏值得注意的是只有定義了類。其他的並沒有定義,從空格發現。這也就是書中所說的“只包含數據結構,不包含具體方法”
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。
  '''
  #圖片解釋:圖例1


########定義一個降採樣的方法########
def subsample(inputs, factor, scope=None): 
  """Subsamples the input along the spatial dimensions.
  Args:
    inputs: A `Tensor` of size [batch, height_in, width_in, channels].
    factor: The subsampling factor.(採樣因子或採樣率)
    scope: Optional variable_scope.

  Returns:
    output: 如果factor爲1,則不做修改直接返回inputs;如果不爲1,則使用
    slim.max_pool2d最大池化來實現,通過1*1的池化尺寸,stride作步長,實
    現降採樣。
  """
  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): 
  """
  Args:
    inputs: A 4-D tensor of size [batch, height_in, width_in, channels].
    num_outputs: An integer, the number of output filters.
    kernel_size: An int with the kernel_size of the filters.
    stride: An integer, the output stride.
    rate: An integer, rate for atrous convolution.
    scope: Scope.

  Returns:
    output: A 4-D tensor of size [batch, height_out, width_out, channels] with
      the convolution output.
  """
  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]])
    # 因爲已經進行了zero padding,所以只需再使用一個padding模式爲VALID的slim.conv2d創建這個卷積層
    # 詳細解釋請見圖二
    return slim.conv2d(inputs, num_outputs, kernel_size, stride=stride,
                       padding='VALID', scope=scope)


########定義堆疊Blocks的函數########
@slim.add_arg_scope
def stack_blocks_dense(net, blocks,
                       outputs_collections=None):
  """
  Args:
    net: A `Tensor` of size [batch, height, width, channels].輸入。
    blocks: 是之前定義的Block的class的列表。
    outputs_collections: 收集各個end_points的collections。

  Returns:
    net: Output tensor 

  """
  # 使用兩層循環,逐個Residual Unit地堆疊
  for block in blocks: # 先使用兩個tf.variable_scope將殘差學習單元命名爲block1/unit_1的形式
    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]):
          # 在第2層循環中,我們拿到每個block中每個Residual Unit的args並展開爲下面四個參數
          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) # 將輸出net添加到collections中

  return net # 當所有block中的所有Residual Unit都堆疊完成之後,再返回最後的net作爲stack_blocks_dense


# 創建ResNet通用的arg_scope,arg_scope用來定義某些函數的參數默認值
def resnet_arg_scope(is_training=True, # 訓練標記
                     weight_decay=0.0001, # 權重衰減速率
                     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': batch_norm_decay,
      'epsilon': batch_norm_epsilon,
      'scale': batch_norm_scale,
      'updates_collections': tf.GraphKeys.UPDATE_OPS,
  }

  with slim.arg_scope( # 通過slim.arg_scope將[slim.conv2d]的幾個默認參數設置好
      [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: # ResNet原論文是VALID模式,SAME模式可讓特徵對齊更簡單
        return arg_sc # 最後將基層嵌套的arg_scope作爲結果返回

# 定義核心的bottleneck殘差學習單元
@slim.add_arg_scope
def bottleneck(inputs, depth, depth_bottleneck, stride,
               outputs_collections=None, scope=None):
  """
  Args:
    inputs: A tensor of size [batch, height, width, channels].
    depth、depth_bottleneck:、stride三個參數是前面blocks類中的args
    rate: An integer, rate for atrous convolution.
    outputs_collections: 是收集end_points的collection
    scope: 是這個unit的名稱。
  """
  with tf.variable_scope(scope, 'bottleneck_v2', [inputs]) as sc: # slim.utils.last_dimension獲取輸入的最後一個維度,即輸出通道數。
    depth_in = slim.utils.last_dimension(inputs.get_shape(), min_rank=4) # 可以限定最少爲四個維度
    # 使用slim.batch_norm對輸入進行batch normalization,並使用relu函數進行預激活preactivate
    preact = slim.batch_norm(inputs, activation_fn=tf.nn.relu, scope='preact') 

    if depth == depth_in:
      shortcut = subsample(inputs, stride, 'shortcut')
      # 如果殘差單元的輸入通道數和輸出通道數一致,那麼按步長對inputs進行降採樣
    else:
      shortcut = slim.conv2d(preact, depth, [1, 1], stride=stride,
                             normalizer_fn=None, activation_fn=None,
                             scope='shortcut')
      # 如果不一樣就按步長和1*1的卷積改變其通道數,使得輸入、輸出通道數一致

    # 先是一個1*1尺寸,步長1,輸出通道數爲depth_bottleneck的卷積
    residual = slim.conv2d(preact, depth_bottleneck, [1, 1], stride=1,
                           scope='conv1')
    # 然後是3*3尺寸,步長爲stride,輸出通道數爲depth_bottleneck的卷積
    residual = conv2d_same(residual, depth_bottleneck, 3, stride,
                                        scope='conv2')
    # 最後是1*1卷積,步長1,輸出通道數depth的卷積,得到最終的residual。最後一層沒有正則項也沒有激活函數
    residual = slim.conv2d(residual, depth, [1, 1], stride=1,
                           normalizer_fn=None, activation_fn=None,
                           scope='conv3')

    output = shortcut + residual # 將降採樣的結果和residual相加

    return slim.utils.collect_named_outputs(outputs_collections, # 將output添加進collection並返回output作爲函數結果
                                            sc.name,
                                            output)


########定義生成resnet_v2的主函數########
def resnet_v2(inputs, # A tensor of size [batch, height_in, width_in, channels].輸入
              blocks, # 定義好的Block類的列表
              num_classes=None, # 最後輸出的類數
              global_pool=True, # 是否加上最後的一層全局平均池化
              include_root_block=True, # 是否加上ResNet網絡最前面通常使用的7*7卷積和最大池化
              reuse=None, # 是否重用
              scope=None): # 整個網絡的名稱
  # 在函數體先定義好variable_scope和end_points_collection
  with tf.variable_scope(scope, 'resnet_v2', [inputs], reuse=reuse) as sc:
    end_points_collection = sc.original_name_scope + '_end_points' # 定義end_points_collection
    with slim.arg_scope([slim.conv2d, bottleneck,
                         stack_blocks_dense],
                        outputs_collections=end_points_collection): # 將三個參數的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') # 創建resnet最前面的64輸出通道的步長爲2的7*7卷積
        net = slim.max_pool2d(net, [3, 3], stride=2, scope='pool1') # 然後接最大池化
        # 經歷過兩個步長爲2的層圖片縮爲1/4
      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) # tf.reduce_mean實現全局平均池化效率比avg_pool高
      if num_classes is not None:  # 是否有通道數
        net = slim.conv2d(net, num_classes, [1, 1], activation_fn=None, # 無激活函數和正則項
                          normalizer_fn=None, scope='logits') # 添加一個輸出通道num_classes的1*1的卷積
      end_points = slim.utils.convert_collection_to_dict(end_points_collection) # 將collection轉化爲python的dict
      if num_classes is not None:
        end_points['predictions'] = slim.softmax(net, scope='predictions') # 輸出網絡結果
      return net, end_points
#------------------------------ResNet的生成函數定義好了----------------------------------------

def resnet_v2_50(inputs, # 圖像尺寸縮小了32倍
                 num_classes=None,
                 global_pool=True,
                 reuse=None, # 是否重用
                 scope='resnet_v2_50'):
  blocks = [
      Block('block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),

      # Args::
      # '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,且中間那層步長3。這個殘差學習單元結構爲:
      # [(1*1/s1,64),(3*3/s2,64),(1*1/s1,256)]

      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)


def resnet_v2_101(inputs, # unit提升的主要場所是block3
                  num_classes=None,
                  global_pool=True,
                  reuse=None,
                  scope='resnet_v2_101'):
  """ResNet-101 model of [1]. See resnet_v2() for arg and return description."""
  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)


def resnet_v2_152(inputs, # unit提升的主要場所是block3
                  num_classes=None,
                  global_pool=True,
                  reuse=None,
                  scope='resnet_v2_152'):
  """ResNet-152 model of [1]. See resnet_v2() for arg and return description."""
  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)


def resnet_v2_200(inputs, # unit提升的主要場所是block2
                  num_classes=None,
                  global_pool=True,
                  reuse=None,
                  scope='resnet_v2_200'):
  """ResNet-200 model of [2]. See resnet_v2() for arg and return description."""
  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


#-------------------評測函數---------------------------------
# 測試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)): # 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") 

# forward計算耗時相比VGGNet和Inception V3大概只增加了50%,是一個實用的卷積神經網絡。

圖例解釋:
圖例1(Block): 


圖例二(padding方式介紹): 
 
由於在代碼中已經做了零填充,所以直接用VALID方式進行填充。

該博客主要以TensorFlow提供的ResNet代碼爲主,但是我並不想把它稱之爲代碼解析,因爲代碼和方法,實踐和理論總是缺一不可。 
github地址,其中:

resnet_model.py爲殘差網絡模型的實現,包括殘差模塊,正則化,批次歸一化,優化策略等等;

resnet_main.py爲主函數,主要定義了測試、訓練、總結、打印的代碼和一些參數。

cifar_input.py爲數據準備函數,主要把cifar提供的bin數據解碼爲圖片tensor,並組合batch

爲了保證行號的一致性,下面的內容如果涉及到行號的話,均以github上的爲準,同時爲了節省篇幅,下面如果出現代碼將去掉註釋,建議在閱讀本博客是同時打開github網址,因爲下面的內容並沒有多少代碼。

既然是在說殘差模型,那麼當然就要說resnet_model.py這個代碼,整個代碼就是在聲明一個類——ResNet:

第38行到55行:

class ResNet(object):

  def __init__(self, hps, images, labels, mode):
    self.hps = hps
    self._images = images
    self.labels = labels
    self.mode = mode

    self._extra_train_ops = []

上面是構造函數在初始化對象時的四個參數,實例化對象時也就完成初始化,參數賦值給類中的數據成員,其中self._images爲私有成員。此外又定義了一個新的私有數組成員:self._extra_train_ops用來執行滑動平均操作。

構造函數的參數有hps,images,labels,mode。

hps在resnet_main.py在初始化的:

  hps = resnet_model.HParams(batch_size=batch_size,
                             num_classes=num_classes,
                             min_lrn_rate=0.0001,
                             lrn_rate=0.1,
                             num_residual_units=5,
                             use_bottleneck=False,
                             weight_decay_rate=0.0002,
                             relu_leakiness=0.1,
                             optimizer='mom')

其中的HParams字典在resnet_mode.py的32行定義,變量的意義分別是:

HParams = namedtuple('HParams',
                     '一個batch內的圖片個數', 
                     '分類任務數目', 
                     '最小的學習率', 
                     '學習率', 
                     '一個殘差組內殘差單元數量', 
                     '是否使用bottleneck',  
                     'relu泄漏',
                     '優化策略')

images和labels是cifar_input返回回來的值(115行),注意這裏的值已經是batch了,畢竟image和label都加了複數。 
mode決定是訓練還是測試,它在resnet_main.py中定義(29行)並初始化(206行)。

除了__init__的構造函數外,類下還定義了12個函數,把殘差模型構建中用到功能模塊化了,12個函數貌似很多的樣子,但是都是一些很簡單的功能,甚至有一些只有一行代碼(比如可以看下65行),之所有單拉出來是因爲功能是獨立的,或者反覆出現,TensorFlow提供的代碼還是非常規範和正規的!

按照自上而下的順序依次是:

build_graph(self): 
構建TensorFlow的graph

_stride_arr(self, stride): 
定義卷積操作中的步長

_build_model(self): 
構建殘差模型

_build_train_op(self): 
構建訓練優化策略

_batch_norm(self, name, x): 
批次歸一化操作

_residual(self, x, in_filter, out_filter, stride,activate_before_residual=False): 
不帶bottleneck的殘差模塊,或者也可以叫做殘差單元,總之注意不是殘差組!

_bottleneck_residual(self, x, in_filter, out_filter, stride,activate_before_residual=False): 
帶bottleneck的殘差模塊

decay(self): 
L2正則化

_conv(self, name, x, filter_size, in_filters, out_filters, strides): 
卷積操作

_relu(self, x, leakiness=0.0): 
激活操作

_fully_connected(self, x, out_dim): 
全鏈接

_global_avg_pool(self, x, out_dim): 
全局池化

注意: 
1.在代碼裏這12個函數是並列的,但是講道理的話它們並不平級(有一些函數在調用另一些)。比如卷積,激活,步長設置之類肯定是被調用的。而有三個函數比較重要,分別是:build_graph(self):、_build_model(self):、_build_train_op(self):。第一個是由於TensorFlow就是在維護一張圖,所有的數據以tensor的形式在圖上流動;第二個決定了殘差模型;第三個決定了優化策略。

2.個人認爲_stride_arr(self, stride):函數不應該出現在該位置(65行),如果把它放後面,前三個函數就分別是構件圖,構建模型,構建優化策略。這樣邏輯上就很清晰。

3.這套代碼沒有常規的池化操作,一方面是因爲RenNet本身就用步長爲2的卷積取代池化,但是在進入殘差組之前還是應該有一個常規池化的,只是這個代碼沒有。

4.這個代碼有一個很不講理的地方,第一層卷積用了3*3的核,不是7*7,也不是3個3*3(73行)

5.這套代碼使用的是bin封裝的cifar數據,所以要想改成自己的數據集需要把input的部分換掉。

6.這套代碼沒有設終止條件,會一直訓練/測試,直到手動停止。

到這裏代碼的結構起碼說清楚了,帶着上面的注意事項,我們就可以看代碼。 
圖構建沒什麼好說的,我們直接進入_build_model(self)好了(69行): 
71-73行定義殘差網絡的第一個卷積層 
。 
75-82行使用哪種殘差單元(帶bottleneck還是不帶bottleneck),並分別對兩種情況定義了殘差組中的特徵通道數。

90-109行構建了三個殘差組,每個組內有4個單元,這個數量是由hps參數決定的。

111-124行是殘差組結束後模型剩餘的部分(池化+全連接+softmax+loss function+L2),這已經和殘差網絡的特性沒什麼關係了,每個卷積神經網絡差不多都是這樣子。

126行將損失函數計算出的cost加入summary。

所以殘差模型最關鍵的東西,最能表徵殘差特性的東西,都在90-109行,當然這十幾行裏是調用了其他函數的。這個本文的最後後再說,下面爲保證代碼部分的連貫性,先往下說_build_train_op(self)(128行):

130-131行獲取學習率並加入到summary。

133-134行根據cost與權係數計算梯度。

136-136行選擇使用隨機梯度下降還是帶動量梯度下降。

141-143行執行梯度下降優化。

145行將梯度下降優化操作與bn操作合併(帶op的變量是一種操作)。

146行得到最後的結果,在這裏定義了一個新的數組成員:self.train_op,而這個變量最終被用到了resnet_main.py中(113行):

while not mon_sess.should_stop():
      mon_sess.run(model.train_op)
1
2
如果沒有達到終止條件的話,代碼將一直執行優化操作,model是類實例化出來的一個對象,在resnet_main.py中的model和在resnet_model.py中的self是一個東西。

到這裏重要的代碼就都說完了,最後說回殘差網絡最核心的東西:兩種殘差單元。 
殘差網絡的結構非常簡單,就是不斷的通過一組一組的殘差組鏈接,這是一個Resnet50的結構圖,不同的網絡結構在不同的組之間會有不同數目的殘差模塊,如下圖: 

舉個例子,比如resnet50中,2-5組中分別有3,4,6,3個殘差模塊。

樸素殘差模塊(不帶bottleneck): 
 
左側爲正常了兩個卷積層,而右側在兩個卷積層前後做了直連,這個直連解釋殘差,左側的輸出爲H(x)=F(x),而加入直連後的H(x)=F(x)+x,一個很簡單的改進,但是取得了非常優異的效果。 
至於爲什麼直連要跨越兩個卷積層,而不是一個?這個是實驗驗證的結果,在一個卷積層上加直連性能並沒有太大提升。

bottleneck殘差模塊: 
bottleneck殘差模塊讓殘差網絡可以向更深的方向上走,原因就是因爲同一通道數的情況下,bottleneck殘差模塊要比樸素殘差模塊節省大量的參數,一個單元內的參數少了,對應的就可以做出更深的結構。 
 
上面這樣圖能夠說明二者的區別,左側的通道數是64(它常出現在50層內的殘差結構中),右側的通道數是256(常出現在50層以上的殘差結構中),從右面的圖可以看到,bottleneck殘差模塊將兩個3*3換成了1*1,3*3,1*1的形式,第一個1*1用來降通道,3*3用來在降通道的特徵上卷積,第二個1*1用於升通道。而參數的減少就是因爲在第一個1*1將通道數降了下來。我們可以舉一個例子驗證一下:

假設樸素殘差模塊與bottleneck殘差模塊通道數都是256,那麼:

樸素殘差模塊的參數個數: 
3*3*256*256+3*3*256*256 = 10616832 
bottleneck殘差模塊的參數個數: 
1*1*256*64+3*3*64*64+1*1*64*256 = 69632 
可以看到,參數的減少非常明顯。

再回到上面的圖: 


Resnet34餘Resnet50層每一組中的模塊個數並沒有變化,層數的上升是因爲以前兩個卷積層變成了3個,前者的參數爲3.6億,後者參數爲3.8億。這樣來看的話參數爲什麼反而多了?這是因爲組內的通道數發生了變化,前者各組通道數爲[64,128,256,512],而後者的各組通道數爲[256,512,1024,2048]。這也是殘差網絡在設計時的一個特點,使用bottleneck殘差模塊時,組內的通道數要明顯高於使用樸素殘差模塊。

TensorFlow提供的代碼也是這樣,可以看下77行:

if self.hps.use_bottleneck:
      res_func = self._bottleneck_residual
      filters = [16, 64, 128, 256]
    else:
      res_func = self._residual
      filters = [16, 16, 32, 64]

通過上面的理論說明,就可以再回頭看下代碼中的:_residual()函數和_bottleneck_residual()函數了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章