深度學習入門(九):卷積層和池化層的實現

本文爲《深度學習入門 基於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維形狀中可能隱藏有值得提取的本質模式。但是,因爲全連接層會忽視形狀,將全部的輸入數據作爲相同的神經元(同一維度的神經元)處理,所以無法利用與形狀相關的信息。
  • 在輸入輸出的數據形狀相同的情況下,全連接層所需要訓練的參數遠遠多於卷積層。例如,輸入爲32×32×332\times 32\times 3, 輸出爲28×28×628\times 28\times 6,如果使用全連接層,則需要32×32×3×28×28×614m(million)32\times 32\times 3 \times 28\times 28\times 6 \approx 14m(million)個參數,而如果使用卷積層(6個5×55 \times 5的濾波器)則只需要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)

  • 應用濾波器的位置間隔稱爲步幅
    在這裏插入圖片描述

小結:卷積層的輸出特徵圖的大小

假設輸入大小爲(H,W)(H,W),濾波器大小爲(FH,FW)(FH, FW),輸出大小爲(OH,OW)(OH,OW),填充爲PP,步幅爲SS。此時,輸出大小爲:
OH=H+2PFHS+1OH = \frac{H+2P-FH}{S} + 1OW=W+2PFWS+1OW = \frac{W+2P-FW}{S} + 1
設定的卷積核參數必須使上面兩式都能除盡,當輸出大小無法除盡時(結果是小數時),需要採取報錯等對策。順便說一下,根據深度學習的框架的不同,當值無法除盡時,有時會向最接近的整數四捨五入,不進行報錯而繼續運行。

3維數據的卷積運算

  • 圖像是3維數據,除了高、長方向之外,還需要處理通道方向。通道方向上有多個特徵圖時,按通道進行輸入數據和濾波器的卷積運算,並將結果相加,從而得到輸出。
    在這裏插入圖片描述
    在3維數據的卷積運算中,輸入數據和濾波器的通道數要設爲相同的值。每個通道的濾波器大小要全部相同

結合方塊思考卷積運算

在這裏插入圖片描述
在這個例子中,數據輸出是1張特徵圖,換句話說,就是通道數爲1的特徵圖。如果要在通道方向上也擁有多個卷積運算的輸出,就需要用到多個濾波器(權重)。

在這裏插入圖片描述
因此,濾波器的形狀可以寫爲(output_channel,input_channel,height,width)(output\_channel, input\_channel, height, width)

如果再加上偏置運算,則結果如下圖所示:
在這裏插入圖片描述

卷積運算的批處理

在這裏插入圖片描述

1 x 1卷積層

參考:《DIVE INTO DEEP LEARNING》,吳恩達深度學習視頻

與全連接層的相似性

1×11×1 卷積層即爲卷積窗口形狀爲 1×11×1的多通道卷積層。因爲使用了最小窗口,1×11×1卷積失去了卷積層可以識別高和寬維度上相鄰元素構成的模式的功能。實際上, 1×11×1 卷積的主要計算髮生在通道維上。
在這裏插入圖片描述
如上圖所示,輸出中的每個元素來自輸入中在高和寬上相同位置的元素在不同通道之間的按權重累加,這就類似於全連接層。
在這裏插入圖片描述
假設我們將通道維當作特徵維,將高和寬維度上的元素當成數據樣本,那麼 1×11×1 卷積層的作用與全連接層等價,而且還使空間信息自然地傳遞到後面的層。即1×11×1 卷積層中可以把通道當作特徵,高和寬上的每個元素相當於樣本。將輸出的通道數設置爲類別數之後,再接上後面要講的全局平均池化層,就可以做到用卷積層代替全連接層了!

可以這麼理解:把每個通道上的二維數據都看作是全連接層上的一個神經元,那麼上圖就可以看作是一個3輸入2輸出的全連接層,(2×3)(2\times3)個卷積核就是全連接層的權重

減小通道數(bottleneck layer)

另外,1×11×1 卷積層還可以達到減小通道數的作用。雖然其他大小的卷積核在配置適當的padding和srtide之後也可以在保持寬高不變的情況下減小通道數,但是1×11×1 卷積層所需的乘法運算次數要少一個數量級。

在這裏插入圖片描述
如上圖所示,用5×55\times5卷積核如果要保持寬高不變,則需要120M次乘法運算

在這裏插入圖片描述
中間的那一層也常被稱爲 bottleneck layer(瓶頸層),由上圖所示,使用1×11\times 1卷積層明顯的降低了計算成本

空洞卷積(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] 的循環結構
  • 滿足下列式子:
    Mi=maxMi+12ri,Mi+12(Mi+1ri),riM_i = max \lceil M_{i+1} - 2r_i, M_{i+1} - 2(M_{i+1} - r_i), r_i \rceil其中rir_iii層的 dilation rate ,MiM_i是在 ii 層的最大dilation rate,那麼假設總共有nn層的話,默認 Mn=rnM_n=r_n

