【深度學習】卷積神經網絡+反向傳播推導+愛因斯坦求和約定+numpy_python實現

參考博客:

卷積神經網絡反向傳播理論推導https://blog.csdn.net/Hearthougan/article/details/72910223

【python】如何用 numpy 實現https://blog.csdn.net/qq_36393962/article/details/99354969

 

目錄

一、最開始的說明(很重要)

二、激活層

三、全連接層

四、池化層

五、愛因斯坦求和約定(numpy::einsum)

六、卷積層

七、舉個例子


一、最開始的說明(很重要)

按照大部分公式的約定,C表示代價,z 表示網絡層輸出,a 表示激活函數值,w 爲權重,b 爲偏置。

如果沒有特別說明,卷積指的是"valid"卷積,激活層爲sigmoid。

其實,神經網絡中的每一層都相當於一個函數f,將“前層”的輸出x映射到y,也就是“後層”輸出爲y=f(x)。不斷地利用“前層”的輸出帶入到“後層”的函數中,求出“後層”的輸出!這就是前向傳播!

神經網絡的最終層輸出是代價函數C,C是一個多維函數,可以對網絡中任何一個變量求偏導,比如對每一層的y求偏導得到

爲什麼要對每一層的輸出y求偏導呢?因爲這樣就可以利用上“多維函數求偏導的鏈式法則”了,大概就是說可以從最終層的偏導開始,不斷地利用“後層”的偏導計算“前層”的偏導,利用每一層的偏導可以進而求出每一層中權重w和偏置b的偏導!有了權重w和偏置b的偏導就可以更新權重w和偏置b啦!這就是反向傳播!

一點點領悟,後面看例子就會明白的!

完整代碼鏈接:https://download.csdn.net/download/jin739738709/11831051

二、激活層

正向公式:

反向公式:

實現代碼:

class FuncLayer(Layer):
    def __init__(self, activate_fn: Func):
        self.f = activate_fn
        self.z: np.ndarray = None

    def __call__(self, x: np.ndarray) -> np.ndarray:
        self.z = x
        return self.f(x)

    def backward(self, dc_da: np.ndarray) -> np.ndarray:
        da_dz = self.f.derivate(self.z)
        if self.f.jacobin:
            # 如果求導結果只能表示成雅克比矩陣,得使用矩陣乘法
            dc_dz = dc_da.dot(da_dz.T)
        else:
            # 求導結果爲對角矩陣,可以採用哈達馬積(逐值相乘)來簡化運算
            dc_dz = dc_da * da_dz
        return dc_dz

 

三、全連接層

 

實現代碼:

class FullConnectedLayer(Layer):
    def __init__(self, input_size, output_size):
        self.i_size = input_size
        self.o_size = output_size
        if self.i_size is not None:
            self.__init(self.i_size)

    def __init(self, input_size):
        self.i_size = input_size
        self.w = np.random.normal(loc=0.0, scale=1.0, size=(self.i_size, self.o_size))
        self.b = np.random.normal(loc=0.0, scale=1.0, size=(1, self.o_size))
        self.x: np.ndarray = None  # input

    def __call__(self, x: np.ndarray) -> np.ndarray:
        x = x.reshape(1, -1)
        # 如果 self.i_size 還沒有確定,則根據x.shape來初始化
        if self.i_size is None:
            self.__init(x.shape[1])
        self.x = x
        self.z = x.dot(self.w)+self.b
        return self.z

    def backward(self, dc_dz: np.ndarray) -> np.ndarray:
        dc_dx = dc_dz.dot(self.w.T)
        self.w += self.x.T.dot(dc_dz)
        self.b += dc_dz
        return dc_dx

四、池化層

實現代碼:

class MeanPoolingLayer(Layer):
    def __init__(self, kernel_size: int, stride: int):
        self.ks = kernel_size
        self.kernel_shape = (kernel_size, kernel_size)
        self.channels: int = None
        self.stride = stride
        self.input_shape: tuple = None  # row_cnt,col_cnt,channels
        self.target_shape: tuple = None  # 目標的shape

    def __call__(self, mat: np.ndarray) -> np.ndarray:
        self.input_shape = mat.shape
        self.channels = mat.shape[2]
        row, col = mat.shape[0], mat.shape[1]
        (kr, kc), s = self.kernel_shape, self.stride
        self.target_shape = ((row-kr)//s+1, (col-kc)//s+1, self.channels)
        target = np.zeros(self.target_shape)
        for i in range(self.target_shape[0]):
            for j in range(self.target_shape[1]):
                r, c = i*s, j*s
                target[i, j] = np.average(mat[r:r+kr, c:c+kc], axis=(0, 1))
        return target

    def backward(self, d_out: np.ndarray) -> np.ndarray:
        d_input = np.zeros(self.input_shape)
        n = self.kernel_shape[0]*self.kernel_shape[1]
        d_mat = d_out/n  # mean-pooling 求導後恰好是 1/n
        (kr, kc), s = self.kernel_shape, self.stride
        for i in range(self.target_shape[0]):
            for j in range(self.target_shape[1]):
                r, c = i*s, j*s
                d_input[r:r+kr, c:c+kc] += d_mat[i, j]
        return d_input

五、愛因斯坦求和約定(numpy::einsum

參考博客:

NumPy中einsum的基本介紹http://www.atyun.com/32288.html

在說明卷積層和卷積層的實現代碼之前,必須說一下numpy的一個強大矩陣運算工具einsum。

由於其強大的表現力和智能循環,它在速度和內存效率方面通常可以超越我們常見的array函數。

但缺點是,可能需要一段時間才能理解符號,有時需要嘗試才能將其正確的應用於棘手的問題。

不過缺點也不是缺點,舉個例子大家就會明白的,其實也沒有多難理解!

import numpy as np

mm =  np.array(
    [
        [[1,2], [1,2]],
        [[1,2], [1,2]]
    ]
)
print(mm,mm.shape)

nn = np.array(
    [
        [[1,2,3], [1,2,0]],
        [[1,2,3], [1,2,3]]
    ]
)
print(nn,nn.shape)
print(np.einsum("ijk,ijl->kl",mm,nn,dtype=np.float64))
#結果:
#[[ 4.  8.  9.]
# [ 8. 16. 18.]]

print(np.einsum("ijk,ijl->l",mm,nn,dtype=np.float64))
# 結果: [12 24 27]

代碼中的矩陣mm和矩陣nn可以由上圖表示,

比如說,對於矩陣nn,當i=0,j=0,l=2,則nn[0,0,2]==3;當i=0,j=1,l=2,則nn[0,1,2]==0

所以,當執行下列語句時,

np.einsum("ijk,ijl->kl",mm,nn,dtype=np.float64)

產生結果如圖所示

這個結果是怎麼產生的呢?比如說當k=0,l=2

矩陣mm中元素mm[:,:,0]和矩陣nn中元素nn[:,:,2],按照對應位置元素相乘,然後把四個相乘的結果相加,也就是1*3+1*0+1*3+1*3=9

 

所以,當執行下列語句時,

np.einsum("ijk,ijl->l",mm,nn,dtype=np.float64)

產生結果如圖所示

式子中"ijk,ijl->l",之前的k被省略了,輸出中省略的字母意味着沿該軸的值將相加,如下圖所示

 

六、卷積層

實現代碼:

class ConvolutionLayer(Layer):
    def __init__(self, in_channels, out_channels, kernel_size, stride):
        self.in_channels = in_channels
        self.out_channels = out_channels
        self.ks = kernel_size
        self.kernel_shape = (kernel_size, kernel_size)
        self.stride = stride
        self.x: np.ndarray = None  # input
        # 卷積核: row,col,channel 順序
        self.kernel = np.random.normal(loc=0.0, scale=1.0, size=(kernel_size, kernel_size, out_channels))
        self.bias = np.random.normal(loc=0.0, scale=1.0, size=(out_channels,))
#        self.kernel =np.ones((kernel_size,kernel_size,out_channels))
#        self.bias = np.ones((out_channels,))
        

    def check_x_mat_shape(self, x_mat):
        '''
            要求卷積核在卷積過程中可以把矩陣鋪滿(stride空隙不算)
            右側(下側)不能有多餘的列(行)
            如 28x28 不能用(5x5,stride=2)的卷積核,因爲它只能覆蓋(27x27)
        '''
        row, col = x_mat.shape[0], x_mat.shape[1]
        k, s = self.ks, self.stride
        assert (row-k)//s*s+k == row
        assert (col-k)//s*s+k == col

    def __call__(self, x_mat: np.ndarray) -> np.ndarray:
        self.check_x_mat_shape(x_mat)
        self.x = x_mat
        return self.__conv(
            stride=self.stride,
            mat=x_mat,
            kernel=self.kernel,
            bias=None)#bias=self.bias

    def backward(self, dc_dz: np.ndarray) -> np.ndarray:
        # 反向卷積的目標是dc_dz補0之後的矩陣(張量)
        # (padding + dilation)
        # 補0規則爲:邊緣padding kernel_size-1 層0;間隔處補 stride-1 層0
        # 只看橫向,如果dc_dz有c列,那該矩陣有 2kernel_size+(m-1)stride-1 列
        # 反向卷積的stride固定爲1
        (kr, kc, ch), s = self.kernel.shape, self.stride
        dc_dz_with_zeros_shape = (
            2*kr + (dc_dz.shape[0]-1)*s-1,
            2*kc + (dc_dz.shape[1]-1)*s-1,
            dc_dz.shape[2]
        )
        D = np.zeros(dc_dz_with_zeros_shape)  # 爲了簡化,用D表示補充0之後的張量
        for i in range(dc_dz.shape[0]):
            for j in range(dc_dz.shape[1]):
                D[kr+i*s-1, kc+j*s-1] = dc_dz[i, j]
                
        # 求 dc_da(a指的是該層的輸入self.x,因爲習慣上稱呼上一層的激活值爲a[l-1])
        # 注意stride(步長)是1,而且是每個通道卷積核分別對對應通道mat卷積,最後加和到一個通道
        # 卷積結果 dc_da_map 要加到 dc_da 結果的每一層
        # (仔細在草稿紙上計算不難證明這點)
        dc_da_map = self.__conv(
            stride=1,
            mat=D,
            kernel=self.kernel[::-1, ::-1],  # 注意不能漏了反向傳播中卷積核的180度旋轉 rot180(w)
            bias=None,
            einsum_formula="ijk,ijk->",
            out_channels=1)
        
        dc_da = np.repeat(dc_da_map.reshape((dc_da_map.shape[0], -1, 1)), self.in_channels, axis=2)
    
        # 求 dc_dw(即dc_d kernel)
        # 也是卷積,只不過是用 rot180(a_input) 對 D 卷積
        dc_dw = self.__conv(
            stride=1,
            mat=D,
            kernel=self.x[::-1, ::-1],
            bias=None,
            einsum_formula="ijk,ijl->k",
            out_channels=self.kernel.shape[2])
        # 求 dc_db
        dc_db = np.einsum("ijk->k", dc_dz)
        # 更新w(kernel)和b(bias),並返回 dc_da
        self.kernel += dc_dw
        self.bias += dc_db
        return dc_da

    def __conv(self,
               stride: int,
               mat: np.ndarray,
               kernel: np.ndarray,
               bias: np.ndarray = None,
               einsum_formula: str = "ijk,ijl->l",
               out_channels: int = None):
        '''
            注意 out_channels 要與 einsum_formula 相對應
        '''
        # 卷積運算 sub_np_tensor * kernel_np_tensor + bias
        if bias is None:
            def f(m): return np.einsum(
                einsum_formula, m, kernel)
        else:
            def f(m): return np.einsum(einsum_formula, m, kernel) + bias
        row, col = mat.shape[0], mat.shape[1]
        (kr, kc, ch), s = kernel.shape, stride
        # out_channels 默認爲 kernel.shape[2]
        out_channels = ch if out_channels is None else out_channels
        target_shape = ((row-kr)//s+1, (col-kc)//s+1, out_channels)
        target = np.zeros(target_shape)
        for i in range(target_shape[0]):
            for j in range(target_shape[1]):
                r, c = i*s, j*s
                target[i, j] = f(mat[r:r+kr, c:c+kc])
        return target

 

七、舉個例子

下圖爲例子的整個網絡模型,圖中展示了網絡的前向傳播、反向傳播以及參數更新。全部公式嚴格按照前面幾章的內容進行推導。

 

測試代碼:

    a = np.array(
        [
            [[1, 1, 3], [2, 2, 3], [3, 3, 5], [4, 4, 5], [3, 3, 5], [4, 4, 5]],
            [[0, 0, 3], [1, 1, 3], [0, 0, 5], [1, 1, 5], [3, 3, 5], [4, 4, 5]],
            [[5, 5, 3], [0, 0, 3], [9, 9, 5], [1, 1, 5], [3, 3, 5], [4, 4, 5]],
            [[5, 5, 3], [0, 0, 3], [9, 9, 5], [1, 1, 5], [3, 3, 5], [4, 4, 5]],
            [[5, 5, 3], [0, 0, 3], [9, 9, 5], [1, 1, 5], [3, 3, 5], [4, 4, 5]],
            [[6, 6, 3], [3, 3, 3], [7, 7, 5], [1, 1, 5], [3, 3, 5], [4, 4, 5]]
        ]
    )

    label = np.array([[1, 0]])

    from funcs import sigmoid
    from lossfuncs import sse
    from nn import NN

    # conv = ConvolutionLayer(2,1,2,1)
    my_nn = NN((6, 6, 3), (1, 2))
    my_nn.set_layers([
        ConvolutionLayer(3, 6, 1, 1),#卷積層
        FuncLayer(sigmoid),#sigmoid激活層
        MeanPoolingLayer(2,2),#平均池化層
        FuncLayer(sigmoid),#sigmoid激活層
        ConvolutionLayer(6, 1, 2, 1),#卷積層
        FuncLayer(sigmoid),#sigmoid激活層
        ReshapeLayer(None,(1,-1)),#reshape
        FullConnectedLayer(None, 2),#全連接層
        FuncLayer(sigmoid),#sigmoid激活層
    ])
    y1 = my_nn.forward(a)
    for i in range(20000):
        my_nn.train(a, label, sse, 0.1)
    y2 = my_nn.forward(a)
    print("訓練前:",y1) # 訓練前: [[0.54831853 0.54148219]]
    print("訓練後:",y2) # 訓練後: [[0.99493565 0.00505818]]
    print("答案:",label) # 答案: [[1 0]]

 

 

 

 

 

 

 

 

 

 

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