iT邦幫忙

5

[筆記]深度學習(Deep Learning)-捲積神經網路

前言

這次要介紹捲積神經網路CNN,常用於取得影像特徵.辨識等等用途,這次簡單的介紹捲積網路,一樣使用O'REILLY Deep Learning書籍,但在捲積神經網路有些部分只提供程式碼所以這裡可以搭配著看會比較好了解原理。

捲積神經網路

捲積網路首先經由一個或多個濾波來取得特徵,像邊緣偵測的濾波也算是一種濾波。在使用池化來取的區塊內最大的值當作特徵,而池化後可讓同一張圖不同大小有縮放和旋轉等等的性質。

捲積網路流程
1.捲積層。
2.池化層。
3.神經網路層。
4.輸出層。
捲積網路主要多了捲積層池化層,這就是接下來要介紹的主題。

捲積層

直接從下圖一理解,紅色矩陣"點對點"乘上藍色矩陣,輸出右邊白色矩陣,每次紅色矩陣移動1個位置(左到右和上到下都移動1),而這移動稱為"步輻",最後結果為圖二的3X3矩陣即是捲積層的輸出。

會發現矩陣會越縮越小,這時候就需使用"填補",將原先輸入5x5陣列填補1,變為7x7(上下左右都會擴充),輸出則會是原先大小5x5如圖三。

無填補測試網址
有填補測試網址

https://ithelp.ithome.com.tw/upload/images/20180909/201105647QxVnT49OB.png
圖一

https://ithelp.ithome.com.tw/upload/images/20180909/20110564tCbbCi7NVV.png
圖二

https://ithelp.ithome.com.tw/upload/images/20180910/20110564MnuGxndnqN.png
圖三

池化層

池化是將區域內選出最大值,若原始資料有些微旋轉在區塊內選取到的還是會是同一個值,這也就是他的特性之一。然而還有不同的取法,例如計算平均等等。

以下為6x6輸入資料區域大小2x2步輻為2。

測試網址

https://ithelp.ithome.com.tw/upload/images/20180910/201105642BEZN1uAdL.png

影像捲積層神經網路

影像主要由RGB組成,也就是三塊色板。在訓練時通常都會用批量訓量,所以捲積的資料維度會變成四維陣列。四維陣列的計算會比較麻煩不好優化,因此會先轉為二維陣列來做計算。以下解說O'REILLY所使用的函數。

im2col

轉換二維主要分為列為資料或行資料,如下圖。下圖維一塊色板,若有三塊色板則繼續像後延伸下去,水平往右,垂直向下,批量則是水平往下,垂直向右。

測試網址

https://ithelp.ithome.com.tw/upload/images/20180910/201105647g6SzPo93Y.png
轉直列。

https://ithelp.ithome.com.tw/upload/images/20180910/20110564kHRPvaVGlF.png
轉橫行。

使用水平資料來做舉例,假設1筆資料7x7的RGB輸入資料和5x5的RGB過濾器,則每一個水平行代表一個區塊,個水平行代表一筆資料,則每一行會有5 X 5 X 3 (5x5為過濾,3為RGB色板)的資料,9行為一筆資料(下敘述推導行列公式),則N筆資料陣列為[9 * N, 75],乘上K個過濾[75, K]再加上偏移量。

經過濾波所產生出來的陣列大小公式為,

  • 列 = 1 + (輸入列 + 2 * 填補 - 過濾列) / 步輻。
  • 行 = 1 + (輸入行 + 2 * 填補 - 過濾行) / 步輻。
    計算公式應該很好理解,接著直接看O'REILLY所使用的函數。

在Python當中使用for迴圈會較慢,因此使用numpy的transpose來快速轉換後再reshape到二維陣列大小。

def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
    """

    Parameters
    ----------
    input_data : (數量, 板塊數量, 高, 寬)四維陣列輸入資料
    filter_h : 過濾器的高
    filter_w : 過濾器的寬
    stride : 步輻
    pad : 填補

    Returns
    -------
    col : 2維列
    """
    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, filter_h, filter_w, out_h, out_w))

    for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
    col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
    return col

1.取得輸入資料的大小。

N, C, H, W = input_data.shape

2.取得最後輸出的二維陣列大小(帶入上述公式)。

