1. 程式人生 > >關於感受野 (Receptive field) 你該知道的事

關於感受野 (Receptive field) 你該知道的事

.org 幾何 mean 而是 con represent 對稱 http 完全

Receptive field 可中譯為“感受野”,是卷積神經網絡中非常重要的概念之一。

我個人最早看到這個詞的描述是在 2012 年 Krizhevsky 的 paper 中就有提到過,當時是各種不明白的,事實上各種網絡教學課程也都並沒有仔細的講清楚“感受野”是怎麽一回事,有什麽用等等。直到我某天看了 UiO 的博士生 Dang Ha The Hien寫了一篇非常流傳甚廣的博文:A guide to receptive field arithmetic for Convolutional Neural Networks,才大徹大悟,世界變得好了,人生都變得有意義了,正如博主自己談到的寫作動機:

This post fills in the gap by introducing a new way to visualize feature maps in a CNN that exposes the receptive field information, accompanied by a complete receptive field calculation that can be used for any CNN architecture.

此文算是上述博文的一個精要版筆記,再加上個人的理解與計算過程。

FYI:讀者已經熟悉 CNN 的基本概念,特別是卷積和池化操作。一個非常好的細致概述相關計算細節的 paper 是:A guide to convolution arithmetic for deep learning。


感受野可視化

我們知曉某一層的卷積核大小對應於在上一層輸出的“圖像”上的“視野”大小。比如,某層有 3x3 的卷積核,那就是一個 3x3 大小的滑動窗口在該層的輸入“圖像”上去掃描,我們就可以談相對於上一層,說該層下特征圖(feature map)當中任一特征點(feature)的“感受野”大小只有 3x3.(打引號說明術語引用不夠嚴謹)。

  • 先看個感受野的較嚴格定義:

The receptive field is defined as the region in the input space that a particular CNN’s feature

is looking at (i.e. be affected by).

