機器學習 | 卷積神經網絡詳解(二)——自己手寫一個卷積神經網絡

上篇文章中我們講解了卷積神經網絡的基本原理,包括幾個基本層的定義、運算規則等。本文主要寫卷積神經網絡如何進行一次完整的訓練,包括前向傳播和反向傳播,並自己手寫一個卷積神經網絡。如果不瞭解基本原理的,可以先看看上篇文章:機器學習 | 卷積神經網絡CNN原理詳解(一)——基本原理

一、卷積神經網絡的前向傳播

首先我們來看一個最簡單的卷積神經網絡:

在這裏插入圖片描述

1.輸入層---->卷積層

以上一節的例子爲例,輸入是一個444*4 的image,經過兩個2*2的卷積核進行卷積運算後,變成兩個333*3的feature_map

在這裏插入圖片描述

以卷積核filter1爲例(stride = 1 ):

在這裏插入圖片描述

計算第一個卷積層神經元o11o_{11}的輸入:

neto11=conv(input,filter)=i11×h11+i12×h12+i21×h21+i22×h22=1×1+0×(1)+1×1+1×(1)=1(1)neto11 =conv(input,filter)\\ \qquad \quad=i11×h11+i12×h12+i21×h21+i22×h22\\ \qquad \quad =1×1+0×(−1)+1×1+1×(−1)\\ \qquad \quad =1\qquad \qquad\qquad \qquad(1)

神經元o11的輸出:(此處使用Relu激活函數)

outo11=activators(neto11)=max(0,neto11)=1(2)outo11=activators(neto11)\\ \qquad \quad=max(0,neto11)\\ \qquad \quad=1\qquad \qquad\qquad \qquad(2)
  
其他神經元計算方式相同

2.卷積層---->池化層

在這裏插入圖片描述

計算池化層m11m_{11} 的輸入(取窗口爲222 * 2),池化層沒有激活函數

netm11=max(o11,o12,o21,o22)=1net_{m_{11}}= max(o_{11},o_{12},o_{21},o_{22})=1
outm11=netm11=1(3)out_{m_{11}}=net_{m_{11}}=1\qquad \qquad\qquad \qquad(3)

3.池化層---->全連接層

池化層的輸出到flatten層把所有元素“拍平”,然後到全連接層。

4.全連接層---->輸出層

全連接層到輸出層就是正常的神經元與神經元之間的鄰接相連,通過softmax函數計算後輸出到output,得到不同類別的概率值,輸出概率值最大的即爲該圖片的類別。

二、卷積神經網絡的反向傳播

傳統的神經網絡是全連接形式的,如果進行反向傳播,只需要由下一層對前一層不斷的求偏導,即求鏈式偏導就可以求出每一層的誤差敏感項,然後求出權重和偏置項的梯度,即可更新權重。而卷積神經網絡有兩個特殊的層:卷積層和池化層。 池化層輸出時不需要經過激活函數,是一個滑動窗口的最大值,一個常數,那麼它的偏導是1。池化層相當於對上層圖片做了一個壓縮,這個反向求誤差敏感項時與傳統的反向傳播方式不同。從卷積後的feature_map反向傳播到前一層時,由於前向傳播時是通過卷積核做卷積運算得到的feature_map,所以反向傳播與傳統的也不一樣,需要更新卷積核的參數。下面我們介紹一下池化層和卷積層是如何做反向傳播的。

在介紹之前,首先回顧一下傳統的反向傳播方法:

  1. 通過前向傳播計算每一層的輸入值neti,jnet_{i,j} (如卷積後的feature_map的第一個神經元的輸入:neti11net_{i_{11}})

  2. 反向傳播計算每個神經元的誤差項δi,jδ_{i,j}δi,j=Eneti,jδ_{i, j} = \frac{∂E} {∂net_{i,j}},其中E爲損失函數計算得到的總體誤差,可以用平方差,交叉熵等表示。

  3. 計算每個神經元權重wi,jw_{i,j} 的梯度,ηi,j=Eneti,jneti,jwi,j=δi,jouti,jη_{i,j}=\frac{∂E}{∂net_{i,j}}⋅ \frac{∂net_{i,j}}{∂w_{i,j}}=δ_{i,j}⋅out_{i,j}

  4. 更新權重 wi,j=wi,jληi,jw_{i,j}=w_{i,j}−λ⋅η_{i,j}(其中λλ爲學習率)

1. 卷積層的反向傳播

由前向傳播可得:

       每一個神經元的值都是上一個神經元的輸入作爲這個神經元的輸入,經過激活函數激活之後輸出,作爲下一個神經元的輸入,在這裏我用i11i_{11}表示前一層,o11o_{11}表示i11i_{11}的下一層。那麼neti11net_{i_{11}}就是i11i_{11}這個神經元的輸入,neti11net_{i_{11}}就是i11i_{11}這個神經元的輸出,同理,neto11net_{o_{11}}就是o11o_{11}這個神經元的輸入,outo11out_{o_{11}}就是o11o_{11}這個神經元的輸出,因爲上一層神經元的輸出 = 下一層神經元的輸入,所以outi11=neto11out_{i_{11}}= net_{o_{11}},這裏我爲了簡化,直接把outi11out_{i_{11}}記爲i11i_{11}

i11=outi11  =activators(neti11)neto11=conv(input,filter)  =i11×h11+i12×h12+i21×h21+i22×h22outo11=activators(neto11)  =max(0,neto11)(4)i_{11} =out_{i_{11}}\\ \quad \ \ =activators(net_{i_{11}})\\ net_{o_{11}}=conv(input,filter)\\ \quad \ \ =i_{11}×h_{11}+i_{12}×h_{12}+i_{21}×h_{21}+i_{22}×h_{22}\\ out_{o_{11}}=activators(net_{o_{11}})\\ \quad \ \ =max(0,net_{o_{11}})\qquad \qquad\qquad \qquad(4)