一個簡單的例子: 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塊,其中使用了1×11\times 1卷積層),更重要的一點是保留了前面各個卷積層和池化層提取到的空間信息\語義信息,所以在實際應用中效果提升也較爲明顯。另外,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的實現存在比普通的實現消耗更多內存的缺點。但是,彙總成一個大的矩陣進行計算,對計算機的計算頗有益處

在這裏插入圖片描述

  • 下面寫一下變換後的各個矩陣的形狀以便理解:
    (N,C,H,W)輸入數據:(N, C, H, W)im2col(N×OH×OW,C×FH×FW)im2col轉換之後的輸入數據:(N \times OH\times OW, C\times FH\times FW) (FN,C,FH,FW)濾波器:(FN, C, FH, FW) FN(C×FH×FW,FN)FN個濾波器轉換之後的矩陣:(C\times FH\times FW, FN) b(FN,)偏置b:(FN, ) (2)(N×OH×OW,FN)輸出數據(2維):(N \times OH\times OW, FN) (N,FN,OH,OW)輸出數據:(N, FN, OH, OW)
    可以看出,在輸出數據(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進行快速矩陣運算

反向傳播

反向傳播類似於全連接層,唯一的區別就是在求得dxdx之後還要通過col2im函數變換形狀

簡單推導一下反向傳播的計算(與之前全連接層的推導是一模一樣的,但還是寫一下)
爲了推理上方便書寫,先引入克羅內克符號:
δi,j=1             if i=j\delta_{i,j} = 1 \ \ \ \ \ \ \ \ \ \ \ \ \ if \ i = j δi,j=0             if ij\delta_{i,j} = 0 \ \ \ \ \ \ \ \ \ \ \ \ \ if \ i \neq j
下面正式進行推導:
Lwab=i,jLyijyijwab=i,jLyij(k(xikwkj)+bj)wab=i,jLyijxikδakδbj=iLyibxiaLW=XTLY\begin{aligned} \frac{\partial L}{\partial w_{ab}} &= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} \frac{\partial y_{ij}}{\partial w_{ab}} \\&= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} \frac{\partial (\sum_{k} (x_{ik} * w_{kj}) + b_{j})}{\partial w_{ab}} \\&= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} x_{ik } \delta_{ak} \delta_{bj} \\&= \sum_{i} \frac{\partial L}{\partial y_{ib}} x_{ia} \\ \therefore \frac{\partial L}{\partial W} &= X^T \cdot \frac{\partial L}{\partial Y} \end{aligned}
Lxab=i,jLyijyijxab=i,jLyij(k(xikwkj)+bj)xab=i,jLyijwkjδaiδbk=jLyajwbjLX=LYWT\begin{aligned} \frac{\partial L}{\partial x_{ab}} &= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} \frac{\partial y_{ij}}{\partial x_{ab}} \\&= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} \frac{\partial (\sum_{k} (x_{ik} * w_{kj}) + b_{j})}{\partial x_{ab}} \\&= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} w_{kj } \delta_{ai} \delta_{bk} \\&= \sum_{j} \frac{\partial L}{\partial y_{aj}} w_{bj} \\ \therefore \frac{\partial L}{\partial X} &= \frac{\partial L}{\partial Y} \cdot W^T \end{aligned}
Lba=i,jLyijyijba=i,jLyij(k(xikwkj)+bj)ba=i,jLyijδaj=iLyiaLB=LY0\begin{aligned} \frac{\partial L}{\partial b_{a}} &=\sum_{i,j} \frac{\partial L}{\partial y_{ij}} \frac{\partial y_{ij}}{\partial b_{a}} \\&= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} \frac{\partial (\sum_{k} (x_{ik} * w_{kj}) + b_{j})}{\partial b_{a}} \\&= \sum_{i,j} \frac{\partial L}{\partial y_{ij}} \delta_{aj}\\&= \sum_{i} \frac{\partial L}{\partial y_{ia}} \\ \therefore \frac{\partial L}{\partial B} &= \frac{\partial L}{\partial Y} 的第0軸上的和 \end{aligned}

代碼實現

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展開輸入數據。不過,池化的情況下,在通道方向上是獨立的,這一點和卷積層不同。池化的應用區域應該按通道單獨展開。
在這裏插入圖片描述
也就是說,(N,C,H,W)(N, C, H, W)的輸入數據在im2col之後形狀變爲(N×OH×OW,C×FH×FW)(N \times OH\times OW, C\times FH\times FW),因此還要多加一步,將其reshape爲(N×C×OH×OW,FH×FW)(N \times C \times OH \times OW, FH \times FW)

像這樣展開之後,只需對展開的矩陣求各行的最大值,並轉換爲合適的形狀即可。轉換爲最大值之後的矩陣形狀爲(N×C×OH×OW,1)(N \times C \times OH \times OW, 1),最後將其reshape爲(N,C,OH,OW)(N , C, OH, OW)即可
在這裏插入圖片描述

反向傳播

反向傳播類似於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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章