基于MindSpore的手写数字识别初体验(二)
【本文代码编译环境为MindSpore1.3.0 CPU版本】
LeNet-5
对于手写数字识别初体验的网络,我们选择的是相对简单的一个卷积神经网络——LeNet-5。它的输入是一个黑白二维图片,先经过两层卷积层提取特征,然后到池化层,再经过全连接层连接,最后使用softmax分类作为输出层(因为它的输出有10个标签),网络结构如下图所示:
LeNet-5是一个非常经典的网络,它虽然规模较小,但已经包含了一个卷积神经网络的基本模块——卷积层,池化层,全连接层。让我们来详细看一下它的各层参数以及作用。
输入层
我们的输入数据(手写数字图片)就是首先传入输入层,LeNet-5网络要求输入层的图片尺寸必须为32 x 32。我们已经通过create_dataset()函数将图片处理为 32 x 32了。
卷积层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个参数就完成了全部的连接,这主要是通过权值共享实现的。(这是卷积层的主要优势之一,可以使要训练的参数减少)。
池化层1(S2层)
输入:28 x 28 x 6
采样区域:2 x 2
采用方式:论文里面使用的是平均池化的方式,但由于现在更加喜欢使用最大池化,所以我们这里采用最大池化。4个输入比较大小,取最大值。
采样种类:6
输出形状:14 x 14 x 6(这里的步长 s = 2)
说明:第一次卷积运算提取特征之后,进行池化运算,将图片的大小进行缩减。2 x 2的采样器通常高和宽缩小一半。
卷积层2(C3层)
输入:14 x 14 x 6
卷积核大小 :5 x 5
卷积核数量 : 16
输出形状:10 x 10 x 16
池化层2(S4层)
输入:10 x 10 x 16
采样区域:2 x 2
采样方式:最大池化
输出 形状:5 x 5 x 16
C5层
输入:5 x 5 x 16
功能:展平张量
输出形状:1 x 1 x 120
F6层(全连接层)
输入:C5
计算方式:计算输入向量和权重向量之间的点积,再加上一个偏置,结果通过Relu函数输出。
Output层-全连接层
该层也是全连接层,不过它对应的是10个标签(0到9的数字)。需采用softmax分类得到。
定义网络
首先说明MindSpore支持多种参数初始化方法,常用的有Normal等。具体可查看https://www.mindspore.cn/docs/api/zh-CN/r1.3/api_python/mindspore.common.initializer.html的接口说明。
另外,MindSpore中所有网络的定义都需要继承自mindspore.nn.cell,并重写父类的init和construct方法,其中init方法中完成一些我们所需算子,网络的定义,construct方法则是写网络的执行逻辑。而且在nn模块中有很多定义好的各网路层可以使用,包括卷积层(nn.Conv2d()),全连接层(nn.Flatten())等。
接下来看一下我们依照LeNet-5网络各层的参数以及功能要求使用MindSpore定义的网络结构:
class LeNet5(nn.Cell):
def __init__(self, num_class=10, num_channel=1):
super(LeNet5, self).__init__() #继承父类nn.cell的__init__方法
#nn.Conv2d的第一个参数是输入图片的通道数,即单个过滤器应有的通道数,第二个参数是输出图片的通道数
#即过滤器的个数,第三个参数是过滤器的二维属性,它可以是一个int元组,但由于一般过滤器都是a x a形
#式的,而且为奇数。所以这里填入单个数即可,参数pad_mode为卷积方式,valid卷积即padding为0的卷积
#现在也比较流行same卷积,即卷积后输出的图片不会缩小。需要注意的是卷积层我们是不需要设置参数的随机
#方式的,因为它默认会给我们选择为Noremal。
self.conv1 = nn.Conv2d(num_channel, 6, 5, pad_mode='valid')
self.conv2 = nn.Conv2d(6, 16, 5, pad_mode='valid')
#nn.Dense为致密连接层,它的第一个参数为输入层的维度,第二个参数为输出的维度,第三个参数为神经网
#络可训练参数W权重矩阵的初始化方式,默认为normal
self.fc1 = nn.Dense(16 * 5 * 5, 120, weight_init=Normal(0.02))
self.fc2 = nn.Dense(120, 84, weight_init=Normal(0.02))
self.fc3 = nn.Dense(84, num_class, weight_init=Normal(0.02))
#nn.ReLU()非线性激活函数,它往往比论文中的sigmoid激活函数具有更好的效益
self.relu = nn.ReLU()
#nn.MaxPool2d为最大池化层的定义,kernel_size为采样器的大小,stride为采样步长,本例中将其
#都设置为2相当于将图片的宽度和高度都缩小一半
self.max_pool2d = nn.MaxPool2d(kernel_size=2, stride=2)
#nn.Flatten为输入展成平图层,即去掉那些空的维度
self.flatten = nn.Flatten()
def construct(self, x):
#输入x,下面即是将x通过LeNet5网络执行前向传播的过程
x = self.max_pool2d(self.relu(self.conv1(x)))
x = self.max_pool2d(self.relu(self.conv2(x)))
x = self.flatten(x)
x = self.relu(self.fc1(x))
x = self.relu(self.fc2(x))
x = self.fc3(x)
return x
你可能一下子看不懂上面的一些代码,但没有关系,只要你懂了LeNet-5网络的特征和运算规则,前往api中的nn模块参看相应说明之后你马上就会懂了。
print(LeNet5())
我们通过该代码来看一下LeNet-5的各层网络参数:
读者可以根据打印的参数选项与我在前面提到过的的参数形式和自己掌握的一些数学矩阵计算规则验证一下,来加强自己对这些参数的理解。
反向传播
前面我们已经定义好了网络的前向传播过程,为了改变我们的可训练参数,即让我们所使用的参数能够预测出更加精确的值。我们需要定义损失函数即优化器,在MindSpore框架中是封装好了损失函数和优化器的,这使得我们的编程可以更快更加高效。
损失函数:又叫目标函数,用于衡量预测值与实际值差异的程度。深度学习通过不停地迭代来缩小损失函数的值。定义一个好的损失函数,可以有效提高模型的性能。常见的有二分类的损失函数L,以及softmax损失函数等。
优化器:用于最小化损失函数,从而在训练过程中改进模型。
from mindspore.nn import SoftmaxCrossEntropyWithLogits
lr = 0.01 #learingrate,学习率,可以使梯度下降的幅度变小,从而可以更好的训练参数
momentum = 0.9
network = LeNet5()
#使用了流行的Momentum优化器进行优化
#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)
#相当于softmax分类器
#sparse指定标签(label)是否使用稀疏模式,默认为false,reduction为损失的减少类型:mean表示平均值,一般
#情况下都是选择平均地减少
net_loss = SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')
自定义一个回调类
自定义一个数据收集的回调类StepLossAccInfo
该类继承自Callback
类。主要用于收集两类信息,step与loss值之间地关系。step与对应模型精度之间联系。
我们将该类作为回调函数,会在后面的高级api——model.train模型训练函数中调用。
from mindspore.train.callback import Callback
# custom callback function
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"])
编写训练网络
from mindspore.train.callback import ModelCheckpoint, CheckpointConfig, LossMonitor
from mindspore.nn import Accuracy
from mindspore import Model
epoch_size = 1 #每个epoch需要遍历完成图片的batch数,这里是只要遍历一次
model_path = "./models/ckpt/mindspore_quick_start/"
eval_dataset = create_dataset(test_data_path)
#调用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)
接下来我们直接开始训练:
从上面的训练截图可以看一看出效果还是很好的,因为我第一次跑手写数字识别项目是别人给我的pytorch版本,一般跑第一个epoch的准确率是94%左右。用MindSpore来跑这个loss值这么小,我觉得可能是处理数据集上有优势一些。这只是一个较小的项目,我在一篇文章上看到,MindSpore结合Ascend处理器跑大一点的项目一般比其它的主流框架平台都要快一些。
同时我们可以看到,我们的项目下有一个models的文件夹,里面储存的ckpt文件就是我们训练过程中的参数,不过LeNet-5的参数量并不是很大。我们可以将该文件的参数加载到LeNet-5网络中,然后用该网络去跑测试集可以得到在测试集上的准确率。
def test_net(network, model, mnist_path):
"""Define the evaluation method."""
print("============== Starting Testing ==============")
# 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("============== Accuracy:{} ==============".format(acc))
test_net(network, model, mnist_path)
可以看到一次训练就有将近96%的准确率,这已经非常不错了。如果想要继续提高它的准确率可以去自定义训练网络来控制训练整个集合的次数,或者修改参数等都可以尝试。
最后一步,我们使用官网的一段代码,提取出一个批次的图片,使用已训练好的上面的模型来预测一下每一张图片的标签,并将其可视化。
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("Row {}, column {} is incorrectly identified as {}, the correct value should be {}".format(int(i/8)+1, i%8+1, pred[i], labels[i]), '\n')
if index:
print("All the figures in this group are predicted correctly!")
print(pred, "<--Predicted figures")
print(labels, "<--The right number")
plt.show()
结果如下所示:
可以看到我抽到的这一组32张图片是属于手气较好的,全部预测正确。上面有些数字确实挺有干扰性的,但机器还是识别出来了(比如第2行最后一张2,写的挺奇葩的)。总之到了这里,基于MindSpore的手写数字识别初体验就已经结束了,写这篇文章不是说要深入手写数字识别,而是说经过这个小型项目的实践,我们可以对MindSpore的各个模块有个大体的理解。比如说dataset模块用于各种数据集的处理和加载,有热门数据集的加载处理方法,有MindSpore专属的数据格式,有一些不同数据集的增强方法。nn模块提供了与网络构建,网络参数相关的各种封装好的高级API。类似的还有很多,我们可以通过这些模块的功能进一步细化深入到该模块中子模块,方法。从功能入手,由表及里,分析源码。