神經(jīng)網(wǎng)絡(luò)的Python實(shí)現(xiàn)(三)卷積神經(jīng)網(wǎng)絡(luò)

推薦在我的博客中給我留言抡爹,這樣我會(huì)隨時(shí)收到你的評論,并作出回復(fù)。


在上一篇神經(jīng)網(wǎng)絡(luò)的Python實(shí)現(xiàn)(二)全連接網(wǎng)絡(luò)中肾档,已經(jīng)介紹了神經(jīng)網(wǎng)絡(luò)的部分激活函數(shù),損失函數(shù)和全連接網(wǎng)絡(luò)的前饋和反向傳播公式及Numpy實(shí)現(xiàn)辫继。這篇博文將要詳細(xì)介紹卷積神經(jīng)網(wǎng)絡(luò)的概念怒见,并且進(jìn)行前饋和反向傳播的公式推導(dǎo)及Numpy實(shí)現(xiàn)。

卷積神經(jīng)網(wǎng)絡(luò)

卷積神經(jīng)網(wǎng)絡(luò)(Convolutional Neural Network)非常擅于處理圖像任務(wù)姑宽,它的靈感來自于視覺神經(jīng)中的感受野這一概念遣耍,卷積神經(jīng)網(wǎng)絡(luò)的卷積核(Convolution Kernel) 好似感受野一樣去掃描數(shù)據(jù)。一個(gè)卷積神經(jīng)網(wǎng)絡(luò)基本包括卷積層炮车、池化層輸出層舵变。

接下來介紹什么是卷積核酣溃、卷積神經(jīng)網(wǎng)絡(luò)中的卷積是怎么運(yùn)算的。

卷積和卷積核

卷積神經(jīng)網(wǎng)絡(luò)中的卷積操作與數(shù)學(xué)中的類似纪隙。就是輸入數(shù)據(jù)中不同數(shù)據(jù)窗口的數(shù)據(jù)和卷積核(一個(gè)權(quán)值矩陣)作內(nèi)積的操作赊豌。其中卷積核是卷積神經(jīng)網(wǎng)絡(luò)中卷積層的最重要的部分。卷積核相當(dāng)于信息處理中的濾波器绵咱,可以提取輸入數(shù)據(jù)的當(dāng)前特征碘饼。卷積核的實(shí)質(zhì)是一個(gè)權(quán)值矩陣,在下圖中的卷積核便是一個(gè)權(quán)值如下的矩陣(圖中黃色色塊中的紅色數(shù)字)

\left[ \begin{matrix} 1 & 0 & 1\\ 0 & 1 & 0\\ 1 & 0 & 1 \end{matrix} \right]

卷積

如果并不理解卷積悲伶,那么我們來看圖中輸出的第一行第一列的4是怎么得到的艾恼。

原輸入數(shù)據(jù)大小為5\times5,我們要使用3\times3的卷積核來進(jìn)行卷積麸锉,我們使用\ast表示卷積操作钠绍。那么圖中第一個(gè)4的運(yùn)算過程就可以表達(dá)為:

\left[ \begin{matrix} \color{red}{1} & 1 & 1\\ 0 & 1 & 1\\ 0 & 0 & 1 \end{matrix} \right] * \left[ \begin{matrix} \color{blue}{1} & 0 & 1\\ 0 & 1 & 0\\ 1 & 0 & 1 \end{matrix} \right]= \color{red}{1}\times\color{blue}{1} + 1\times0+1 \times 1+ 0\times0+1\times1+1\times0+ 0\times1+0\times0 +1\times1=4

剩下位置的輸出就是卷積核在輸入矩陣上從左到右從上到下移動(dòng)一格做如上卷積操作過程的結(jié)果。

步長(strides)和填充(padding)

步長

上面例子說到的一格表示的就是步長(strides)花沉,步長分為橫向步長和縱向步長柳爽,步長是多少就表示一次卷積操作之后卷積核移動(dòng)的距離。知道步長的概念了主穗,我們就可以去計(jì)算一下根據(jù)輸入大小泻拦,卷積核大小,我們得到的輸出的大小忽媒。假設(shè)用C表示邊長,那么:

C_{output} = \frac{C_{input}-C_{kernel}}{strides}+1

