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 網絡上的效果如下: