神经网络的Python实现(三)卷积神经网络

推荐在我的博客中给我留言,这样我会随时收到你的评论,并作出回复。


在上一篇神经网络的Python实现(二)全连接网络中,已经介绍了神经网络的部分激活函数,损失函数和全连接网络的前馈和反向传播公式及Numpy实现。这篇博文将要详细介绍卷积神经网络的概念,并且进行前馈和反向传播的公式推导及Numpy实现。

卷积神经网络

卷积神经网络(Convolutional Neural Network)非常擅于处理图像任务,它的灵感来自于视觉神经中的感受野这一概念,卷积神经网络的卷积核(Convolution Kernel) 好似感受野一样去扫描数据。一个卷积神经网络基本包括卷积层池化层输出层

接下来介绍什么是卷积核、卷积神经网络中的卷积是怎么运算的。

卷积和卷积核

卷积神经网络中的卷积操作与数学中的类似。就是输入数据中不同数据窗口的数据和卷积核(一个权值矩阵)作内积的操作。其中卷积核是卷积神经网络中卷积层的最重要的部分。卷积核相当于信息处理中的滤波器,可以提取输入数据的当前特征。卷积核的实质是一个权值矩阵,在下图中的卷积核便是一个权值如下的矩阵(图中黄色色块中的红色数字)

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

卷积

如果并不理解卷积,那么我们来看图中输出的第一行第一列的4是怎么得到的。

原输入数据大小为5\times5,我们要使用3\times3的卷积核来进行卷积,我们使用\ast表示卷积操作。那么图中第一个4的运算过程就可以表达为:

\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

剩下位置的输出就是卷积核在输入矩阵上从左到右从上到下移动一格做如上卷积操作过程的结果。

步长(strides)和填充(padding)

步长

上面例子说到的一格表示的就是步长(strides),步长分为横向步长和纵向步长,步长是多少就表示一次卷积操作之后卷积核移动的距离。知道步长的概念了,我们就可以去计算一下根据输入大小,卷积核大小,我们得到的输出的大小。假设用C表示边长,那么:

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

根据公式当步长为1或是输入大小能够被步长整除时很好处理,无法整除时也就是卷积核移动到最后,输入数据的剩下的部分不足卷积核大小,这时我们会想到要么将输入变大点让它能够整除要么是干脆边界直接丢弃让它能够整除。这两种处理办法对应于填充(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就是在输入周围补0,我们先计算补0后的输出大小:

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

接下来便根据应得到输出的大小去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就几乎对称地分布在输入四周。

多通道的卷积

一般卷积神经网络处理的都是3通道或是多通道的图像数据,那么对于多通道如何卷积呢?对于多通道,卷积公式并不变,只是要求卷积核通道与输入通道数一致,不同通道分别做内积,然后不同通道得到的值相加起来作为最后的输出。如图。

channels.gif

对于计算,我们使用2\times2\times2的输入和2\times2\times2的卷积核举个例子:

\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}

可以看到,不论输入通道数是多少最后的输出仍是一个矩阵。在卷积层如果有多个卷积核,每个卷积核会提取一种特征,输出一个二维矩阵。最终的结果就是把这些卷积核的输出看作不同通道。如下图。

channels2.jpg

下面以图像处理为例,来看一下卷积神经网络的前馈和反向传播。

前向传播

卷积层的前向传播方式与全连接层类似,我们回顾一下全连接层的前向传播:

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

卷积层只不过把全连接层的矩阵乘法运算换成了卷积运算。详细的步骤如下
知道前一层的输出之后:

  1. 定义好卷积核数目,卷积核大小,步长和填充方式。根据输入大小,计算输出大小并进行相应的padding,得到了卷积层的输入 a^{l-1}
  2. 初始化所有卷积和的权重 W 和偏置 b
  3. 根据前向传播的公式(M个通道):
    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)
    计算出卷积层输出,其中*是卷积运算、\sigma是激活函数。

反向传播

现在已知卷积层的 \delta^l ,我们通过反向传播算法来计算上一层的 \delta^{l-1}
我们也先回顾一下反向传播公式,根据链式法则:

\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}}

要计算\delta^l的值,必须知道\frac{\partial z^{l}}{\partial z^{l-1}}的值,所以根据前向传播公式:

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

现在就差卷积运算的偏导该如何求,我们先把正确公式写出来,之后再解释:

\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) 表示将卷积核旋转180°,即卷积核左右翻转之后再上下翻转。可以拿张正反内容不一样的纸转一转。然后我们解释为什么卷积的求导就是将卷积核旋转180°再做卷积的结果。

我们拿 3\times 3 大小矩阵作为例子,卷积核大小为 2\times 2,步长为1(步长不是1时后面会提到):

