【深度学习】卷积神经网络+反向传播推导+爱因斯坦求和约定+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]]

 

 

 

 

 

 

 

 

 

 

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