手写数字识别是每个学习神经网络的人上手操作的必由之路,此次实验基于paddlepaddle框架在百度AI Studio上进行实战,在fork的学习项目基础上做了数据集的修改以及网络结构的改良,最终可正确识别黑底白字及白底黑字的手写数字,在20k张的测试集上呈现99.313%的准确率,基本达到实验目的。
本次实验原有基础网络结构为两层全连接层+一层全连接输出层,经过多次调参优化,实验发现在测试集上的准确率始终无法超过98%,实验效果并不理想。
最终,在Lenet-5结构的启发下,实验在原有基础上对图片做了卷积-池化-卷积-池化的处理,即两层卷积-池化层+两层全连接层+全连接输出层。经过多次调参,测试集上表现效果优秀,准确率最终可达 99.313%
其中部分超参数设置如下:
1. 卷积-池化层:滤波器大小5,滤波器数量20,池化层kernel大小2*2,池化层步长2,激活函数“relu”
2. 卷积-池化层:滤波器大小5,滤波器数量20,池化层kernel大小2*2,池化层步长2,激活函数“relu”
3. 全连接层: 神经元数量100,激活函数“relu”
4. 全连接层: 神经元数量100,激活函数“relu”
5. 全连接输出层: 输出神经元数量10,激活函数“softmax”
本次实验数据集来源有二,其一为黑底白字的minst开源数据集,已由paddlepaddle封装提供,其二为自制白底黑字的压缩包文件(含标签)
数据集大小为,训练集(60k黑底白字+60k白底黑字)+测试集(10k黑底白字+10k白底黑字)
共进行五轮训练,每轮训练都会在测试集上测试。
此处只展示第五轮训练结束后最终网络表现: 损失:0.01924 准确率:0.99313
import numpy as np
import paddle as paddle
import paddle.fluid as fluid
from PIL import Image
import matplotlib.pyplot as plt
import os
import zipfile
import struct
import re
因本实验采用两个数据集来源,其一是被制作为压缩包的白底黑字数据集,其二是由paddlepaddle官方提供的封装好的paddle.dataset.mnist。因此次步骤目的为处理白底黑字数据集的压缩包,解压至"data/"下以便读取
is_extract_zip = True
if is_extract_zip:
train_zip = zipfile.ZipFile('/home/aistudio/data/data7475/train_examples_labels.zip')
test_zip = zipfile.ZipFile('/home/aistudio/data/data7475/test_examples_labels.zip')
train_zip.extractall('data/')
test_zip.extractall('data/')
标注训练集与测试集路径
# 训练集标签文件
train_labels_idx1_ubyte_file = 'data/train_examples_labels/train-labels.idx1-ubyte'
# 测试集标签文件
test_labels_idx1_ubyte_file = 'data/test_examples_labels/t10k-labels.idx1-ubyte'
定义解析minist数据集idx1文件的通用函数及加载训练集和测试集的函数
def decode_idx1_ubyte(idx1_ubyte_file):
"""
解析idx1文件的通用函数
:param idx1_ubyte_file: idx1文件路径
:return: 数据集
"""
# 读取二进制数据
bin_data = open(idx1_ubyte_file, 'rb').read()
# 解析文件头信息,依次为魔数和标签数
offset = 0
fmt_header = '>ii'
magic_number, num_images = struct.unpack_from(fmt_header, bin_data, offset)
print ('魔数:%d, 图片数量: %d张' % (magic_number, num_images))
# 解析数据集
offset += struct.calcsize(fmt_header)
fmt_image = '>B'
labels = np.empty(num_images)
for i in range(num_images):
if (i + 1) % 10000 == 0:
print ('已解析 %d' % (i + 1) + '张')
labels[i] = struct.unpack_from(fmt_image, bin_data, offset)[0]
offset += struct.calcsize(fmt_image)
return labels
def load_train_labels(idx_ubyte_file=train_labels_idx1_ubyte_file):
return decode_idx1_ubyte(idx_ubyte_file)
def load_test_labels(idx_ubyte_file=test_labels_idx1_ubyte_file):
return decode_idx1_ubyte(idx_ubyte_file)
标注图片路径,加载label
# 图片路径
train_img = 'data/train_examples_labels/train_turn/'
test_img = 'data/test_examples_labels/test_new/'
# 加载label
train_labels = load_train_labels()
test_labels = load_test_labels()
定义load_image函数用于对图片进行预处理,将RGB转化为灰度图像,L代表灰度图像,灰度图像的像素值在0~255之间。图像大小为28*28,返回新形状的数组,把它变成一个 numpy 数组以匹配数据馈送格式,之后归一化到【-1~1】之间。
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 数组以匹配数据馈送格式。
im = im / 255.0 * 2.0 - 1.0 #归一化到【-1~1】之间
im = im.reshape((28*28))
# print(im.size)
return im
加载训练集与测试集入list以便创建reader。因白底黑字数据集中label会出现图片与label对应不一致情况,所以这里使用文件名中的图片序号大小为key进行排序。
# 加载训练data
train_data = []
train_file_names = []
list = os.listdir(train_img)
for i in range(0,len(list)):
file = os.path.join(train_img,list[i])
train_file_names.append(file)
train_file_names.sort(key = lambda x: int(x[38:-4])) #按照index排序
for file in train_file_names:
train_data.append(load_image(file))
# 加载测试data
test_data = []
test_file_names = []
list = os.listdir(test_img)
for i in range(0,len(list)):
file = os.path.join(test_img,list[i])
test_file_names.append(file)
test_file_names.sort(key = lambda x: int(x[35:-4])) #按照index排序
for file in test_file_names:
test_data.append(load_image(file))
以迭代循环创建reader
def reader_createor(data, label):
def reader():
for i in range(len(data)):
yield data[i], int(label[i])
return reader
调用paddle.batch合并黑底白字reader及白底黑字reader,其中黑底白字reader为paddlepaddle直接提供已封装好的。
train_reader = paddle.batch(paddle.reader.shuffle(paddle.reader.chain(reader_createor(train_data, train_labels),paddle.dataset.mnist.train()),
buf_size=512),
batch_size=128)
test_reader = paddle.batch(paddle.reader.chain(reader_createor(test_data, test_labels),paddle.dataset.mnist.test()),
batch_size=128)
在此网络结构采用两个卷积-池化层+两个全连接层+一个全连接输出层
def multilayer_perceptron(input):
conv_pool_1 = fluid.nets.simple_img_conv_pool(
input=input, # 输入图像
filter_size=5, # 滤波器的大小
num_filters=20, # filter 的数量。它与输出的通道相同
pool_size=2, # 池化层大小2*2
pool_stride=2, # 池化层步长
act="relu") # 激活类型
conv_pool_2 = fluid.nets.simple_img_conv_pool(
input=conv_pool_1, # 输入图像
filter_size=5, # 滤波器的大小
num_filters=20, # filter 的数量。它与输出的通道相同
pool_size=2, # 池化层大小2*2
pool_stride=2, # 池化层步长
act="relu") # 激活类型
# 第一个全连接层,激活函数为ReLU
hidden1 = fluid.layers.fc(input=conv_pool_2, 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
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)
在此使用交叉熵损失函数,描述真实样本标签和预测概率之间的差值
cost = fluid.layers.cross_entropy(input=model, label=label) #使用交叉熵损失函数,描述真实样本标签和预测概率之间的差值
avg_cost = fluid.layers.mean(cost)
acc = fluid.layers.accuracy(input=model, label=label)
使用Adam算法进行优化
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())
feeder = fluid.DataFeeder(place=place, feed_list=[image, label])
共进行五轮训练,每训练一轮,进行一次测试
# 开始训练和测试
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