out_h = (H + 2*pad - filter_h)//stride + 1
out_w = (W + 2*pad - filter_w)//stride + 1

3.將高和寬填補。

img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')

4.創建一個移動的步輻陣列[資料筆數, 塊板數量, 過濾高度, 過濾寬度, 輸出高度, 輸出寬度]。假設填補後為7x7,過濾為5x5,帶入公式輸出為3x3,在上面使用水平行講過結果為[輸出高 * 輸出寬 * N, 板塊 * 過濾高 * 過濾寬],但目前我們只需塞資料所以改為上方方式塞入資料,方便塞資料和使用transpose來轉置矩陣(後方會說明)。
註:主要[輸出高度, 輸出寬度]和[塊板數量, 過濾高度, 過濾寬度]內的相連在一塊即可。

col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))

5.這裡使用過濾器來跑迴圈,因為上面宣告的陣列為[N, C, FH, FW, OH, OW]所以跑FH * FW,每次複製大小為OH * OW。當然還可以改別的寫法,怕混亂這裡先不提。
之前都是使用5x5直接走訪7x7放到輸出3x3陣列裡,也就是5x5走了3x3次,所以放到5x5濾波大小的陣列裡時大小就是3x3。直接將走訪的位置對應到5x5的存入即可。

  • y_max取得目前移動的最大高度,stride*out_h = 1 * 3 = 3,3為固定。
  • x_max取得目前移動的最大寬度,stride*out_w = 1 * 3 = 3,3為固定。
  • numpy的[y:y_max:stride]為y ~ y_max步輻為stride,例如[0:3:2]則是[0, 1, 2]步輻2,結果為[0, 2]。所以取的其中一塊為img[:, :, y:y_max:stride, x:x_max:stride],塞入col[:, :, y, x, :, :]。

測試網址
https://ithelp.ithome.com.tw/upload/images/20180911/20110564vO3SW11Vzd.png

for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]

6.[N, 3, 5, 5, 3, 3]是目前的資料,而輸出的資料為3X3所以使用transpose將矩陣轉置後reshape到二維大小,reshape的-1則是自動分配大小也可以自己把大小填入。

transpose簡易解說:
現在資料的順序為[0, 1, 2, 3, 4, 5],現在要將3x3和N放在一起,這樣reshape就是資料量 * 輸出大小,這時候就要變為[N, 3, 3, 3, 5, 5],對應原先順序則為[0, 4, 5, 1, 2, 3],這樣解釋會比較容易理解transpose在這的作用。

col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)

上面都是跟課本相同依照[N, 3, 5, 5, 3, 3]來塞資料,你會發現若使用[N, 3, 3, 3, 5, 5]也是可以,只是逆向傳播時要互相對應。

col2im(im2col反向傳播)

def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
    """

    Parameters
    ----------
    col :
    input_shape : (數量, 板塊數量, 高, 寬)四維陣列輸入資料
    filter_h :
    filter_w
    stride
    pad

    Returns
    -------

    """
    N, C, H, W = input_shape
    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, 4, 5, 1, 2)

    img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
    for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]

    return img[:, :, pad:H + pad, pad:W + pad]

1.取得資料的大小。

N, C, H, W = input_shape

2.取得最後輸出的二維陣列大小(帶入上述公式)。

out_h = (H + 2*pad - filter_h)//stride + 1
out_w = (W + 2*pad - filter_w)//stride + 1

3.原先為transpose在reshape,所以反向傳播先reshape在transpose,reshape到原先大小[N, 3, 3, 3, 5, 5]在transpose到原先的大小[N, 3, 5, 5, 3, 3],所以位置[0, 1, 2, 3, 4, 5]對應過去為[0, 3, 4, 5, 1, 2]。

col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)

4.宣告原始資料大小。由上述可知道每一筆資料的H和W為3x3,所以導出原始大小的公式為H + 2 * 填補 + 步輻 - 1,W + 2 * 填補 + 步輻 - 1。

img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))

5.一樣使用過濾器大小來將資料塞入,img與上面,差別在於要加上每一區塊的影響,簡單來說計算每一個位置所影響的數值是多少,所以將有影響的做加總。

for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]

6.回傳無填補的資料。

img[:, :, pad:H + pad, pad:W + pad]