根據(jù)公式當(dāng)步長為1或是輸入大小能夠被步長整除時(shí)很好處理争拐,無法整除時(shí)也就是卷積核移動(dòng)到最后,輸入數(shù)據(jù)的剩下的部分不足卷積核大小晦雨,這時(shí)我們會(huì)想到要么將輸入變大點(diǎn)讓它能夠整除要么是干脆邊界直接丟棄讓它能夠整除架曹。這兩種處理辦法對應(yīng)于填充(padding) 的兩種方式,'SAME''VALID'闹瞧。

VALID

O_w = ceil\left(\frac{I_w-k_w+1}{s_w}\right)\\ O_h = ceil\left(\frac{I_h-k_h+1}{s_h}\right)\\

其中ceil向上取整,w是寬方向绑雄,h是長方向, I,O,k,s分別代表輸入、輸出奥邮、卷積核和步長万牺。

超過O_w,O_h部分就舍棄不要了。

真正輸入大小 I_w = s_w(O_w-1)+k_w\\I_h = s_h(O_h-1)+k_h

SAME

same

SAME就是在輸入周圍補(bǔ)0洽腺,我們先計(jì)算補(bǔ)0后的輸出大薪潘凇:

O_w = ceil\left(\frac{I_w}{s_w}\right)\\ O_h = ceil\left(\frac{I_h}{s_h}\right)

接下來便根據(jù)應(yīng)得到輸出的大小去padding。

\begin{aligned} P_h &= \max \left((O_h-1)\times s_h + k_h-I_h\ \ ,\ 0\right)\\ P_w &= \max \left((O_w-1)\times s_w + k_w-I_w\ \ ,\ 0\right)\\ P_{top} &= floor\left(\frac{P_h}{2}\right) \ \ \ \ \ \ \ \ P_{bottom} = P_h - P_{top} \\ P_{left} &= floor\left(\frac{P_w}{2}\right) \ \ \ \ \ \ \ \ P_{right} = P_w - P_{left} \end{aligned}

其中 floor向下取整蘸朋。

這樣0就幾乎對稱地分布在輸入四周核无。

多通道的卷積

一般卷積神經(jīng)網(wǎng)絡(luò)處理的都是3通道或是多通道的圖像數(shù)據(jù),那么對于多通道如何卷積呢藕坯?對于多通道团南,卷積公式并不變噪沙,只是要求卷積核通道與輸入通道數(shù)一致,不同通道分別做內(nèi)積吐根,然后不同通道得到的值相加起來作為最后的輸出正歼。如圖。

channels.gif

對于計(jì)算拷橘,我們使用2\times2\times2的輸入和2\times2\times2的卷積核舉個(gè)例子:

\begin{aligned} &\ \ \ \ \left[ \begin{matrix} \left[ \begin{matrix} 1 & 2\\ 3 & 4 \end{matrix} \right], \left[ \begin{matrix} 5 & 6\\ 7 & 8 \end{matrix} \right] \end{matrix} \right] *\left[ \begin{matrix} \left[ \begin{matrix} 1 & 2\\ 3 & 4 \end{matrix} \right], \left[ \begin{matrix} 5 & 6\\ 7 & 8 \end{matrix} \right] \end{matrix} \right] \\ \\ &=\left[ \begin{matrix} 1\times1+5\times5 & 2 \times 2+6 \times 6 \\ 3\times3+7\times7 & 4 \times4+8\times8 \end{matrix} \right]\\ \\ &=\left[ \begin{matrix} 26 & 40\\ 58 & 80 \end{matrix} \right] \end{aligned}

可以看到朋腋,不論輸入通道數(shù)是多少最后的輸出仍是一個(gè)矩陣。在卷積層如果有多個(gè)卷積核膜楷,每個(gè)卷積核會(huì)提取一種特征,輸出一個(gè)二維矩陣贞奋。最終的結(jié)果就是把這些卷積核的輸出看作不同通道赌厅。如下圖。

channels2.jpg

下面以圖像處理為例轿塔,來看一下卷積神經(jīng)網(wǎng)絡(luò)的前饋和反向傳播特愿。

前向傳播

卷積層的前向傳播方式與全連接層類似,我們回顧一下全連接層的前向傳播:

\begin{align} a^{(l)} &= \sigma(z^{l})=\sigma(W^{(l)} a^{(l-1)} + b^{(l)}) \end{align}

