本文爲《深度學習入門 基於Python的理論與實現》的部分讀書筆記,也參考吳恩達深度學習視頻
代碼以及圖片均參考此書
目錄
卷積神經網絡(CNN: Convolutional Neural Network)整體結構
CNN 的層的連接順序是“Convolution - ReLU - (Pooling)”(Pooling 層有時會被省略)。還需要注意的是,在上的CNN中,靠近輸出的層中使用了之前的“Affine - ReLU”組合。此外,最後的輸出層中使用了之前的“Affine - Softmax”組合。這些都是一般的CNN中比較常見的結構。
卷積層負責對圖像進行特徵提取,池化層負責降採樣:保留顯著特徵、降低特徵維度的同時增大感受野。最後的全連接層負責對特徵圖進行降維後進行激活分類。
- 最後的全連接層參數多,易造成過擬合。因此在很多場景中,可以使用全局平均池化層(Global Average Pooling, GAP)來取代最後的全連接層進行降維。因爲GAP是利用池化實現降維,因此減少了參數量,防止過擬合;同時可以實現任意圖像尺度的輸入
卷積層
- 將卷積層的輸入輸出數據稱爲特徵圖(feature map)。其中,卷積層的輸入數據稱爲輸入特徵圖(input feature map),輸出數據稱爲輸出特徵圖(output feature map)
全連接層存在的問題
- 數據的形狀被“忽視”了。比如,輸入數據是圖像時,圖像通常是高、長、通道方向上的3維形狀。但是,向全連接層輸入時,需要將3維數據拉平爲1維數據。圖像是3維形狀,這個形狀中應該含有重要的空間信息。比如,空間上鄰近的像素爲相似的值、RBG的各個通道之間分別有密切的關聯性、相距較遠的像素之間沒有什麼關聯等,3維形狀中可能隱藏有值得提取的本質模式。但是,因爲全連接層會忽視形狀,將全部的輸入數據作爲相同的神經元(同一維度的神經元)處理,所以無法利用與形狀相關的信息。
- 在輸入輸出的數據形狀相同的情況下,全連接層所需要訓練的參數遠遠多於卷積層。例如,輸入爲, 輸出爲,如果使用全連接層,則需要個參數,而如果使用卷積層(6個的濾波器)則只需要150個參數,加上偏置也只需要156個參數。
總結:
- 相比於全連接層,卷積層可以用較少的參數提取有效特徵
Parameter sharing: A feature detector (such as a vertical edge detector) that’s useful in one part of the image is probably useful in another part of the image.
卷積運算
乘積累加運算
對於輸入數據,卷積運算以一定間隔滑動濾波器的窗口並應用。如下圖所示,將各個位置上濾波器的元素和輸入的對應元素相乘,然後再求和,將這個結果保存到輸出的對應位置。把這個過程在所有位置都進行一遍,就可以得到卷積運算的輸出。
偏置
填充(padding)
- 使用填充主要是爲了調整輸出的大小。比如,對大小爲(4, 4) 的輸入數據應用(3, 3) 的濾波器時,輸出大小變爲(2, 2),相當於輸出大小比輸入大小縮小了2 個元素。這在反覆進行多次卷積運算的深度網絡會成爲問題。爲什麼呢?因爲如果每次進行卷積運算都會縮小空間,那麼在某個時刻輸出大小就有可能變爲1,導致無法再應用卷積運算。爲了避免出現這樣的情況,就要使用填充。在剛纔的例子中,將填充的幅度設爲1,那麼相對於輸入大小(4, 4),輸出大小也保持爲原來的(4, 4)。因此,卷積運算就可以在保持空間大小不變的情況下將數據傳給下一層。
步幅(stride)
- 應用濾波器的位置間隔稱爲步幅
小結:卷積層的輸出特徵圖的大小
假設輸入大小爲,濾波器大小爲,輸出大小爲,填充爲,步幅爲。此時,輸出大小爲:
設定的卷積核參數必須使上面兩式都能除盡,當輸出大小無法除盡時(結果是小數時),需要採取報錯等對策。順便說一下,根據深度學習的框架的不同,當值無法除盡時,有時會向最接近的整數四捨五入,不進行報錯而繼續運行。
3維數據的卷積運算
- 圖像是3維數據,除了高、長方向之外,還需要處理通道方向。通道方向上有多個特徵圖時,按通道進行輸入數據和濾波器的卷積運算,並將結果相加,從而得到輸出。
在3維數據的卷積運算中,輸入數據和濾波器的通道數要設爲相同的值。每個通道的濾波器大小要全部相同。
結合方塊思考卷積運算
在這個例子中,數據輸出是1張特徵圖,換句話說,就是通道數爲1的特徵圖。如果要在通道方向上也擁有多個卷積運算的輸出,就需要用到多個濾波器(權重)。
因此,濾波器的形狀可以寫爲。
如果再加上偏置運算,則結果如下圖所示:
卷積運算的批處理
1 x 1卷積層
參考:《DIVE INTO DEEP LEARNING》,吳恩達深度學習視頻
與全連接層的相似性
卷積層即爲卷積窗口形狀爲 的多通道卷積層。因爲使用了最小窗口,卷積失去了卷積層可以識別高和寬維度上相鄰元素構成的模式的功能。實際上, 卷積的主要計算髮生在通道維上。
如上圖所示,輸出中的每個元素來自輸入中在高和寬上相同位置的元素在不同通道之間的按權重累加,這就類似於全連接層。
假設我們將通道維當作特徵維,將高和寬維度上的元素當成數據樣本,那麼 卷積層的作用與全連接層等價,而且還使空間信息自然地傳遞到後面的層。即 卷積層中可以把通道當作特徵,高和寬上的每個元素相當於樣本。將輸出的通道數設置爲類別數之後,再接上後面要講的全局平均池化層,就可以做到用卷積層代替全連接層了!
可以這麼理解:把每個通道上的二維數據都看作是全連接層上的一個神經元,那麼上圖就可以看作是一個3輸入2輸出的全連接層,個卷積核就是全連接層的權重
減小通道數(bottleneck layer)
另外, 卷積層還可以達到減小通道數的作用。雖然其他大小的卷積核在配置適當的padding和srtide之後也可以在保持寬高不變的情況下減小通道數,但是 卷積層所需的乘法運算次數要少一個數量級。
如上圖所示,用卷積核如果要保持寬高不變,則需要120M次乘法運算
中間的那一層也常被稱爲 bottleneck layer(瓶頸層),由上圖所示,使用卷積層明顯的降低了計算成本
空洞卷積(Dilated Convolution)
參考:https://www.zhihu.com/question/54149221
空洞卷積最初是爲解決圖像分割而提出的。常見的圖像分割算法通常使用池化層來增大感受野,同時也縮小了特徵圖尺寸,然後再利用上採樣還原圖像尺寸。特徵圖縮小再放大的過程造成了精度損失。而空洞卷積則可以在增大感受野的同時保持特徵圖的尺寸不變,解決了這個問題。
空洞卷積就是卷積核中間帶有一些洞,跳過一些元素進行卷積
標準的卷積過程:
空洞數爲2的空洞卷積:
可以看出,空洞卷積在不增加參數量的前提下,增大了感受野
缺陷:
- 網格效應(Gridding Effect):由於空洞卷積是一種稀疏的採樣方式,當多個空洞卷積疊加時,有些像素根本沒有被利用,會損失信息的連續性與相關性
- 遠距離的信息沒有相關性
- 大的空洞數(dilation rate)對於大物體分割與檢測有利,但是對於小物體則有弊無利,如何處理好多尺度問題的檢測,是空洞卷積設計的重點
Hybrid Dilated Convolution (HDC)
參考:https://www.zhihu.com/question/54149221
爲了彌補空洞卷積的缺陷,HDC被設計了出來
HDC的設計準則:
- 疊加捲積的 dilation rate 不能有大於1的公約數
- 將 dilation rate 設計成鋸齒狀結構,例如 [1, 2, 5, 1, 2, 5] 的循環結構
- 滿足下列式子:
其中是層的 dilation rate ,是在 層的最大dilation rate,那麼假設總共有層的話,默認 。
一個簡單的例子: dilation rate [1, 2, 5] with 3 x 3 kernel (可行的方案)
池化層
Max池化
池化是縮小高、長方向上的空間的運算。
一般來說,池化的窗口大小會和步幅設定成相同的值。
全局平均池化層(Global Average Pooling, GAP)
參考:https://www.jianshu.com/p/510072fc9c62
在常見的卷積神經網絡中,全連接層之前的卷積層負責對圖像進行特徵提取,在獲取特徵後,傳統的方法是接上全連接層之後再進行激活分類,而GAP的思路是使用GAP來替代該全連接層(即使用池化層的方式來降維)(在NiN網絡中,它的前面還接了一個NiN塊,其中使用了卷積層),更重要的一點是保留了前面各個卷積層和池化層提取到的空間信息\語義信息,所以在實際應用中效果提升也較爲明顯。另外,GAP去除了對輸入大小的限制,而且在卷積可視化Grad-CAM中也有重要的應用.
GAP直接從 feature map 的通道信息下手,比如我們現在的分類有N種,那麼最後一層的卷積輸出的 feature map 就只有N個通道,然後對這個 feature map 進行全局池化操作,獲得長度爲N的向量,這就相當於直接賦予了每個通道類別的意義。
GAP優點:
- 由於有GAP,特徵圖的各個通道可以更直觀的被解讀爲圖片屬於每個類別的概率
- 利用池化實現了降維,減少了參數量,防止過擬合
- 保留了前面各個卷積層和池化層提取到的空間信息\語義信息,更具有魯棒性
- 可以實現任意圖像尺度的輸入
池化層的特徵
- 沒有要學習的參數
- 通道數不發生變化
- 對微小的位置變化具有魯棒性(健壯)。輸入數據發生微小偏差時,池化仍會返回相同的結果。因此,池化對輸入數據的微小偏差具有魯棒性。
因此,池化層可以降低特徵圖的參數量,提升計算速度,增加感受野,是一種降採樣的操作。可是模型更關注全局特徵而非局部出現的位置,可提升容錯能力,一定程度上防止過擬合
卷積層和池化層的實現
基於im2col的展開
im2col是一個函數,將輸入數據展開以適合濾波器(權重)。如下圖所示,對3維的輸入數據應用im2col後,數據轉換爲2維矩陣(正確地講,是把包含批數量的4維數據轉換成了2維數據)
im2col對於輸入數據,將應用濾波器的區域(3 維方塊)橫向展開爲1 列。im2col會在所有應用濾波器的地方進行這個展開處理。
在上圖中,爲了便於觀察,將步幅設置得很大,以使濾波器的應用區域不重疊。而在實際的卷積運算中,濾波器的應用區域幾乎都是重疊的。在濾波器的應用區域重疊的情況下,使用im2col展開後,展開後的元素個數會多於原方塊的元素個數。因此,使用im2col的實現存在比普通的實現消耗更多內存的缺點。但是,彙總成一個大的矩陣進行計算,對計算機的計算頗有益處。
-
下面寫一下變換後的各個矩陣的形狀以便理解:
可以看出,在輸出數據(2維)中,矩陣的一列即爲輸入特徵圖經過一個濾波器之後的結果 -
im2col的代碼實現:
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
N, C, H, W = input_data.shape
out_h = (H + 2 * pad - filter_h) // stride + 1 # 向下取整
out_w = (W + 2 * pad - filter_w) // stride + 1
img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
col = np.zeros((N, C, out_h, out_w, filter_h, filter_w))
# x_min 和 y_min 用於界定一個濾波器作用的方塊區域
for y in range(out_h):
y_min = y * stride
for x in range(out_w):
x_min = x * stride
col[:, :, y, x, :, :] = img[:, :, y_min:y_min+filter_h, x_min:x_min+filter_w]
col = col.transpose(0, 2, 3, 1, 4, 5).reshape(N*out_h*out_w, -1)
return col
當然,在反向傳播的時候還需要im2col函數的逆處理col2im函數。由於在卷積運算的過程中,濾波器的作用區域可能是重合的,因此img中的一個元素可能會多次出現在col中,根據鏈式法則,img中元素的偏導即爲col中所有該元素所在位置的偏導之和。
- col2im的代碼實現:
def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
N, C, H, W = input_shape # padding之前的圖像大小
out_h = (H + 2 * pad - filter_h) // stride + 1
out_w = (W + 2 * pad - filter_w) // stride + 1
col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 1, 2, 4, 5)
img = np.zeros((N, C, H + 2 * pad, W + 2 * pad))
for y in range(out_h):
y_min = y * stride
for x in range(out_w):
x_min = x * stride
# 要注意這裏是 += 而非 = ,原因就是上面的那段話
img[:, :, y_min:y_min+filter_h, x_min:x_min+filter_w] += col[:, :, y, x, :, :]
return img[:, :, pad:H+pad, pad:W+pad]
卷積層的實現
正向傳播
利用前面講的im2col進行快速矩陣運算
反向傳播
反向傳播類似於全連接層,唯一的區別就是在求得之後還要通過col2im函數變換形狀
簡單推導一下反向傳播的計算(與之前全連接層的推導是一模一樣的,但還是寫一下)
爲了推理上方便書寫,先引入克羅內克符號:
下面正式進行推導:
代碼實現
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
self.x = None
self.col = None
self.col_W = None
self.db = None
self.dW = None
def forward(self, x):
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = (H + 2 * self.pad - FH) // self.stride + 1
out_w = (W + 2 * self.pad - FW) // self.stride + 1
col = im2col(x, FH, FW, self.stride, self.pad)
col_W = self.W.reshape(FN, -1).T
out = (np.dot(col, col_W) + self.b).reshape(N, out_h, out_w, FN).transpose(0, 3, 1, 2)
self.x = x
self.col = col
self.col_W = col_W
return out
def backward(self, dout):
FN, C, FH, FW = self.W.shape
dout = dout.transpose(0, 2, 3, 1).reshape(-1, FN)
self.db = dout.sum(axis=0)
self.dW = np.dot(self.col.T, dout)
self.dW = self.dW.T.reshape(FN, C, FH, FW)
dcol = np.dot(dout, self.col_W.T)
dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)
return dx
Max池化層的實現
正向傳播
池化層的實現和卷積層相同,都使用im2col展開輸入數據。不過,池化的情況下,在通道方向上是獨立的,這一點和卷積層不同。池化的應用區域應該按通道單獨展開。
也就是說,的輸入數據在im2col之後形狀變爲,因此還要多加一步,將其reshape爲
像這樣展開之後,只需對展開的矩陣求各行的最大值,並轉換爲合適的形狀即可。轉換爲最大值之後的矩陣形狀爲,最後將其reshape爲即可
反向傳播
反向傳播類似於Relu的反向傳播,只要在正向傳播時保存好各個濾波器中最大值的索引位置即可
代碼實現
class Pooling:
def __init__(self, pool_h, pool_w, stride=1, pad=0):
self.pool_h = pool_h
self.pool_w = pool_w
self.stride = stride
self.pad = pad
self.mask = None
def forward(self, x):
N, C, H, W = x.shape
out_h = (H + 2 * self.pad - self.pool_h) // self.stride + 1
out_w = (W + 2 * self.pad - self.pool_w) // self.stride + 1
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
col = col.reshape(N, out_h * out_w, C, self.pool_h * self.pool_w).transpose(0, 2, 1, 3).reshape(N * C * out_h * out_w, self.pool_h * self.pool_w)
mask = np.argmax(col, axis=1)
out = col[np.arange(mask.size), mask]
out = out.reshape(N, C, out_h, out_w)
self.mask = mask
self.input_shape = x.shape
return out
def backward(self, dout):
N, C, H, W = self.input_shape
out_h = (H + 2 * self.pad - self.pool_h) // self.stride + 1
out_w = (W + 2 * self.pad - self.pool_w) // self.stride + 1
dout = dout.reshape(N * C * out_h * out_w)
dcol = np.zeros((N * C * out_h * out_w, self.pool_h * self.pool_w))
dcol[np.arange(self.mask.size), self.mask] = dout
dcol = dcol.reshape(N, C, out_h * out_w, self.pool_h * self.pool_w).transpose(0, 2, 1, 3).reshape(N * out_h * out_w, C * self.pool_h * self.pool_w)
dx = col2im(dcol, self.input_shape, self.pool_h, self.pool_w, self.stride, self.pad)
return dx