neti11net_{i_{11}}表示上一層的輸入,outi11out_{i_{11}}表示上一層的輸出

首先計算卷積的上一層的第一個元素i11的誤差項δ11δ_{11}

δ11=Eneti11=Eouti11outi11neti11=Ei11i11neti11δ11=\frac{∂E}{∂net_{i_{11}}}=\frac{∂E}{∂out_{i_{11}}}⋅ \frac{∂out_{i_{11}}}{∂net_{i_{11}}}=\frac{∂E}{∂i_{11}}⋅\frac{∂i_{11}}{∂net_{i_{11}}}

先計算Ei11\frac{∂E}{∂i_{11}}

此處我們並不清楚Ei11\frac{∂E}{∂i_{11}}怎麼算,那可以先把input層通過卷積核做完卷積運算後的輸出feature_map寫出來:

neto11=i11×h11+i12×h12+i21×h21+i22×h22neto12=i12×h11+i13×h12+i22×h21+i23×h22neto12=i13×h11+i14×h12+i23×h21+i24×h22neto21=i21×h11+i22×h12+i31×h21+i32×h22neto22=i22×h11+i23×h12+i32×h21+i33×h22neto23=i23×h11+i24×h12+i33×h21+i34×h22neto31=i31×h11+i32×h12+i41×h21+i42×h22neto32=i32×h11+i33×h12+i42×h21+i43×h22neto33=i33×h11+i34×h12+i43×h21+i44×h22(5)net_{o_{11}}=i_{11}×h_{11}+i_{12}×h_{12}+i_{21}×h_{21}+i_{22}×h_{22}\\ net_{o_{12}}=i_{12}×h_{11}+i_{13}×h_{12}+i_{22}×h_{21}+i_{23}×h_{22}\\ net_{o_{12}}=i_{13}×h_{11}+i_{14}×h_{12}+i_{23}×h_{21}+i_{24}×h_{22}\\ net_{o_{21}}=i_{21}×h_{11}+i_{22}×h_{12}+i_{31}×h_{21}+i_{32}×h_{22}\\ net_{o_{22}}=i_{22}×h_{11}+i_{23}×h_{12}+i_{32}×h_{21}+i_{33}×h_{22}\\ net_{o_{23}}=i_{23}×h_{11}+i_{24}×h_{12}+i_{33}×h_{21}+i_{34}×h_{22}\\ net_{o_{31}}=i_{31}×h_{11}+i_{32}×h_{12}+i_{41}×h_{21}+i_{42}×h_{22}\\ net_{o_{32}}=i_{32}×h_{11}+i_{33}×h_{12}+i_{42}×h_{21}+i_{43}×h_{22}\\ net_{o{33}}=i_{33}×h_{11}+i_{34}×h_{12}+i_{43}×h_{21}+i_{44}×h_{22}\\ \qquad \qquad\qquad \qquad\qquad \qquad\qquad \qquad\qquad \qquad\qquad \qquad\qquad \qquad(5)

然後依次對輸入元素ii,j求偏導

i11i_{11}的偏導:

Ei11=Eneto11neto11i11=δ11h11(6)\frac {∂E}{∂i_{11}} =\frac {∂E}{∂net_{o_{11}}}⋅\frac{∂net_{o_{11}}}{∂i_{11}}=δ_{11}⋅h_{11}\qquad \qquad\qquad \qquad(6)

i12i_{12}的偏導:

Ei12=Eneto11neto11i12+Eneto12neto12i12=δ11h12+δ12h11(7)\frac {∂E}{∂i_{12}}=\frac{∂E}{∂net_{o_{11}}}⋅\frac{∂net_{o_{11}}}{∂i_{12}}+\frac{∂E}{∂net_{o_{12}}}⋅\frac{∂net_{o_{12}}}{∂i_{12}}=δ_{11}⋅h_{12}+δ_{12}⋅h_{11}\qquad \qquad\qquad \qquad(7)

i13i_{13}的偏導:

Ei13=Eneto12neto12i13+Eneto13neto13i13=δ12h13+δ12h11(8)\frac {∂E}{∂i_{13}}=\frac{∂E}{∂net_{o_{12}}}⋅\frac{∂net_{o_{12}}}{∂i_{13}}+\frac{∂E}{∂net_{o_{13}}}⋅\frac{∂net_{o_{13}}}{∂i_{13}}=δ_{12}⋅h_{13}+δ_{12}⋅h_{11}\qquad \qquad\qquad \qquad(8)

i21i_{21}的偏導:
Ei21=Eneto11neto11i21+Eneto21neto21i21=δ11h21+δ21h11(9)\frac {∂E}{∂i_{21}}=\frac{∂E}{∂net_{o_{11}}}⋅\frac{∂net_{o_{11}}}{∂i_{21}}+\frac{∂E}{∂net_{o_{21}}}⋅\frac{∂net_{o_{21}}}{∂i_{21}}=δ_{11}⋅h_{21}+δ_{21}⋅h_{11}\qquad \qquad\qquad \qquad(9)

i22i_{22}的偏導:

