【深度学习系列】卷积神经网络详解(二)——自己手写一个卷积神经网络
上篇文章中我们讲解了卷积神经网络的基本原理,包括几个基本层的定义、运算规则等。本文主要写卷积神经网络如何进行一次完整的训练,包括前向传播和反向传播,并自己手写一个卷积神经网络。如果不了解基本原理的,可以先看看上篇文章:【深度学习系列】卷积神经网络CNN原理详解(一)——基本原理
卷积神经网络的前向传播
首先我们来看一个最简单的卷积神经网络:
1.输入层---->卷积层
以上一节的例子为例,输入是一个4*4 的image,经过两个2*2的卷积核进行卷积运算后,变成两个3*3的feature_map
以卷积核filter1为例(stride = 1 ):
计算第一个卷积层神经元o11的输入:
\begin{equation}\begin{aligned}\ net_{o_{11}}&= conv (input,filter)\\ &= i_{11} \times h_{11} + i_{12} \times h_{12} +i_{21} \times h_{21} + i_{22} \times h_{22}\\ &=1 \times 1 + 0 \times (-1) +1 \times 1 + 1 \times (-1)=1\end{aligned}\end{equation}
神经元o11的输出:(此处使用Relu激活函数)
\begin{equation}\begin{aligned}out_{o_{11}} &= activators(net_{o_{11}}) \\&=max(0,net_{o_{11}}) = 1\end{aligned}\end{equation}
其他神经元计算方式相同
2.卷积层---->池化层
计算池化层m11 的输入(取窗口为 2 * 2),池化层没有激活函数
\begin{equation}\begin{aligned}net_{m_{11}} &= max(o_{11},o_{12},o_{21},o_{22}) = 1\\&out_{m_{11}} = net_{m_{11}} = 1\end{aligned}\end{equation}
3.池化层---->全连接层
池化层的输出到flatten层把所有元素“拍平”,然后到全连接层。
4.全连接层---->输出层
全连接层到输出层就是正常的神经元与神经元之间的邻接相连,通过softmax函数计算后输出到output,得到不同类别的概率值,输出概率值最大的即为该图片的类别。
卷积神经网络的反向传播
传统的神经网络是全连接形式的,如果进行反向传播,只需要由下一层对前一层不断的求偏导,即求链式偏导就可以求出每一层的误差敏感项,然后求出权重和偏置项的梯度,即可更新权重。而卷积神经网络有两个特殊的层:卷积层和池化层。池化层输出时不需要经过激活函数,是一个滑动窗口的最大值,一个常数,那么它的偏导是1。池化层相当于对上层图片做了一个压缩,这个反向求误差敏感项时与传统的反向传播方式不同。从卷积后的feature_map反向传播到前一层时,由于前向传播时是通过卷积核做卷积运算得到的feature_map,所以反向传播与传统的也不一样,需要更新卷积核的参数。下面我们介绍一下池化层和卷积层是如何做反向传播的。
在介绍之前,首先回顾一下传统的反向传播方法:
1.通过前向传播计算每一层的输入值$net_{i,j}$ (如卷积后的feature_map的第一个神经元的输入:$net_{i_{11}}$)
2.反向传播计算每个神经元的误差项$\delta_{i,j}$ ,$\delta_{i,j} = \frac{\partial E}{\partial net_{i,j}}$,其中E为损失函数计算得到的总体误差,可以用平方差,交叉熵等表示。
3.计算每个神经元权重$w_{i,j}$ 的梯度,$\eta_{i,j} = \frac{\partial E}{\partial net_{i,j}} \cdot \frac{\partial net_{i,j}}{\partial w_{i,j}} = \delta_{i,j} \cdot out_{i,j}$
4.更新权重 $w_{i,j} = w_{i,j}-\lambda \cdot \eta_{i,j}$(其中$\lambda$为学习率)
卷积层的反向传播
由前向传播可得:
每一个神经元的值都是上一个神经元的输入作为这个神经元的输入,经过激活函数激活之后输出,作为下一个神经元的输入,在这里我用$i_{11}$表示前一层,$o_{11}$表示$i_{11}$的下一层。那么$net_{i_{11}}$就是i11这个神经元的输入,$out_{i_{11}}$就是i11这个神经元的输出,同理,$net_{o_{11}}$就是o11这个神经元的输入,$out_{o_{11}}$就是$o_{11}$这个神经元的输出,因为上一层神经元的输出 = 下一层神经元的输入,所以$out_{i_{11}}$= $net_{o_{11}}$,这里我为了简化,直接把$out_{i_{11}}$记为$i_{11}$
\begin{equation}\begin{aligned}\ i_{11}&=out_{i_{11}} \\&= activators(net_{i_{11}})\\\ net_{o_{11}}&= conv (input,filter)\\ &= i_{11} \times h_{11} + i_{12} \times h_{12} +i_{21} \times h_{21} + i_{22} \times h_{22}\\ out_{o_{11}} &= activators(net_{o_{11}}) \\&=max(0,net_{o_{11}})\end{aligned}\end{equation}
$net_{i_{11}}$表示上一层的输入,$out_{i_{11}}$表示上一层的输出
首先计算卷积的上一层的第一个元素$i_{11}$的误差项$\delta_{11}$:
$$\delta_{11} = \frac{\partial E}{\partial net_{i_{11}}} =\frac{\partial E}{\partial out_{i_{11}}} \cdot \frac{\partial out_{i_{11}}}{\partial net_{i_{11}}} = \frac{\partial E}{\partial i_{11}} \cdot \frac{\partial i_{11}}{\partial net_{i_{11}}}$$
先计算$\frac{\partial E}{\partial i_{11}} $
此处我们并不清楚$\frac{\partial E}{\partial i_{11}}$怎么算,那可以先把input层通过卷积核做完卷积运算后的输出feature_map写出来:
\begin{equation}\begin{aligned}net_{o_{11}} = i_{11} \times h_{11} + i_{12} \times h_{12} +i_{21} \times h_{21} + i_{22} \times h_{22} \\net_{o_{12}} = i_{12} \times h_{11} + i_{13} \times h_{12} +i_{22} \times h_{21} + i_{23} \times h_{22} \\net_{o_{12}} = i_{13} \times h_{11} + i_{14} \times h_{12} +i_{23} \times h_{21} + i_{24} \times h_{22} \\net_{o_{21}} = i_{21} \times h_{11} + i_{22} \times h_{12} +i_{31} \times h_{21} + i_{32} \times h_{22} \\net_{o_{22}} = i_{22} \times h_{11} + i_{23} \times h_{12} +i_{32} \times h_{21} + i_{33} \times h_{22} \\net_{o_{23}} = i_{23} \times h_{11} + i_{24} \times h_{12} +i_{33} \times h_{21} + i_{34} \times h_{22} \\net_{o_{31}} = i_{31} \times h_{11} + i_{32} \times h_{12} +i_{41} \times h_{21} + i_{42} \times h_{22} \\net_{o_{32}} = i_{32} \times h_{11} + i_{33} \times h_{12} +i_{42} \times h_{21} + i_{43} \times h_{22} \\net_{o_{33}} = i_{33} \times h_{11} + i_{34} \times h_{12} +i_{43} \times h_{21} + i_{44} \times h_{22} \\\end{aligned}\end{equation}
然后依次对输入元素$i_{i,j}$求偏导
$i_{11}$的偏导:
\begin{equation}\begin{aligned}\frac{\partial E}{\partial i_{11}}&=\frac{\partial E}{\partial net_{o_{11}}} \cdot \frac{\partial net_{o_{11}}}{\partial i_{11}}\\&=\delta_{11} \cdot h_{11}\end{aligned}\end{equation}
$i_{12}$的偏导:
\begin{equation}\begin{aligned}\frac{\partial E}{\partial i_{12}}&=\frac{\partial E}{\partial net_{o_{11}}} \cdot \frac{\partial net_{o_{11}}}{\partial i_{12}} +\frac{\partial E}{\partial net_{o_{12}}} \cdot \frac{\partial net_{o_{12}}}{\partial i_{12}}\\&=\delta_{11} \cdot h_{12}+\delta_{12} \cdot h_{11}\end{aligned}\end{equation}
$i_{13}$的偏导:
\begin{equation}\begin{aligned}\frac{\partial E}{\partial i_{13}}&=\frac{\partial E}{\partial net_{o_{12}}} \cdot \frac{\partial net_{o_{12}}}{\partial i_{13}} +\frac{\partial E}{\partial net_{o_{13}}} \cdot \frac{\partial net_{o_{13}}}{\partial i_{13}}\\&=\delta_{12} \cdot h_{12}+\delta_{13} \cdot h_{11}\end{aligned}\end{equation}
$i_{21}$的偏导:
\begin{equation}\begin{aligned}\frac{\partial E}{\partial i_{21}}&=\frac{\partial E}{\partial net_{o_{11}}} \cdot \frac{\partial net_{o_{11}}}{\partial i_{21}} +\frac{\partial E}{\partial net_{o_{21}}} \cdot \frac{\partial net_{o_{21}}}{\partial i_{21}}\\&=\delta_{11} \cdot h_{21}+\delta_{21} \cdot h_{11}\end{aligned}\end{equation}
$i_{22}$的偏导:
\begin{equation}\begin{aligned}\frac{\partial E}{\partial i_{22}}&=\frac{\partial E}{\partial net_{o_{11}}} \cdot \frac{\partial net_{o_{11}}}{\partial i_{22}} +\frac{\partial E}{\partial net_{o_{12}}} \cdot \frac{\partial net_{o_{12}}}{\partial i_{22}}\\&+\frac{\partial E}{\partial net_{o_{21}}} \cdot \frac{\partial net_{o_{21}}}{\partial i_{22}}+\frac{\partial E}{\partial net_{o_{22}}} \cdot \frac{\partial net_{o_{22}}}{\partial i_{22}}\\&=\delta_{11} \cdot h_{22}+\delta_{12} \cdot h_{21}+\delta_{21} \cdot h_{12}+\delta_{22} \cdot h_{11}\end{aligned}\end{equation}
观察一下上面几个式子的规律,归纳一下,可以得到如下表达式:
\begin{equation}{\left[ \begin{array}{ccc}0& 0& 0& 0& 0& \\ 0& \delta_{11} & \delta_{12} & \delta_{13}&0\\ 0&\delta_{21} & \delta_{22} & \delta_{23} &0\\ 0&\delta_{31} & \delta_{32} & \delta_{33} &0\\ 0& 0& 0& 0& 0& \\\end{array} \right ]\cdot \left[ \begin{array}{ccc} h_{22}& h_{21} \\ h_{12}& h_{11} \\\end{array}\right]}=\left[ \begin{array}{ccc} \frac{\partial E}{\partial i_{11}}& \frac{\partial E}{\partial i_{12}}& \frac{\partial E}{\partial i_{13}}& \frac{\partial E}{\partial i_{14}} \\ \frac{\partial E}{\partial i_{21}}& \frac{\partial E}{\partial i_{22}}& \frac{\partial E}{\partial i_{23}}& \frac{\partial E}{\partial i_{24}} \\ \frac{\partial E}{\partial i_{31}}& \frac{\partial E}{\partial i_{32}}& \frac{\partial E}{\partial i_{33}}& \frac{\partial E}{\partial i_{34}} \\ \frac{\partial E}{\partial i_{41}}& \frac{\partial E}{\partial i_{42}}& \frac{\partial E}{\partial i_{43}}& \frac{\partial E}{\partial i_{44}} \\\end{array}\right]\end{equation}
图中的卷积核进行了180°翻转,与这一层的误差敏感项矩阵${delta_{i,j})}$周围补零后的矩阵做卷积运算后,就可以得到${\frac{\partial E}{\partial i_{11}}}$,即
$\frac{\partial E}{\partial i_{i,j}} = \sum_m \cdot \sum_n h_{m,n}\delta_{i+m,j+n}$
第一项求完后,我们来求第二项$\frac{\partial i_{11}}{\partial net_{i_{11}}}$
\begin{equation}\begin{aligned}\because i_{11} &= out_{i_{11}} \\&= activators(net_{i_{11}})\\\therefore \frac{\partial i_{11}}{\partial net_{i_{11}}}&=f'(net_{i_{11}})\\\therefore \delta_{11} &=\frac{\partial E}{\partial net_{i_{11}}} \\&=\frac{\partial E}{\partial i_{11}} \cdot \frac{\partial i_{11}}{\partial net_{i_{11}}}\\&=\sum_m \cdot \sum_n h_{m,n}\delta_{i+m,j+n} \cdot f'(net_{i_{11}})\end{aligned}\end{equation}
此时我们的误差敏感矩阵就求完了,得到误差敏感矩阵后,即可求权重的梯度。
由于上面已经写出了卷积层的输入$net_{o_{11}}$与权重$h_{i,j}$之间的表达式,所以可以直接求出:
\begin{equation}\begin{aligned}\frac{\partial E}{\partial h_{11}}&=\frac{\partial E}{\partial net_{o_{11}}} \cdot \frac{\partial net_{o_{11}}}{\partial h_{11}}+...\\&+\frac{\partial E}{\partial net_{o_{33}}} \cdot \frac{\partial net_{o_{33}}}{\partial h_{11}}\\&=\delta_{11} \cdot h_{11} +...+ \delta_{33} \cdot h_{11}\end{aligned}\end{equation}
推论出权重的梯度:
\begin{equation}\begin{aligned}\frac{\partial E}{\partial h_{i,j}} = \sum_m\sum_n\delta_{m,n}out_{o_{i+m,j+n}}\end{aligned}\end{equation}
偏置项的梯度:
\begin{equation}\begin{aligned}\frac{\partial E}{\partial b} &=\frac{\partial E}{\partial net_{o_{11}}} \frac{\partial net_{o_{11}}}{\partial w_b} +\frac{\partial E}{\partial net_{o_{12}}} \frac{\partial net_{o_{12}}}{\partial w_b}\\&+\frac{\partial E}{\partial net_{o_{21}}} \frac{\partial net_{o_{21}}}{\partial w_b} +\frac{\partial E}{\partial net_{o_{22}}} \frac{\partial net_{o_{22}}}{\partial w_b}\\&=\delta_{11}+\delta_{12}+\delta_{21}+\delta_{22}\\&=\sum_i\sum_j\delta_{i,j}\end{aligned}\end{equation}
可以看出,偏置项的偏导等于这一层所有误差敏感项之和。得到了权重和偏置项的梯度后,就可以根据梯度下降法更新权重和梯度了。
池化层的反向传播
池化层的反向传播就比较好求了,看着下面的图,左边是上一层的输出,也就是卷积层的输出feature_map,右边是池化层的输入,还是先根据前向传播,把式子都写出来,方便计算:
假设上一层这个滑动窗口的最大值是$out_{o_{11}}$\begin{equation}\begin{aligned}&\because net_{m_{11}} = max(out_{o_{11}},out_{o_{12}},out_{o_{21}},out_{o_{22}})\\&\therefore \frac{\partial net_{m_{11}}}{\partial out_{o_{11}}} = 1\\& \frac{\partial net_{m_{11}}}{\partial out_{o_{12}}}=\frac{\partial net_{m_{11}}}{\partial out_{o_{21}}}=\frac{\partial net_{m_{11}}}{\partial out_{o_{22}}} = 0\\&\therefore \delta_{11}^{l-1} = \frac{\partial E}{\partial out_{o_{11}}} = \frac{\partial E}{\partial net_{m_{11}}} \cdot \frac{\partial net_{m_{11}}}{\partial out_{o_{11}}} =\delta_{11}^l\\&\delta_{12}^{l-1} = \delta_{21}^{l-1} =\delta_{22}^{l-1} = 0\end{aligned}\end{equation}
这样就求出了池化层的误差敏感项矩阵。同理可以求出每个神经元的梯度并更新权重。
手写一个卷积神经网络
1.定义一个卷积层
首先我们通过ConvLayer来实现一个卷积层,定义卷积层的超参数
其中calculate_output_size用来计算通过卷积运算后输出的feature_map大小
2.构造一个激活函数
此处用的是RELU激活函数,因此我们在activators.py里定义,forward是前向计算,backforward是计算公式的导数:
其他常见的激活函数我们也可以放到activators里,如sigmoid函数,我们可以做如下定义:
如果我们需要自动以其他的激活函数,都可以在activator.py定义一个类即可。
3.定义一个类,保存卷积层的参数和梯度
4.卷积层的前向传播
1).获取卷积区域
2).进行卷积运算
3).增加zero_padding
4).进行前向传播
其中element_wise_op函数是将每个组的元素对应相乘
5.卷积层的反向传播
1).将误差传递到上一层
2).保存传递到上一层的sensitivity map的数组
3).计算代码梯度
4).按照梯度下降法更新参数
6.MaxPooling层的训练
1).定义MaxPooling类
2).前向传播计算
3).反向传播计算
完整代码请见:cnn.py (https://github.com/huxiaoman7/PaddlePaddle_code/blob/master/1.mnist/cnn.py)
View Code
最后,我们用之前的4 * 4的image数据检验一下通过一次卷积神经网络进行前向传播和反向传播后的输出结果:
运行一下:
运行结果:
PaddlePaddle卷积神经网络源码解析
卷积层
在上篇文章中,我们对paddlepaddle实现卷积神经网络的的函数简单介绍了一下。在手写数字识别中,我们设计CNN的网络结构时,调用了一个函数simple_img_conv_pool(上篇文章的链接已失效,因为已经把framework--->fluid,更新速度太快了 = =)使用方式如下:
这个函数把卷积层和池化层两个部分封装在一起,只用调用一个函数就可以搞定,非常方便。如果只需要单独使用卷积层,可以调用这个函数img_conv_layer,使用方式如下:
我们来看一下这个函数具体有哪些参数(注释写明了参数的含义和怎么使用)
我们了解这些参数的含义后,对比我们之前自己手写的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
栏目分类
- HOD中文网
- GUSD中文网