捲積正向傳播函數

def forward(self, x):
        FN, C, FH, FW = self.W.shape
        N, C, H, W = x.shape
        out_h = 1 + int((H + 2 * self.pad - FH) / self.stride)
        out_w = 1 + int((W + 2 * self.pad - FW) / self.stride)

        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
        out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
        
        self.x = x
        self.col = col
        self.col_W = col_W

        return out

1.FN: 過濾器數量,C: 每一筆資料的板塊數量,FH: 過濾器高度,FW: 過濾器寬度。

FN, C, FH, FW = self.W.shape

2.N: 輸入資料數量,C:資料的板塊數量,H: 資料高度,W: 資料寬度。

N, C, H, W = x.shape

3.輸出大小直接帶入公式。

out_h = 1 + int((H + 2 * self.pad - FH) / self.stride)
out_w = 1 + int((W + 2 * self.pad - FW) / self.stride)

4.將輸入資料轉為二維陣列。

col = im2col(x, FH, FW, self.stride, self.pad)

5.輸入資料轉換為過濾高 * 過濾寬 * 色板數量,所以先將大小轉為二維在轉置才可以做矩陣乘法運算([A, B] * [B, C],B必須要一樣)。

col_W = self.W.reshape(FN, -1).T

6.矩陣相乘再加上偏權值。

out = np.dot(col, col_W) + self.b

7.在上述得知,輸入大小 * 過濾大小 = 輸出大小,[9 * N, 75] * [75, FN] = [9 * N, FN]大小,所以先reshape到原先輸入的四維陣列[資料大小, 輸出高, 輸出寬, FN],transpose至[資料數量, 版塊數量, 高度, 寬度],對應過去所以transpose(0, 3, 1, 2)。

out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)

捲積反向傳播函數

def backward(self, dout):
        FN, C, FH, FW = self.W.shape
        dout = dout.transpose(0,2,3,1).reshape(-1, FN)

        self.db = np.sum(dout, axis=0)
        self.dW = np.dot(self.col.T, dout)
        self.dW = self.dW.transpose(1, 0).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

1.FN: 過濾器數量,C: 每一筆資料的板塊數量,FH: 過濾器高度,FW: 過濾器寬度。

FN, C, FH, FW = self.W.shape

2.原先為reshape在transpose,所以反向傳播先transpose在reshape,[N, OC, H, W]transpose到原先的位置[N, H, W, OC],所以位置[0, 1, 2, 3]對應過去為[0, 2, 3, 1],現在是[N, out_h, out_w, OC]在reshape到原始資料的大小[9 * N, FN]。

dout = dout.transpose(0,2,3,1).reshape(-1, FN)

3.計算偏權重加法反向傳播。b原先是自動擴充到跟資料大小一樣做加總,所以必須把每一行加總。

self.db = np.sum(dout, axis=0)

4.計算權重陣列反向傳播。

self.dW = np.dot(self.col.T, dout)

5.原先是先reshape再轉置,這裡先轉置(transpose可改為T)再轉回原本大小[過濾數量, 版塊數量, 過濾高度, 過濾寬度]。

self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)

6.計算col陣列的反向傳播。

dcol = np.dot(dout, self.col_W.T)

7.計算x的反向傳播,原先使用im2col所以帶入col2im計算微分。

dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)

影像池化層神經網路

池化層的正向傳播

    def forward(self, x):
        N, C, H, W = x.shape
        out_h = int(1 + (H - self.pool_h) / self.stride)
        out_w = int(1 + (W - self.pool_w) / self.stride)
        
        col = im2col(x, self.pool_h, self.pool_w, stride = self.stride, pad = self.pad)
        col = col.reshape(-1, self.pool_h * self.pool_w)
        
        arg_max = np.argmax(col, axis=1)
        out = np.max(col, axis = 1)
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)

        self.x = x
        self.arg_max = arg_max

        return out

1.N: 資料數量,C: 每一筆資料的板塊數量,H: 高度,W: 寬度。

N, C, H, W = x.shape

2.計算輸出高度和寬度。

out_h = int(1 + (H - self.pool_h) / self.stride)
out_w = int(1 + (W - self.pool_w) / self.stride)

3.四維轉為二維。

col = im2col(x, self.pool_h, self.pool_w, stride = self.stride, pad = self.pad)

