1. 程式人生 > >NN入門,手把手教你用Numpy手撕NN(三)

NN入門,手把手教你用Numpy手撕NN(三)

NN入門,手把手教你用Numpy手撕NN(3)

這是一篇包含極少數學的CNN入門文章

上篇文章中簡單介紹了NN的反向傳播,並利用反向傳播實現了一個簡單的NN,在這篇文章中將介紹一下CNN。

CNN

CV(計算機視覺)作為AI的一大研究方向,越來越多的人選擇了這個方向,其中使用的深度學習的方法基本以卷積神經網路(CNN)為基礎。因此,這篇文章將介紹CNN的實現。

CNN與我們之前介紹的NN的相比,出現了卷積層(Convolution層)和池化層(Pooling層)。其網路架構大致如下圖所示

卷積層

與全連線神經網路的對比

為什麼在有存在全連線神經網路的情況下還會出現卷積神經網路呢?

這就得來看看全連線神經網路存在的問題了,全連線神經網路中存在的問題是資料的形狀被“忽略了”。比如,輸入的資料是影象時,影象通常是高、長、通道方向上的3為形狀。

但是,全連線層輸入時,需要將3維資料拉平為1維資料,因此,我們可能會丟失影象資料中存在有的空間資訊(如空間上臨近畫素為相似的值、RGB的各個通道之間的關聯性、相距較遠畫素之間的關聯性等),這些資訊都會被全連線層丟失。

而卷積層可以保持形狀不變,將輸入的資料以相同的維度輸出,因此,可能可以正確理解影象等具有形狀的資料。

卷積運算

卷積層進行的處理就是卷積運算,運算方式如下圖所示

一般在計算中也會加上偏置

填充

在進行卷積層的處理之前,有時要向輸入資料的周圍填入固定的資料(如0),稱為填充(padding)。如下圖所示

向輸入資料的周圍填入0,將大小為(4, 4)的輸入資料變成了(6, 6)的形狀。

為什麼要進行填充操作?

在對大小為(4, 4)的輸入資料使用(3, 3)的濾波器時,輸出的大小會變成(2, 2),如果反覆進行多次卷積運算,在某個時刻輸出大小就有可能變成1,導致無法再應用卷積運算。為了避免這種情況,就要使用填充,使得卷積運算可以在保持空間大小不變的情況下將資料傳給下一層。

步幅

應用濾波器的位置間隔稱為步幅(stride)。在上面的例子中,步幅都為1,如果將步幅設為2,則如下圖所示

可以發現,增大步幅後,輸出大小會變小,增大填充後,輸出大小會變大。

假設輸入大小為(H, W),濾波器大小為(FH, FW),輸出大小為(OH, OW),填充為P,步幅為S,則輸出大小可以表示為

\[ OH=\frac{H+2P-FH}{S}+1 \\ OW=\frac{W+2P-FW}{S}+1 \]

三維資料卷積運算

上面卷積運算的例子都是二維的資料,但是,一般來說我們的影象資料都是三維的,除了高、寬之外,還需要處理通道方向的資料。如下圖所示

其計算方式為每個通道處的資料與對應通道的卷積核相乘,最後將各個通道得到的結果相加,從而得到輸出。

這裡需要注意的是,一般情況下,卷積核的通道數需要與輸入資料的通道數相同。(有時會使用1x1卷積核來對通道數進行降/升維操作)。可參考這篇文章

上面給出的例子輸出的結果還是一個通道的,如果我們想要輸出的結果在通道上也有多個輸出,該怎麼做呢?如下圖所示

即使用多個卷積核

池化層

池化是縮小高、長方向上的空間的運算。比如下圖所示的最大池化

除了上圖所示的Max池化之外,還有Average池化。一般來說,池化的視窗大小會和步幅設定成相同的值。

im2col

從前面的例子來看,會發現,如果完全按照計算過程來寫程式碼的話,要用上好幾層for迴圈,這樣的話不僅寫起來麻煩,估計在執行的時候計算速度也很慢。這裡將介紹im2col的方法。

im2col將輸入資料展開以適合卷積核,如下圖所示

對3維的輸入資料應用im2col之後,資料轉化維2維矩陣。

使用im2col展開輸入資料後,之後就只需將卷積層的卷積核縱向展開為1列,並計算2個矩陣的乘積即可。這和全連線層的Affine層進行的處理基本相同。

程式碼實現

講了這麼多,這裡將給出程式碼實現

import numpy as np

def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
    """
    input_data : 由(資料量,通道,高,長)的4維陣列構成的輸入資料 
    filter_h : 濾波器的高 
    filter_w : 濾波器的長 
    stride : 步幅 
    pad : 填充
    """
    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

def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
    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]


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.dW = None
        self.db = None
    
    def forward(self, x):
        # [N, C, H, W]
        FN, C, FH, FW = self.W.shape
        N, C, H, W = x.shape
        out_h = int(1 + (H + 2 * self.pad - FH) / self.stride)
        out_w = int(1 + (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
    
    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

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.x = None
        self.arg_max = None

    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, self.stride, 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, -1).transpose(0, 3, 1, 2)

        self.x = x
        self.arg_max = arg_max

        return out

    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

小節

這篇文章斷斷續續地寫了好久,中間還順便在學tensorflow 2.0 ,還是框架用的舒服 orz。。。這幾天還是決定把這篇文章寫完,坑挖了還是得填,numpy手撕NN系列也算是暫時完成了,RNN後面再考慮。。。這之後準備再補補一些學過的演算法的總結以及前段時間看的一些論文的總結。

本文首發於我的知