用Keras搭建深度学习框架的时候,我一般都是看着别人的代码,照着敲一遍完事,感觉自己会了,但是实际上却不是这样。刚好,下周需要用钉钉讲解深度学习框架,因为对Keras相对熟悉,所以打算用Keras搭建一个最简单的LeNet-5神经网络,用于最简单的mnist手写数字的识别,也算是对之前学习知识的一个强化。

LeNet-5卷积神经网络

LeNet5网络是计算机科学家Yann LeCun于1998年发布的一篇论文《Gradient based learning applied to document-recognition》中提出的,这篇论文对于现代卷积神经网络的研究仍具有指导意义,可以说是CNN领域的第一篇经典之作。(摘自网络)

下面我将展示一下LeNet-5模型的整体框架结构:

微信截图_20200429011227.png

LeNet-5模式含有两个卷积层,两个池化层,两个全连接层和一个高斯连接组成。输入图像是32×32的手写数字,采用的5×5的卷积核,下采样使用的是2×2的卷积核。前期使用tanh激活函数,在最后一层全连接使用sigmoid激活函数。

接着我将展示LeNet-5网络的参数情况:

Jo9Cq0.png

后面,我将上图原始的LeNet-5网络结构,用Keras来实现它。

MNIST手写数据集

MNIST手写数据集是学习深度学习中最常用的一个数据集。该数据集包含60,000个用于训练的示例和10,000个用于测试的示例。这些数字已经过尺寸标准化并位于图像中心,图像是固定大小(28x28像素),其值为0到1。为简单起见,每个图像都被平展并转换为784(28 * 28)个特征的一维numpy数组。(摘自网络)

手写数据集概览如下:

Jo9Bo8.png

现如今,几乎所有的深度学习框架都会自带mnist手写数据集,使用起来非常方便,在Keras中只需使用from keras.models import mnist这段代码就可以载入mnist数据集。

LeNet-5实现MNIST数字识别

如果想要实现一个深度学习模型,那么导入数据集,对数据集做处理,搭建模型, 训练模型,用训练集检查模型的好坏都必不可少。我会按照以上介绍的步骤一步步实现这个简单的项目。

#1.导入必须的Python包

想要实现这个项目,首先必备的工具不可少,导入必须的python包是第一步。以下是完成这个项目所必需的packages。

1
2
3
4
5
6
7
8
from keras.datasets import mnist
from keras.layers.convolutional import Conv2D, MaxPooling2D
from keras.layers import Flatten, Dense
from keras.models import Sequential
from keras.optimizers import SGD
from sklearn.metrics import classification_report
from sklearn.preprocessing import LabelBinarizer
import matplotlib.pyplot as plt

以上这些包的作用我不多做介绍,下面用到的时候会做简单介绍,如果有疑问的话,请自行百度。

#2.加载数据集,并对数据做预处理

1
2
3
4
5
6
7
8
9
10
11
# 加载数据集
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# 对数据做预处理
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1)
x_test = x_test.reshape(x_test.shape[0], 28, 28, 1)

# 将label进行one-hot编码
lb = LabelBinarizer()
y_train = lb.fit_transform(y_train)
y_test = lb.transform(y_test)

使用mnist.load_data()就可以加载出数据,并将数据分为训练集合测试集。系统自带的数据集已经分好过了,不需要我们自己写代码拆分。包含60000张训练图片和10000张测试图片。

在我们将图片输入卷积神经网络之前需要对数据做一下预处理,因为卷积神经网络需要一个四维的数据,因此我们也应该将输入数据变成四维的。x_train.shape[0]表示输入数据集的数目,两个28分别表示图片的高和宽,而最后的1表示通道数,因为手写数据是黑白的,所以通道数是1,如果输入图片是彩色的,则通道数目应该改为3。

除了对输入图像做处理,我们还得对标签进行一下预处理,使用LabelBinarizer()方法将标签变成one-hot编码,方便我们后期进行分类。如果你不会这个方法的话,可以参考官方文档

#3.搭建LeNet-5卷积神经网络

对数据集做完处理之后,接下来就应该搭建我们的框架了,代码如下:

1
2
3
4
5
6
7
8
9
10
# 搭建LeNet框架
model = Sequential()
model.add(Conv2D(6, (5, 5), padding='valid', activation='tanh', input_shape=(28, 28, 1)))
model.add(AveragePooling2D((2, 2)))
model.add(Conv2D(16, (5, 5), padding='valid', activation='tanh'))
model.add(AveragePooling2D(2, 2))
model.add(Flatten())
model.add(Dense(120, activation='tanh'))
model.add(Dense(84, activation='tanh'))
model.add(Dense(10, activation='softmax'))

上面的代码,完全是按照Yann LeCun原始论文搭建,如果对于Model方法不熟悉的话,可以参看Keras的官方文档,这里不做过多阐述。

#4.对搭建的模型进行训练

1
2
3
4
5
6
7
8
9
10
# 设置优化器
sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)

# 对模型进行编译
model.compile(loss="categorical_crossentropy", optimizer=sgd, metrics=['accuracy'])
print(model.summary())

# 对模型进行训练
H = model.fit(x_train, y_train, validation_data=(x_test, y_test), batch_size=128, epochs=20, verbose=1, shuffle=True)
model.save("./lenet-5-MNIST.hdf5")

我们在对我们构建的模型进行训练之前,应该先设置优化器optimizer和损失函数loss。**优化器用来更新和计算影响模型训练和模型输出的网络参数,使其逼近或达到最优值,从而最小化(或最大化)损失函数,而损失函数用来衡量模型预测的好坏。**在这里我们使用的是SGD(随机梯度下降)优化器和categorical_crossentropy损失函数。而categorical_crossentropy损失函数常被用于多分类,如果只进行二分类的话binary_crossentropy会是更好的选择。

设置完优化器之后,接着就是要对模型进行编译操作,编译之后我们将对模型进行训练,我们将训练完成的模型用model.save()方法进行保存,方便下次直接使用。这里设置的batch_size是128,表示一个batch包含128张图片,epochs设置成20,表示将训练集训练20次。如果对.model方法不熟悉的话,可以参考我上上篇博客,这里不多做赘述。

#5.使用测试集对我们的模型进行评估

在训练完模型之后,我们需要在测试机上进行测试,以检测我们模型的好坏,代码如下:

1
2
3
4
# 使用测试集预测结果
preds = model.predict(x_test, batch_size=128)
print(classification_report(y_test.argmax(axis=1), preds.argmax(axis=1),
target_names=[str(x) for x in lb.classes_]))

我们使用model.predict()方法对我们的测试集进行测试,预测的结果用preds表示,接着我们使用classification_report()来表示我们预测的准确度。使用方法请参考以下这篇博客:机器学习笔记--classification_report&精确度/召回率/F1值

#6.绘制训练时候的精确度和损失的变化

项目完成后,我们往往想看一下训练时候的Loss和Accuracy的变化,当然图片走势必比数字更直观。我参考了官方文档的方法,来对训练数据进行了可视化,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 绘制训练 & 验证的准确率值
plt.plot(H.history['accuracy'])
plt.plot(H.history['val_accuracy'])
plt.title('Model accuracy')
plt.ylabel('Accuracy')
plt.xlabel('Epoch')
plt.legend(['Train', 'Test'], loc='upper left')
plt.savefig("Accuracy.png")
plt.show()

# 绘制训练 & 验证的损失值
plt.plot(H.history['loss'])
plt.plot(H.history['val_loss'])
plt.title('Model loss')
plt.ylabel('Loss')
plt.xlabel('Epoch')
plt.legend(['Train', 'Test'], loc='upper left')
plt.savefig("Loss.png")
plt.show()

上面代码是将Accuracy和Loss分开绘制的,当然你也可以将二者绘制到一起,这里参考了之前在pyimagesearch博客里编写的代码,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
# 保存可视化训练结果
plt.style.use("ggplot")
plt.figure()
plt.plot(np.arange(0, 20), H.history["loss"], label="train_loss")
plt.plot(np.arange(0, 20), H.history["val_loss"], label="val_loss")
plt.plot(np.arange(0, 20), H.history["acc"], label="train_acc")
plt.plot(np.arange(0, 20), H.history["val_acc"], label="val_acc")
plt.title("Training Loss and Accuracy")
plt.xlabel("# Epoch")
plt.ylabel("Loss/Accuracy")
plt.legend()
plt.savefig("./lenet-5-loss_acc.png")

