本文介绍了如何使用Python从底层实现一个简单但经典的卷积神经网络结构——LeNet,并用它完成MNIST手写数字识别任务。
具体的完整代码以及代码的使用方法,可以光顾我的Github
ProfessorHuang/Python_LeNet_UnderlyingImplementationgithub.com我的代码对算法实现方法比较直观,基本上是直接对着公式翻译过来,使用的也主要是函数式编程,只涉及基础的Python语法以及基础的Numpy使用。优点在于对于新手小白比较友好,缺点在于算法效率略低。
之前我写了三篇文章,分别介绍全连接神经网络反向传播理论,全连接神经网络底层Python实现,以及卷积神经网络的反向传播理论。这三篇文章涉及的知识点主要是为这篇卷积神经网络的实现打基础。避免这篇文章中需要牵扯太多的细节。
南柯一梦宁沉沦:全连接神经网络中反向传播算法数学推导zhuanlan.zhihu.com
在这三篇文章中我分别介绍了:
- 全连接层反向传播的数学推导,随机梯度下降法进行训练的原理。
- MNIST数据集下载以及预处理的方法,全连接层反向传播以及随机梯度下降法的Python实现。
- 卷积神经网络中,卷积层以及池化层的反向传播数学推导。
对于上面提到的一些知识点,在这篇文章中会简要带过,如果读者朋友还不太熟悉可以参考我的文章或的别人写的教程。
MNIST数据集预处理:
要进行训练,首先需要准备好数据集,我们使用的仍是从Yann Lecun网站上下载下来的是idx格式的数据。
与之前文章中全连接神经网络对MNIST数据集处理方法略有不一样的是。图片需要存储为一个4维的张量,格式为:图片数×行数×列数×通道数。标签需要存储为一个3维的张量,格式为:标签数×类别数×1.
另外由于LeNet神经网络输入为32×32的图像,而原始的MNIST数据集图片大小为28×28,我们需要将MNIST图片先进行零填充,使它符合LeNet的输入要求。
# 零填充
最终我们得到的数据格式如下:
训练图片:60000×32×32×1,训练标签:60000×10×1
验证图片:10000×32×32×1,验证标签:10000×10×1
LeNet卷积神经网络前向传播:
LeNet包含两个卷积层,两个池化层以及三层全连接层。
输入是32×32×1的图片张量,经过6×5×5×1的卷积核卷积后,图片格式变为28×28×6。再经过最大池化,图片尺寸缩小一半,变为14×14×6。再经过16×5×5×6的卷积核卷积后,图片格式变为10×10×16。再经过一次最大池化,图片再缩小一半,变为5×5×16。
接下来是全连接神经网络了,不过需要先将图片张量拉直,变为400×1的列向量。再分别经过120个结点的隐藏层和84个结点的隐藏层后,输出到输出层的10个结点作为预测结果。
卷积的实现:
第一种实现卷积的方法是依据卷积计算的定义,直接去进行实现。即让卷积核在图片上移动,每移动一次,逐元素相乘再相加,得到输出图片的一个值。
def
conv函数将一个三维图片张量与一个四维卷积核张量进行卷积操作。就像之前文章说的,卷积神经网络中的卷积实际上是若干次二维卷积操作,在conv函数中调用了conv_函数,conv_函数用于实现二维卷积操作。
def
尽管上面卷积的实现尽管非常直观,但缺点是计算效率低,在训练神经网络时会消耗大量的时间。为了提高的计算效率,我们可以将三维图片张量转为二维矩阵,四维卷积核张量也转为二维矩阵,将张量卷积操作转化为矩阵的乘法操作,以此大幅提高计算效率。具体转化方法见下图:
具体参见贾神的回答:
在 Caffe 中如何计算卷积? - 贾扬清的回答 - 知乎 https://www.zhihu.com/question/28385679/answer/44297845
下面给出第二个版本的卷积代码:
def
最大池化的实现:
在进行最大池化操作时,不仅需要找出每个区域的最大值,还需要同时记录下最大值在区域中所处的位置,确保之后delta误差反向传播的正确进行。
def
找最大值用np.max函数,找最大值位置用np.argmax函数
LeNet前向传播实现:
在前向传播中,我们只需要将卷积函数,池化函数,全连接操作进行相应地堆叠即可。只要定义好了基本的模块单元,要实现什么样结构的神经网络都可以。
我们先定义ConvNet类,初始化各层的参数,方便类中其它的方法使用或者修改。
class
再依据LeNet结构,写出前向传播的函数:
def
限于篇幅,就不将relu函数和卷积层中add_bias函数的实现代码放出来,直接根据定义来写即可,但需要重点说一下softmax函数:
def
softmax函数写的时候不能直接按照定义来写,而需要增加一步先对每个元素进行缩放,即减去最大值后,再代入公式,不然一定会发生溢出,从而导致无法训练。
LeNet卷积神经网络反向传播训练
反向传播算法主要涉及各层delta误差的计算以及得到某层delta误差后如何计算该层参数的导数。关于LeNet全连接层部分的实现方法,与我之前全连接神经网络的实现文章中一致,在此不再重复。我们在此主要介绍卷积层以及池化层反向传播的实现。
卷积层中delta误差的反向传播
卷积层delta误差反向传播公式如下:
我看了下我的源代码,没有为这个计算单独写函数,而是直接用一行语句实现的
delta_pool1
公式与我们写的代码有两点不一样。一是公式为了简洁,忽略了
rot180度本质上将一个二维矩阵上下颠倒一次再左右颠倒,numpy中有对应的函数np.flipud和np.fliplr:
def
池化层中delta误差的反向传播:
由于我们LeNet中使用的是最大池化,因此反向传播时将delta误差误差放到前向传播时记录的最大位置处,其它点置零即可。
def
计算卷积核的导数
每对一个通道的输入图片和一个通道的输出图片的delta误差进行二维卷积,我们就得到一个二维卷积核的导数。
我们将原图通道数×卷积结果通道数个二维卷积核的导数重新进行组合成4维张量,即可得到整个卷积核的导数。
def
计算卷积层偏置项的导数
def
直接使用np.sum即可将一个通道的delta误差全部相加
使用随机梯度下降法更新参数
这部分与我之前介绍的多层感知机的实现一致,限于篇幅就不放代码了。
即每次从训练数据中取出一个batch数据,再每次从一个batch数据中取出一组数据,用反向传播计算参数导数后,将一个batch的导数相加后用梯度下降法更新参数。
测试我们实现的LeNet
net = ConvNet()
net.SGD(train_image, train_label, 30, 10, 3e-5)
注意我们的学习率设置为3e-5。之前我训练的时候,正确率总上不去,到处找原因。最后瞎改学习率到1e-5,正确率才上去。之后又尝试了各种参数,最终发现3e-5效果比较好。
每训练一个epoch大概需要50分钟,实现算法效率还有优化的空间。训练到第3个epoch之后,准确度反而下降,有可能是学习率过大导致,也有可能是没有进行正则化导致过拟合。
总结:
本文对经典卷积神经网络LeNet的实现方法进行了介绍,算法主要是根据之前数学的推导直接进行实现,唯一的优化是在卷积的地方使用到了im2col,提高了卷积的计算效率,代码在MNIST数据集上基本正常运行。
但实际上在算法上还有较大的优化空间。在参数的设定,比如学习率,batchsize的选择上,大家也可以多进行尝试。
参考:
[1]卷积神经网络的前向传播主要参考了机器之心
如何使用纯NumPy代码从头实现简单的卷积神经网络www.jiqizhixin.com[2]卷积神经网络的实现代码是基于全连接神经网络实现代码
用Python从底层实现一个多层感知机 - 南柯一梦宁沉沦的文章 - 知乎
南柯一梦宁沉沦:用Python从底层实现一个多层感知机zhuanlan.zhihu.com[3]卷积神经网络反向传播理论参考
卷积神经网络(CNN)反向传播算法推导 - 南柯一梦宁沉沦的文章 - 知乎
南柯一梦宁沉沦:卷积神经网络(CNN)反向传播算法推导zhuanlan.zhihu.com