018:实战:从零开始完成手写数字识别训练和推理

55 阅读9分钟

本文为合集收录,欢迎查看合集/专栏链接进行全部合集的系统学习。

合集完整版在这里

本文进行一个实战,我选择了一个经典的图像识别神经网络 —— Mnist 手写数字识别的神经网络。

接下来,就从获取训练数据集、训练模型到预测图片走一遍,感受一下一个神经网络是如何被训练出来,又是如何工作的。

为什么选择这个神经网络?

该神经网络以数据集小、神经网络简单、任务简单为优势,并且集合了很多计算机视觉尤其是卷积神经网络中经典的算法,比如卷积和池化算法等。可谓麻雀虽小,五脏俱全,非常适合新手上手练习。

并且这个神经网络用到的训练数据集很小,一般个人笔记本的CPU就可以完成这个神经网络的训练,不用担心训练时间太长的问题,很适合熟悉和练手。

什么是手写数字识别

简答来说,就是搭建了一个卷积神经网络,可以完成手写数字的识别。

用笔在纸上写一个数字 6,这个神经网络就能认识出这张图片是一个6,写一个数字 8,它就识别出来这是个8,就这么简单。

之所以说该任务简单,是因为它的标签只有 0-9 这 10 个数字的十种分类,相比于 resnet 等网络在 ImageNet 上 1000 个实际物品的分类(比如汽车、高楼等),确实小很多。

虽然神经网络简单,但背后的原理却一点都不少,典型的 CNN 训练和算法无一缺席,与该神经网络一起出名的,便是大名鼎鼎的 MNIST(Mathematical Numbers In Text) 数据集。

该数据集中包含了 60,000 个训练图像和 10,000 个测试图像,图像都是各种手写的数字,基本上数字都长这样的。

1.png

开始训练

需要说明的是,这一节暂时不需要理解太多神经网络中的算法细节,比如为什么这一层要用卷积、下一层为什么要用激活函数等,这些在后面会专门来解析。

本节的目标只有一个,熟悉一个神经网络训练和推理的流程,最终识别出一张图片,就成功了。

在简单了解了项目背景后,下面一步步完成该神经网络的训练。

第一步:导入必要的库

# 导入NumPy数学工具箱
import numpy as np 
# 导入Pandas数据处理工具箱
import pandas as pd
# 从 Keras中导入 mnist数据集
from keras.datasets import mnist
keras 是一个开源的人工神经网络库,里面有很多经典的神经网络和数据集,要用的 mnist 数据集就在其中。

第二步:加载数据集

(x_train_image, y_train_lable), (x_test_image, y_test_lable) =  mnist.load_data() 

这条命令利用 keras 中自带的 mnist 模块,加载数据集 load_data 进来,并且将加载的数据集分别赋值给四个变量。

其中:x_train_image 保存用来训练的图像,y_train_lable 是与之对应的标签。假设图像中的数字是1,那么标签就是1。

x_test_image 和 y_test_lable 分别为用来验证的图像和标签,也就是验证集。训练完神经网络后,可以使用验证集中的数据进行验证。

第三步:数据预处理

其中一个预处理内容是改变数据集的 shape,使其满足模型的要求。

 # 导入keras.utils工具箱的类别转换工具
from tensorflow.keras.utils import to_categorical 
# 给标签利用reshape增加维度,使其满足模型的需要
# 比如训练集标签的维度信息是[60000, 28, 28, 1]
x_train = x_train_image.reshape(60000,28,28,1)
x_test = x_test_image.reshape(10000,28,28,1) 
# 特征转换为one-hot编码
y_train = to_categorical(y_train_lable, 10)
y_test = to_categorical(y_test_lable, 10)

这个数据集中的共 60000 张图像用于训练,10000 张图像用于验证训练效果如何,每张图像的长宽均为 28 个像素,通道数为 1。

那么对于训练集 x_train 而言,将其形状变为 NHWC = [60000, 28, 28, 1], 验证集类似。

to_categorical 的作用是将样本标签转为 one-hot 编码,而 one-hot 编码的作用是可以对于类别更好的计算概率或得分。

什么是 one-hot 以及为什么要用 onehot

之所以用 onehot 编码,是因为对于输出 0-9 这10个标签而言,每个标签的地位应该是相等的,并不存在标签数字 2 大于数字 1 的情况。

但如果我们直接利用标签的原始值(0-9)进行最终结果的计算,就会出现标签 2 大于标签 1 的情况。

因此,在大部分情况下,都需要将标签转换为 one-hot 编码,也就独热编码,这样标签之间便没有任何大小而言。

这个例子中,数字 0-9 转换为的独热编码为:

array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.], 
          [0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],       
          [0., 0., 1., 0., 0., 0., 0., 0., 0., 0.], 
          [0., 0., 0., 1., 0., 0., 0., 0., 0., 0.], 
          [0., 0., 0., 0., 1., 0., 0., 0., 0., 0.], 
          [0., 0., 0., 0., 0., 1., 0., 0., 0., 0.],
          [0., 0., 0., 0., 0., 0., 1., 0., 0., 0.], 
          [0., 0., 0., 0., 0., 0., 0., 1., 0., 0.], 
          [0., 0., 0., 0., 0., 0., 0., 0., 1., 0.], 
          [0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]]

