本小节中,我们将实现一个包含一个输入层、一个隐层和一个输出层的多层感知器,并用它来识别MNIST数据集中的图像。我们尽可能使代码做到简单易读。不过第一次接触可能会感觉稍许复杂,建议读者从Packt出版社的官网上下载本章示例代码,其中,注释和语法高亮显示使得代码更加易读。如果读者并未在IPython Notebook中运行这些代码,建议将其复制到当前工作目录下的一个Python脚本文件中,如neuralnet.py,进而可以使用如下命令在当前Python工作进程中将其导入:
代码中包含我们尚未讲解的内容,比如反向传播算法,不过其中大部分代码都是基于第2章中自适应线性神经元(Adaline)实现的,因此读起来应该比较熟悉。如果有不理解的代码也不必担心,我们将在本章后续内容中进行讲解。不过,在当前阶段先熟悉代码有利于理解后面理论部分内容。
现在,我们来初始化一个784-50-10的感知器模型,该神经网络包含784个输入单元(n_features),50个隐层单元(n_hidden),以及10个输出单元(n_output):
读者也许已经注意到,在重复实现多层感知器的同时,我们还额外实现了一些功能,总结如下:
·l2:L2正则化系数λ,用于降低过拟合程度,类似地,l1对应L1正则化参数λ。
·epochs:遍历训练集的次数(迭代次数)。
·eta:学习速率η。
·alpha:动量学习进度的参数,它在上一轮迭代的基础上增加一个因子,用于加快权重更新的学习Δwt=η▽J(wt+Δwt-1)(其中,t为当前所在的步骤,也就是当前迭代次数)。
·decrease_const:用于降低自适应学习速率n的常数d,随着迭代次数的增加而随之递减以更好地确保收敛。
·shuffle:在每次迭代前打乱训练集的顺序,以防止算法陷入死循环。
·Minibatches:在每次迭代中,将训练数据划分为k个小的批次,为加速学习的过程,梯度由每个批次分别计算,而不是在整个训练数据集上进行计算。
接下来,我们将使用重排后的MNIST训练数据集中的60000个样本来训练多层感知器。执行下列代码前请注意:在当前主流配置的台式计算机上,训练神经网络所需的时间大约为10~30分钟:
与前面自适应线性神经元的实现类似,我们使用cost_列表保存了每轮迭代中的代价并且可以进行可视化,以确保优化算法能够收敛。在此,我们仅绘制了50个子批次的每50次迭代的结果(50个子批次×1000次迭代)。代码如下:
通过下图可见,代价函数的图像中有很明显的噪声。这是由于我们使用了随机梯度下降算法的一个变种(子批次学习),来训练神经网络所造成的。
虽然通过上图可以看出,优化算法在经过大约800(40000/50=800)轮迭代后收敛,使用所有子批次的平均值,我们绘制出了一个相对平滑的代价函数图像。代码如下:
从下图可以清楚地看出,训练算法在经过约800次迭代后随即收敛:
现在,我们通过计算预测精度来评估模型的性能:
正如我们所见,模型能够正确识别大部分的训练数字,不过现在还不知道将其泛化到未知数据上的效果如何?我们来计算一下模型在测试数据集上10000个图像上的准确率:
由于模型在训练集与测试集上的精度仅有微小的差异,我们可以推断,模型对于训练数据仅轻微地过拟合。为了进一步对模型进行调优,我们可以改变隐层单元的数量、正则化参数的值、学习速率、衰减常数的值,或者使用第6章中介绍过的自适应学习等技术(此问题留给读者作为练习作业)。
现在,我们看一下多层感知器难以处理的一些图像:
我们得到一个包含5×5子图矩阵的图像,每个子图标题中的第一个数字为图像索引,第二个数字为真实的类标(t),第三个数字则是预测的类标(p)。
从上图可以看出,某些图像即便让我们人工去分类也存在一定难度。例如,数字9的下部呈弯钩状,因此被识别成3或者8(参见第3、16和17子图)。