4.im2col回來的資料大小為[資料數量 * 輸出高度 * 輸出寬度, -1(色板 * 池化高度 * 池化寬度)],因要取出每一行的每個色板最大值所以將色板分開,reshape轉換為[(資料數量 * 輸出高度 * 輸出寬度 * 色板), (池化高度 * 池化寬度)]。

col = col.reshape(-1, self.pool_h * self.pool_w)

5.取得每一行最大的"索引位置"反向傳播時要使用,取得每行最大"值"。

arg_max = np.argmax(col, axis=1)
out = np.max(col, axis = 1)

6.轉換為原始的大小和位置[資料數量, 輸出色板, 輸出高度, 輸出寬度],所以將[(資料數量 * 輸出高度 * 輸出寬度 * 色板), (池化高度 * 池化寬度)]reshape為[資料數量, 輸出高度, 輸出寬度, 輸出色板],轉置使用transpose為[資料數量, 輸出色板, 輸出高度, 輸出寬度]即可。

out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)

池化層的反向傳播

    def backward(self, dout):
        dout = dout.transpose(0, 2, 3, 1)
        
        pool_size = self.pool_h * self.pool_w
        dmax = np.zeros((dout.size, pool_size))
        dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
        dmax = dmax.reshape(dout.shape + (pool_size,)) 
        
        dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
        dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
        
        return dx

1.dout位置為[資料數量, 輸出色板, 輸出高度, 輸出寬度]使用transpose往回推至,[資料數量, 輸出高度, 輸出寬度, 輸出色板],所以[0, 1, 2, 3]轉為[0, 2, 3, 1]。

dout = dout.transpose(0, 2, 3, 1)

2.宣告一個二維原先未池化的大小,每一個dout都是pool_size選出來的其中一個,所以原先大小是dout.size * pool_size。

dmax = np.zeros((dout.size, pool_size))

3.將dout的值塞入每一行對應arg_max索引值的位置。

dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()

4.資料塞完後恢復原本大小,這時候這列大小為五維陣列[dout.shape, pool_size]。

dmax = dmax.reshape(dout.shape + (pool_size,))

5.reshape回去原先im2col回傳的大小[N * out_h * out_w, -1]。

dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)

6.帶入col2im得取x反向傳播。

dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)

捲積的隱藏層

隱藏層正向傳播

原先神經網路介紹隱藏層是矩陣相乘,而在捲積傳過來可能是四維陣列,所以先將x大小轉為二維陣列[資料數量, 資料],並記錄原先大小為了做反向傳播。

    def forward(self, x):
        self.original_x_shape = x.shape
        x = x.reshape(x.shape[0], -1)
        self.x = x

        out = np.dot(self.x, self.W) + self.b

        return out

隱藏層反向傳播

反向傳播一樣先使用矩陣的反向傳播取得dW,加法反向傳播取得db,唯一不同的是為了配合捲積的四維所以再將大小轉為原先的大小。

    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)
        
        dx = dx.reshape(*self.original_x_shape)
        return dx

結果

使用MNIST數字大小28x28,過濾器30個,過濾大小5x5,填補0,步輻1,隱藏層大小100,輸出層大小10,預設權重倍數0.01,訓練3次(原先是20次但需要很長的時間)。

https://ithelp.ithome.com.tw/upload/images/20180913/201105647eQibWle8t.png
顯示25個5x5過濾器訓練後結果。

https://ithelp.ithome.com.tw/upload/images/20180913/201105643WobtF5v83.png
灰階Lena使用25個過濾器顯示特徵結果。

完整程式碼在O'REILLY Github的ch07,有興趣可以去買完整書籍搭配看效果應該會更好。

結論

這次有點小偷懶文章拖到現在才發,但也加入了動態的捲積讓大家比較好容易理解,接下來可能會先複習一下影像處理部分,最後再回來講捲積辨識部分,若有問題或錯誤可以在下面留言或私訊。
最後祝各位鐵人賽加油/images/emoticon/emoticon12.gif

參考文獻

[1]斎藤康毅、吳嘉芳(譯者)(2017)。Deep Learning:用Python進行深度學習的基礎理論實作。台灣:歐萊禮。


尚未有邦友留言

立即登入留言