Ei22=Eneto11neto11i22+Eneto12neto12i22+Eneto21neto21i22+Eneto22neto22i22   =δ11h22+δ12h21+δ21h12+δ22h11(10)\frac {∂E}{∂i_{22}}=\frac{∂E}{∂net_{o_{11}}}⋅\frac{∂net_{o_{11}}}{∂i_{22}}+\frac{∂E}{∂net_{o_{12}}}⋅\frac{∂net_{o_{12}}}{∂i_{22}}\\ \qquad\qquad+\frac{∂E}{∂net_{o_{21}}}⋅\frac{∂net_{o_{21}}}{∂i_{22}}+\frac{∂E}{∂net_{o_{22}}}⋅\frac{∂net_{o_{22}}}{∂i_{22}}\\ \qquad\qquad\qquad \ \quad \ \ =δ_{11}⋅h_{22}+δ_{12}⋅h_{21}+δ_{21}⋅h_{12}+δ_{22}⋅h_{11}\qquad \qquad\qquad \qquad(10)

觀察一下上面幾個式子的規律,歸納一下,可以得到如下表達式:

tupian

圖中的卷積核進行了180°翻轉,與這一層的誤差敏感項矩陣delta(i,j)delta_{(i,j)}周圍補零後的矩陣做卷積運算後,就可以得到∂E∂i11,即

Ei,j=mnhm,nδi+m,j+n\frac{∂E}{∂_{i,j}}=∑_m⋅∑_nh_{m,n}δ_{i+m,j+n}

第一項求完後,我們來求第二項i11neti11\frac{∂i_{11}}{∂net_{i_{11}}}

i11=outi11 =activators(neti11)i11neti11=f(neti11)δ11=Eneti11 =Ei11i11neti11 =mnhm,nδi+m,j+nf(neti11)(12)∵i_{11}=out_{i_{11}}\\ \qquad \ =activators(net_{i_{11}})\\ ∴\frac{∂i_{11}}{∂net_{i_{11}}} = f′(net_{i_{11}})\\ ∴δ_{11}=\frac{∂E}{∂net_{i_{11}}}\\ \qquad \ =\frac{∂E}{∂i_{11}}⋅\frac{∂i_{11}}{∂net_{i_{11}}}\\ \qquad \ =∑_m⋅∑_nh_{m,n}δ_{i+m,j+n}⋅f′(net_{i_{11}})\\ \qquad \qquad\qquad \qquad\qquad \qquad\qquad \qquad\qquad \qquad\qquad \qquad(12)

此時我們的誤差敏感矩陣就求完了,得到誤差敏感矩陣後,即可求權重的梯度。

由於上面已經寫出了卷積層的輸入neto11net_{o_{11}}與權重hi,jh_{i,j}之間的表達式,所以可以直接求出:

Eh11=Eneto11neto11h11+...+Eneto33neto33h11=δ11h11+...+δ33h11  (13)\frac{∂E}{∂h_{11}}=\frac{∂E}{∂net_{o_{11}}}⋅\frac{∂net_{o_{11}}}{∂h_{11}}+...+\frac{∂E}{∂net_{o_{33}}}⋅\frac{∂net_{o_{33}}}{∂h_{11}}=δ_{11}⋅h_{11}+...+δ_{33}⋅h_{11} \ \ \qquad(13)

推論出權重的梯度

Ehi,j=mnδm,noutoi+m,j+n  (14)\frac{∂E}{∂h_{i,}}j=∑_m∑_nδ_{m,n}out_{o_{i+m,j+n}} \ \ \qquad(14)

偏置項的梯度

Eb=Eneto11neto11wb+Eneto12neto12wb\frac{∂E}{∂b}=\frac{∂E}{∂net_{o_{11}}}\frac{∂net_{o_{11}}}{∂w_b}+\frac{∂E}{∂net_{o_{12}}}\frac{∂net_{o_{12}}}{∂w_b}
 +Eneto21neto21wb+Eneto22neto22wb   =δ11+δ12+δ21+δ22=ijδi,j  (15)\qquad \ +\frac{∂E}{∂net_{o_{21}}}\frac{∂net_{o_{21}}}{∂w_b}+\frac{∂E}{∂net_{o_{22}}}\frac{∂net_{o_{22}}}{∂wb}\\ \quad \ \ \ =δ_{11}+δ_{12}+δ_{21}+δ_{22}=∑_i∑_jδ_{i,j}\ \ \qquad(15)

可以看出,偏置項的偏導等於這一層所有誤差敏感項之和。得到了權重和偏置項的梯度後,就可以根據梯度下降法更新權重和梯度了。

2. 池化層的反向傳播

池化層的反向傳播就比較好求了,看着下面的圖,左邊是上一層的輸出,也就是卷積層的輸出feature_map,右邊是池化層的輸入,還是先根據前向傳播,把式子都寫出來,方便計算:
在這裏插入圖片描述

假設上一層這個滑動窗口的最大值是outo11out_{o_{11}}
∵netm11=max(outo11,outo12,outo21,outo22)∴∂netm11∂outo11=1∂netm11∂outo12=∂netm11∂outo21=∂netm11∂outo22=0∴δl−111=∂E∂outo11=∂E∂netm11⋅∂netm11∂outo11=δl11δl−112=δl−121=δl−122=0(16)
這樣就求出了池化層的誤差敏感項矩陣。同理可以求出每個神經元的梯度並更新權重。

三、手寫一個卷積神經網絡

1.定義一個卷積層

首先我們通過ConvLayer來實現一個卷積層,定義卷積層的超參數

