神经网络

作者:欧新宇(Xinyu OU)

本文档所展示的测试结果,均运行于:Intel Core i7-7700K CPU 4.2GHz

AlphaGo、无人驾驶、人脸识别、智能翻译……这一个个耳熟能详的名词无不预示着人工智能时代的到来。人工智能的本质,是对人的思维过程和行为方式的模拟。研究人的认知机理,引入人的神经元概念,势在必行。

人工神经网络,可以说是这一切的起源。或者说是这一切起源的核心。

1. 神经网络的前世今生


1.1 神经网络的起源

人工智能领域的一个重要任务是让计算机能够像人一样对输入的信息进行判定。比如:

  • 当计算机读入一幅图像后,能否判定里面有没有苹果,如果有,苹果在图中的哪个位置;
  • 当计算机读入一段语音后,能否判定里面有没有提到"中国"二字,如果有,在什么时间点。

正是因为希望模拟人的认知,所以需要通过研究人的认知机理来指导机器提升智能。

众所周知,人对世界的感知和理解主要通过数以亿计的神经元来完成,神经元之间彼此连接构成巨大的神经元网络,输入的信号(如视网膜上的神经元感受到的光线等)经过一层层的神经元往脑部传递,不断做出决策,再通过一层层的神经元输出到反馈端(如影响手脚部动作等)。

所谓神经元(Neuron)是大脑中互相连接的神经细胞,它可以处理和传递化学和电信号。有意思的是,神经元只有两种工作状态————兴奋和抑制,这与计算机中的"1"和"0"是完全一致的。因此,我们也用1来表示神经网络神经元的激活状态,用0表示关闭状态。

下图展示了一个由多个神经元彼此连接组成的网络

神经元

1.2 神经网络的两次寒冬

1943年,逻辑学家Walter Pitts和神经生理学家Warren McCulloch联合发表文章,首次将神经元概念引入计算领域,提出了第一个人工神经元模型,开启了神经网络的大门。

1957年,知名学者Frank Rosenblatt提出了感知器(Perceptron)的概念,该概念非常接近神经元的实际机理,通过将多层感知器前后连接,可以构成一个决策网络,从而为神经网络的研究奠定了基石。

然而好景不长,1969年,被誉为人工智能之父的Marvin Minsky和Seymour Papert出版《Perceptron》一书探讨感知器的优劣,认为仅靠局部连接的神经网络无法有效地开展训练,而全连接的神经网络则过于复杂而不实用。更重要的是,限于当时的计算方法能力,复杂神经网络核心权重参数的计算时长将无法忍受。这些情况影响了学界和业界对神经网络的信心,神经网络的研究陷入了第一次低谷期

近二十年后,当代神经网络三巨头相继发文,推动了神经网络研究的再次兴起。1986年,Geoffrey Hinton和David Rumelhart联合在Nature上发表论文,将BP算法用于神经网络模型,实现了对权重参数的快速计算。1990年,Yann LeCun发表文章,采用BP神经网络实现对手写数字的识别,这可以被视作神经网络的"第一个"重大应用,直到上世纪九十年代末,超过10%的美国支票都采用该技术进行自动识别。

1998年,Yann LeCun又发文提出了LeNet-5的框架,即现在热火朝天的卷积神经网络(Convolutional Neural Network)的基本框架。然而卷积要消耗大量计算资源,BP方法又会带来梯度弥散的问题,从而限制了神经网络的深度和效果。

相反,俄罗斯学者Vladmir Vapnik在1963年提出的支撑向量机(Support Vector Machine,SVM)概念则不断深入发展。到2002年,已将手写数字识别的错误率降至0.56%,远高于同期神经网络的效果。神经网络的研究迎来了第二次寒冬

1.3 神经网络的发展历史

在介绍神经网络的发展历史之前,首先介绍一下神经网络的概念。神经网络主要是指一种仿造人脑设计的简化的计算模型,这种模型中包含了大量的用于计算的神经元,这些神经元之间会通过一些带有权重的连边以一种层次化的方式组织在一起。每一层的神经元之间可以进行大规模的并行计算,层与层之间进行消息的传递。

下图展示了整个神经网络的发展历程。

神经元

神经网络的发展历史甚至要早于计算机的发展,早在上个世纪四十年代就已经出现了最早的神经网络模型。

第一代的神经元模型是验证型的,当时的设计者只是为了验证神经元模型可以进行计算,这种神经元模型既不能训练也没有学习能力,可以简单的把它看成是一个定义好的逻辑门电路,因为它的输入和输出都是二进制的,而中间层的权重都是提前定义好的。

神经网络的第二个发展时代是十九世纪五六十年代,以Rosenblatt提出的感知器模型和赫伯特学习原则等一些工作为代表。

以下几个重要的网络是神经网络一步一步发展到今天的关键

  • 感知器模型
  • 前馈神经网络
  • BP神经网络
  • 深度学习(卷积神经网络CNN、限制玻尔兹曼机RBM、深度自编码机DAE)

2. 神经网络的原理及使用


2.1 神经网络的原理

在本课程中,我们只是介绍神经网络中最基本的一种————多层感知机(Multi-layered Perceptron, MLP), 多层感知机也被称为前馈神经网络。

在前面介绍线性模型的时候,我们给出了一个线性模型的一般公式:

$\hat{y} = w[0]*x[0] + w[1]*x[1] + ... + w[p]*x[p] + b$

