【深度學習】卷積神經網絡的實現與理解

本系列由斯坦福大學CS231n課後作業提供
CS231N - Assignment2 - Q4 - ConvNet on CIFAR-10


問題描述:使用IPython Notebook(現版本爲jupyter notebook,如果安裝anaconda完整版會內置),在ConvolutionalNetworks.ipynb文件中,你將實現幾個卷積神經網絡中常用的新層。使用CIFAR-10數據,訓練出一個深度較淺的卷積神經網絡,最後盡你所能訓練出一個最佳的神經網絡。


任務

實現卷積神經網絡卷積層的前向計算與反向傳導
實現卷積神經網絡池化層的前向計算與反向傳導
卷積層與池化層的加速


卷積神經網絡結構

常規神經網絡的輸入是一個向量,經一系列隱層的轉換後,全連接輸出。在分類問題中,它輸出的值被看做是不同類別的評分值。
神經網絡的輸入可不可以是圖片呢?
常規神經網絡對於大尺寸圖像效果不盡人意。圖片的像素點過多處理起來極爲複雜。因此在處理圖片的過程中較爲合理地降維成爲一個模糊的研究方向。因此基於神經網絡結構產生了一個新的神經網絡層結構,我們稱之爲卷積神經網絡
下圖是多層卷積神經網絡的一種結構:
圖片來自斯坦福大學CS231n第七課ppt

可以看出,網絡結構開始爲“卷積層(CONV),relu層(RELU),池化層(POOL)”C-R-P週期循環,最後由全連接層(FC)輸出結果。
圖片來自斯坦福大學CS231n第七課ppt
注:實際應用的過程中常常不限於C-R-P循環,也有可能是C-C-R-P等等

設激活函數fsigmoid() ,池化操作pool() ,x爲輸入數據代表圖像像素矩陣,w爲權重代表過濾層(卷積核),b代表偏置。C-R-P週期則有下面的計算公式:

xjl=frelu(pool(iMjxil1wijl+bjl))

卷積神經網絡的理解比較困難,爲了更好地瞭解,我們先講解過程再討論實際作用。

卷積層的樸素(無加速算法)實現與理解

(在實際應用過程中,一般使用加速過程處理的卷積層,這裏表現的是原始卷積版本)

卷積層元素

下圖是卷積層的元素:輸入圖片與過濾參數。
圖片來自斯坦福大學CS231n第七課ppt

輸入圖片(image):輸入層(Input Layer)有3個深度(D1,D2,D3,通常代表圖片的三個通道RGB)。我可以將每個深度獨立出來,看成三幅圖片。圖片的大小爲32*32。

過濾參數(filter):過濾器有很多稱呼,如“卷積核”、“過濾層”或者“特徵檢測器”。不要被名詞坑了。過濾器也有3個深度(D1,D2,D3),就是與輸入圖片的深度進行一一對應,方便乘積操作。過濾器窗口一般比輸入圖片窗口小。

卷積層的前向計算

下圖是卷積層的具體實現方法。
這裏寫圖片描述
x代表圖片image矩陣,w代表過濾層矩陣。過濾器與image一樣窗口大小的部分乘積求和,這就是卷積過程。下面的動圖就是卷積過程。
這裏寫圖片描述
上圖中我們看到每次窗口移動2格。這2格就是每次卷積的移動步長
我們看到原來的圖片在周圍填充了一圈0。填充0的寬度即爲每次卷積的填充寬度
移動步長很好理解,但爲什麼要填充呢?假設我們不填充。如下圖:
這裏寫圖片描述
會發現每次卷積之後都會有維度降低。淺層卷積網絡可能沒有什麼問題。但是深層卷積可能在網絡沒到最後的時候維度即降爲0。
這顯然不是我們所希望的。
當然,如果有意願用卷積計算去降維也可以,不過我們更喜歡用池化層的池化操作降維。爲甚?我們必須先了解卷積層的作用。

卷積層正向卷積過程代碼實現