卷積層只不過把全連接層的矩陣乘法運(yùn)算換成了卷積運(yùn)算勾缭。詳細(xì)的步驟如下
知道前一層的輸出之后:

  1. 定義好卷積核數(shù)目揍障,卷積核大小,步長和填充方式俩由。根據(jù)輸入大小毒嫡,計(jì)算輸出大小并進(jìn)行相應(yīng)的padding,得到了卷積層的輸入 a^{l-1} 幻梯。
  2. 初始化所有卷積和的權(quán)重 W 和偏置 b
  3. 根據(jù)前向傳播的公式(M個(gè)通道):
    a^l= \sigma(z^l) = \sigma(\sum\limits_{k=1}^{M}z_k^l) = \sigma(\sum\limits_{k=1}^{M}a_k^{l-1}*W_k^l +b^l)
    a^l= \sigma(z^l) = \sigma(W^la^{l-1} +b^l)
    計(jì)算出卷積層輸出兜畸,其中*是卷積運(yùn)算、\sigma是激活函數(shù)碘梢。

反向傳播

現(xiàn)在已知卷積層的 \delta^l 咬摇,我們通過反向傳播算法來計(jì)算上一層的 \delta^{l-1}
我們也先回顧一下反向傳播公式煞躬,根據(jù)鏈?zhǔn)椒▌t:

\delta^{l-1} = \frac{\partial J(W,b)}{\partial z^{l-1}} = \frac{\partial J(W,b)}{\partial z^{l}}\frac{\partial z^{l}}{\partial z^{l-1}} = \delta^{l}\frac{\partial z^{l}}{\partial z^{l-1}}

要計(jì)算\delta^l的值肛鹏,必須知道\frac{\partial z^{l}}{\partial z^{l-1}}的值,所以根據(jù)前向傳播公式:

a^l= \sigma(z^l) = \sigma(a^{l-1}*W^l +b^l)

這里我們將 z^{l}z^{l-1} 拿出來看:

z^l = \sigma(z^{l-1})*W^l+b^l

現(xiàn)在就差卷積運(yùn)算的偏導(dǎo)該如何求恩沛,我們先把正確公式寫出來在扰,之后再解釋:

\delta^{l-1} = \delta^{l}\frac{\partial z^{l}}{\partial z^{l-1}} = \delta^{l}*rot180(W^{l}) \odot \sigma^{'}(z^{l-1})

這里的 rot180(W^l) 表示將卷積核旋轉(zhuǎn)180°,即卷積核左右翻轉(zhuǎn)之后再上下翻轉(zhuǎn)复唤〗√铮可以拿張正反內(nèi)容不一樣的紙轉(zhuǎn)一轉(zhuǎn)。然后我們解釋為什么卷積的求導(dǎo)就是將卷積核旋轉(zhuǎn)180°再做卷積的結(jié)果佛纫。

我們拿 3\times 3 大小矩陣作為例子妓局,卷積核大小為 2\times 2总放,步長為1(步長不是1時(shí)后面會(huì)提到):

\left[ \begin{array}{ccc} a_{11}&a_{12}&a_{13} \\ a_{21}&a_{22}&a_{23}\\ a_{31}&a_{32}&a_{33} \end{array} \right] * \left[ \begin{array}{ccc} w_{11}&w_{12}\\ w_{21}&w_{22} \end{array} \right] = \left[ \begin{array}{ccc} z_{11}&z_{12}\\ z_{21}&z_{22} \end{array} \right]

上面是前向的卷積運(yùn)算,我們把它展開來:

\begin{aligned} z_{11} &= a_{11}w_{11} + a_{12}w_{12} + a_{21}w_{21} + \color{red}{a_{22}}w_{22}\\ z_{12} &= a_{12}w_{11} + a_{13}w_{12} + \color{red}{a_{22}}w_{21} + a_{23}w_{22}\\ z_{21} &= a_{21}w_{11} + \color{red}{a_{22}}w_{12} + a_{31}w_{21} + a_{32}w_{22}\\ z_{22} &= \color{red}{a_{22}}w_{11} + a_{23}w_{12} + a_{32}w_{21} + a_{33}w_{22} \end{aligned}

這樣就變成了簡單的運(yùn)算好爬,根據(jù)反向傳播公式:

\nabla a^{l-1} = \frac{\partial J(W,b)}{\partial a^{l-1}} = \frac{\partial J(W,b)}{\partial z^{l}} \frac{\partial z^{l}}{\partial a^{l-1}} = \delta^{l} \frac{\partial z^{l}}{\partial a^{l-1}}

我們就要對每個(gè) a 求其梯度:

比如 a_{11} 只和 z_{11} 有關(guān)局雄,所以:

\nabla a_{11} = \delta_{11}w_{11}

復(fù)雜點(diǎn)的對于為了明顯標(biāo)紅的 a_{22} ,它與 z_{11},z_{12},z_{21},z_{22} 都有關(guān)存炮,所以:

\nabla a_{22} = \delta_{11}w_{22} + \delta_{12}w_{21} + \delta_{21}w_{12} + \delta_{22}w_{11}

類似地我們把所有的 \nabla a 都求出來:

\begin{aligned} \nabla a_{11} &= \delta_{11}w_{11}\\ \nabla a_{12} &= \delta_{11}w_{12} + \delta_{12}w_{11}\\ \nabla a_{13} &= \delta_{12}w_{12}\\ \nabla a_{21} &= \delta_{11}w_{21} + \delta_{21}w_{11}\\ \nabla a_{22} &= \delta_{11}w_{22} + \delta_{12}w_{21} + \delta_{21}w_{12} + \delta_{22}w_{11}\\ \nabla a_{23} &= \delta_{12}w_{22} + \delta_{22}w_{12}\\ \nabla a_{31} &= \delta_{21}w_{21}\\ \nabla a_{32} &= \delta_{21}w_{22} + \delta_{22}w_{21}\\ \nabla a_{33} &= \delta_{22}w_{22} \end{aligned}

如果你嘗試過padding過的卷積運(yùn)算炬搭,你會(huì)發(fā)現(xiàn)上面的式子就是下面列出的卷積(步長為1):

\left[ \begin{array}{ccc} 0&0&0&0 \\ 0&\delta_{11}& \delta_{12}&0 \\ 0&\delta_{21}&\delta_{22}&0 \\ 0&0&0&0 \end{array} \right] * \left[ \begin{array}{ccc} w_{22}&w_{21}\\ w_{12}&w_{11} \end{array} \right] = \left[ \begin{array}{ccc} \nabla a_{11}&\nabla a_{12}&\nabla a_{13} \\ \nabla a_{21}&\nabla a_{22}&\nabla a_{23}\\ \nabla a_{31}&\nabla a_{32}&\nabla a_{33} \end{array} \right]

這里最好動(dòng)手寫一下就可以發(fā)現(xiàn)這種運(yùn)算關(guān)系。(這里的周圍的0填充寬度是卷積核邊長-1)

以上是步長為1時(shí)的求解過程穆桂,當(dāng)步長大于1時(shí)宫盔,我們就需要在 \delta 矩陣的值之間填充0來實(shí)現(xiàn)步長。

先說結(jié)論享完,每兩個(gè) \delta 之間需要填充步長-1個(gè)0(對應(yīng)方向上的)灼芭。也舉個(gè)例子來看看是不是這樣,步長為2般又。

\left[ \begin{matrix} a_{11} & a_{12} & a_{13} & a_{14}\\ a_{21} & a_{22} & a_{23} & a_{24}\\ a_{31} & a_{32} & a_{33} & a_{34}\\ a_{41} & a_{42} & a_{43} & a_{44} \end{matrix} \right] * \left[ \begin{matrix} w_{11} & w_{12} \\ w_{21} & w_{22} \end{matrix} \right] = \left[ \begin{matrix} z_{11} & z_{12} \\ z_{21} & z_{22} \end{matrix} \right]

展開來:

\begin{aligned} z_{11} &= a_{11}w_{11} + a_{12}w_{12} + a_{21}w_{21} + a_{22}w_{22}\\ z_{12} &= a_{13}w_{11} + a_{14}w_{12} + a_{23}w_{21} + a_{24}w_{22}\\ z_{21} &= a_{31}w_{11} + a_{32}w_{12} + a_{41}w_{21} + a_{42}w_{22}\\ z_{22} &= a_{33}w_{11} + a_{34}w_{12} + a_{43}w_{21} + a_{44}w_{22} \end{aligned}

計(jì)算梯度:

\begin{aligned} \nabla a_{11} &= \delta_{11}w_{11}\ \ \ \nabla a_{12} = \delta_{11}w_{12}\ \ \ \nabla a_{13} = \delta_{12}w_{11}\ \ \ \nabla a_{14} = \delta_{12}w_{12}\\ \nabla a_{21} &= \delta_{11}w_{21}\ \ \ \nabla a_{22} = \delta_{11}w_{22}\ \ \ \nabla a_{23} = \delta_{12}w_{21}\ \ \ \nabla a_{24} = \delta_{12}w_{22}\\ \nabla a_{31} &= \delta_{21}w_{11}\ \ \ \nabla a_{32} = \delta_{21}w_{12}\ \ \ \nabla a_{33} = \delta_{22}w_{11}\ \ \ \nabla a_{34} = \delta_{22}w_{12}\\ \nabla a_{41} &= \delta_{21}w_{21}\ \ \ \nabla a_{42} = \delta_{21}w_{22}\ \ \ \nabla a_{43} = \delta_{22}w_{21}\ \ \ \nabla a_{44} = \delta_{22}w_{22} \end{aligned}

即:

\left[ \begin{matrix} 0 & 0 & 0 & 0 & 0 \\ 0 & \delta_{11} & 0 & \delta_{12} & 0 \\ 0 & 0 & 0 & 0 & 0 \\ 0 &\delta_{21} & 0 & \delta_{22} & 0\\ 0 & 0 & 0 & 0 & 0 \end{matrix} \right] * \left[ \begin{matrix} w_{22} & w_{21} \\ w_{12} & w_{11} \end{matrix} \right] = \left[ \begin{matrix} \nabla a_{11} & \nabla a_{12} & \nabla a_{13} & \nabla a_{14}\\ \nabla a_{21} & \nabla a_{22} & \nabla a_{23} & \nabla a_{24}\\ \nabla a_{31} & \nabla a_{32} & \nabla a_{33} & \nabla a_{34}\\ \nabla a_{41} & \nabla a_{42} & \nabla a_{43} & \nabla a_{44} \end{matrix} \right]

:無論前向步長為多少彼绷,旋轉(zhuǎn)后的卷積步長一直是1。

其余計(jì)算過程類比全連接層是一樣的茴迁。至此寄悯,卷積層的反向傳播就結(jié)束了。

下面我們使用Numpy來實(shí)現(xiàn)卷積層的前向和反向傳播堕义。

CODE

代碼是在上一篇全連接網(wǎng)絡(luò)基礎(chǔ)上增加的猜旬,繼承自Layer類,使得不同類型的層可以疊加成網(wǎng)絡(luò)倦卖。

卷積層

首先我們定義一個(gè)卷積核類昔馋,用來實(shí)現(xiàn)每個(gè)卷積核的卷積計(jì)算和前向傳播反向傳播。

class ConvKernel(Layer):
    """
    這里不需要繼承自Layer糖耸,但是把激活函數(shù)求導(dǎo)過程放在了這里秘遏,沒改所以還是繼承了。
    """
    def __init__(self, kernel_size, input_shape, strides):
        """
        :param kernel_size: 卷積核大小
        :param input_shape: 輸入大小
        :param strides:     步長大小
        """
        super().__init__()
        self.__kh = kernel_size[0]
        self.__kw = kernel_size[1]
        self.__input_shape = input_shape
        self.__channel = input_shape[2]
        self.__strides = strides
        # self.__padding = padding
        self.__w = np.random.randn(kernel_size[0], kernel_size[1],
                                   input_shape[2])  # np.array([[1,0,1],[0,1,0],[1,0,1]])
        self.__output_shape = (int((input_shape[0] - kernel_size[0]) / strides[0]) + 1,
                               int((input_shape[1] - kernel_size[1]) / strides[1]) + 1)
        self.__input = None
        self.__output = None
        self.__b = np.random.randn(self.__output_shape[0], self.__output_shape[1])

    def __flip_w(self):
        """
        :return: w after flip 180
        """
        return np.fliplr(np.flipud(self.__w))

    def __updata_params(self, w_delta, b_delta, lr):
        self.__w -= w_delta * lr
        self.__b -= b_delta * lr

    def __conv(self, _input, weights, strides, _axis=None):
        """
        卷積運(yùn)算
        :param _input:      輸入
        :param weights:     權(quán)重
        :param strides:     步長
        :param _axis:       維度
        :return: 
        """
        if _axis is None:  # 矩陣情況
            result = np.zeros((int((_input.shape[0] - weights.shape[0]) / strides[0]) + 1,
                               int((_input.shape[1] - weights.shape[1]) / strides[1]) + 1))
            for h in range(result.shape[0]):
                for w in range(result.shape[1]):
                    result[h, w] = np.sum(_input[h * strides[0]:h * strides[0] + weights.shape[0],
                                          w * strides[1]:w * strides[1] + weights.shape[1]] * weights)
        else:
            result = np.zeros((int((_input.shape[0] - weights.shape[0]) / strides[0]) + 1,
                               int((_input.shape[1] - weights.shape[1]) / strides[1]) + 1,
                               self.__input_shape[2]))
            for h in range(result.shape[0]):
                for w in range(result.shape[1]):
                    result[h, w, :] = np.sum(_input[h * strides[0]:h * strides[0] + weights.shape[0],
                                             w * strides[1]:w * strides[1] + weights.shape[1]] * weights,
                                             axis=_axis)

        return result

    def forward_pass(self, X):
        self.__input = X
        self.__output = self.__conv(X, self.__w, self.__strides) + self.__b
        return self.__output

    def back_pass(self, error, lr, activation_name='none'):
        o_delta = np.zeros((self.__output_shape[0], self.__output_shape[1], self.__channel))
        # 將delta擴(kuò)展至通道數(shù)
        for i in range(self.__channel):
            o_delta[:, :, i] = error
        # 根據(jù)輸入嘉竟、步長邦危、卷積核大小計(jì)算步長
        X = np.zeros(
            shape=(self.__input_shape[0] + self.__kh - 1, self.__input_shape[1] + self.__kw - 1, self.__channel))

        o_delta_ex = np.zeros(
            (self.__output_shape[0], self.__output_shape[1],
             self.__channel))
        
        #  根據(jù)步長填充0
        for i in range(o_delta.shape[0]):
            for j in range(o_delta.shape[1]):
                X[self.__kh - 1 + i * self.__strides[0],
                self.__kw - 1 + j * self.__strides[1], :] = o_delta[i, j, :]
                # print(o_delta_ex.shape,o_delta.shape)
                o_delta_ex[i, j, :] = o_delta[i, j, :]

        flip_conv_w = self.__conv(X, self.__flip_w(), (1, 1), _axis=(0, 1))
        delta = flip_conv_w * np.reshape(
            self._activation_prime(activation_name, self.__input),
            flip_conv_w.shape)

        w_delta = np.zeros(self.__w.shape)
        for h in range(w_delta.shape[0]):
            for w in range(w_delta.shape[1]):
                if self.__channel == 1:
                    w_delta[h, w, :] = np.sum(self.__input[h:h + o_delta_ex.shape[0],
                                              w:w + o_delta_ex.shape[1]] * o_delta_ex)
                else:
                    w_delta[h, w, :] = np.sum(self.__input[h:h + o_delta_ex.shape[0],
                                              w:w + o_delta_ex.shape[1]] * o_delta_ex, axis=(0, 1))
        self.__updata_params(w_delta, error, lr)
        return delta

之后再定義卷積層

class ConvLayer(Layer):
    def __init__(self, filters, kernel_size, input_shape, strides, padding, activation, name="conv"):
        """
        :param filters:         卷積核個(gè)數(shù)
        :param kernel_size:     卷積核大小
        :param input_shape:     輸入shape
        :param strides:         步長
        :param padding:         填充方式
        :param activation:      激活函數(shù)名
        :param name:            層名稱
        """
        super().__init__()
        self.__filters = filters
        self.__kernel_size = kernel_size
        self.__strides = strides
        self.__padding = padding
        self.activation_name = activation
        self.__input_shape = input_shape  # eg 64*64*3
        self.__input_padding_shape = input_shape
        self.__input = np.zeros(self.__input_shape)
        self.name = name
        self.flag = False

    def _padding_X(self, X):
        """
        對輸入進(jìn)行padding
        :param X:  輸入
        :return:   輸入padding后的值
        """
        if self.__padding == 'SAME':
            o_w = int(np.ceil(X.shape[0] / self.__strides[0]))
            o_h = int(np.ceil(X.shape[1] / self.__strides[1]))
            self.__output_size = (o_w, o_h, self.__filters)
            p_w = np.max((o_w - 1) * self.__strides[0] + self.__kernel_size[0] - X.shape[0], 0)
            p_h = np.max((o_h - 1) * self.__strides[1] + self.__kernel_size[1] - X.shape[1], 0)
            self.p_l = int(np.floor(p_w / 2))
            self.p_t = int(np.floor(p_h / 2))
            res = np.zeros((X.shape[0] + p_w, X.shape[1] + p_h, X.shape[2]))
            res[self.p_t:self.p_t + X.shape[0], self.p_l:self.p_l + X.shape[1], :] = X
            return res
        elif self.__padding == 'VALID':
            o_w = int(np.ceil((X.shape[0] - self.__kernel_size[0] + 1) / self.__strides[0]))
            o_h = int(np.ceil((X.shape[1] - self.__kernel_size[1] + 1) / self.__strides[1]))
            self.__output_size = (o_w, o_h, self.__filters)
            return X[:self.__strides[0] * (o_w - 1) + self.__kernel_size[0],
                   :self.__strides[1] * (o_h - 1) + self.__kernel_size[1], :]
        else:
            raise ValueError("padding name is wrong")

    def forward_propagation(self, _input):
        """
        前向傳播,在前向傳播過程中得到輸入值舍扰,并計(jì)算輸出shape
        :param _input:  輸入值
        :return:        輸出值
        """
        self.__input = self._padding_X(_input)
        self.__input_padding_shape = self.__input.shape
        self.__output = np.zeros(self.__output_size)
        if not self.flag: # 初始化
            self.__kernels = [ConvKernel(self.__kernel_size, self.__input_padding_shape, self.__strides) for _ in
                              range(self.__filters)]  # 由于隨機(jī)函數(shù)倦蚪,所以不能使用[]*n來創(chuàng)建多個(gè)(數(shù)值相同)。
            self.flag = True
        for i, kernel in enumerate(self.__kernels):
            self.__output[:, :, i] = kernel.forward_pass(self.__input)
        return self._activation(self.activation_name, self.__output)

    def back_propagation(self, error, lr):
        """
        反向傳播過程边苹,對于誤差也需要根據(jù)padding進(jìn)行截取或補(bǔ)0
        :param error:   誤差
        :param lr:      學(xué)習(xí)率
        :return:        上一層誤差(所有卷積核的誤差求平均)
        """
        delta = np.zeros(self.__input_shape)
        for i in range(len(self.__kernels)):
            index = len(self.__kernels) - i - 1
            tmp = self.__kernels[index].back_pass(error[:, :, index], lr, self.activation_name)
            if self.__padding == 'VALID':
                bd = np.ones(self.__input_shape)
                bd[:self.__input_padding_shape[0], :self.__input_padding_shape[1]] = tmp
            elif self.__padding == 'SAME':
                bd = tmp[self.p_t:self.p_t + self.__input_shape[0], self.p_l:self.p_l + self.__input_shape[1]]
            else:
                raise ValueError("padding name is wrong")
            delta += bd

        return delta / len(self.__kernels)

以上是卷積層的前向和反向傳播實(shí)現(xiàn)陵且。需要自己定義好輸入維度,不正確會(huì)報(bào)錯(cuò)。卷積神經(jīng)網(wǎng)絡(luò)大多用作圖像的分類任務(wù)慕购,所以我們還要實(shí)現(xiàn)分類任務(wù)需要的softmax激活函數(shù)和交叉熵(cross entropy) 損失函數(shù)聊疲。

softmax激活函數(shù)和交叉熵(cross entropy)損失函數(shù)詳細(xì)的推導(dǎo)將會(huì)放在下一篇中講解,這里先給出代碼實(shí)現(xiàn)沪悲。

softmax 和 cross entropy

softmax

def _activation(self, name, x):
    #···
    #···其他激活函數(shù)(詳細(xì)見上篇)
    #···
    elif name == 'softmax':
        x = x - np.max(x)  # 防止過大
        exp_x = np.exp(x)
        return exp_x / np.sum(exp_x)
        
def _activation_prime(self, name, x): 
    elif name == 'softmax':
      x = np.squeeze(x)
      #print(x)
      length = len(x)
      res = np.zeros((length,length))
      # print("length", length)
      for i in range(length):
          for j in range(length):
              res[i,j] = self.__softmax(i, j, x)

    return res

def __softmax(self, i, j, a):
    if i == j:
        return a[i] * (1 - a[i])
    else:
        return -a[i] * a[j]

cross entropy

    def __cross_entropy(self, output, y, loss):
        output[output == 0] = 1e-12
        if loss:
            return -y * np.log(output)
        else:
            return - y / output

經(jīng)過卷積層得到的輸出一般是多通道的获洲,我們想要接全連接層去進(jìn)行分類還需要將多通道數(shù)據(jù)展成一維向量,就需要Flatten層殿如。只是數(shù)據(jù)位置的變換贡珊,看代碼就好。

import numpy as np
from Layer import Layer

class FlattenLayer(Layer):
    def __init__(self):
        super().__init__()
        self.__input_shape = None
        self.activation_name = 'none'

    def forward_propagation(self, _input):
        self.__input_shape = _input.shape
        return _input.flatten()

    def back_propagation(self, error, lr=1):
        return np.resize(error, self.__input_shape)

結(jié)果

我使用了200張MNIST手寫數(shù)據(jù)涉馁,以0.03的學(xué)習(xí)率訓(xùn)練了100輪之后门岔,對測試集的40張進(jìn)行了預(yù)測,(隨便瞎寫的模型)結(jié)果如下:

epochs 1 / 100 loss : 1.6076663151543635
epochs 2 / 100 loss : 1.51308051414868
epochs 3 / 100 loss : 1.4435877198762985
epochs 4 / 100 loss : 1.4170579907154772
epochs 5 / 100 loss : 1.2782959961456577
epochs 6 / 100 loss : 0.9999002367380303
···

TODO

卷積層常常需要搭配池化層進(jìn)行數(shù)據(jù)的降維烤送,所以下一篇會(huì)繼續(xù)實(shí)現(xiàn)池化層和講解Softmax與cross entropy固歪。

參考內(nèi)容

感謝以下博主的文章,感謝YJango的過程可視化圖片胯努。

[1] 能否對卷積神經(jīng)網(wǎng)絡(luò)工作原理做一個(gè)直觀的解釋?-知乎
[2] 【TensorFlow】一文弄懂CNN中的padding參數(shù)
[3] 卷積神經(jīng)網(wǎng)絡(luò)(CNN)反向傳播算法

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末逢防,一起剝皮案震驚了整個(gè)濱河市叶沛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌忘朝,老刑警劉巖灰署,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異局嘁,居然都是意外死亡溉箕,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進(jìn)店門悦昵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肴茄,“玉大人,你說我怎么就攤上這事但指」烟担” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵棋凳,是天一觀的道長拦坠。 經(jīng)常有香客問我,道長剩岳,這世上最難降的妖魔是什么贞滨? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮拍棕,結(jié)果婚禮上晓铆,老公的妹妹穿的比我還像新娘勺良。我一直安慰自己,他們只是感情好尤蒿,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布郑气。 她就那樣靜靜地躺著,像睡著了一般腰池。 火紅的嫁衣襯著肌膚如雪尾组。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天示弓,我揣著相機(jī)與錄音讳侨,去河邊找鬼。 笑死奏属,一個(gè)胖子當(dāng)著我的面吹牛跨跨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播囱皿,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼勇婴,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了嘱腥?” 一聲冷哼從身側(cè)響起耕渴,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎齿兔,沒想到半個(gè)月后橱脸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡分苇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年添诉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片医寿。...
    茶點(diǎn)故事閱讀 38,654評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡栏赴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出靖秩,到底是詐尸還是另有隱情艾帐,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布盆偿,位于F島的核電站柒爸,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏事扭。R本人自食惡果不足惜捎稚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧今野,春花似錦葡公、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至宰睡,卻和暖如春蒲凶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拆内。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工旋圆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人麸恍。 一個(gè)月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓灵巧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親抹沪。 傳聞我的和親對象是個(gè)殘疾皇子刻肄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容

  • 姓名:尤學(xué)強(qiáng) 學(xué)號:17101223374 轉(zhuǎn)載自:http://mp.weixin.qq.com/s/C6cID...
    51fb659a6d6f閱讀 3,537評論 0 16
  • 卷積神經(jīng)網(wǎng)絡(luò)類似于一般的神經(jīng)網(wǎng)絡(luò),由可學(xué)習(xí)的權(quán)重和誤差組成融欧,每一個(gè)神經(jīng)元接受一些輸入敏弃,完成一些非線性的操作。整個(gè)神...
    與爾巖說閱讀 2,330評論 0 4
  • 相關(guān)基本概念 CPU利用率 = CPU忙時(shí) / 運(yùn)行總時(shí)間引入多道程序設(shè)計(jì)蹬癌,讓多個(gè)進(jìn)程競爭使用資源,目的就是為了提...
    啦啦哇哈哈閱讀 2,502評論 0 4
  • 今年來一直有個(gè)心思縈繞不斷虹茶,既訝異于年輕一代對自我的規(guī)化心是那么的清晰逝薪,明白。而于我自己而言蝴罪,卻全然是隨波逐流董济,因...
    xx73閱讀 223評論 0 0
  • 庭前柏樹子 聞得檐雨滴聲嗎 浪漫的穿鑿 萬象流傳,毫厘是必失的要门,所以千里必差虏肾。 浪漫主義,是自己說的欢搜。象征主義封豪,也...
    yanyan雜林閱讀 554評論 0 0