class ConvLayer(object):
    '''
    參數含義:
    input_width:輸入圖片尺寸——寬度
    input_height:輸入圖片尺寸——長度
    channel_number:通道數,彩色爲3,灰色爲1
    filter_width:卷積核的寬
    filter_height:卷積核的長
    filter_number:卷積核數量
    zero_padding:補零長度
    stride:步長
    activator:激活函數
    learning_rate:學習率
    '''
    def __init__(self, input_width, input_height,
                 channel_number, filter_width,
                 filter_height, filter_number,
                 zero_padding, stride, activator,
                 learning_rate):
        self.input_width = input_width
        self.input_height = input_height
        self.channel_number = channel_number
        self.filter_width = filter_width
        self.filter_height = filter_height
        self.filter_number = filter_number
        self.zero_padding = zero_padding
        self.stride = stride
        self.output_width = \
            ConvLayer.calculate_output_size(
            self.input_width, filter_width, zero_padding,
            stride)
        self.output_height = \
            ConvLayer.calculate_output_size(
            self.input_height, filter_height, zero_padding,
            stride)
        self.output_array = np.zeros((self.filter_number,
            self.output_height, self.output_width))
        self.filters = []
        for i in range(filter_number):
            self.filters.append(Filter(filter_width,
                filter_height, self.channel_number))
        self.activator = activator
        self.learning_rate = learning_rate

其中calculate_output_size用來計算通過卷積運算後輸出的feature_map大小

@staticmethod
     def calculate_output_size(input_size,
            filter_size, zero_padding, stride):
         return (input_size - filter_size +
             2 * zero_padding) / stride + 1

2.構造一個激活函數

此處用的是RELU激活函數,因此我們在activators.py裏定義,forward是前向計算,backforward是計算公式的導數:

class ReluActivator(object):
    def forward(self, weighted_input):
        #return weighted_input
        return max(0, weighted_input)

    def backward(self, output):
        return 1 if output > 0 else 0

其他常見的激活函數我們也可以放到activators裏,如sigmoid函數,我們可以做如下定義:

class SigmoidActivator(object):
    def forward(self, weighted_input):
        return 1.0 / (1.0 + np.exp(-weighted_input))
    #the partial of sigmoid
    def backward(self, output):
        return output * (1 - output)

如果我們需要自動以其他的激活函數,都可以在activator.py定義一個類即可。

3.定義一個類,保存卷積層的參數和梯度

class Filter(object):
    def __init__(self, width, height, depth):
        #初始權重
        self.weights = np.random.uniform(-1e-4, 1e-4,
            (depth, height, width))
        #初始偏置
        self.bias = 0
        self.weights_grad = np.zeros(
            self.weights.shape)
        self.bias_grad = 0

    def __repr__(self):
        return 'filter weights:\n%s\nbias:\n%s' % (
            repr(self.weights), repr(self.bias))

    def get_weights(self):
        return self.weights

    def get_bias(self):
        return self.bias

    def update(self, learning_rate):
        self.weights -= learning_rate * self.weights_grad
        self.bias -= learning_rate * self.bias_grad

4.卷積層的前向傳播

1).獲取卷積區域

# 獲取卷積區域
def get_patch(input_array, i, j, filter_width,
              filter_height, stride):
    '''
    從輸入數組中獲取本次卷積的區域,
    自動適配輸入爲2D和3D的情況
    '''
    start_i = i * stride
    start_j = j * stride
    if input_array.ndim == 2:
        input_array_conv = input_array[
            start_i : start_i + filter_height,
            start_j : start_j + filter_width]
        print "input_array_conv:",input_array_conv
        return input_array_conv

    elif input_array.ndim == 3:
        input_array_conv = input_array[:,
            start_i : start_i + filter_height,
            start_j : start_j + filter_width]
        print "input_array_conv:",input_array_conv
        return input_array_conv

2).進行卷積運算

def conv(input_array,
         kernel_array,
         output_array,
         stride, bias):
    '''
    計算卷積,自動適配輸入爲2D和3D的情況
    '''
    channel_number = input_array.ndim
    output_width = output_array.shape[1]
    output_height = output_array.shape[0]
    kernel_width = kernel_array.shape[-1]
    kernel_height = kernel_array.shape[-2]
    for i in range(output_height):
        for j in range(output_width):
            output_array[i][j] = (
                get_patch(input_array, i, j, kernel_width,
                    kernel_height, stride) * kernel_array
                ).sum() + bias

3).增加zero_padding

#增加Zero padding
def padding(input_array, zp):
    '''
    爲數組增加Zero padding,自動適配輸入爲2D和3D的情況
    '''
    if zp == 0:
        return input_array
    else:
        if input_array.ndim == 3:
            input_width = input_array.shape[2]
            input_height = input_array.shape[1]
            input_depth = input_array.shape[0]
            padded_array = np.zeros((
                input_depth,
                input_height + 2 * zp,
                input_width + 2 * zp))
            padded_array[:,
                zp : zp + input_height,
                zp : zp + input_width] = input_array
            return padded_array
        elif input_array.ndim == 2:
            input_width = input_array.shape[1]
            input_height = input_array.shape[0]
            padded_array = np.zeros((
                input_height + 2 * zp,
                input_width + 2 * zp))
            padded_array[zp : zp + input_height,
                zp : zp + input_width] = input_array
            return padded_array

4).進行前向傳播

def forward(self, input_array):
        '''
        計算卷積層的輸出
        輸出結果保存在self.output_array
        '''
        self.input_array = input_array
        self.padded_input_array = padding(input_array,
            self.zero_padding)
        for f in range(self.filter_number):
            filter = self.filters[f]
            conv(self.padded_input_array,
                filter.get_weights(), self.output_array[f],
                self.stride, filter.get_bias())
        element_wise_op(self.output_array,
                        self.activator.forward)