def conv_forward_naive(x, w, b, conv_param):
    """
    A naive implementation of the forward pass for a convolutional layer.

    The input consists of N data points, each with C channels, height H and
    width W. We convolve each input with F different filters, where each filter
    spans all C channels and has height HH and width HH.

    Input:
    - x: Input data of shape (N, C, H, W)
    - w: Filter weights of shape (F, C, HH, WW)
    - b: Biases, of shape (F,)
    - conv_param: A dictionary with the following keys:
      - 'stride': The number of pixels between adjacent receptive fields in the
        horizontal and vertical directions.
      - 'pad': The number of pixels that will be used to zero-pad the input.

    Returns a tuple of:
    - out: Output data, of shape (N, F, H', W') where H' and W' are given by
      H' = 1 + (H + 2 * pad - HH) / stride
      W' = 1 + (W + 2 * pad - WW) / stride
    - cache: (x, w, b, conv_param)
    """
    ###########################################################################
    # TODO: Implement the convolutional forward pass.                  #
    # Hint: you can use the function np.pad for padding.               #    ###########################################################################
    N, C, H, W = x.shape
    F, C, HH, WW = w.shape
    stride = conv_param['stride']
    pad = conv_param['pad']

    ##計算卷積後新矩陣的大小並分配全零值佔位
    new_H = 1 + int((H + 2 * pad - HH) / stride)
    new_W = 1 + int((W + 2 * pad - WW) / stride)
    out = np.zeros([N, F, new_H, new_W])

    ##卷積開始
    for n in range(N):
        for f in range(F):
            ##臨時分配(new_H, new_W)大小的全便宜香卷積矩陣,(即提前加上偏移項b[f])
            conv_newH_newW = np.ones([new_H, new_W])*b[f]
            for c in range(C):
                ##填充原始矩陣,填充大小爲pad,填充值爲0
                pedded_x = np.lib.pad(x[n, c], pad_width=pad, mode='constant', constant_values=0)
                for i in range(new_H):
                    for j in range(new_W):
                        conv_newH_newW[i, j] +=  np.sum( pedded_x[i*stride:i*stride+HH, j*stride:j*stride+WW] * w[f,c,:,:] )
                out[n,f] = conv_newH_newW
    ###########################################################################
    #                             END OF YOUR CODE                     #
###########################################################################
    cache = (x, w, b, conv_param)
    return out, cache

卷積層的個人理解

對卷積層的作用,很多人的說法莫衷一是。我這裏談談自己的理解。

1. 實現某像素點多通道以及周圍信息的整合

說白了就是將一個像素與其周圍的點建立聯繫。我們用將一點及其周邊“卷”起來求和計算。那麼每一層卷積必然是將一個像素點與周圍建立聯繫的過程。

這麼說起來“Convolution”翻譯爲“卷和”更恰當,其實卷積的“積”是積分的意思

2. 我們先講一個喪心病狂的故事

如果你每天玩遊戲不陪女朋友,那麼女朋友每天都要扇你一巴。打你一巴掌後,臉的一部分就腫了。你的臉就是圖片,女朋友的巴掌就是卷積核層。每打一巴掌,相當於“卷積核”作用臉部一個地方“做卷積”。臉腫了相當於臉部“卷積後”輸出的結果。
如果有一天,女友忍無可忍,連續扇你嘴巴,那麼問題就出現了。上一次扇你鼓起來的包還沒消腫,第二個巴掌就來了。女友不斷扇你,卷積不地作用在你臉上,效果不斷疊加了,這樣這些效果就可以求和了。

女友再狠一點,頻率越來越高,以至於你都辨別不清時間間隔了。那麼,求和就變成積分了。這就是“卷積”一詞的由來。

女友打你在不同的位置,自然會有不同的身體反應。根據打你後卷積後身體的反應卷積結果可以判斷出打到什麼位置了。

身體反應(卷積結果) 可能推論
腫了 打到臉了
紅了 打到肚子了
沒反應 打到骨頭了
女友手疼 打到骨刺了
更愛她了 變態
沒有感覺 單身狗的幻想
這麼一解釋卷積層的解釋果然很明顯。。。。嗯,對。。。。我自己都信了。。。。