\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]

上面是前向的卷积运算,我们把它展开来:

\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}

这样就变成了简单的运算,根据反向传播公式:

\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}}

我们就要对每个 a 求其梯度:

比如 a_{11} 只和 z_{11} 有关,所以:

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

复杂点的对于为了明显标红的 a_{22} ,它与 z_{11},z_{12},z_{21},z_{22} 都有关,所以:

\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过的卷积运算,你会发现上面的式子就是下面列出的卷积(步长为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]

这里最好动手写一下就可以发现这种运算关系。(这里的周围的0填充宽度是卷积核边长-1)

以上是步长为1时的求解过程,当步长大于1时,我们就需要在 \delta 矩阵的值之间填充0来实现步长。

先说结论,每两个 \delta 之间需要填充步长-1个0(对应方向上的)。也举个例子来看看是不是这样,步长为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}

计算梯度:

\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]

:无论前向步长为多少,旋转后的卷积步长一直是1。

其余计算过程类比全连接层是一样的。至此,卷积层的反向传播就结束了。

下面我们使用Numpy来实现卷积层的前向和反向传播。

CODE

代码是在上一篇全连接网络基础上增加的,继承自Layer类,使得不同类型的层可以叠加成网络。

卷积层

首先我们定义一个卷积核类,用来实现每个卷积核的卷积计算和前向传播反向传播。

class ConvKernel(Layer):
    """
    这里不需要继承自Layer,但是把激活函数求导过程放在了这里,没改所以还是继承了。
    """
    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):
        """
        卷积运算
        :param _input:      输入
        :param weights:     权重
        :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扩展至通道数
        for i in range(self.__channel):
            o_delta[:, :, i] = error
        # 根据输入、步长、卷积核大小计算步长
        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))
        
        #  根据步长填充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:         卷积核个数
        :param kernel_size:     卷积核大小
        :param input_shape:     输入shape
        :param strides:         步长
        :param padding:         填充方式
        :param activation:      激活函数名
        :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):
        """
        对输入进行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):
        """
        前向传播,在前向传播过程中得到输入值,并计算输出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)]  # 由于随机函数,所以不能使用[]*n来创建多个(数值相同)。
            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):
        """
        反向传播过程,对于误差也需要根据padding进行截取或补0
        :param error:   误差
        :param lr:      学习率
        :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)

以上是卷积层的前向和反向传播实现。需要自己定义好输入维度,不正确会报错。卷积神经网络大多用作图像的分类任务,所以我们还要实现分类任务需要的softmax激活函数和交叉熵(cross entropy) 损失函数。

softmax激活函数和交叉熵(cross entropy)损失函数详细的推导将会放在下一篇中讲解,这里先给出代码实现。

softmax 和 cross entropy

softmax

def _activation(self, name, 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

经过卷积层得到的输出一般是多通道的,我们想要接全连接层去进行分类还需要将多通道数据展成一维向量,就需要Flatten层。只是数据位置的变换,看代码就好。

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)

结果

我使用了200张MNIST手写数据,以0.03的学习率训练了100轮之后,对测试集的40张进行了预测,(随便瞎写的模型)结果如下:

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

卷积层常常需要搭配池化层进行数据的降维,所以下一篇会继续实现池化层和讲解Softmax与cross entropy。

参考内容

感谢以下博主的文章,感谢YJango的过程可视化图片。

[1] 能否对卷积神经网络工作原理做一个直观的解释?-知乎
[2] 【TensorFlow】一文弄懂CNN中的padding参数
[3] 卷积神经网络(CNN)反向传播算法

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 姓名:尤学强 学号:17101223374 转载自:http://mp.weixin.qq.com/s/C6cID...
    51fb659a6d6f阅读 3,525评论 0 16
  • 卷积神经网络类似于一般的神经网络,由可学习的权重和误差组成,每一个神经元接受一些输入,完成一些非线性的操作。整个神...
    与尔岩说阅读 2,323评论 0 4
  • 相关基本概念 CPU利用率 = CPU忙时 / 运行总时间引入多道程序设计,让多个进程竞争使用资源,目的就是为了提...
    啦啦哇哈哈阅读 2,490评论 0 4
  • 今年来一直有个心思萦绕不断,既讶异于年轻一代对自我的规化心是那么的清晰,明白。而于我自己而言,却全然是随波逐流,因...
    xx73阅读 219评论 0 0
  • 庭前柏树子 闻得檐雨滴声吗 浪漫的穿凿 万象流传,毫厘是必失的,所以千里必差。 浪漫主义,是自己说的。象征主义,也...
    yanyan杂林阅读 546评论 0 0