其中element_wise_op函數是將每個組的元素對應相乘

# 對numpy數組進行element wise操作,將矩陣中的每個元素對應相乘
def element_wise_op(array, op):
    for i in np.nditer(array,
                       op_flags=['readwrite']):
        i[...] = op(i)

5.卷積層的反向傳播

1).將誤差傳遞到上一層

def bp_sensitivity_map(self, sensitivity_array,
                           activator):
        '''
        計算傳遞到上一層的sensitivity map
        sensitivity_array: 本層的sensitivity map
        activator: 上一層的激活函數
        '''
        # 處理卷積步長,對原始sensitivity map進行擴展
        expanded_array = self.expand_sensitivity_map(
            sensitivity_array)
        # full卷積,對sensitivitiy map進行zero padding
        # 雖然原始輸入的zero padding單元也會獲得殘差
        # 但這個殘差不需要繼續向上傳遞,因此就不計算了
        expanded_width = expanded_array.shape[2]
        zp = (self.input_width +
              self.filter_width - 1 - expanded_width) / 2
        padded_array = padding(expanded_array, zp)
        # 初始化delta_array,用於保存傳遞到上一層的
        # sensitivity map
        self.delta_array = self.create_delta_array()
        # 對於具有多個filter的卷積層來說,最終傳遞到上一層的
        # sensitivity map相當於所有的filter的
        # sensitivity map之和
        for f in range(self.filter_number):
            filter = self.filters[f]
            # 將filter權重翻轉180度
            flipped_weights = np.array(map(
                lambda i: np.rot90(i, 2),
                filter.get_weights()))
            # 計算與一個filter對應的delta_array
            delta_array = self.create_delta_array()
            for d in range(delta_array.shape[0]):
                conv(padded_array[f], flipped_weights[d],
                    delta_array[d], 1, 0)
            self.delta_array += delta_array
        # 將計算結果與激活函數的偏導數做element-wise乘法操作
        derivative_array = np.array(self.input_array)
        element_wise_op(derivative_array,
                        activator.backward)
        self.delta_array *= derivative_array

2).保存傳遞到上一層的sensitivity map的數組

def create_delta_array(self):
        return np.zeros((self.channel_number,
            self.input_height, self.input_width))

3).計算代碼梯度

def bp_gradient(self, sensitivity_array):
        # 處理卷積步長,對原始sensitivity map進行擴展
        expanded_array = self.expand_sensitivity_map(
            sensitivity_array)
        for f in range(self.filter_number):
            # 計算每個權重的梯度
            filter = self.filters[f]
            for d in range(filter.weights.shape[0]):
                conv(self.padded_input_array[d],
                     expanded_array[f],
                     filter.weights_grad[d], 1, 0)
            # 計算偏置項的梯度
            filter.bias_grad = expanded_array[f].sum()

4).按照梯度下降法更新參數

def update(self):
        '''
        按照梯度下降,更新權重
        '''
        for filter in self.filters:
            filter.update(self.learning_rate)

6.MaxPooling層的訓練

1).定義MaxPooling類

class MaxPoolingLayer(object):
    def __init__(self, input_width, input_height,
                 channel_number, filter_width,
                 filter_height, stride):
        self.input_width = input_width
        self.input_height = input_height
        self.channel_number = channel_number
        self.filter_width = filter_width
        self.filter_height = filter_height
        self.stride = stride
        self.output_width = (input_width -
            filter_width) / self.stride + 1
        self.output_height = (input_height -
            filter_height) / self.stride + 1
        self.output_array = np.zeros((self.channel_number,
            self.output_height, self.output_width))

2).前向傳播計算

# 前向傳播
    def forward(self, input_array):
        for d in range(self.channel_number):
            for i in range(self.output_height):
                for j in range(self.output_width):
                    self.output_array[d,i,j] = (
                        get_patch(input_array[d], i, j,
                            self.filter_width,
                            self.filter_height,
                            self.stride).max())

3).反向傳播計算

#反向傳播
    def backward(self, input_array, sensitivity_array):
        self.delta_array = np.zeros(input_array.shape)
        for d in range(self.channel_number):
            for i in range(self.output_height):
                for j in range(self.output_width):
                    patch_array = get_patch(
                        input_array[d], i, j,
                        self.filter_width,
                        self.filter_height,
                        self.stride)
                    k, l = get_max_index(patch_array)
                    self.delta_array[d,
                        i * self.stride + k,
                        j * self.stride + l] = \
                        sensitivity_array[d,i,j]

完整代碼公衆號邁微電子研發社,後臺回覆"手寫字識別"獲取,請見:[cnn.py]

#coding:utf-8
'''
Created by huxiaoman 2017.11.22

'''

import numpy as np
from activators import ReluActivator,IdentityActivator

class ConvLayer(object):
    def __init__(self,input_width,input_weight,
             channel_number,filter_width,
             filter_height,filter_number,
             zero_padding,stride,activator,
             learning_rate):
        self.input_width = input_width
        self.input_height = input_height
        self.channel_number = channel_number
        self.filter_width = filter_width
        self.filter_height = filter_height
        self.filter_number = filter_number
        self.zero_padding = zero_padding
        self.stride = stride #此處可以加上stride_x, stride_y
        self.output_width = ConvLayer.calculate_output_size(
                self.input_width,filter_width,zero_padding,
                stride)
        self.output_height = ConvLayer.calculate_output_size(
                self.input_height,filter_height,zero_padding,
                stride)
        self.output_array = np.zeros((self.filter_number,
                self.output_height,self.output_width))
        self.filters = []
        for i in range(filter_number):    
            self.filters.append(Filter(filter_width,
                filter_height,self.channel_number))
        self.activator = activator
        self.learning_rate = learning_rate
    def forward(self,input_array):
        '''
        計算卷積層的輸出
        輸出結果保存在self.output_array
        '''
        self.input_array = input_array
        self.padded_input_array = padding(input_array,
            self.zero_padding)
        for i in range(self.filter_number):
            filter = self.filters[f]
            conv(self.padded_input_array,
                 filter.get_weights(), self.output_array[f],
                 self.stride, filter.get_bias())
            element_wise_op(self.output_array,
                    self.activator.forward)