在该公式中 $\hat{y}$ 表示对真实 $y$ 值的估计值.

其中,$x[0],x[1],...x[p]$ 是数据集中样本的特征值;$w$ 和 $b$ 是模型的参数,其中 $w$ 具体表示为每个特征的权重,也就是指每个神经元对于模型贡献的重要程度。因此,从数学表示表达式来看,我们可以认为 $\hat{y}$ 就是每个特征的加权和, 这个过程可以用下图表示。

In [2]:
import mglearn
mglearn.plots.plot_logistic_regression_graph()
Out[2]:
%3 cluster_0 inputs cluster_2 output x[0] x[0] y y x[0]->y w[0] x[1] x[1] x[1]->y w[1] x[2] x[2] x[2]->y w[2] x[3] x[3] x[3]->y w[3]

上图给出了线性模型求解过程,其中 $X[i]$ 称为输入层(inputs),$y$ 称为输出层(output)。当$x[i](i = 0, 1,2,3,...,n)$, 表示样本 $x$ 有 $n+1$ 个特征输入(上图表示有4个特征输入)。输入 $x$ 和输出 $y$ 之间的连线用系数 $w$ 表示,可以将这个过程理解为线性关系 $x[i]*w[i] = y[i]$。

为了便于理解,此处省略了偏移量 $b$。

在MLP中,我们在输入层(inputs)和输出层(output)增加一个隐藏层(Hidden Layer),然后模拟线性模型中加权求和的过程。

In [2]:
mglearn.plots.plot_single_hidden_layer_graph()
Out[2]:
%3 cluster_0 inputs cluster_1 hidden layer cluster_2 output x[0] x[0] h0 h[0] x[0]->h0 h1 h[1] x[0]->h1 h2 h[2] x[0]->h2 x[1] x[1] x[1]->h0 x[1]->h1 x[1]->h2 x[2] x[2] x[2]->h0 x[2]->h1 x[2]->h2 x[3] x[3] x[3]->h0 x[3]->h1 x[3]->h2 y y h0->y h1->y h2->y

如上图所示,我们可以将这个MLP理解为一个两层的线性模型。

  • 第一层模型的输入层是原来的输入层(inputs),输出层是新增加的隐层(hidden layer),此时输出变成了隐层单元 $h[j]$.

  • 第二层模型的输入层变成了新增加的隐层(hidden layer),而输出层依然是原来的输出层(output),此时输入变成了隐层单元 $h[j]$.

与线性模型相似,在第一层模型中的每一个 $x[i]$和隐层单元 $h[j]$ 之间和第二层模型中的每一个隐层单元 $h[j]$ 和 $y$ 之间都有一个系数 $w$。相似的,在每个模型中也都有一个偏移量 $b$.

此时,最终的输出 $y$ 等于 $j$ 个加权和的加权和。

2.2 神经网络中的非线性矫正

值得注意的是,上面所介绍的MLP模型,并非完整的MLP模型。从数学的角度来分析,不难想象,输出 $y$ 变成了 $j$ 个加权和的加权和,但其结果依然不会有什么区别,为了让模型更强大,我们还需要增加一些处理,例如:非线性(限制线性)激活、Dropout、Pooling、shortcut(ResNet)、batchNorm等。

对于MLP,我们仅引入激活函数。基本思路是在每个隐层后面增加一个激活函数。

激活函数

假设,我们用 $h(x)$ 表示线性变化,$ReLU(x)$ 表示激活函数,则原来的线性变换 $y = f(x)$ 可以表示为:$y = ReLU(f(x))$.

而两层MLP就可以表示为 $y = ReLU(f_2(ReLU(f_1(x))))$

【知识点】[激活函数Activation Function](knowledgement/ActivationFunction.ipynb)

下面给出ReLU和tanh对特征进行限制激活的效果:

  • ReLU,限制线性矫正单元 Rectified Linear Unit
  • tanh,双曲正切 Tangens Hyperbolicus
In [3]:
import numpy as np
import matplotlib.pyplot as plt
line = np.linspace(-5,5,200)
plt.plot(line, np.tanh(line),label='tanh')
plt.plot(line, np.maximum(line,0),label='relu')
plt.legend(loc='best')
plt.xlabel('x')
plt.ylabel('ReLU(x) and tanh(x)')
plt.show()

【结果分析】

从图中可以看出,双曲正切函数tanh将特征限制到了 $[-1, 1]$之间,而限制线性单元ReLU则将小于0的部分直接归0.

假设我们使用ReLU激活函数对特征进行激活矫正,则原来的公式:

$\hat{y} = w[0]*x[0] + w[1]*x[1] + ... + w[p]*x[p] + b$

将变为:

$h[0] = ReLU(w[0]*x[0] + w[1]*x[1] + ... + w[p]*x[p] + b)$

$h[1] = ReLU(w[0]*x[0] + w[1]*x[1] + ... + w[p]*x[p] + b)$

...

$h[j] = ReLU(w[0]*x[0] + w[1]*x[1] + ... + w[p]*x[p] + b)$

$\hat{y} = v[0]*h[0] + v[1]*h[1] + ... + v[j]*h[j]$

此处,MLP又多了一组权重系数 $v[i]$ 用来调节每个隐层单元 $h$ 之间的权重.

2.3 更深的网络

当我们继续增加隐层数量的时候,网络将变为如下的形状:

In [4]:
mglearn.plots.plot_two_hidden_layer_graph()
Out[4]:
%3 cluster_2 hidden layer 2 cluster_0 inputs cluster_1 hidden layer 1 cluster_3 output x[0] x[0] h1[0] h1[0] x[0]->h1[0] h1[1] h1[1] x[0]->h1[1] h1[2] h1[2] x[0]->h1[2] x[1] x[1] x[1]->h1[0] x[1]->h1[1] x[1]->h1[2] x[2] x[2] x[2]->h1[0] x[2]->h1[1] x[2]->h1[2] x[3] x[3] x[3]->h1[0] x[3]->h1[1] x[3]->h1[2] h2[0] h2[0] h1[0]->h2[0] h2[1] h2[1] h1[0]->h2[1] h2[2] h2[2] h1[0]->h2[2] h1[1]->h2[0] h1[1]->h2[1] h1[1]->h2[2] h1[2]->h2[0] h1[2]->h2[1] h1[2]->h2[2] y y h2[0]->y h2[1]->y h2[2]->y

2.4 更更深的网络

下面展示几个重要的深度神经网络,一个是5层的LeNet5, 一个是7层的AlexNet,一个是19层(16层的)VGG,一个是34层的ResNet34,更常用的还包括ResNet101.

  • LeNet5

LeNet5

  • AlexNet

AlexNet

  • VGG和ResNet

VGG-ResNet

3. 神经网络的使用


3.1 Baseline基础模型

  • 数据载入、预处理及模型训练
In [ ]:
# TODO: 1. 导入必须库 以及 定义必要的函数
import numpy as np
# 导入数据集工具包
from sklearn import datasets
from sklearn.model_selection import train_test_split
# 导入MLP神经网络包
from sklearn.neural_network import MLPClassifier

# TODO: 2. 创建/导入数据
wine = datasets.load_wine()

# TODO: 3. 数据预处理,包括训练集、测试集划分,数据正则化,数据清洗等
X = wine.data[:, :2]  # 为便于可视化仍然仅使用前两个特征
y = wine.target
X_train, X_test, y_train, y_test = train_test_split(X, y,random_state=16)

# TODO: 4. 构建模型,并进行模型训练(或称为拟合数据)---
mlp_1L100 = MLPClassifier(solver='lbfgs')
mlp_1L100.fit(X_train, y_train)

【结果分析】

  • solver: 优化器,可选参数{'lbfgs', 'sgd', 'adam'},默认值'adam'. lbfgs拟牛顿算法,适合较小数据集,一般会使用整个数据集来进行运算和优化;sgd随机梯度下降算法,是神经网络广泛使用的优化算法,结合mini-batch方法,可以实现对大规模和超大规模数据集的优化;adam是对sgd算法的改进,它通过计算梯度的一阶矩估计和二阶矩估计而自动调节学习率。
  • activation: 激活函数,可选参数{'identity', 'logistic', 'tanh', 'relu'},默认值'relu'. 其中identity表示恒等,不对样本特征做处理,返回值$f(x)=x$;logistic就是sigmoid激活函数,返回值$f(x) = \frac{1}{1+e^{-x}}$;relu是限制线性激活,是目前深度学习应用最广的一种激活函数,返回值$f(x) = max(0, x)$。
  • hidden_layer_sizes: 表示隐藏层的数量,数组形式.[100, 100, 100],表示包含三层,每层100个神经元。
  • alpha: 正则化参数,默认值为0.0001
  • batch_size: 批处理时,每批数据的大小。当数据集较大时,通常会设置为mini-batch模式进行优化。lbfgs由于是对所有样本进行统一处理,所以无法使用mini-batch模式。一般来说,批大小的设置以设备所支持的最大性能为依据,例如:GPU的显存的大小,内存所能支持的最大容量。
  • max_iter: 最大迭代次数
  • learning_rate:学习率,用于设置每次权重更新的大小,它决定了权重更新的速度。在sklearn中只有使用随机梯度算法(SGD)时需要设置,可选参数{'constant', 'invscaling', 'adaptive'},默认值为'constant'.
  • learning_rate_init: 学习率初始值,默认为0.001. 在使用深度卷积神经网络进行训练时,学习率的设置非常有讲究,一般会根据迭代次数进行手动调整。通常情况会使用一个较小的学习率(例如0.001)做模型激活(慢启动),然后随着迭代次数设置3个不同的学习率,这三个学习率一般采用从大到小的方式设置,例如一个典型值:{0.01, 0.001, 0.0001}(注意,典型值并不是绝对实用所有情况)
  • momentum: 动量,默认值0.9, 一般是在SGD算法中使用
  • shuffle: 设置是否在每次迭代时都打乱数据,该超参数只能引用与SGD和ADAM。为了增加多样性,一般设置为True。通常在深度学习中,每次迭代都打乱数据是非常重要的设置。
  • 其他在参数在真实应用中再进行讲解和设置
  • 可视化训练结果

导入绘图工具集并使用不同色块显示不同分类

In [6]:
# 导入绘图工具集
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap

# 使用不同色块显示不同分类
cmap_light = ListedColormap(['#FFAAAA', '#AAFFAA', '#AAAAFF'])
cmap_bold = ListedColormap(['#FF0000', '#00FF00', '#0000FF'])
x_min, x_max = X_train[:, 0].min() - 1, X_train[:, 0].max() + 1
y_min, y_max = X_train[:, 1].min() - 1, X_train[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, .02),
                     np.arange(y_min, y_max, .02))