我在运行代码的时候,有出现过一个报错,显示KeyError:acc,我在网上搜索一番后,发现是因为Keras版本不同H.history['accuracy']写法不同,有的是写成H.history['acc'],如果你有遇到过类似错误,建议尝试二者中另外的一种,当然对应的val_accuracy也需要进行修改。

#7.完整代码

这里贴上完整代码,方便大家粘贴到编译器运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
from keras.datasets import mnist
from keras.layers.convolutional import Conv2D, AveragePooling2D
from keras.layers import Flatten, Dense
from keras.models import Sequential
from keras.optimizers import SGD
from sklearn.metrics import classification_report
from sklearn.preprocessing import LabelBinarizer
import matplotlib.pyplot as plt

# 加载数据集
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# 对数据做预处理
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1)
x_test = x_test.reshape(x_test.shape[0], 28, 28, 1)

# 将label进行one-hot编码
lb = LabelBinarizer()
y_train = lb.fit_transform(y_train)
y_test = lb.transform(y_test)

# 搭建LeNet框架
model = Sequential()
model.add(Conv2D(6, (5, 5), padding='valid', activation='tanh', input_shape=(28, 28, 1)))
model.add(AveragePooling2D((2, 2)))
model.add(Conv2D(16, (5, 5), padding='valid', activation='tanh'))
model.add(AveragePooling2D(2, 2))
model.add(Flatten())
model.add(Dense(120, activation='tanh'))
model.add(Dense(84, activation='tanh'))
model.add(Dense(10, activation='softmax'))

# 设置优化器,和损失函数
sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)

# 对模型进行编译
model.compile(loss="categorical_crossentropy", optimizer=sgd, metrics=['accuracy'])
print(model.summary())
# 对模型进行训练
H = model.fit(x_train, y_train, validation_data=(x_test, y_test), batch_size=128, epochs=20, verbose=1, shuffle=True)
model.save("./lenet-5-MNIST.hdf5")

# 使用测试集预测结果
preds = model.predict(x_test, batch_size=128)
print(classification_report(y_test.argmax(axis=1), preds.argmax(axis=1),
target_names=[str(x) for x in lb.classes_]))

# 绘制训练 & 验证的准确率值
plt.plot(H.history['accuracy'])
plt.plot(H.history['val_accuracy'])
plt.title('Model accuracy')
plt.ylabel('Accuracy')
plt.xlabel('Epoch')
plt.legend(['Train', 'Test'], loc='upper left')
plt.savefig("Accuracy.png")
plt.show()

# 绘制训练 & 验证的损失值
plt.plot(H.history['loss'])
plt.plot(H.history['val_loss'])
plt.title('Model loss')
plt.ylabel('Loss')
plt.xlabel('Epoch')
plt.legend(['Train', 'Test'], loc='upper left')
plt.savefig("Loss.png")
plt.show()

"""
# 保存可视化训练结果
plt.style.use("ggplot")
plt.figure()
plt.plot(np.arange(0, 20), H.history["loss"], label="train_loss")
plt.plot(np.arange(0, 20), H.history["val_loss"], label="val_loss")
plt.plot(np.arange(0, 20), H.history["acc"], label="train_acc")
plt.plot(np.arange(0, 20), H.history["val_acc"], label="val_acc")
plt.title("Training Loss and Accuracy")
plt.xlabel("# Epoch")
plt.ylabel("Loss/Accuracy")
plt.legend()
plt.savefig("./lenet-5-loss_acc.png")
"""

结果和总结

这篇博客的最后,我们来看一下LeNet-5模型在mnist手写数字识别上的效果。

首先看一下在训练集和测试集上分别的精确度和损失,如下图所示:

微信截图_20200429025210.png

在训练集上,精确度达到了99.38%,损失是1.97%。而在测试集上,精确度达到了99.02%,损失是3.3%。这个结果已经非常不错了,因为这是在最原始的框架上。

然后我们再看一下在测试集10000张图片上的预测结果:

微信截图_20200429025304.png

在1-9的10个数字上,测试集的精确度和召回率都达到了99%,说明模型的预测效果还是非常不错的。

我们最后在看一下,我们在训练模型时Accuracy和Loss的变化趋势:

JoiCqI.png
Joi9sA.png

从图片中,我们可以看出在测试集的训练中,在17.5的epoch的时候,准确度和损失有点波动,但是不影响最终的结果。


以上是这篇博客的所有内容,差不多花了两三个小时写完,觉得很有成就感,感谢您的关注与阅读。