def get_batch(input_array, i, j, filter_width,filter_height,stride):
    '''
    從輸入數組中獲取本次卷積的區域,
    自動適配輸入爲2D和3D的情況
    '''
    start_i = i * stride
    start_j = j * stride
    if input_array.ndim == 2:
        return input_array[
            start_i : start_i + filter_height,
            start_j : start_j + filter_width]
    elif input_array.ndim == 3:
        return input_array[
            start_i : start_i + filter_height,
                        start_j : start_j + filter_width]

# 獲取一個2D區域的最大值所在的索引
def get_max_index(array):
    max_i = 0
    max_j = 0
    max_value = array[0,0]
    for i in range(array.shape[0]):
        for j in range(array.shape[1]):
            if array[i,j] > max_value:
                max_value = array[i,j]
                max_i, max_j = i, j
    return max_i, max_j

def conv(input_array,kernal_array,
    output_array,stride,bias):
    '''
    計算卷積,自動適配輸入2D,3D的情況
    '''
    channel_number = input_array.ndim
    output_width = output_array.shape[1]
    output_height = output_array.shape[0]
    kernel_width = kernel_array.shape[-1]
    kernel_height = kernel_array.shape[-2]
    for i in range(output_height):
        for j in range(output_width):
            output_array[i][j] = (
                get_patch(input_array, i, j, kernel_width,
                    kernel_height,stride) * kernel_array).sum() +bias


def element_wise_op(array, op):
    for i in np.nditer(array,
               op_flags = ['readwrite']):
        i[...] = op(i)


class ReluActivators(object):
    def forward(self, weighted_input):
        # Relu計算公式 = max(0,input)
        return max(0, weighted_input)

    def backward(self,output):
        return 1 if output > 0 else 0

class SigmoidActivator(object):
        
    def forward(self,weighted_input):
        return 1 / (1 + math.exp(- weighted_input))
    
    def backward(self,output):
        return output * (1 - output)

最後,我們用之前的444 * 4的image數據檢驗一下通過一次卷積神經網絡進行前向傳播和反向傳播後的輸出結果:

def init_test():
    a = np.array(
        [[[0,1,1,0,2],
          [2,2,2,2,1],
          [1,0,0,2,0],
          [0,1,1,0,0],
          [1,2,0,0,2]],
         [[1,0,2,2,0],
          [0,0,0,2,0],
          [1,2,1,2,1],
          [1,0,0,0,0],
          [1,2,1,1,1]],
         [[2,1,2,0,0],
          [1,0,0,1,0],
          [0,2,1,0,1],
          [0,1,2,2,2],
          [2,1,0,0,1]]])
    b = np.array(
        [[[0,1,1],
          [2,2,2],
          [1,0,0]],
         [[1,0,2],
          [0,0,0],
          [1,2,1]]])
    cl = ConvLayer(5,5,3,3,3,2,1,2,IdentityActivator(),0.001)
    cl.filters[0].weights = np.array(
        [[[-1,1,0],
          [0,1,0],
          [0,1,1]],
         [[-1,-1,0],
          [0,0,0],
          [0,-1,0]],
         [[0,0,-1],
          [0,1,0],
          [1,-1,-1]]], dtype=np.float64)
    cl.filters[0].bias=1
    cl.filters[1].weights = np.array(
        [[[1,1,-1],
          [-1,-1,1],
          [0,-1,1]],
         [[0,1,0],
         [-1,0,-1],
          [-1,1,0]],
         [[-1,0,0],
          [-1,0,1],
          [-1,0,0]]], dtype=np.float64)
    return a, b, cl

運行一下:

def test():
    a, b, cl = init_test()
    cl.forward(a)
    print "前向傳播結果:", cl.output_array
    cl.backward(a, b, IdentityActivator())
    cl.update()
    print "反向傳播後更新得到的filter1:",cl.filters[0]
    print "反向傳播後更新得到的filter2:",cl.filters[1]

if __name__ == "__main__":
        test()

運行結果:

前向傳播結果: [[[ 6.  7.  5.]
  [ 3. -1. -1.]
  [ 2. -1.  4.]]

 [[ 2. -5. -8.]
  [ 1. -4. -4.]
  [ 0. -5. -5.]]]
反向傳播後更新得到的filter1: filter weights:
array([[[-1.008,  0.99 , -0.009],
        [-0.005,  0.994, -0.006],
        [-0.006,  0.995,  0.996]],

       [[-1.004, -1.001, -0.004],
        [-0.01 , -0.009, -0.012],
        [-0.002, -1.002, -0.002]],

       [[-0.002, -0.002, -1.003],
        [-0.005,  0.992, -0.005],
        [ 0.993, -1.008, -1.007]]])