In [7]:
# TODO: 5. 输出预测结果
score_train = mlp_1L100.score(X_train, y_train)
score_test = mlp_1L100.score(X_test, y_test)
print("训练集准确率: {0:.4f}, 测试集准确率: {1:.4f}.".format(score_train, score_test))

# TODO: 6. 可视化预测结果
Z1 = mlp_1L100.predict(np.c_[xx.ravel(), yy.ravel()])

Z1 = Z1.reshape(xx.shape)
plt.figure()
plt.pcolormesh(xx, yy, Z1, cmap=cmap_light)

# 将数据特征用散点图绘制出来
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolor='k', s=60)
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())

#设置图的标题,并显示图形
plt.title("MLPClassifier: solver=lbfgs")
plt.show()
训练集准确率: 0.8045, 测试集准确率: 0.8000.

3.2 调整隐藏神经元数量(调整网络的宽度)

In [8]:
# TODO: 4. 构建模型,并进行模型训练(或称为拟合数据)---
mlp_1L10 = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[10], random_state=16)
mlp_1L10.fit(X_train, y_train)

# TODO: 5. 输出预测结果
score_train = mlp_1L10.score(X_train, y_train)
score_test = mlp_1L10.score(X_test, y_test)
print("训练集准确率: {0:.4f}, 测试集准确率: {1:.4f}.".format(score_train, score_test))

# TODO: 6. 可视化预测结果
Z2 = mlp_1L10.predict(np.c_[xx.ravel(), yy.ravel()])

Z2 = Z2.reshape(xx.shape)
plt.figure()
plt.pcolormesh(xx, yy, Z2, cmap=cmap_light)

# 将数据特征用散点图绘制出来
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolor='k', s=60)
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())

#设置图的标题,并显示图形
plt.title("MLPClassifier: solver=lbfgs, nodes=10")
plt.show()
训练集准确率: 0.8045, 测试集准确率: 0.8000.

【结果分析】

相比mlp_1L100模型mlp_1L10模型只调整节点数$nodes = [100] -> [10] $, 可以看到输出图的边界丢失了很多细节,不是那么的平滑了。我们可以理解的是,神经元的数量代表了超平面的非线性程度,减少神经元就意味着降低了超平面的非线性程度。这样影响到了分类器的性能。

从准确率来看,不管是训练集还是测试集,都有一定程度的下降。

  • Nodes=100: 训练集准确率: 0.8195, 测试集准确率: 0.8444.
  • Nodes=10: 训练集准确率: 0.8045, 测试集准确率: 0.8000.

从以上的实验结果, 我们可以得到以下几个粗略的结论:

  1. 在一定范围内适当的增加神经元的数量有利于提高模型的判别能力。
  2. 但是值得注意的是,并不是一味地增加网络的宽度就能持续提高性能,当神经元过多,而特征过于简单、样本不足的时候,数据无法很好地拟合所有的神经元,这就会导致欠拟合问题的发生,导致系统性能下降。
  3. 此外,随着神经元的增加,也会增加系统内存(显存)的消耗,当超过系统所能承载的负担时就会发生宕机现象。上面的模型,当node=20000时,大约需要16G的内存。

除了增加单层网络神经元的数量(宽度)以外,还可以通过以下两种办法调节非线性特性:

  1. 调整激活函数
  2. 增加网络的深度(隐藏层的层数)

3.3 调整神经网络的深度

In [9]:
# TODO: 4. 构建模型,并进行模型训练(或称为拟合数据)---
mlp_2L = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[30,30], 
                       random_state=16)
mlp_2L.fit(X_train, y_train)

# TODO: 5. 输出预测结果
score_train = mlp_2L.score(X_train, y_train)
score_test = mlp_2L.score(X_test, y_test)
print("训练集准确率: {0:.4f}, 测试集准确率: {1:.4f}.".format(score_train, score_test))

# TODO: 6. 可视化预测结果
Z3 = mlp_2L.predict(np.c_[xx.ravel(), yy.ravel()])

Z3 = Z3.reshape(xx.shape)
plt.figure()
plt.pcolormesh(xx, yy, Z3, cmap=cmap_light)

# 将数据特征用散点图绘制出来
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolor='k', s=60)
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())

#设置图的标题,并显示图形
plt.title("MLPClassifier: solver=lbfgs, nodes=[10, 10]")
plt.show()
训练集准确率: 0.8271, 测试集准确率: 0.8444.

【结果分析】

  • Nodes=10: 训练集准确率: 0.8045, 测试集准确率: 0.8000.
  • Nodes=[10, 10]: 训练集准确率: 0.8421, 测试集准确率: 0.8222.
  • Nodes=100: 训练集准确率: 0.8195, 测试集准确率: 0.8444.
  • Nodes=[100, 100]: 训练集准确率: 0.8120, 测试集准确率: 0.7778.
  • Nodes=[30, 30]: 训练集准确率: 0.8271, 测试集准确率: 0.8444.