每一行的向量代表一个标签。

假设 [1., 0., 0., 0., 0., 0., 0., 0., 0., 0.] 代表 0 而 [0., 1., 0., 0., 0., 0., 0., 0., 0., 0.] 代表1,可以看到这两者之间是正交独立的,不存在谁比谁大的问题。

关于one-hot算法可以参考这里

第四步:创建神经网络。

# 从 keras 中导入模型
from keras import models 
# 从 keras.layers 中导入神经网络需要的计算层
from keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPooling2D
# 构建一个最基础的连续的模型,所谓连续,就是一层接着一层
model = models.Sequential()
# 第一层为一个卷积,卷积核大小为(3,3), 输出通道32
# 使用 relu 作为激活函数
model.add(Conv2D(32, (3, 3), activation='relu', 
                 input_shape=(28,28,1)))
# 第二层为一个最大池化层,池化核为(2,2)
# 最大池化的作用,是取出池化核(2,2)范围内最大的
# 像素点代表该区域,可以降低运算量。
model.add(MaxPooling2D(pool_size=(2, 2)))
# 又经过一个(3,3)的卷积,输出通道变为64,
# 也就是提取了64个特征,同样为 relu 激活函数
model.add(Conv2D(64, (3, 3), activation='relu'))
# 上面通道数增大,运算量增大
# 此处再加一个最大池化,降低运算
model.add(MaxPooling2D(pool_size=(2, 2)))
# dropout 随机设置一部分神经元的权值为零
# 在训练时用于防止过拟合
# 这里设置25%的神经元权值为零,你可以调整这个比例
model.add(Dropout(0.25)) 
# 将结果展平成1维的向量
model.add(Flatten())
# 增加一个全连接层,用来进一步特征融合
model.add(Dense(128, activation='relu'))
# 再设置一个dropout层,将50%的神经元权值为零
# 由于一半的神经元处于关闭状态,这样也可以加速训练
model.add(Dropout(0.5)) 
# 最后添加一个全连接+softmax激活,输出10个分类
# 分别对应0-9 这10个数字
model.add(Dense(10, activation='softmax'))

上面每一行代码都加了注释,说明每一行的作用,短短几行,便是这个手写数字识别神经网络的全部了。

第五步:训练

# 编译上述构建好的神经网络模型
# 指定优化器为 rmsprop
# 指定损失函数为交叉熵损失
model.compile(optimizer='rmsprop',
              loss='categorical_crossentropy',
              metrics=['accuracy'])

# 开始训练 
model.fit(x_train, y_train, # 指定训练特征集和训练标签集
          validation_split = 0.3, # 部分训练集数据拆分成验证集
          epochs=5, # 训练轮次为5轮
          batch_size=128) # 以128为批量进行训练

训练结果如上,可以看到最后的训练精度达到了98.%,还是挺高的。

第6步:验证集验证

# 在测试集上进行模型评估
score = model.evaluate(x_test, y_test) 
print('测试集预测准确率:', score[1]) # 打印测试集上的预测准确率
313/313 [==============================] - 1s 4ms/step - loss: 0.0662 - accuracy: 0.9815

测试集预测准确率: 0.9815000295639038

可以看到在验证集上也能有98%的准确率。

第7步:验证一张图片

# 预测验证集第一个数据
pred = model.predict(x_test[0].reshape(1, 28, 28, 1)) 
# 把softmax分类器输出转换为数字
print(pred[0],"转换一下格式得到:",pred.argmax())
 # 导入绘图工具包
import matplotlib.pyplot as plt
# 输出这个图片
plt.imshow(x_test[0].reshape(28, 28),cmap='Greys')
plt.show()

以验证集中的第一张图片为例来进行验证,输出

转换一下格式得到:7

模型的输出是7,我们把验证集中的第一张图片显示出来,确实是7。

2.png 说明我们训练的模型确实可以识别出数字来,而且很准确,你也可以用验证集中的其他图片来验证。

总结

手写数字识别项目比较简单,仅仅两个卷积层,整体运算量不大,就目前计算机的配置,即使普通的笔记本都可以完成该神经网络的训练和验证。

一些补充说明:

代码中的 model 就是我们训练的模型,具体算法暂时不用太关心。

在你训练的过程中,可以关注打印出来的 loss 值的变化,应该是逐渐变小的

这里用的交叉熵损失函数,可以查看这一节的内容。

你可以调整 dropout 层中每次丢弃神经元的比例,来试一下效果。

在第7步验证的过程中,你可以自己在一张纸上手写一个数字,用这个模型来推理一下,看看是否是你写的数字。

注意写的数字要粗一些,否则太细的话,像素太少很可能预测不出来。

代码可从以下链接获取:gitee.com/iwaihou/cv_…

以上链接中有python代码,也有 jupyter notebook 的代码。

你也可以使用 jupyter notebook 测试练手,方便打印,并且查看中间信息。

我创建了一个《小而精的AI学习圈子》的知识星球,星球上会有非常多高质量的技术专栏分享,同时你也可以在星球向我提问。 在星球你还可以学到很多关于 CV 、大模型以及 AIGC 相关的技术,看到别的同学都在如何学习。不要犹豫,戳下面的链接加入吧,这可能是你学习AI 的道路上非常重要的一次点击呀。 点击这里,我们星球见!