bias:
0.99099999999999999
反向傳播後更新得到的filter2: filter weights:
array([[[  9.98000000e-01,   9.98000000e-01,  -1.00100000e+00],
        [ -1.00400000e+00,  -1.00700000e+00,   9.97000000e-01],
        [ -4.00000000e-03,  -1.00400000e+00,   9.98000000e-01]],

       [[  0.00000000e+00,   9.99000000e-01,   0.00000000e+00],
        [ -1.00900000e+00,  -5.00000000e-03,  -1.00400000e+00],
        [ -1.00400000e+00,   1.00000000e+00,   0.00000000e+00]],

       [[ -1.00400000e+00,  -6.00000000e-03,  -5.00000000e-03],
        [ -1.00200000e+00,  -5.00000000e-03,   9.98000000e-01],
        [ -1.00200000e+00,  -1.00000000e-03,   0.00000000e+00]]])
bias:
-0.0070000000000000001

四、PaddlePaddle卷積神經網絡源碼解析

卷積層

在上篇文章中,我們對paddlepaddle實現卷積神經網絡的的函數簡單介紹了一下。在手寫數字識別中,我們設計CNN的網絡結構時,調用了一個函數simple_img_conv_pool(上篇文章的鏈接已失效,因爲已經把framework—>fluid,更新速度太快了 = =)使用方式如下:

conv_pool_1 = paddle.networks.simple_img_conv_pool(
        input=img,
        filter_size=5,
        num_filters=20,
        num_channel=1,
        pool_size=2,
        pool_stride=2,
        act=paddle.activation.Relu())

這個函數把卷積層和池化層兩個部分封裝在一起,只用調用一個函數就可以搞定,非常方便。如果只需要單獨使用卷積層,可以調用這個函數img_conv_layer,使用方式如下:

conv = img_conv_layer(input=data, filter_size=1, filter_size_y=1,
                              num_channels=8,
                              num_filters=16, stride=1,
                              bias_attr=False,
                              act=ReluActivation())

我們來看一下這個函數具體有哪些參數(註釋寫明瞭參數的含義和怎麼使用)

def img_conv_layer(input,
                   filter_size,
                   num_filters,
                   name=None,
                   num_channels=None,
                   act=None,
                   groups=1,
                   stride=1,
                   padding=0,
                   dilation=1,
                   bias_attr=None,
                   param_attr=None,
                   shared_biases=True,
                   layer_attr=None,
                   filter_size_y=None,
                   stride_y=None,
                   padding_y=None,
                   dilation_y=None,
                   trans=False,
                   layer_type=None):
    """
    適合圖像的卷積層。Paddle可以支持正方形和長方形兩種圖片尺寸的輸入
    
    也可適用於圖像的反捲積(Convolutional Transpose,即deconv)。
    同樣可支持正方形和長方形兩種尺寸輸入。

    num_channel:輸入圖片的通道數。可以是1或者3,或者是上一層的通道數(卷積核數目 * 組的數量)
    每一個組都會處理圖片的一些通道。舉個例子,如果一個輸入如偏的num_channel是256,設置4個group,
    32個卷積核,那麼會創建32*4 = 128個卷積核來處理輸入圖片。通道會被分成四塊,32個卷積核會先
    處理64(256/4=64)個通道。剩下的卷積核組會處理剩下的通道。

    name:層的名字。可選,自定義。
    type:basestring

    input:這個層的輸入
    type:LayerOutPut

    filter_size:卷積核的x維,可以理解爲width。
                如果是正方形,可以直接輸入一個元祖組表示圖片的尺寸
    type:int/ tuple/ list

    filter_size_y:卷積核的y維,可以理解爲height。
                PaddlePaddle支持長方形的圖片尺寸,所以卷積核的尺寸爲(filter_size,filter_size_y)

    type:int/ None

    act: 激活函數類型。默認選Relu
    type:BaseActivation

    groups:卷積核的組數量
    type:int
    

    stride: 水平方向的滑動步長。或者世界輸入一個元祖,代表水平數值滑動步長相同。
    type:int/ tuple/ list

    stride_y:垂直滑動步長。
    type:int 
    
    padding: 補零的水平維度,也可以直接輸入一個元祖,水平和垂直方向上補零的維度相同。
    type:int/ tuple/ list

    padding_y:垂直方向補零的維度
    type:int

    dilation:水平方向的擴展維度。同樣可以輸入一個元祖表示水平和初值上擴展維度相同
    :type:int/ tuple/ list

    dilation_y:垂直方向的擴展維度
    type:int

    bias_attr:偏置屬性
              False:不定義bias   True:bias初始化爲0
    type: ParameterAttribute/ None/ bool/ Any

    num_channel:輸入圖片的通道channel。如果設置爲None,自動生成爲上層輸出的通道數
    type: int

    param_attr:卷積參數屬性。設置爲None表示默認屬性
    param_attr:ParameterAttribute

    shared_bias:設置偏置項是否會在卷積核中共享
    type:bool

    layer_attr: Layer的 Extra Attribute
    type:ExtraLayerAttribute

    param trans:如果是convTransLayer,設置爲True,如果是convlayer設置爲conv
    type:bool

    layer_type:明確layer_type,默認爲None。
               如果trans= True,必須是exconvt或者cudnn_convt,否則的話要麼是exconv,要麼是cudnn_conv
               ps:如果是默認的話,paddle會自動選擇適合cpu的ExpandConvLayer和適合GPU的CudnnConvLayer
               當然,我們自己也可以明確選擇哪種類型
    type:string
    return:LayerOutput object
    rtype:LayerOutput

    """