从输出结果,我们可以得到以下几个结论:

  1. 当单层节点Nodes=10时,增加了神经网络的深度后,模型的性能可以达到一个较优的值。这体现了深度网络对于模型性能的改善是有利的。
  2. 当单层节点Nodes=100时,增加神经网络的深度后,模型的性能并没有出现预期的上升,而是出现了一定程度的下降, 这主要原因可能是由于模型特征较简单,而过多的神经元会让网络出现欠拟合问题。此外,也有可能是因为训练并没有达到最终的收敛状态,所以在某个时刻,由于数据多样性的振荡,而产生的某个局部低谷。[所以,进行模型训练时,一般需要模型的验证集的性能在一定时间内保持稳定才能终止训练]
  3. 当设置Nodes=[30,30]时,我们发现训练集和测试集的性能都得到了进一步提升,这说明足够的宽度、深度,而不产生过拟合的情况,都是保证模型性能的关键因素。

有兴趣的同学,可以尝试调整max_iter的次数来让模型训练得更充分一些,或者设置更深的模型,看看能得到什么样的结论。

例如:hidden_layer_sizes=[30,30,50], max_iter={10, 50, 100, 200}

3.4 调整激活函数

In [10]:
# TODO: 4. 构建模型,并进行模型训练(或称为拟合数据)---
mlp_tanh = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[30,30], 
                         activation='tanh', random_state=16)
mlp_tanh.fit(X_train, y_train)

# TODO: 5. 输出预测结果
score_train = mlp_tanh.score(X_train, y_train)
score_test = mlp_tanh.score(X_test, y_test)
print("训练集准确率: {0:.4f}, 测试集准确率: {1:.4f}.".format(score_train, score_test))

# TODO: 6. 可视化预测结果
Z4 = mlp_tanh.predict(np.c_[xx.ravel(), yy.ravel()])

Z4 = Z4.reshape(xx.shape)
plt.figure()
plt.pcolormesh(xx, yy, Z4, cmap=cmap_light)

# 将数据特征用散点图绘制出来
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolor='k', s=60)
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())

#设置图的标题,并显示图形
plt.title("MLPClassifier: solver=lbfgs, activation=tanh, nodes=[10, 10]")
plt.show()
训练集准确率: 0.8045, 测试集准确率: 0.8222.

【结果分析】

修改激活函数为tanh后,可以看到的的是分类器的决策面完全变成了平滑的曲线,这主要是因为相比于ReLU的硬边界,tanh的双曲曲线本身就更加平滑一些。

至于性能,我们并没有强调更平滑的曲线就一定比硬边界就好,这都需要在具体的数据集和具体的超参数设置下调整。实际上,在很多时候:

  1. 更复杂非线性,比平滑具有更优的优势
  2. ReLU所采用的硬边界由于其简单性,带来了计算速度快的优势

换句话说,ReLU激活函数,通过结合深度神经网络更宽、更深的特性,总是能在获得更好性能的同时,降低计算的复杂性(相比其他激活函数具有更好的计算效率)。

3.5 调节超参数 —— 正则化超参数alpha

In [11]:
# TODO: 4. 构建模型,并进行模型训练(或称为拟合数据)---
mlp_alpha = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[30,30], 
                          activation='tanh', alpha=1, random_state=16)
mlp_alpha.fit(X_train, y_train)

# TODO: 5. 输出预测结果
score_train = mlp_alpha.score(X_train, y_train)
score_test = mlp_alpha.score(X_test, y_test)
print("训练集准确率: {0:.4f}, 测试集准确率: {1:.4f}.".format(score_train, score_test))

# TODO: 6. 可视化预测结果
Z5 = mlp_alpha.predict(np.c_[xx.ravel(), yy.ravel()])

Z5 = Z5.reshape(xx.shape)
plt.figure()
plt.pcolormesh(xx, yy, Z5, cmap=cmap_light)

# 将数据特征用散点图绘制出来
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolor='k', s=60)
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())

#设置图的标题,并显示图形
plt.title("MLPClassifier: solver=lbfgs, activation=tanh, nodes=[10, 10], alpha=1")
plt.show()
训练集准确率: 0.8120, 测试集准确率: 0.8444.

【结果分析】

超参数alpha用于调节模型的正则化程度,增加alpha会使模型更加简单,从可视化结果上看,即弯曲程度更大,褶皱更少。

同样,这并不代表性能更好更差

【小节】

神经网络,特别是基于卷积神经网络(CNN)及其各种变种的深度学习模型是目前人工智能领域最好的模型。也正是基于这些模型,人工智能才真正的具有实用性可用性

但是,对于如何训练模型,具有一定的要求,特别是对数据集的处理和模型超参数的调节。我们将在“深度学习”的相关课程中进一步讲解有关神经网络的知识。 </font>

4. 神经网络实例——MNIST手写字体识别


MNIST数据集被誉为人工智能领域的“Hello World”,不仅因为其“简单”,更因为其实用,同时也因为它并不是真的那么“简单”,它既可以用来验证普通的机器学习算法,也可以被应用到深度学习领域。当然,直到卷积神经网络的出现,MNIST的性能才真正提高到了99.8%以上。

4.1 MNIST数据集

MNIST手写字体(数字)数据集目前由三驾马车之一的纽约大学教授Yann LeCun实验室维护,包含多种数据格式。整个数据集包含0-9十个数字的灰度图片共计70000张,都是分辨率为$28\times28$的图片,其中训练集样本60000张,测试集10000张。数据原始的地址为:http://yann.lecun.com/exdb/mnist/

MNIST数据集都是分辨率为$28\times28$的灰度图片,每个像素的值为 0~255 之间的灰度值。为了便于建模需要将特征值转换成 0~1 之间的值。

下面使用sklearn库的fetch_mldata库来获取MNIST数据集