3. 圖像處理中模板的概念

在傳統圖像處理中,模板即一個矩陣方塊。在這裏你可以認爲是卷積核,計算爲方法爲卷積運算。如下面的一個模板

\frac{1}{9}\begin{bmatrix}
    1&1&1\\
    1&1&1\\
    1&1&1
\end{bmatrix}

一幅圖片經過它的卷積運算,可以得到這樣的結果。

這裏寫圖片描述

上面的那種模板就是“低通濾波模板”的一種。
通過改變方陣的數值與大小,可以生成很多新鮮的模板。如“高通濾波模板”,“邊緣檢測模板”,“匹配濾波邊緣檢測模板”等等。。
那麼問題來了:我們該在什麼時候,用什麼數值的模板?對傳統的模板選擇,憑藉的是算法工程師的經驗。但現在,我們不怕了!~~深度學習幫我們訓練模板的參數。圖片在卷積神經網絡的一次前向過程,相當於對圖像做一次特定的處理。

卷積層反向求導

前面我們介紹了卷積層的前向計算。大致瞭解了卷積的作用,但是神經網絡的參數是怎麼來的?參數的獲取是一個迭代的訓練的過程,每次反向傳播是糾正參數,減小誤差。(具體細節請看別人關於神經網絡“前向計算,反向傳播”的講解。)
上文提到了C-R-P週期計算公式。

xjl=frelu(pool(iMjxil1wijl+bjl))

frelu() 爲relu激活函數,池化操作用pool() 表示,x爲輸入數據代表圖像像素矩陣,w爲權重代表過濾器(卷積核),b代表偏置。
編寫卷積層反向傳播時,暫時不用考慮池化層,與激活函數。我們可以用g()代指卷積層後所有的操作。所以這一層的反向對x求導可以簡化爲如下操作。
g(xw+b)=g(out)out=xw+bgx=goutoutx

在斯坦福CS231n課程作業中,把無實際用處的g忽略。記dx=gx,dout=gout
對於中國大陸學生來說這是一個深淵巨坑的記法。在高等數學教材裏。dout,dx有專門的含義。但這裏我們要遵循老師的記法。
下圖爲卷積層反向求導的過程圖片。
這裏寫圖片描述

卷積層反向求導過程代碼實現

def conv_backward_naive(dout, cache):
    """
    A naive implementation of the backward pass for a convolutional layer.

    Inputs:
    - dout: Upstream derivatives.
    - cache: A tuple of (x, w, b, conv_param) as in conv_forward_naive

    Returns a tuple of:
    - dx: Gradient with respect to x
    - dw: Gradient with respect to w
    - db: Gradient with respect to b
    """
   ###########################################################################
    # TODO: Implement the convolutional backward pass.             #
###########################################################################
    # 數據準備
    x, w, b, conv_param = cache
    pad = conv_param['pad']
    stride = conv_param['stride']
    F, C, HH, WW = w.shape
    N, C, H, W = x.shape
    N, F, new_H, new_W = dout.shape

    # 下面,我們模擬卷積,首先填充x。
    padded_x = np.lib.pad(x,
                          ((0, 0), (0, 0), (pad, pad), (pad, pad)),
                          mode='constant',
                          constant_values=0)
    padded_dx = np.zeros_like(padded_x)  # 填充了的dx,後期去填充即可得到dx
    dw = np.zeros_like(w)
    db = np.zeros_like(b)

    for n in range(N):  # 第n個圖像
        for f in range(F):  # 第f個過濾器
            for i in range(new_H):
                for j in range(new_W):
                    db[f] += dout[n, f, i, j] #求導爲1,無爭議
                    dw[f] += padded_x[n, :, i*stride : HH + i*stride, j*stride : WW + j*stride] * dout[n, f, i, j]
                    padded_dx[n, :, i*stride : HH + i*stride, j*stride : WW + j*stride] += w[f] * dout[n, f, i, j]
    # 反填充
    dx = padded_dx[:, :, pad:pad + H, pad:pad + W]
    ###########################################################################
    #                             END OF YOUR CODE                 #  ###########################################################################
    return dx, dw, db

