原文鏈接:https://www.zhihu.com/collection/172241377
感受野(receptive field)可能是卷積神經網絡(Convolutional Neural Network,CNNs)中最重要的概念之一,值得我們關注和學習。當前流行的物體識別方法的架構大都圍繞感受野的設計。但是,當前並沒有關於CNN感受野計算和可視化的完整指南。本教程擬填補空白,介紹CNN中特征圖的可視化方法,從而揭示感受野的原理以及任意CNN架構中感受野的計算。我們還提供了代碼實現證明計算的正確性,這樣大家可以從感受野的計算開始研究CNN,從而更加深刻的理解CNN的架構。
本文假設讀者已經熟悉CNN的思想,特別是卷積(convolutional)和池化(pooling)操作,當然你可以參考[1603.07285] A guide to convolution arithmetic for deep learning,回顧CNN的相關知識。如果你對CNNs已經有所了解,相信不超過半個小時就可以完成本文的閱讀。實際上,本文受上述論文的啟發,文中也采用了相似的表示符號。
The fixed-sized CNN feature map visualization
圖1 CNN特征圖可視化的兩種方式。
如圖1所示,我們采用卷積核C的核大小(kernel size)k=3*3,填充大小(padding size)p=1*1,步長(stride)s=2*2。(圖中上面一行)對5*5的輸入特征圖進行卷積生成3*3的綠色特征圖。(圖中下面一行)對上面綠色的特征圖采用相同的卷積操作生成2*2的橙色特征圖。(圖中左邊一列)按列可視化CNN特征圖,如果只看特征圖,我們無法得知特征的位置(即感受野的中心位置)和區域大小(即感受野的大小),而且無法深入了解CNN中的感受野信息。(圖中右邊一列)CNN特征圖的大小固定,其特征位置即感受野的中心位置。
感受野表示輸入空間中一個特定CNN特征的范圍區域(The receptive field is defined as the region in the input space that a particular CNN’s feature is looking at)。一個特征的感受野可以采用區域的中心位置和特征大小進行描述。圖1展示了一些感受野的例子,采用核大小(kernel size)k=3*3,填充大小(padding size)p=1*1,步長(stride)s=2*2的卷積核C對5*5大小的輸入圖進行卷積操作,將輸出3*3大小的特征圖(綠色圖)。對3*3大小的特征圖進行相同的卷積操作,將輸出2*2的特征圖(橙色)。輸出特征圖在每個維度上的大小可以采用下面的公式進行計算([1603.07285] A guide to convolution arithmetic for deep learning):

為了簡單,本文假設CNN的架構是對稱的,而且輸入圖像長寬比為1,因此所有維度上的變量值都相同。若CNN架構或者輸入圖像不是對稱的,你也可以分別計算每個維度上的特征圖大小。如圖1所示,左邊一列展示了一種CNN特征圖的常見可視化方式。這種可視化方式能夠獲取特征圖的個數,但無法計算特征的位置(感受野的中心位置)和區域大小(感受野尺寸)。圖1右邊一列展示了一種固定大小的CNN特征圖可視化方式,通過保持所有特征圖大小和輸入圖大小相同來解決上述問題,接下來每個特征位於其感受野的中心。由於特征圖中所有特征的感受野尺寸相同,我們就可以非常方便畫出特征對應的包圍盒(bounding box)來表示感受野的大小。因為特征圖大小和輸入圖像相同,所以我們無需將包圍盒映射到輸入層。

圖2 另外一種固定大小的CNN特征圖表示。采用相同的卷積核C對7*7大小的輸入圖進行卷積操作,這里在特征中心周圍畫出了感受野的包圍盒。為了表達更清楚,這里忽略了周圍的填充像素。固定尺寸的CNN特征圖可以采用3D(左圖)或2D(右圖)進行表示。
圖2展示了另外一個例子,采用相同的卷積核C對7*7大小的輸入圖進行卷積操作。這里給出了3D(左圖)和2D(右圖)表示下的固定尺寸CNN特征圖。注意:圖2中感受野尺寸逐漸擴大,第二個特征層的中心特征感受野很快就會覆蓋整個輸入圖。這一點對於CNN設計架構的性能提升非常重要。
感受野的計算 (Receptive Field Arithmetic)
除了每個維度上特征圖的個數,還需要計算每一層的感受野大小,因此我們需要了解每一層的額外信息,包括:當前感受野的尺寸r,相鄰特征之間的距離(或者jump)j,左上角(起始)特征的中心坐標start,其中特征的中心坐標定義為其感受野的中心坐標(如上述固定大小CNN特征圖所述)。假設卷積核大小k,填充大小p,步長大小s,則其輸出層的相關屬性計算如下:

- 公式一基於輸入特征個數和卷積相關屬性計算輸出特征的個數
- 公式二計算輸出特征圖的jump,等於輸入圖的jump與輸入特征個數(執行卷積操作時jump的個數,stride的大小)的乘積
- 公式三計算輸出特征圖的receptive field size,等於k個輸入特征覆蓋區域
加上邊界上輸入特征的感受野覆蓋的附加區域
。
- 公式四計算第一個輸出特征的感受野的中心位置,等於第一個輸入特征的中心位置,加上第一個輸入特征位置到第一個卷積核中心位置的距離
,再減去填充區域大小
。注意:這里都需要乘上輸入特征圖的jump,從而獲取實際距離或間隔。

圖3 對圖1中的例子執行感受野計算。第一行給出一些符號和等式;第二行和最后一行說明給定輸入層信息下輸出層感受野的計算過程。
CNN的第一層是輸入層,n = image size,r = 1,j = 1,start = 0.5。圖3采用的坐標系中輸入層的第一個特征中心位置在0.5。遞歸執行上述四個公式,就可以計算CNN中所有特征圖中的感受野信息。圖3給出這些公式計算的樣例。
這里給出一個python小程序,用於計算給定CNN架構下所有層的感受野信息。程序允許輸入任何特征圖的名稱和圖中特征的索引號,輸出相關感受野的尺寸和位置。圖4給出AlexNet下的例子。

圖4 AlexNet下感受野計算樣例
# [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))