4.1.1 使用内置集成工具载入数据集

  • 载入数据集

DeprecationWarning: Function fetch_mldata is deprecated; fetch_mldata was deprecated in version 0.20 and will be removed in version 0.22. Please use fetch_openml.

由于数据获取函数fetch_mldata()已经被弃用,因此我们改用另外一个数据函数fetch_openml(), 该函数通过一个开源的网站OpenML进行下载。

In [12]:
# 导入数据集获取工具
from sklearn.datasets import fetch_openml
import time
start = time.time()

# 加载MNIST手写数字数据集
mnist = fetch_openml('mnist_784', version=1, cache=True)

print("载入数据集共耗时: {:.3f}s".format(time.time() - start))
载入数据集共耗时: 16.409s
  • 观察数据集
In [13]:
print(mnist.keys())
dict_keys(['data', 'target', 'feature_names', 'DESCR', 'details', 'categories', 'url'])
In [14]:
print('data = {}, \n target = {}'.format(mnist['data'], mnist['target']))
data = [[0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 ...
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]], 
 target = ['5' '0' '4' ... '4' '5' '6']
In [15]:
print('{}'.format(mnist['DESCR']))
**Author**: Yann LeCun, Corinna Cortes, Christopher J.C. Burges  
**Source**: [MNIST Website](http://yann.lecun.com/exdb/mnist/) - Date unknown  
**Please cite**:  

The MNIST database of handwritten digits with 784 features, raw data available at: http://yann.lecun.com/exdb/mnist/. It can be split in a training set of the first 60,000 examples, and a test set of 10,000 examples  

It is a subset of a larger set available from NIST. The digits have been size-normalized and centered in a fixed-size image. It is a good database for people who want to try learning techniques and pattern recognition methods on real-world data while spending minimal efforts on preprocessing and formatting. The original black and white (bilevel) images from NIST were size normalized to fit in a 20x20 pixel box while preserving their aspect ratio. The resulting images contain grey levels as a result of the anti-aliasing technique used by the normalization algorithm. the images were centered in a 28x28 image by computing the center of mass of the pixels, and translating the image so as to position this point at the center of the 28x28 field.  

With some classification methods (particularly template-based methods, such as SVM and K-nearest neighbors), the error rate improves when the digits are centered by bounding box rather than center of mass. If you do this kind of pre-processing, you should report it in your publications. The MNIST database was constructed from NIST's NIST originally designated SD-3 as their training set and SD-1 as their test set. However, SD-3 is much cleaner and easier to recognize than SD-1. The reason for this can be found on the fact that SD-3 was collected among Census Bureau employees, while SD-1 was collected among high-school students. Drawing sensible conclusions from learning experiments requires that the result be independent of the choice of training set and test among the complete set of samples. Therefore it was necessary to build a new database by mixing NIST's datasets.  

The MNIST training set is composed of 30,000 patterns from SD-3 and 30,000 patterns from SD-1. Our test set was composed of 5,000 patterns from SD-3 and 5,000 patterns from SD-1. The 60,000 pattern training set contained examples from approximately 250 writers. We made sure that the sets of writers of the training set and test set were disjoint. SD-1 contains 58,527 digit images written by 500 different writers. In contrast to SD-3, where blocks of data from each writer appeared in sequence, the data in SD-1 is scrambled. Writer identities for SD-1 is available and we used this information to unscramble the writers. We then split SD-1 in two: characters written by the first 250 writers went into our new training set. The remaining 250 writers were placed in our test set. Thus we had two sets with nearly 30,000 examples each. The new training set was completed with enough examples from SD-3, starting at pattern # 0, to make a full set of 60,000 training patterns. Similarly, the new test set was completed with SD-3 examples starting at pattern # 35,000 to make a full set with 60,000 test patterns. Only a subset of 10,000 test images (5,000 from SD-1 and 5,000 from SD-3) is available on this site. The full 60,000 sample training set is available.

Downloaded from openml.org.
In [16]:
print('样本数量: {}, 样本特征数量: {}, 样本标签数量: {}'.format(mnist.data.shape[0], mnist.data.shape[1], mnist.target.shape[0]))
样本数量: 70000, 样本特征数量: 784, 样本标签数量: 70000
  • 数据预处理

前面说过,MNIST数据集都是分辨率为$28\times28$的灰度图片,每个像素的值为 0~255 之间的灰度值。为了便于建模需要将特征值转换成 0~1 之间的值。

具体方法很简单,只需要使用矩阵除法即可实现。

In [17]:
from sklearn.model_selection import train_test_split

X = mnist.data/255
y = mnist.target

# 为了提高训练速度,我们只提取10%的样本进行演示
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=6000, test_size=1000, random_state=60)

【特别说明】

  1. 在实际应用中,对于公共数据集(例如MNIST),由于官方已经做了训练集测试集的划分,所以一般我们都应该按照官方的划分方式进行使用。这样的好处是,当我们创造了一个新的算法或者模型时,我们所获得的性能是具有可比性的。因为,所有公开公布的评分都是按照这样的规范来进行设置。

  2. 虽然,本例只是用于学习算法,我们可以任意划分数据集,但是以规范的方式进行划分是一种比较好的习惯。

  3. 当然,对于官方没有提供训练集和测试集划分方式的数据集,我们可以自己进行划分,但是应该注意的是,所有对比算法都应该采用同样的划分方式,以满足公平的原则。


4.1.2 使用文件读取方式载入

