MINST手写识别文档
(一) 准备工作
MindSpore安装,数据准备
(二) 实验步骤代码及结果
-
1. 配置运行信息
from mindspore import context
context.set_context(mode=context.GRAPH_MODE, device_target="CPU")
l 导入context模块,调用context的set_context方法,参数mode表示运行模式。
MindSpore支持动态图和静态图两种模式,GRAPH_MODE表示静态图模式,PYNATIVE_MODE表示动态图模式,对于此项目手写数字识别问题,选择采用静态图模式。参数device_target表示硬件信息,有三个选项(CPU,GPU,Ascend),在电脑windows环境下,选择硬件平台使用CPU。
-
数据集的详细信息概览
import matplotlib.pyplot as plt
import matplotlib
import numpy as np
import mindspore.dataset as ds
train_data_path = "./MNIST_Data/train"
test_data_path = "./MNIST_Data/test"
mnist_ds = ds.MnistDataset(train_data_path)
#生成的图像有两列[img, label],列图像张量是uint8类型,
#因为像素点的取值范围是(0, 255),需要注意的是这两列数据的数据类型是Tensor
print('The type of mnist_ds:', type(mnist_ds))
print("Number of pictures contained in the mnist_ds:", mnist_ds.get_dataset_size())
#int, number of batches.
#print(mnist_ds.get_batch_size()) #Return the size of batch.
dic_ds = mnist_ds.create_dict_iterator() #数据集上创建迭代器,为字典数据类型,输出的为Tensor类型
item = next(dic_ds)
img = item["image"].asnumpy() #asnumpy为Tensor中的方法,功能是将张量转化为numpy数组,因为matplotlib.pyplot中不
#能接受Tensor数据类型的参数
label = item["label"].asnumpy()
print("The item of mnist_ds:", item.keys())
print("Tensor of image in item:", img.shape)
print("The label of item:", label)
plt.imshow(np.squeeze(img)) #squeeze将shape中为1的维度去掉,plt.imshow()函数负责对图像进行处理,并显示其格式
plt.title("number:%s"% item["label"].asnumpy())
plt.show() #show显示图像
l MindSpore提供的内置数据集处理方法默认输出一般都是在框架中通用的Tensor,但是对于非框架优化包含的python库,包括matplotlib,它们就无法处理接受Tensor,这是我们就要采用Tensor类中定义的asnumpy方法将张量转化为numpy数组。
l 得出结论:训练集中有60000张图片,以image和lable两个标签分别储存,并且每一张图片的大小为28 x 28,通道数为1说明里面是黑白图片。图中显示的图片是6,它的标签值也为6。多次验证,后得到数字8与数字1.
加载及处理数据集
定义一个create_dataset函数
a) 将原始的MNIST数据集加载进来, mnist_ds = ds.MnistDataset(data_path)
b) 设置一些数据增强和处理所需要的参数(parameters)
c) 使用2中设置的参数得到一些实例化的数据增强和处理方法
d) 将3中的方法映射到(使用)在对应数据集的相应部分(image,label)
e) 返回处理好的数据集(不同的神经网络可以接受不同的输入数据格式)
import mindspore.dataset.vision.c_transforms as CV
import mindspore.dataset.transforms.c_transforms as C
from mindspore.dataset.vision import Inter
from mindspore import dtype as mstype
import mindspore.dataset as ds
l 导入的模块说明:
#mindspore.dataset.vision.c_transforms:MindSpore提供的通过数据增强操作对图像进行预处理,从而提高模型的广泛性。基于C++的OpenCV实现,具有较高的性能。在增强MNIST数据集中,所使用到该模块中的方法
a) CV.Resize(size, interpolation=Inter.LINEAR):
功能:使用给定的插值模式将输入图像调整为给定大小。
参数:size:调整后输出图像的大小;interpolation:图像的插值模式(5个常量可供选择),一般情况下使用默认的Inter.LINEAR
b) CV.Rescale(rescale, shift):
功能:使用给定的重缩放和移位重缩放输入图像。重新缩放输入图像命令:输出=图像*重新缩放+移位(output = image * rescale + shift),可以用来消除图片在不同位置给模型带来的影响。
参数:rescale:缩放因子;shift:偏移因子
c) CV.HWC2CHW():
功能:将输入图像从形状(H,W,C)转换为形状(C,H,W)。输入图像应为3通道图像。
#mindspore.dataset.transforms.c_transforms:基于C++的OpenCV实现用于支持常见的图形增强功能的模块。
a) C.TypeCast(data_type):
功能:一个转化为给定MindSpore数据类型(data_type)的张量操作。
参数:data_type:MindSpore中dtype模块中所指定的数据类型。
#mindspore.dataset.MnistDataset,MindSpore中专门提供的加载处理MNIST数据集为MindSpore数据类型的类,使用了map方法,进行分析理解。
ds.map(operations, input_columns=None, output_columns=None, column_order=None, num_parallel_workers=None, python_multiprocessing=False, cache=None, callbacks=None):
功能:将具体的操作和方法应用到数据集上。
参数:
operations:用于数据集的操作列表,操作将按照他们在列表中的顺序进行应用。 input_columns:将作为输入传递给第一个操作的列的名称列表。num_parallel_workers:用于并行处理数据集的线程数,默认值为无。
l 参数含义以及定义原因理解说明
def create_dataset(data_path, batch_size=32, repeat_size=1,
num_parallel_workers=1):
mnist_ds = ds.MnistDataset(data_path)
# 定义数据增强和处理所需的一些参数
resize_height, resize_width = 32, 32
#我们要将MNIST数据中的28 x 28大小的图片转化为32 x 32的大小,这是因为后面我们要采用的LeNet-5的卷积神经网络训练模型,该模型要求输入图像的尺寸统一归一化为32 x 32,具体的关于该网络的细节,我会在构建网络时详细介绍。
rescale = 1.0 / 255.0
shift = 0.0
rescale_nml = 1 / 0.3081
shift_nml = -1 * 0.1307 / 0.3081
#这里将数据集中图像进行缩放,选择除以255是因为像素点的取值范围是(0,255)。该操作可以使得每个像素的数值大小在(0,1)范围中,可以提升训练效率。进行完该操作之后,有对图像进行了依次缩放,缩放的值选择直接使用官方代,没有进行多次训练实验。
# 根据上面所定义的参数生成对应的数据增强方法,即实例化对象
resize_op = CV.Resize((resize_height, resize_width), interpolation=Inter.LINEAR)
rescale_nml_op = CV.Rescale(rescale_nml, shift_nml)
rescale_op = CV.Rescale(rescale, shift)
hwc2chw_op = CV.HWC2CHW()
#使用它对图片张量进行转化的原因是原图片张量形状是(28 x 28 x 1)(高 x 宽 x通道),在python中用高维数组解释是28个 28 x 1的矩阵,这不是很符合计算习惯,所以我们将它转化为1 x 28 x 28(通道 x 高 x 宽),相对于是1个 28 x 28的矩阵,方便进行数据训练。
type_cast_op = C.TypeCast(mstype.int32)
# 将数据增强处理方法映射到(使用)在对应数据集的相应部分(image,label)
mnist_ds = mnist_ds.map(operations=type_cast_op, input_columns="label", num_parallel_workers=num_parallel_workers)
mnist_ds = mnist_ds.map(operations=resize_op, input_columns="image", num_parallel_workers=num_parallel_workers)
mnist_ds = mnist_ds.map(operations=rescale_op, input_columns="image", num_parallel_workers=num_parallel_workers)
mnist_ds = mnist_ds.map(operations=rescale_nml_op, input_columns="image", num_parallel_workers=num_parallel_workers)
mnist_ds = mnist_ds.map(operations=hwc2chw_op, input_columns="image", num_parallel_workers=num_parallel_workers)
# 处理生成的数据集
buffer_size = 10000
mnist_ds = mnist_ds.shuffle(buffer_size=buffer_size)
#已经将MNIST数据集处理成网络和框架所能接受的数据,下面对处理好的数据集进行一个增强操作。
shuffle用于将数据集随机打乱,减少人为因素干扰训练,降低 MNIST数据集本身的规律。
mnist_ds = mnist_ds.batch(batch_size, drop_remainder=True)
#将整个数据集按照batch_size的大小分为若干批次,每一次训练的时候都是按一个批次的数据进行训练, drop_remainder:确定是否删除数据行数小于批大小的最后一个块,这里设置为True就是只保留数据集个数整除batch_size后得到的批次,多余得则从minst_ds中舍弃。
mnist_ds = mnist_ds.repeat(repeat_size)
return mnist_ds
train_data_path = "./MNIST_Data/train"
test_data_path = "./MNIST_Data/test"
ms_dataset = create_dataset(train_data_path)
print('Number of each group',ms_dataset.get_batch_size())
print('Number of groups in the dataset:', ms_dataset.get_dataset_size())
- 展示处理过后的数据集
将32张图片作为一个批次,将训练集总共分为了60000/32 = 1875个批次。
拿出一个批次的数据将其形状等属性显示,进行可视化。
每一次使用一个这样的大型矩阵去训练。这样可以避免过高维度数组造成的内存过大,训练时间过长的问题。
-
卷积神经网络LeNet-5
l 了解图像基础知识:
图片是通过像素来定义的,即每个像素点的颜色不同,其对应的颜色值不同,例如黑白图片的颜色值为0到255,手写体字符,白色的地方为0,黑色为1,像素其实就是图片的最小组成,黑白只是0-255,彩色其实就是RGB即三层基色合成的,就可以通过三层下图数值代表即可。
卷积层是卷积神经网络的核心基石。在图像识别里是二维卷积,即离散二维滤波器(卷积核)与二维图像做卷积操作,简单的讲是二维滤波器滑动到二维图像上所有位置,并在每个位置上与该像素点及其领域像素点做内积。卷积操作被广泛应用与图像处理领域,不同卷积核可以提取不同的特征,例如边沿、线性、角等特征。
在深层卷积神经网络中,通过卷积操作可以提取出图像低级到复杂的特征。
通过卷积核就可以提取图片的特征和压缩图片了,它的输入是一个黑白二维图片,先经过两层卷积层提取特征,然后到池化层,再经过全连接层连接,最后使用softmax分类作为输出层(因为它的输出有10个标签)
池化是非线性下采样的一种形式,主要作用是通过减少网络的参数来减小计算量,并且能够在一定程度上控制过拟合。通常在卷积层的后面会加上一个池化层。池化包括最大池化、平均池化等。其中最大池化是用不重叠的矩形框将输入层分成不同的区域,对于每个矩形框的数取最大值作为输出层
1) 输入层
我们的输入数据(手写数字图片)就是首先传入输入层,LeNet-5网络要求输入层的图片尺寸必须为32 x 32。
已经通过create_dataset()函数将图片处理为 32 x 32
2) 卷积层1(C1层)
输入:32 x 32 x 1
卷积核大小: 5 x 5(过滤器)
卷积核数量:6
输出形状:28 x 28 x 6
可训练参数:(5 x 5 +1)x 6 =156(5 x 5过滤器参数加一个偏差b)
说明:对输入图像做卷积运算,一个过滤器与图片进行卷积运算得到28 x 28(32 - 5 +1)的矩阵,使用6个过滤器自然得到28 x 28 x 6的高阶数组。可以理解为提取了6个不同的图片特征。对于32 x 32中的每一个像素点,它都和6个过滤器以及他们的偏差有连接,我们通过156个参数就完成了全部的连接,这主要是通过权值共享实现的。(这是卷积层的主要优势之一,可以使要训练的参数减少)。
3) 池化层1(S2层)
输入:28 x 28 x 6
采样区域:2 x 2
采用方式:这里采用最大池化。4个输入比较大小,取最大值。
采样种类:6
输出形状:14 x 14 x 6(这里的步长 s = 2)
说明:第一次卷积运算提取特征之后,进行池化运算,将图片的大小进行缩减。2 x 2的采样器通常高和宽缩小一半。
4) 卷积层2(C3层)
输入:14 x 14 x 6
卷积核大小 :5 x 5
卷积核数量 : 16
输出形状:10 x 10 x 16
5) 池化层2(S4层)
输入:10 x 10 x 16
采样区域:2 x 2
采样方式:最大池化
输出 形状:5 x 5 x 16
6) C5层
输入:5 x 5 x 16
功能:展平张量
输出形状:1 x 1 x 120
7) F6层(全连接层)
输入:C5
计算方式:计算输入向量和权重向量之间的点积,再加上一个偏置,结果通过Relu函数输出。
8) Output层-全连接层
对应的是10个标签(0到9的数字)。需采用softmax分类得到。
- 定义网络
MindSpore中所有网络的定义都需要继承自mindspore.nn.cell,并重写父类的init和construct方法,其中init方法中完成一些我们所需算子,网络的定义,construct方法则是写网络的执行逻辑。而且在nn模块中有很多定义好的各网路层可以使用,包括卷积层(nn.Conv2d()),全连接层(nn.Flatten())等。
- 反向传播
已经定义好了网络的前向传播过程,为了改变我们的可训练参数,即让我们所使用的参数能够预测出更加精确的值。需要定义损失函数即优化器,在MindSpore框架中是封装好了损失函数和优化器的,这使得我们的编程可以更快更加高效。
概念:
损失函数:又叫目标函数,用于衡量预测值与实际值差异的程度。深度学习通过不停地迭代来缩小损失函数的值。定义一个好的损失函数,可以有效提高模型的性能。常见的有二分类的损失函数L,以及softmax损失函数等。
优化器:用于最小化损失函数,从而在训练过程中改进模型。
此实验使用Momentum算法——为了让寻找最优解的曲线能够不那么振荡、波动,希望让他能够更加的平滑,在水平方向的速度更快,运用了动量的定义。
基于梯度的移动指数加权平均
lr = 0.01
#learingrate,学习率,可以使梯度下降的幅度变小,从而可以更好的训练参数
momentum = 0.9
network = LeNet5()
vt+1=vt∗u+gradients
pt+1=pt−(grad∗lr+vt+1∗u∗lr)
pt+1=pt−lr∗vt+1
其中grad、lr、p、v和u分别表示梯度、学习率、参数、力矩和动量。
net_opt = nn.Momentum(network.trainable_params(), lr, momentum)
-
自定义一个回调类
自定义一个数据收集的回调类StepLossAccInfo该类继承自Callback类。主要用于收集两类信息,step与loss值之间地关系。step与对应模型精度之间联系。
我们将该类作为回调函数,会在后面的高级api——model.train模型训练函数中调用
# 回调
class StepLossAccInfo(Callback):
def __init__(self, model, eval_dataset, steps_loss, steps_eval):
self.model = model # 计算图模型Model
self.eval_dataset = eval_dataset # 测试数据集
self.steps_loss = steps_loss
# 收集step和loss值之间的关系,数据格式{"step": [], "loss_value": []},会在后面定义
self.steps_eval = steps_eval
# 收集step对应模型精度值accuracy的信息,数据格式为{"step": [], "acc": []},会在后面定义
def step_end(self, run_context):
cb_params = run_context.original_args()
# cur_epoch_num是CallbackParam中的定义,获得当前处于第几个epoch,一个epoch意味着训练集
# 中每一个样本都训练了一次
cur_epoch = cb_params.cur_epoch_num
# 同理,cur_step_num是CallbackParam中的定义,获得当前执行到多少step
cur_step = (cur_epoch - 1) * 1875 + cb_params.cur_step_num
self.steps_loss["loss_value"].append(str(cb_params.net_outputs))
self.steps_loss["step"].append(str(cur_step))
if cur_step % 125 == 0:
# 调用model.eval返回测试数据集下模型的损失值和度量值,dic对象
acc = self.model.eval(self.eval_dataset, dataset_sink_mode=False)
self.steps_eval["step"].append(cur_step)
self.steps_eval["acc"].append(acc["Accuracy"])
- 训练网络
epoch_size = 1 #每个epoch需要遍历完成图片的batch数,这里是只要遍历一次
eval_dataset = create_dataset(test_data_path)
model_path = "./models/ckpt/mindspore_quick_start/"
#调用Model高级API,将LeNet-5网络与损失函数和优化器连接到一起,具有训练和推理功能的对象。
#metrics 参数是指训练和测试期,模型要评估的一组度量,这里设置的是"Accuracy"准确度
model = Model(network, net_loss, net_opt, metrics={"Accuracy": Accuracy()} )
#保存训练好的模型参数的路径
config_ck = CheckpointConfig(save_checkpoint_steps=375, keep_checkpoint_max=16)
ckpoint_cb = ModelCheckpoint(prefix="checkpoint_lenet", directory=model_path, config=config_ck)
#回调类中提到的我们要声明的数据格式
steps_loss = {"step": [], "loss_value": []}
steps_eval = {"step": [], "acc": []}
#使用model等对象实例化StepLossAccInfo,得到具体的对象
step_loss_acc_info = StepLossAccInfo(model , eval_dataset, steps_loss, steps_eval)
#调用Model类的train方法进行训练,LossMonitor(125)每隔125个step打印训练过程中的loss值,dataset_sink_mode为设置数据下沉模式,但该模式不支持CPU,所以这里我们只能设置为False
model.train(epoch_size, ms_dataset, callbacks=[ckpoint_cb, LossMonitor(125), step_loss_acc_info], dataset_sink_mode=False)
- 提取出一个批次的图片,使用已训练好的上面的模型来预测一下每一张图片的标签,并将其可视化,得到识别的结果
from mindspore import load_checkpoint, load_param_into_net
def test_net(network, model):
"""Define the evaluation method."""
print("开始验证")
# load the saved model for evaluation
param_dict = load_checkpoint("./models/ckpt/mindspore_quick_start/checkpoint_lenet-1_1875.ckpt")
# load parameter to the network
load_param_into_net(network, param_dict)
# load testing dataset
acc = model.eval(eval_dataset, dataset_sink_mode=False)
print("准确率:{}".format(acc))
test_net(network, model)
ds_test = eval_dataset.create_dict_iterator()
data = next(ds_test)
images = data["image"].asnumpy()
labels = data["label"].asnumpy()
output = model.predict(Tensor(data['image']))
#利用加载好的模型的predict进行预测,注意返回的是对应的(0到9)的概率
pred = np.argmax(output.asnumpy(), axis=1)
err_num = []
index = 1
for i in range(len(labels)):
plt.subplot(4, 8, i+1)
color = 'blue' if pred[i] == labels[i] else 'red'
plt.title("pre:{}".format(pred[i]), color=color)
plt.imshow(np.squeeze(images[i]))
plt.axis("off")
if color == 'red':
index = 0
print("{}行,{}列 错误识别成 {}, 正确值应为 {}".format(int(i/8)+1, i%8+1, pred[i], labels[i]), '\n')
if index:
print("已成功识别组内全部数字!")
print(pred, "(预测值)")
print(labels, "(准确值)")
plt.show()
(三) 实验结果与模型性能分析
在进行了多次训练预测后,截取部分具有代表性的实验结果,有一些字迹的确较难识别。
l 准确率:指的是分类正确的样本数量占样本总数的比例,此模型所得准确率在97%左右。
epoch值进行改变,分别epoch=1 ,lr = 0.01;epoch=5,lr=0.01
随着迭代次数的增多,误识别率逐渐减小。
l 卷积层 1、2 不同特征图数时的误识别率与识别时间
改变特征图数,发现得出:卷积层 1、2 的特征图个数对手写数字识别正确率有直接影响。随着特征图个数增多,误识别率会逐渐下降,识别时间则相应增加。特征图个数增加到一定值后,误识别率不会再继续降低,甚至有时会变高。
l 样本个数 batch
控制一切参数不变的情况下,改变每次训练样本个数 batch 的大小,所得误识别率和时间如表 1 所示。随着样本个数的增多,误识别率增大,识别时间减少,综合考虑