手写数字识别任务:
数字识别是计算机从纸质文档、照片或其他来源接收、理解并识别可读的数字的能力。手写数字识别是一个经典的图像分类问题,目前已经被广泛应用于汇款单号识别、手写邮政编码识别,大大缩短了业务处理时间,提高了工作效率和质量。
在处理如图所示的手写邮政编码的简单图像分类任务时,可以使用基于MNIST数据集的手写数字识别模型。MNIST是深度学习领域标准、易用的成熟数据集,包含60000个训练数据样本和10000个测试数据样本。
图 :手写数字识别任务示意图
简述解决的问题:将手写数字的灰度图像划分到10个类别中(0~9).
任务输入:一系列手写数字图片,其中每张图片都是28*28的像素矩阵。
任务输出:经过了大小归一化和居中处理,输出对应的0~9数字标签。
数据集:
MNIST数据集:是从Special Database 3(SD-3)和Special Database 1(SD-1)构建而来。Yann LeCun等人从SD-1和SD-3中各取一半作为MNIST训练集和测试集,其中训练集来自250位不同的标注员,且训练集和测试集的标注员完全不同。MNIST数据集的发布,吸引了大量的科学家训练模型。1998年,LeCun分别用单层线性分类器、多层感知机(MLP)和多层卷积神经网络LeNet进行实验使得测试集的误差不断下降(从12%下降到0.7%)。在研究过程中,LeCun提出了卷积神经网络(CNN),大幅度的提高了手写字符的识别能力,也因此成为了深度学习领域的奠基人之一。
如今在深度学习领域,卷积神经网络占据了至关重要的地位,从最早Yann LeCun提出的简单LeNet,到如今ImageNet大赛上的优胜模型VGGNet、GoogLeNet、ResNet等,人们在图像分类领域,利用卷积神经网络得到了一系列惊人的结果。
手写数字识别是深度学习中相对简单的模型,非常适用初学者。
构建手写数字识别的神经网络模型:
接下来使用两种方法对手写数字识别进行实验:
使用飞桨完成手写数字识别模型构建的代码结构如图所示。
图 :使用飞桨框架构建神经网络过程
一.准备数据;
1.数据集:MNIST,包含60000张训练图像和10000张测试图像。分为图片和标签,图片是28*28的像素矩阵,标签为0~9共十个数字。
2.定义读取MNIST数据集的train_reader和test_reader,指定一个Batch的大小为128,也就是一次训练或验证128张图像。
3.这里使用的paddle.dataset.train()或test()接口已经对图像进行了灰度处理、归一化、居中处理等。
在数据处理前,首先要加载飞桨平台与手写数字识别模型相关类库,代码如下:
#导入需要的包
#加载飞桨和相关类库
import paddle
import paddle.fluid as fluid
import paddle.fluid.dygraph as dygraph #动态图
from paddle.fluid.dygraph import Linear
import numpy as np
import os
from PIL import Image
#以下使用两种方法进行数据读取
#通过paddle.dataset.mnist.train()函数设置数据读取器,可以直接获取处理好的MNIST训练集、验证集和测试集
trainset = paddle.dataset.mnist.train()
#包装数据读取器,每次读取的数据数量设置为batch_size=8
train_reader = paddle.batch(trainset, batch_size=8)
#打印一下,观察数据集
train_reader=next(train_reader())
print(train_reader)
#下面是不通过paddle的数据集进行手动设置数据读取器的代码。
train_reader = paddle.batch(paddle.reader.shuffle(paddle.dataset.mnist.train(),
buf_size=512),
batch_size=8)
test_reader = paddle.batch(paddle.dataset.mnist.test(),
batch_size=8)
#打印一下,观察数据集
temp_reader = paddle.batch(paddle.dataset.mnist.train(),
batch_size=1)
temp_data=next(temp_reader())
print(temp_data)
paddle.batch函数将MNIST数据集拆分成多个批次,通过如下代码读取第一个批次的数据内容,观察数据打印结果。
飞桨将维度是28*28的手写数字数据图像转成向量形式存储,因此使用飞桨数据读取到的手写数字图像的长度是784(28*28)的向量。
方法一::::
大致的方法看一下,里面有个代码没有跑出来,但思想是正确的
模型设计:
这里我们使用单层且没有线性变换的模型,其中,模型的输入为784维(28*28)数据,输出是1维数据,具体如图所示。
图 :手写数字识别网络模型
输入像素的位置排布信息对理解图像内容很重要(如将原始尺寸为28*28图像的像素按照7*112的尺寸排布,那么其中的数字将不可识别),因此网络的输入设计为28*28的尺寸,以便于模型能够正确处理像素之间的空间信息。
说明:
事实上,采用只有一层的简单网络(对输入求加权和)时并没有处理位置关系信息,因此可以猜测出此模型的预测效果可能有限。在后续优化环节介绍的卷积神经网络则更好的考虑了这种位置关系信息,模型的预测效果也会有显著提升。
下面以类的方式组建手写数字识别的网络,代码如下所示。
#定义mnist数据识别网络结构
class MNIST(fluid.dygraph.Layer):
def __init__(self, name_scope):
super(MNIST, self).__init__(name_scope)
name_scope = self.full_name()
# 定义一层全连接层,输出维度是1,激活函数为None,即不使用激活函数
self.fc = FC(name_scope, size=1, act=None)
# 定义网络结构的前向计算过程
def forward(self, inputs):
outputs = self.fc(inputs)
return outputs
训练配置:
训练配置需要先生成模型实例(设为“训练”状态),再设置优化算法和学习率(使用随机梯度下降SGD,学习率设置为0.01),代码如下所示:
#定义飞桨动态图工作环境
with fluid.dygraph.guard():
#声明网络结构
model = MNIST("mnist")
#启动训练模式
model.train()
#定义数据读取函数,数据读取batch_size设置为16
train_loader = paddle.batch(paddle.dataset.mnist.train(),batch_size=16)
#定义优化器,使用随机梯度下降SGD优化器,学习率设置为0.001
optimizer = fluid.optimizer.SGD0ptimizer(learning_rate=0.001)
训练过程
训练过程采用二层循环嵌套方式,训练完成后需要保存模型参数,以便后续使用。
内层循环:负责整个数据集的一次遍历,遍历数据集采用分批次(BATCH)方式
外层循环:定义遍历数据集的次数,本次训练中外层循环10次,通过参数epoch_num设置
# 通过with语句创建一个dygraph运行的context,
# 动态图下的一些操作需要在guard下进行
with fluid.dygraph.guard():
model = MNIST("mnist")
model.train()
train_loader = paddle.batch(paddle.dataset.mnist.train(), batch_size=16)
optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.001)
EPOCH_NUM = 10
for epoch_id in range(EPOCH_NUM):
for batch_id, data in enumerate(train_loader()):
#准备数据,格式需要转换成符合框架要求的
image_data = np.array([x[0] for x in data]).astype('float32')
label_data = np.array([x[1] for x in data]).astype('float32').reshape(-1, 1)
# 将数据转为飞桨动态图格式
image = fluid.dygraph.to_variable(image_data)
label = fluid.dygraph.to_variable(label_data)
#前向计算的过程
predict = model(image)
#计算损失,取一个批次样本损失的平均值
loss = fluid.layers.square_error_cost(predict, label)
avg_loss = fluid.layers.mean(loss)
#每训练了1000批次的数据,打印下当前Loss的情况
if batch_id !=0 and batch_id % 1000 == 0:
print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy()))
#后向传播,更新参数的过程
avg_loss.backward()
optimizer.minimize(avg_loss)
model.clear_gradients()
# 保存模型
fluid.save_dygraph(model.state_dict(), 'mnist')
模型测试:
模型测试的主要目的是验证训练好的模型是否能正确识别出数字,包括如下四步:
- 声明实例
- 加载模型:加载训练过程中保存的模型参数,
- 灌入数据:将测试样本传入模型,模型的状态设置为校验状态(eval),显式告诉框架我们接下来只会使用前向计算的流程,不会计算梯度和梯度反向传播。
- 获取预测结果,取整后作为预测标签输出。
在模型测试之前,需要先从'./demo/example_0.jpg'文件中读取样例图片,并进行归一化处理。
# 导入图像读取第三方库
import matplotlib.image as mpimg
import matplotlib.pyplot as plt
# 读取图像
example = mpimg.imread('./work/example_0.png')
# 显示图像
plt.imshow(example)
代码执行结果:
# 读取一张本地的样例图片,转变成模型输入的格式
def load_image(img_path):
# 从img_path中读取图像,并转为灰度图
im = Image.open(img_path).convert('L')
print(np.array(im))
im = im.resize((28, 28), Image.ANTIALIAS)
im = np.array(im).reshape(1, -1).astype(np.float32)
# 图像归一化,保持和数据集的数据范围一致
im = 1 - im / 127.5
return im
# 定义预测过程
with fluid.dygraph.guard():
model = MNIST("mnist")
params_file_path = 'mnist'
img_path = './work/example_0.png'
# 加载模型参数
model_dict, _ = fluid.load_dygraph("mnist")
model.load_dict(model_dict)
# 灌入数据
model.eval()
tensor_img = load_image(img_path)
result = model(fluid.dygraph.to_variable(tensor_img))
# 预测输出取整,即为预测的数字,打印结果
print("本次预测的数字是", result.numpy().astype('int32'))
从结果看,模型预测出的数字与实际输出的图片不一致。这里只是验证了一个样本的情况,接下来会对手写数字识别模型进行改进。
如下:
方法二:
读取数据的形式是一样的。
二:配置网络
以下代码就是定义一个简单的多层感知机一共有三层,两个大小为100的隐层和一个大小为10的输出层,10层是因为MNIST数据集是手写0~9的灰度图像,类别有10个,所以输出的大小是10 .最后输出层的激活函数是Sostmax,所以最后的输出层相当于一个分类器。加上一个输入层的话,多层感知器的结构是:输入层---隐层---隐层---输出层
# 定义多层感知器
def multilayer_perceptron(input):
# 第一个全连接层,激活函数为ReLU
hidden1 = fluid.layers.fc(input=input, size=100, act='relu')
# 第二个全连接层,激活函数为ReLU
hidden2 = fluid.layers.fc(input=hidden1, size=100, act='relu')
# 以softmax为激活函数的全连接输出层,大小为10
prediction = fluid.layers.fc(input=hidden2, size=10, act='softmax')
return prediction
定义输入层,输入的是图像数据。图像是28*28的灰度图像,所以输入的形状是[1,28,28],如果图像是32*32的彩色图,那么输入的形状是[3,32,32],因为灰度图只是一个通道,而彩色图有RGB三个通道。
# 定义输入输出层
image = fluid.layers.data(name='image', shape=[1, 28, 28], dtype='float32') #单通道,28*28像素值
label = fluid.layers.data(name='label', shape=[1], dtype='int64') #图片标签
在这里调用定义好的网络来获取分类器
# 获取分类器
model = multilayer_perceptron(image)
接着定义损失函数,因为这里属于分类任务,所以使用的损失函数是交叉损失函数。定义了一个损失函数之后,还有对它求平均值,因为定义的是一个Batch的损失值。我们同时还定义一个准确率函数,这个可以在训练的时候输出分类的准确率。
# 获取损失函数和准确率函数
cost = fluid.layers.cross_entropy(input=model, label=label) #使用交叉熵损失函数,描述真实样本标签和预测概率之间的差值
avg_cost = fluid.layers.mean(cost)
acc = fluid.layers.accuracy(input=model, label=label)
接着就是定义优化方法,这里使用的优化方法是Adarm优化方法,同时指定学习率是0.001.
# 定义优化方法
optimizer = fluid.optimizer.AdamOptimizer(learning_rate=0.001) #使用Adam算法进行优化
opts = optimizer.minimize(avg_cost)
三及四:模型训练及模型评估
接着就是定义一个解析器和初始化参数
# 定义一个使用CPU的解析器
place = fluid.CPUPlace()
exe = fluid.Executor(place)
# 进行参数初始化
exe.run(fluid.default_startup_program())
输入的数据维度就是图像数据和图像对应的标签,每个类别的图像都要对应一个标签,这个标签是从0递增的整型数值
# 定义输入数据维度
feeder = fluid.DataFeeder(place=place, feed_list=[image, label])
最后就开始训练了,这次训练5个pass。在上面已经定义了一个求准确率的函数,所以在训练的时候让它输出当前的准确率,计算准确率的原理简单,就是把训练是预测的结果和真实结果进行比较,求出一个准确率。每一个pass训练结束后,在进行一次测试,使用测试集的数据进行测试,并求出当前的cost和准确率的平均值。
# 以下是开始训练和测试
for pass_id in range(5):
# 进行训练
for batch_id, data in enumerate(train_reader()): #遍历train_reader
train_cost, train_acc = exe.run(program=fluid.default_main_program(),#运行主程序
feed=feeder.feed(data), #给模型喂入数据
fetch_list=[avg_cost, acc]) #fetch 误差、准确率
# 每100个batch打印一次信息 误差、准确率
if batch_id % 100 == 0:
print('Pass:%d, Batch:%d, Cost:%0.5f, Accuracy:%0.5f' %
(pass_id, batch_id, train_cost[0], train_acc[0]))
# 进行测试
test_accs = []
test_costs = []
#每训练一轮 进行一次测试
for batch_id, data in enumerate(test_reader()): #遍历test_reader
test_cost, test_acc = exe.run(program=fluid.default_main_program(), #执行训练程序
feed=feeder.feed(data), #喂入数据
fetch_list=[avg_cost, acc]) #fetch 误差、准确率
test_accs.append(test_acc[0]) #每个batch的准确率
test_costs.append(test_cost[0]) #每个batch的误差
# 求测试结果的平均值
test_cost = (sum(test_costs) / len(test_costs)) #每轮的平均误差
test_acc = (sum(test_accs) / len(test_accs)) #每轮的平均准确率
print('Test:%d, Cost:%0.5f, Accuracy:%0.5f' % (pass_id, test_cost, test_acc))
#保存模型
model_save_dir = "/home/aistudio/data/hand.inference.model"
# 如果保存路径不存在就创建
if not os.path.exists(model_save_dir):
os.makedirs(model_save_dir)
print ('save models to %s' % (model_save_dir))
fluid.io.save_inference_model(model_save_dir, #保存推理model的路径
['image'], #推理(inference)需要 feed 的数据
[model], #保存推理(inference)结果的 Variables
exe) #executor 保存 inference model
每100个batch打印一次信息 误差、准确率。每训练一轮进行一次测试,结果如下
五。模型预测
在预测之前,也是要对图像进行预处理的,处理方式与训练时候的处理一致:首先进行灰度化,然后把图像压缩为大小28*28像素的矩阵,随后对图像进行拉伸为一维向量,最后对一维向量进行归一化处理。
# 对图片进行预处理
def load_image(file):
im = Image.open(file).convert('L') #将RGB转化为灰度图像,L代表灰度图像,灰度图像的像素值在0~255之间
im = im.resize((28, 28), Image.ANTIALIAS) #resize image with high-quality 图像大小为28*28
im = np.array(im).reshape(1, 1, 28, 28).astype(np.float32)#返回新形状的数组,把它变成一个 numpy 数组以匹配数据馈送格式。
# print(im)
im = im / 255.0 * 2.0 - 1.0 #归一化到【-1~1】之间
print(im)
return im
img = Image.open('data/data27012/6.png')#提前放置的测试图像
plt.imshow(img) #根据数组绘制图像
plt.show() #显示图像
显示测试图像
infer_exe = fluid.Executor(place)
inference_scope = fluid.core.Scope()
最后把图像转换成一维向量并进行预测,数据从feed的image传入。fetch_list的值是网络模型的最后一层分类器,所以输出的结果是10个标签,所以输出的结果是10个标签的概率值,这些值的总和为1.
# 加载数据并开始预测
with fluid.scope_guard(inference_scope):
#获取训练好的模型
#从指定目录中加载 推理model(inference model)
[inference_program, #推理Program
feed_target_names, #是一个str列表,它包含需要在推理 Program 中提供数据的变量的名称。
fetch_targets] = fluid.io.load_inference_model(model_save_dir,#fetch_targets:是一个 Variable 列表,从中我们可以得到推断结果。model_save_dir:模型保存的路径
infer_exe) #infer_exe: 运行 inference model的 executor
img = load_image('data/data27012/6.png')
results = exe.run(program=inference_program, #运行推测程序
feed={feed_target_names[0]: img}, #喂入要预测的img
fetch_list=fetch_targets) #得到推测结果,
拿到每个标签的概率值之后,我们要获取概率最大的标签,并打印出来。
# 获取概率最大的label
lab = np.argsort(results) #argsort函数返回的是result数组值从小到大的索引值
#print(lab)
print("该图片的预测结果的label为: %d" % lab[0][0][-1]) #-1代表读取数组中倒数第一列
该图片的预测结果的label为:6
结束啦~~~~~~