该方法使用文件载入的方式进行数据集加载,是比较通用的方法。 但由于本数据集使用二进制码进行保存,因此对于初学者来说有一定的难度,所以在此处推荐各位同学使用我写的一个文件读取进行载入。

基本步骤如下:

  1. 下载mnist数据集,并保存到根目录 /Datasets/mnist/ 文件夹中
  2. 下载mnist数据库载入程序,并保存到根目录 /Modules/load_MNIST.py 文件中。下载地址:http://ouxinyu.cn/Teaching/MachineLearning/Modules/load_MNIST.py
  3. 按照如下方式使用
In [18]:
import sys
import os
sys.path.append(os.path.join(os.getcwd(), '..', 'Modules'))
import load_MNIST

import time
start = time.time()

train_images = load_MNIST.load_train_images()
train_labels = load_MNIST.load_train_labels()
test_images = load_MNIST.load_test_images()
test_labels = load_MNIST.load_test_labels()

print("载入数据集共耗时: {:.3f}s".format(time.time() - start))
开始载入MNIST手写数字数据集:
 训练集图片大小: 28*28, 已载入60000/60000.
训练集标签数量: 60000...已完成。
 测试集图片大小: 28*28, 已载入10000/10000.
测试集标签数量: 10000...已完成。
载入数据集共耗时: 2.747s
  • 观察数据集
In [19]:
print('训练集样本形态: {}, 训练集标签形态: {}, 测试集样本形态: {}, 测试集标签形态: {},'
      .format(train_images.shape, train_labels.shape, test_images.shape, test_labels.shape))
训练集样本形态: (60000, 28, 28), 训练集标签形态: (60000,), 测试集样本形态: (10000, 28, 28), 测试集标签形态: (10000,),
In [20]:
# 查看图片及图片的标签
import matplotlib.pyplot as plt # plt 用于显示图片

id = 34
plt.imshow(test_images[id])
plt.show()
print('图片的ID = {}'.format(test_labels[id]))
图片的ID = 7.0
  • 数据预处理
  1. 由于原始的MNIST数据集数据是三维矩阵,即第一维是样本的数量,第二、三维是图片的长宽。因此,我们首先需要将样本的第二、三维拉成一个$28\times28$的向量,以方便后续的处理。此时,整个样本将由一个三维矩阵转换为二维矩阵。
  2. MNIST数据集都是分辨率为 28×28 的灰度图片,每个像素的值为 0~255 之间的灰度值。为了便于建模需要将特征值转换成 0~1 之间的值。
In [21]:
# 标准调整形态的方法
# X_train = train_images.reshape(train_images.shape[0], train_images.shape[1]*train_images.shape[2])/255
# 此处,因为我们已经知道的样本的形态,所以可以直接书写值

X_train = train_images.reshape(60000, 28*28)/255
y_train = train_labels
X_test = test_images.reshape(10000, 28*28)/255
y_test = test_labels
In [22]:
# 数据预处理
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

【知识点】 StandardScaler: 去均值和方差归一化。且是针对每一个特征维度来做的,而不是针对样本。 StandardScaler对每列分别标准化,因为 shape of data: [n_samples, n_features]

In [23]:
# 为了提高训练速度,我们只提取10%的样本进行演示
X_train_lite = X_train[0:5999,:]
y_train_lite = y_train[0:5999]
X_test_lite = X_test[0:999,:]
y_test_lite = y_test[0:999]

4.2 训练 MLP神经网络

  • 优化方法 lbfgs, 简化版数据集
In [24]:
# 导入多层感知机MLP神经网络
from sklearn.neural_network import MLPClassifier
import time

start = time.time()

mlp = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[100, 100], activation='relu', alpha=1e-5, random_state=62)
mlp.fit(X_train_lite, y_train_lite)

print('训练结束,用时{:.2f}s.'.format(time.time() - start))
print('训练集得分: {:.4f}, 测试集得分: {:.4f}'.format(mlp.score(X_train_lite, y_train_lite), mlp.score(X_test_lite, y_test_lite)))
训练结束,用时2.19s.
训练集得分: 1.0000, 测试集得分: 0.9309

执行结果:用时4.82s, 训练集得分: 1.000, 测试集得分: 0.9239

  • 优化方法 lbfgs, 完整版数据集
In [25]:
# 导入多层感知机MLP神经网络
from sklearn.neural_network import MLPClassifier
import time

start = time.time()

mlp = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[100, 100], activation='relu', alpha=1e-5, random_state=62)
mlp.fit(X_train, y_train)

print('训练结束,用时{:.2f}s.'.format(time.time() - start))
print('训练集得分: {:.4f}, 测试集得分: {:.4f}'.format(mlp.score(X_train, y_train), mlp.score(X_test, y_test)))
训练结束,用时38.99s.
训练集得分: 1.0000, 测试集得分: 0.9732

执行结果:用时90.682s., 训练集得分: 0.9999, 测试集得分: 0.9764用时

  • 优化方法 sgd, 简化版数据集
In [26]:
# 导入多层感知机MLP神经网络
from sklearn.neural_network import MLPClassifier
import time

start = time.time()

mlp = MLPClassifier(solver='sgd', hidden_layer_sizes=[100, 100], activation='relu', alpha=1e-5, random_state=62, max_iter=2000)
mlp.fit(X_train_lite, y_train_lite)