池化層的樸素(無加速算法)實現與理解

池化層的前向計算

在“卷積層需要填充”這一部分我們就說過:卷積層負責像素間建立聯繫,池化層負責降維。從某種層度上來說,池化操作也算是一種不需要填充操作的特殊的卷積操作。
池化操作也需要過濾器(也有翻譯爲池化核),每次過濾層移動的距離叫做步長。
不過在池化操作中,過濾器與原圖可以不做卷積運算,僅僅是降維功能。下圖就表示過濾器選擇窗口最大項的“最大池化操作”
這裏寫圖片描述

最大池化操作的前向計算

基於卷積的經驗,很快地寫出最大池化操作的前向計算與反向求導的代碼。

def max_pool_forward_naive(x, pool_param):
    """
    A naive implementation of the forward pass for a max pooling layer.

    Inputs:
    - x: Input data, of shape (N, C, H, W)
    - pool_param: dictionary with the following keys:
      - 'pool_height': The height of each pooling region
      - 'pool_width': The width of each pooling region
      - 'stride': The distance between adjacent pooling regions

    Returns a tuple of:
    - out: Output data
    - cache: (x, pool_param)
    """
    ###########################################################################
    # TODO: Implement the max pooling forward pass                      #
###########################################################################
    # 準備數據
    N, C, H, W = x.shape
    pool_height = pool_param['pool_height']
    pool_width  = pool_param['pool_width']
    pool_stride = pool_param['stride']
    new_H = 1 + int((H - pool_height) / pool_stride)
    new_W = 1 + int((W - pool_width) / pool_stride)

    out = np.zeros([N, C, new_H, new_W])
    for n in range(N):
        for c in range(C):
            for i in range(new_H):
                for j in range(new_W):
                    out[n,c,i,j] = np.max(x[n, c, i*pool_stride : i*pool_stride+pool_height, j*pool_stride : j*pool_stride+pool_width])
###########################################################################
    #                             END OF YOUR CODE                            #
###########################################################################
    cache = (x, pool_param)
    return out, cache

最大池化的反向求導

def max_pool_backward_naive(dout, cache):
    """
    A naive implementation of the backward pass for a max pooling layer.

    Inputs:
    - dout: Upstream derivatives
    - cache: A tuple of (x, pool_param) as in the forward pass.

    Returns:
    - dx: Gradient with respect to x
    """
###########################################################################
    # TODO: Implement the max pooling backward pass                     #
###########################################################################
    # 數據準備
    x, pool_param = cache
    N, C, H, W = x.shape
    pool_height = pool_param['pool_height']
    pool_width  = pool_param['pool_width']
    pool_stride = pool_param['stride']
    new_H = 1 + int((H - pool_height) / pool_stride)
    new_W = 1 + int((W - pool_width) / pool_stride)
    dx = np.zeros_like(x)
    for n in range(N):
        for c in range(C):
            for i in range(new_H):
                for j in range(new_W):
                    window = x[n, c, i * pool_stride: i * pool_stride + pool_height,j * pool_stride: j * pool_stride + pool_width]
                    dx[n, c, i * pool_stride: i * pool_stride + pool_height, j * pool_stride: j * pool_stride + pool_width] = (window == np.max(window))*dout[n,c,i,j]
###########################################################################
    #                             END OF YOUR CODE                            #
###########################################################################
    return dx

三明治卷積層(Convolutional “sandwich” layers)

“三明治”卷積層是我得到資料是斯坦福大學CS231n的專門講法,其實就是將多個操作組合成一個常用模式。前文我一直說的C-R-P組合,可以看做一種三明治卷積層。卷積神經網絡在實際應用上,也往往跳過底層實現,直接面向組合操作。
在文件cs231n/layer_utils.py 裏keras的源碼裏。都會找到這樣的“三明治”卷積層。它們的簡化了複雜深度學習神經網絡的實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章