def img_conv_layer(input,
                   filter_size,
                   num_filters,
                   name=None,
                   num_channels=None,
                   act=None,
                   groups=1,
                   stride=1,
                   padding=0,
                   dilation=1,
                   bias_attr=None,
                   param_attr=None,
                   shared_biases=True,
                   layer_attr=None,
                   filter_size_y=None,
                   stride_y=None,
                   padding_y=None,
                   dilation_y=None,
                   trans=False,
                   layer_type=None):

    if num_channels is None:
        assert input.num_filters is not None
        num_channels = input.num_filters

    if filter_size_y is None:
        if isinstance(filter_size, collections.Sequence):
            assert len(filter_size) == 2
            filter_size, filter_size_y = filter_size
        else:
            filter_size_y = filter_size

    if stride_y is None:
        if isinstance(stride, collections.Sequence):
            assert len(stride) == 2
            stride, stride_y = stride
        else:
            stride_y = stride

    if padding_y is None:
        if isinstance(padding, collections.Sequence):
            assert len(padding) == 2
            padding, padding_y = padding
        else:
            padding_y = padding

    if dilation_y is None:
        if isinstance(dilation, collections.Sequence):
            assert len(dilation) == 2
            dilation, dilation_y = dilation
        else:
            dilation_y = dilation

    if param_attr.attr.get('initial_smart'):
        # special initial for conv layers.
        init_w = (2.0 / (filter_size**2 * num_channels))**0.5
        param_attr.attr["initial_mean"] = 0.0
        param_attr.attr["initial_std"] = init_w
        param_attr.attr["initial_strategy"] = 0
        param_attr.attr["initial_smart"] = False

    if layer_type:
        if dilation > 1 or dilation_y > 1:
            assert layer_type in [
                "cudnn_conv", "cudnn_convt", "exconv", "exconvt"
            ]
        if trans:
            assert layer_type in ["exconvt", "cudnn_convt"]
        else:
            assert layer_type in ["exconv", "cudnn_conv"]
        lt = layer_type
    else:
        lt = LayerType.CONVTRANS_LAYER if trans else LayerType.CONV_LAYER

    l = Layer(
        name=name,
        inputs=Input(
            input.name,
            conv=Conv(
                filter_size=filter_size,
                padding=padding,
                dilation=dilation,
                stride=stride,
                channels=num_channels,
                groups=groups,
                filter_size_y=filter_size_y,
                padding_y=padding_y,
                dilation_y=dilation_y,
                stride_y=stride_y),
            **param_attr.attr),
        active_type=act.name,
        num_filters=num_filters,
        bias=ParamAttr.to_bias(bias_attr),
        shared_biases=shared_biases,
        type=lt,
        **ExtraLayerAttribute.to_kwargs(layer_attr))
    return LayerOutput(
        name,
        lt,
        parents=[input],
        activation=act,
        num_filters=num_filters,
        size=l.config.size)

我們瞭解這些參數的含義後,對比我們之前自己手寫的CNN,可以看出paddlepaddle有幾個優點:

  • 支持長方形和正方形的圖片尺寸
  • 支持滑動步長stride、補零zero_padding、擴展dilation在水平和垂直方向上設置不同的值
  • 支持偏置項卷積核中能夠共享
  • 自動適配cpu和gpu的卷積網絡

在我們自己寫的CNN中,只支持正方形的圖片長度,如果是長方形會報錯。滑動步長,補零的維度等也只支持水平和垂直方向上的維度相同。瞭解卷積層的參數含義後,我們來看一下底層的源碼是如何實現的:ConvBaseLayer.py 有興趣的同學可以在這個鏈接下看看底層是如何用C++寫的ConvLayer

池化層同理,可以按照之前的思路分析,有興趣的可以一直順延看到底層的實現,下次有機會再詳細分析。(佔坑明天補一下tensorflow的源碼實現)

總結

本文主要講解了卷積神經網絡中反向傳播的一些技巧,包括卷積層和池化層的反向傳播與傳統的反向傳播的區別,並實現了一個完整的CNN,後續大家可以自己修改一些代碼,譬如當水平滑動長度與垂直滑動長度不同時需要怎麼調整等等,最後研究了一下paddlepaddle中CNN中的卷積層的實現過程,對比自己寫的CNN,總結了4個優點,底層是C++實現的,有興趣的可以自己再去深入研究。寫的比較粗糙,如果有問題歡迎留言:)

參考文章:

1.https://www.cnblogs.com/pinard/p/6494810.html

2.https://www.zybuluo.com/hanbingtao/note/476663

推薦文章

[1] 機器學習算法之——走近卷積神經網絡(CNN)
[2] 機器學習算法之——卷積神經網絡(CNN)原理講解
[3] 卷積神經網絡中十大拍案叫絕的操作
[4] 機器學習算法之——梯度提升(Gradient Boosting) 算法講解及Python實現
[5] 機器學習算法之——邏輯迴歸(Logistic Regression)
[6] 機器學習算法之——決策樹模型(Decision Tree Model)算法講解及Python實現
[7] 機器學習算法之——K最近鄰(k-Nearest Neighbor,KNN)分類算法原理講解
[8] 機器學習算法之——K最近鄰(k-Nearest Neighbor,KNN)算法Python實現

傳送門
在這裏插入圖片描述

關注微信公衆號:邁微電子研發社,回覆 “深度學習實用教程” 獲取Github開源項目,回覆“手寫字識別”獲取本文的完整代碼。

在這裏插入圖片描述

△微信掃一掃關注「邁微電子研發社」公衆號

知識星球:社羣旨在分享AI算法崗的秋招/春招準備攻略(含刷題)、面經和內推機會、學習路線、知識題庫等。

在這裏插入圖片描述

△掃碼加入「邁微電子研發社」學習輔導羣

在這裏插入圖片描述

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