print('训练结束,用时{:.2f}s.'.format(time.time() - start))
print('训练集得分: {:.4f}, 测试集得分: {:.4f}'.format(mlp.score(X_train_lite, y_train_lite), mlp.score(X_test_lite, y_test_lite)))
训练结束,用时37.37s.
训练集得分: 0.9993, 测试集得分: 0.9149

执行结果:用时71.60s, 训练集得分: 0.9985, 测试集得分: 0.9249

  • 优化方法 sgd, 完整版数据集
In [27]:
# 导入多层感知机MLP神经网络
from sklearn.neural_network import MLPClassifier
import time

start = time.time()

mlp = MLPClassifier(solver='sgd', hidden_layer_sizes=[100, 100], activation='relu', alpha=1e-5, random_state=62, max_iter=2000)
mlp.fit(X_train, y_train)

print('训练结束,用时{:.2f}s.'.format(time.time() - start))
print('训练集得分: {:.4f}, 测试集得分: {:.4f}'.format(mlp.score(X_train, y_train), mlp.score(X_test, y_test)))
训练结束,用时219.89s.
训练集得分: 0.9996, 测试集得分: 0.9727

执行结果:用时381.56s, 训练集得分: 0.9990, 测试集得分: 0.9771

4.3 使用模型进行数字识别

4.3.1 输出权值图
In [28]:
# 查看图片及图片的标签
import matplotlib.pyplot as plt # plt 用于显示图片

fig, axes = plt.subplots(4, 4)
# use global min / max to ensure all weights are shown on the same scale
vmin, vmax = mlp.coefs_[0].min(), mlp.coefs_[0].max()
for coef, ax in zip(mlp.coefs_[0].T, axes.ravel()):
    ax.matshow(coef.reshape(28, 28), cmap=plt.cm.gray, vmin=.5 * vmin, vmax=.5 * vmax)
    ax.set_xticks(())
    ax.set_yticks(())

plt.show()
4.3.2 对给定图片进行预测
  • 将图片矩阵保存为图片
In [29]:
import os
import numpy as np
from PIL import Image

id = 342
# 将numpy数组转换为PIL图像
img = Image.fromarray(np.uint8(test_images[id]))
 
# 保存PIL图像到磁盘指定路径
filename = 'mnist' + str(id) + '.png'
img.save(os.path.join(os.getcwd(), 'tmp', filename))

# 所保存图片的标签label
print('图片的ID = {:d}'.format(int(test_labels[id])))
图片的ID = 1
  • 从本地读取图片
In [30]:
import os
from PIL import Image

id = 342
filename = 'mnist' + str(id) + '.png'
image=Image.open(os.path.join(os.getcwd(), 'tmp', filename)).convert('F')
image=image.resize((28,28))

# 显示图像
# image.show()

# 将PIL的Image图像转换为numpy的数组
img = np.asarray(image)

# 将二维矩阵转换为一维向量以便于预测
im = img.reshape(1,28*28)
  • 对图片进行预测
In [31]:
# 输出预测结果
print('图片中的数字是:{:.0f}'.format(mlp.predict(im)[0]))

# 在Notebook中显示PIL图像
# image.show()
图片中的数字是:1

4.4 保存和载入模型

以下我们将做两个重要的工作:

  1. 保存和载入模型
  2. 对自定义的样本进行预测
4.4.1 保存模型
In [32]:
# 载入保存和载入模型的库工具
import joblib
# 导入多层感知机MLP神经网络
from sklearn.neural_network import MLPClassifier
import time

start = time.time()

mlp = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[100, 100], activation='relu', alpha=1e-5, random_state=62)
mlp.fit(X_train_lite, y_train_lite)

print('训练结束,用时{:.2f}s.'.format(time.time() - start))
print('训练集得分: {:.4f}, 测试集得分: {:.4f}'.format(mlp.score(X_train_lite, y_train_lite), mlp.score(X_test_lite, y_test_lite)))


ModelPath = os.path.join(os.getcwd(), '..', 'Models', 'Ch08MNIST_lbfgs_lite.pkl')
joblib.dump(mlp, ModelPath)
训练结束,用时2.09s.
训练集得分: 1.0000, 测试集得分: 0.9309
Out[32]:
['D:\\CloudStation\\MyWebsites\\Teaching\\MachineLearning\\Notebooks\\..\\Models\\Ch08MNIST_lbfgs_lite.pkl']
4.4.2 载入模型并对自定义样本进行预测
In [33]:
import os
import numpy as np
import time
# 导入多层感知机MLP神经网络
from PIL import Image
import joblib

# TODO: 1: 载入模型
start = time.time()

ModelPath = os.path.join(os.getcwd(), '..', 'Models', 'Ch08MNIST_lbfgs.pkl')
model = joblib.load(ModelPath)

print("载入模型共耗时: {:.3f}s".format(time.time() - start))


# TODO: 2. 预测给定图像
# 此处我们属于自己的手写字体进行测试

filename = 'mydigital03.png'
filepath = os.path.join(os.getcwd(), '..', 'Attachments', filename)
image = Image.open(filepath).convert('F')
image = image.resize((28, 28))

# 将PIL的Image图像转换为numpy的数组
img = np.asarray(image)
# 将二维矩阵转换为一维向量以便于预测
im = img.reshape(1, 28*28)

# 输出预测结果
pred = model.predict(im)[0]
print('预测结果是:{:.0f}'.format(pred))
载入模型共耗时: 0.003s
预测结果是:8