1. 程式人生 > 其它 >ResNet原理及其在TF-Slim中的實現

ResNet原理及其在TF-Slim中的實現

摘要

微軟的深度殘差網路ResNet源於2016年CVPR最佳論文---影象識別中的深度殘差學習(Deep Residual Learning for Image Recognition)(https://www.leiphone.com/news/201606/BhcC5LV32tdot6DD.html), 論文來源(https://link.jianshu.com/?t=https://arxiv.org/pdf/1512.03385v1.pdf),翻譯地址(https://tower.im/users/sign_in)

這個152層ResNet架構深,除了在層數上面創紀錄,ResNet 的錯誤率也低得驚人,達到了3.6%,人類都大約在5%~10%的水平。這是目前為止最好的深度學習框架。可以看作人工神經網路領域的又一里程碑。

2016年8月31日,Google團隊宣佈針對TensorFlow開源了最新發布的TF-slim資料庫,它是一個可以定義、訓練和評估模型的輕量級的軟體包,也能對影象分類領域中幾個主要有競爭力的網路進行檢驗和定義模型。這其中,就包括了ResNet網路結構。本文將結合TF-slim庫中的ResNet模型的程式碼,介紹一下ResNet網路的結構和原理。

ResNet的原理

論文中提到,近幾年的研究發現網路的深度是使網路效能更優化的一個關鍵因素,但是隨著網路深度的加深,梯度消失&爆炸問題十分明顯,網路甚至出現了退化。在論文中通過一個20層和一個56層的普通網路進行了對比,發現56層網路的效能遠低於20層網路,如圖1所示。

圖1

而在ResNet的這篇論文中,通過引入一個深度殘差學習框架,解決了這個退化問題。它不期望每一層能直接吻合一個對映,而是明確的讓這些層去吻合殘差對映。形式上看,就是用H(X)來表示最優解對映,但我們讓堆疊的非線性層去擬合另一個對映F(X):=H(X) - X, 此時原最優解對映H(X)就可以改寫成F(X)+X,我們假設殘差對映跟原對映相比更容易被優化。極端情況下,如果一個對映是可優化的,那也會很容易將殘差推至0,把殘差推至0和把此對映逼近另一個非線性層相比要容易的多。

F(X)+X的公式可以通過在前饋網路中做一個“快捷連線”來實現(如圖2) ,快捷連線跳過一個或多個層。在我們的用例中,快捷連線簡單的執行自身對映,它們的輸出被新增到疊加層的輸出中。自身快捷連線既不會新增額外的引數也不會增加計算複雜度。整個網路依然可以用SGD+反向傳播來做端到端的訓練。

圖2.殘差網路:一個結構塊

它有二層,如下表達式,其中σ代表非線性函式ReLU

然後通過一個shortcut,和第2個ReLU,獲得輸出y

而在論文的後續,又提出來深度瓶頸結構,如圖3右側.在文中是這樣描述這個結構的:接下來我們描述我們為ImageNet準備的更深的網路。因為太過漫長的訓練時間我們負擔不起,所以修改了單元塊,改為一種瓶頸設計。對於每個殘差函式F,我們使用3層來描述,而不是2層。這三層分別是1×1、3×3,和1×1的卷積層,其中1×1層負責先減少後增加(恢復)尺寸的,使3×3層具有較小的輸入/輸出尺寸瓶頸。

圖3.普通shortcut和深度瓶頸結構對比

這個深度瓶頸結構在TF-Slim庫中的程式碼實現如下所示:

def bottleneck(inputs, depth, depth_bottleneck, stride, rate=1,
               outputs_collections=None, scope=None):

  with tf.variable_scope(scope, 'bottleneck_v1', [inputs]) as sc:
    depth_in = slim.utils.last_dimension(inputs.get_shape(), min_rank=4)    if depth == depth_in:
      shortcut = resnet_utils.subsample(inputs, stride, 'shortcut')    else:
      shortcut = slim.conv2d(inputs, depth, [1, 1], stride=stride,
                             activation_fn=None, scope='shortcut')

    residual = slim.conv2d(inputs, depth_bottleneck, [1, 1], stride=1,
                           scope='conv1')
    residual = resnet_utils.conv2d_same(residual, depth_bottleneck, 3, stride,
                                        rate=rate, scope='conv2')
    residual = slim.conv2d(residual, depth, [1, 1], stride=1,
                           activation_fn=None, scope='conv3')

    output = tf.nn.relu(shortcut + residual)    return slim.utils.collect_named_outputs(outputs_collections,
                                            sc.original_name_scope,
                                 output)

需要注意的是,在論文中提到的當輸入輸出尺寸發生增加時(圖4中的虛線的快捷連線),會考慮兩個策略:(a)快捷連線仍然使用自身對映,對於維度的增加用零來填補空缺。此策略不會引入額外的引數;(b)投影捷徑(公式2)被用來匹配尺寸(靠1×1的卷積完成)。對於這兩種選項,當快捷連線在兩個不同大小的特徵圖譜上出現時,用stride=2來處理。而在TF-Slim的程式碼實現中我們可以看到採用了第二種解決方式,即通過通過1X1的卷積核卷積來達成尺寸匹配。(雖然論文中說這樣提高不多但需要更多引數所以最後沒有使用。)

同時,在程式碼中對於下采樣操作(subsample)是通過1x1的池化來完成的。

ResNet的結構

所以我們可以根據一個普通的神經網路來構造一個ResNet,如圖4所示,論文中選擇的基礎網路是VGG-Net。

圖4.普通網路結構和ResNet網路結構對比

而它的具體網路結構如圖5的表中所示。

圖5.ResNet網路結構

在TF-Slim中的程式碼實現如下(以ResNet-50為例):

def resnet_v1_50(inputs,
                 num_classes=None,
                 is_training=True,
                 global_pool=True,
                 output_stride=None,
                 reuse=None,
                 scope='resnet_v1_50'):
  """ResNet-50 model of [1]. See resnet_v1() for arg and return description."""
  blocks = [
      resnet_utils.Block(          'block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),
      resnet_utils.Block(          'block2', bottleneck, [(512, 128, 1)] * 3 + [(512, 128, 2)]),
      resnet_utils.Block(          'block3', bottleneck, [(1024, 256, 1)] * 5 + [(1024, 256, 2)]),
      resnet_utils.Block(          'block4', bottleneck, [(2048, 512, 1)] * 3)
  ]  

return resnet_v1(inputs, blocks, num_classes, is_training,
       global_pool=global_pool, output_stride=output_stride,
    include_root_block=True, reuse=reuse, scope=scope)

在這段程式碼中,其實只是聲明瞭一個通過Block組合成的List,Block的宣告如下,其中的關鍵是collections.namedtuple這個函式,它把前面元組的值和後面的命名對應了起來。

class Block(collections.namedtuple('Block', ['scope', 'unit_fn', 'args'])):  
"""
A named tuple describing a ResNet block.  
Its parts are:    
scope: The scope of the `Block`.    
unit_fn: The ResNet unit function which takes as input a `Tensor` and      
returns another `Tensor` with the output of the ResNet unit.    
args: A list of length equal to the number of units in the `Block`. The list      
contains one (depth, depth_bottleneck, stride) tuple for each unit in the     
 block to serve as argument to unit_fn.

 """

而將個元素為block的 LIst轉換為一個網路的函式,則是resnet_v1,這個函式是ResNet的核心,而不同層數的ResNet只需要改變上述函式blocks中block的個數就可以了。

class Block(collections.namedtuple('Block', ['scope', 'unit_fn', 'args'])):   
"""
A named tuple describing a ResNet block.    
Its parts are:     
scope: The scope of the `Block`.    
 unit_fn: The ResNet unit function which takes as input a `Tensor` and       
returns another `Tensor` with the output of the ResNet unit.     
args: A list of length equal to the number of units in the `Block`. The list       
contains one (depth, depth_bottleneck, stride) tuple for each unit in the       
block to serve as argument to unit_fn.
  """
def resnet_v1(inputs,               
blocks,               
num_classes=None,               
is_training=True,               
global_pool=True,               
output_stride=None,               
include_root_block=True,               
reuse=None,               
scope=None):    with tf.variable_scope(scope, 'resnet_v1', [inputs], reuse=reuse) as sc:     end_points_collection = sc.name + '_end_points'     
with slim.arg_scope([slim.conv2d, bottleneck,                          
resnet_utils.stack_blocks_dense],                         
outputs_collections=end_points_collection):      
with slim.arg_scope([slim.batch_norm], is_training=is_training):         
net = inputs        
if include_root_block:          
if output_stride is not None:            
if output_stride % 4 != 0:              
raise ValueError('The output_stride needs to be a multiple of 4.')             
output_stride /= 4          
net = resnet_utils.conv2d_same(net, 64, 7, stride=2, scope='conv1')           
net = slim.max_pool2d(net, [3, 3], stride=2, scope='pool1')         
net = resnet_utils.stack_blocks_dense(net, blocks, output_stride)        
if global_pool:          
# Global average pooling.          
 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')        
# Convert end_points_collection into a dictionary of end_points.         
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

在這個函式中,將blocks轉換為net的語句是

net = resnet_utils.stack_blocks_dense(net, blocks, output_stride)

這個函式的具體實現如下,它通過一個迴圈將list中的每個block讀取出來,然後將block中相應的引數代入到前文提到的bottleneck這個函式中,這樣就生成了相應的ResNet網路結構。

def stack_blocks_dense(net,                        
blocks,                        
output_stride=None,                        
outputs_collections=None):    
# The current_stride variable keeps track of the effective stride of the   
# activations. This allows us to invoke atrous convolution whenever applying   
# the next residual unit would result in the activations having stride larger   
# than the target output_stride.   current_stride = 1    
# The atrous convolution rate parameter.   rate = 1    for block in blocks:    
with variable_scope.variable_scope(block.scope, 'block', [net]) as sc:      
for i, unit in enumerate(block.args):        
if output_stride is not None and current_stride > output_stride:          
raise ValueError('The target output_stride cannot be reached.')        
with variable_scope.variable_scope('unit_%d' % (i + 1), values=[net]):           
unit_depth, unit_depth_bottleneck, unit_stride = unit          
# If we have reached the target output_stride, then we need to employ          
 # atrous convolution with stride=1 and multiply the atrous rate by the           
# current unit's stride for use in subsequent layers.           
if output_stride is not None and current_stride == output_stride:             
net = block.unit_fn(                 
net,                 
depth=unit_depth,                 
depth_bottleneck=unit_depth_bottleneck,                 
stride=1,                
 rate=rate)             
rate *= unit_stride         
else:             
net = block.unit_fn(                 
net,                 
depth=unit_depth,                 
depth_bottleneck=unit_depth_bottleneck,                 
stride=unit_stride,                 
rate=1)             
current_stride *= unit_stride       
net = utils.collect_named_outputs(outputs_collections, sc.name, net)  
if output_stride is not None and current_stride != output_stride:    
raise ValueError('The target output_stridecannot be reached.')  
return net

在這裡,程式碼中提到了 atrous convolution這個結構,簡單來說,它是如圖6(b)所示的一個結構,可以起到在使用了步長為1的池化層後扔使得原結構保持相同的感受野。

圖6.atrous convolution

參考文獻

[1]Deep Residual Learning for Image Recognition [2]http://blog.csdn.net/tiandijun/article/details/52526317 [3]http://blog.csdn.net/mao_feng/article/details/52734438 [4]http://blog.csdn.net/helei001/article/details/52692128 [5]http://blog.csdn.net/u012759136/article/details/52434826#t9