一個特征點的感受野可以用其所在的中心點位置(center location)大小(size)來描述。然而,某卷積特征點所對應的感受野上並不是所有像素都是同等重要的,就好比人的眼睛所在的有限視野範圍內,總有要 focus 的焦點。對於感受野來說,距離中心點越近的像素肯定對未來輸出特征圖的貢獻就越大。換句話說,一個特征點在輸入圖像(Input) 上所關註的特定區域(也就是其對應的感受野)會在該區域的中心處聚焦,並以指數變化向周邊擴展(need more explanation

廢話不多說,我們直接先算起來。


首先假定我們所考慮的 CNN 架構對稱的,並且輸入圖像也是方形的。這樣的話,我們就忽略掉不同長寬所造成的維度不同。

Way1 對應為通常的一種理解感受野的方式。在下方左側的上圖中,是在 5x5 的圖像(藍色)上做一個 3x3 卷積核的卷積計算操作,步長為2,padding 為1,所以輸出為 3x3 的特征圖(綠色)。那麽該特征圖上的每個特征(1x1)對應的感受野,就是 3x3。在下方左側的下圖中,是在上述基礎上再加了一個完全一樣的卷積層。對於經過第二層卷積後其上的一個特征(如紅色圈)在上一層特征圖上“感受”到 3x3 大小,該 3x3 大小的每個特征再映射回到圖像上,就會發現由 7x7 個像素點與之關聯,有所貢獻。於是,就可以說第二層卷積後的特征其感受野大小是 7x7(需要自己畫個圖,好好數一數)。Way2 (下方右側的圖像)是另一種理解的方式,主要的區別僅僅是將兩層特征圖上的特征不進行“合成”,而是保留其在前一層因“步長”而產生的影響。

技術分享圖片

Way2 的理解方式其實更具有一般性,我們可以無需考慮輸入圖像的大小對感受野進行計算。如下圖:

技術分享圖片

雖然,圖上繪制了輸入 9x9 的圖像(藍色),但是它的大小情況是無關緊要的,因為我們現在只關註某“無限”大小圖像某一像素點為中心的一塊區域進行卷積操作。首先,經過一個 3x3 的卷積層(padding=1,stride=2)後,可以得到特征輸出(深綠色)部分。其中深綠色的特征分別表示卷積核掃過輸入圖像時,卷積核中心點所在的相對位置。此時,每個深綠色特征的感受野是 3x3 (淺綠)。這很好理解,每一個綠色特征值的貢獻來源是其周圍一個 3x3 面積。再疊加一個 3x3 的卷積層(padding=1,stride=2)後,輸出得到 3x3 的特征輸出(橙色)。此時的中心點的感受野所對應的是黃色區域 7x7,代表的是輸入圖像在中心點橙色特征所做的貢獻。

這就是為何在 《VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITION》 文章中提到:

It is easy to see that a stack of two 3 × 3 conv. layers (without spatial pooling in between) has an effective receptive field of 5 × 5; three such layers have a 7 × 7 effective receptive field.

也就是說兩層 3x3 的卷積層直接堆疊後(無池化)可以算的有感受野是 5x5,三層堆疊後的感受野就是 7x7。


感受野計算

直觀的感受了感受野之後,究竟該如何定量計算嗯?只要依據 Way2 圖像的理解,我們對每一層的特征“順藤摸瓜”即可。

我們已經發覺到,某一層特征上的感受野大小依賴的要素有:每一層的卷積核大小 k,padding 大小 p,stride s。在推導某層的感受野時,還需要考慮到該層之前各層上特征的的感受野大小 r,以及各層相鄰特征之間的距離 j(jump)。

所以對於某一卷積層(卷積核大小 k,padding 大小 p,stride s)上某特征的感受野大小公式為:

技術分享圖片

  • 第一行計算的是,相鄰特征之間的距離(jump)。各層裏的特征之間的距離顯然是嚴重依賴於 stride ,並且逐層累積。值得註意的是,輸入圖像的作為起始像素特征,它的特征距離(jump) 為1。
  • 第二行計算的就是某層的特征的感受大小它依賴於上一層的特征的感受野大小 和特征之間的距離 ,以及該層的卷積核大小 k。輸入圖像的每個像素作為特征的感受野就是其自身,為1。
  • 第三行公式計算的是特征感受野的幾何半徑。對於處於特征圖邊緣處的特征來說,這類特征的感受野並不會完整的對應到原輸入圖像上的區域,都會小一些。初始特征的感受野幾何半徑為 0.5。
下面,我們繼續拿可視化時用的例子,看看具體是怎麽計算和對應的:

技術分享圖片

上圖中除了公式和說明部分外,有兩行分別代表的是第一層卷積和第二層卷積。在每行中,應從左往右觀察卷積核計算和操作。

第一層比較簡單,最後輸出 3x3 綠色的特征圖,每個特征有陰影框大小來表示每個特征對應的感受野大小 3x3。其中 技術分享圖片 表示的 0.5 幾何半徑,我已經用紅色標識出來,對應於陰影面積覆蓋到的綠色面積的幾何半徑。

第二層,由於有一個單位的 padding,所以 3x3 卷積核是按照藍色箭頭標記作為的起始方向開始,在所有的綠色位置上挪動的。最後算得特征的感受野大小為 7x7,亦對應於陰影框和陰影區域部分。其中 技術分享圖片是 0.5 也已經用紅色標記了出來。

技術分享圖片


Python Script

這個代碼其實很好寫,我就直接挪用 Dang Ha The Hien 的 python 腳本了:

# [filter size, stride, padding]
#Assume the two dimensions are the same
#Each kernel requires the following parameters:
# - k_i: kernel size
# - s_i: stride
# - p_i: padding (if padding is uneven, right padding will higher than left padding; "SAME" option in tensorflow)
# 
#Each layer i requires the following parameters to be fully represented: 
# - n_i: number of feature (data layer has n_1 = imagesize )
# - j_i: distance (projected to image pixel distance) between center of two adjacent features
# - r_i: receptive field of a feature in layer i
# - start_i: position of the first feature‘s receptive field in layer i (idx start from 0, negative means the center fall into padding)

import math
convnet =   [[11,4,0],[3,2,0],[5,1,2],[3,2,0],[3,1,1],[3,1,1],[3,1,1],[3,2,0],[6,1,0], [1, 1, 0]]
layer_names = [conv1,pool1,conv2,pool2,conv3,conv4,conv5,pool5,fc6-conv, fc7-conv]
imsize = 227

def outFromIn(conv, layerIn):
  n_in = layerIn[0]
  j_in = layerIn[1]
  r_in = layerIn[2]
  start_in = layerIn[3]
  k = conv[0]
  s = conv[1]
  p = conv[2]
  
  n_out = math.floor((n_in - k + 2*p)/s) + 1
  actualP = (n_out-1)*s - n_in + k 
  pR = math.ceil(actualP/2)
  pL = math.floor(actualP/2)
  
  j_out = j_in * s
  r_out = r_in + (k - 1)*j_in
  start_out = start_in + ((k-1)/2 - pL)*j_in
  return n_out, j_out, r_out, start_out
  
def printLayer(layer, layer_name):
  print(layer_name + ":")
  print("\t n features: %s \n \t jump: %s \n \t receptive size: %s \t start: %s " % (layer[0], layer[1], layer[2], layer[3]))
 
layerInfos = []
if __name__ == __main__:
#first layer is the data layer (image) with n_0 = image size; j_0 = 1; r_0 = 1; and start_0 = 0.5
  print ("-------Net summary------")
  currentLayer = [imsize, 1, 1, 0.5]
  printLayer(currentLayer, "input image")
  for i in range(len(convnet)):
    currentLayer = outFromIn(convnet[i], currentLayer)
    layerInfos.append(currentLayer)
    printLayer(currentLayer, layer_names[i])
  print ("------------------------")
  layer_name = raw_input ("Layer name where the feature in: ")
  layer_idx = layer_names.index(layer_name)
  idx_x = int(raw_input ("index of the feature in x dimension (from 0)"))
  idx_y = int(raw_input ("index of the feature in y dimension (from 0)"))
  
  n = layerInfos[layer_idx][0]
  j = layerInfos[layer_idx][1]
  r = layerInfos[layer_idx][2]
  start = layerInfos[layer_idx][3]
  assert(idx_x < n)
  assert(idx_y < n)
  
  print ("receptive field: (%s, %s)" % (r, r))
  print ("center: (%s, %s)" % (start+idx_x*j, start+idx_y*j))

在 AlexNet 網絡上的效果如下:

技術分享圖片

關於感受野 (Receptive field) 你該知道的事