【项目02】基于前馈神经网络的手写字体识别(静态图)

作者:欧新宇(Xinyu OU)
开发平台:Paddle 2.1
运行环境:Intel Core i7-7700K CPU 4.2GHz, nVidia GeForce GTX 1080 Ti
特别注意:本案例使用Paddle 2.1进行开发,但所使用的静态图库函数为Paddle 1.8,这些API将在未来的版本中被废弃。

本教案所涉及的数据集仅用于教学和交流使用,请勿用作商用。

最后更新:2021年8月12日


【实验目的】

  1. 熟悉神经网络的基本结构,包括层(输入、输出、隐层)、损失函数、激活函数、优化方法等
  2. 学会使用mini-batch方法实现深度神经网络的训练并进行预测
  3. 学会保存模型,并使用保存的模型进行预测(即应用模型到生产环境)
  4. 学会使用PaddlePaddle构建多层感知机
  5. 学会使用静态图模式Static设计和训练神经网络

PS: 查看显卡利用率

  1. 打开命令提示行, 并进入文件夹: C:\Program Files\NVIDIA Corporation\NVSMI
  2. 执行命令: nvidia-smi, 观察Memory-Usage的值. 例如: 1857MiB / 11264MiB, 前者表示当前正在使用的显存量, 后者表示显卡总显存量.

【实验流程图】

Project0201FlowChart

【实验一】 数据准备

实验目的:

  1. 理解训练集、验证集、训练验证集及测试集在模型训练中的作用
  2. 学会图像列表的构建和数据读取器的定义
  3. 学会图像的基本预处理方法,包括尺寸缩放、归一化和格式规范
  4. 学会切换CPU和GPU两种训练模式

1.0 导入依赖及全局参数设置

在Paddle 2.0版本之后,动态图成为默认工作模式,因此需要使用静态图进行训练时,需要使用paddle.enable_static()手动设置Paddle的工作模式为静态图。

1.1 数据准备

1.1.1 数据集简介

MNIST数据集包含60000个训练集和10000测试数据集。分为图片和标签,图片是28*28的像素矩阵,标签为0~9共10个数字。

Ch02ex01

1.1.2 观察和展示数据

该步骤是非必须的必要步骤非必须指它并不是训练和展示数据所必须的步骤;必要步骤是因为观察数据集有利于我们更好地选择合适的方法进行数据预处理。

和很多基础数据集一样,mnist数据集已经是paddlepaddle的内置数据集,所以可以通过 paddle.dataset.mnist.train()类来直接获取train_data训练数据集。 此外,我们还可以使用next(train_data())来顺序遍历train_data中的数据并保存到样例数据变量sampledata中。该数据包含两个元素,可以直接使用image,label=sampledata来获取. 之后,可以考虑输入imagelabel,以及他们的形态来对其进行观察。

1.1.3 定义数据读取器获取数据

由于MNIST是paddle内置的toy数据集,因此可以使用paddle.dataset.mnist.train()paddle.dataset.mnist.test()从直接从库中获取数量为BATCH_SIZE的数据,该接口在载入数据的同时,实现了对样本的灰度化、归一化和居中等预处理。以下为关键函数介绍:

image_reader = paddle.batch(paddle.reader.shuffle([dataset|reader=data_reader(data_list)], buf_size={Int}), batch_size=[Int], drop_last=[True|False])

# paddle.dataset.mnist.train()和test():分别用于获取mnist训练集和测试集
# paddle.reader.shuffle():表示每次缓存BUF_SIZE个数据项,并进行打乱。适当载入缓存,有利于提高数据读取的速度。
# paddle.batch():表示每BATCH_SIZE组成一个batch
# drop_last: 用于设置在分配batch时,最后一个批量和前面数量不同的时候,是否丢弃

第一次运行以上代码,会自动去下载mnist数据集,提示信息如下:

Cache file C:\Users\Administrator\.cache\paddle\dataset\mnist\t10k-images-idx3-ubyte.gz not found, downloading https://dataset.bj.bcebos.com/mnist/t10k-images-idx3-ubyte.gz 
Begin to download

Download finished
Cache file C:\Users\Administrator\.cache\paddle\dataset\mnist\t10k-labels-idx1-ubyte.gz not found, downloading https://dataset.bj.bcebos.com/mnist/t10k-labels-idx1-ubyte.gz 
Begin to download
..
Download finished

【实验二】 模型配置和训练

实验目的:

  1. 理解训练集、验证集、训练验证集及测试集在模型训练中的作用
  2. 掌握基于静态图的前馈神经网络的构建、训练方法
  3. 学会可视化训练过程
  4. 学会在线测试和离线测试两种方法

2.1 配置网络

在Paddle的静态图模式中,深度神经网络的配置主要包括以下几个方面:

  1. 网络结构的定义
  2. 输入层的定义
  3. 分类器及损失函数、准确率函数的定义
  4. 优化方法的定义
  5. Exector执行器定义

2.1.1 网络结构的定义

在本范例中使用一个4层的前馈神经网络(多层感知机)对MNIST数据集进行建模。其中第一层为输入层,后面紧跟两个大小为100的隐层和一个大小为10的输出层。其中,输出层的每个神经元对应于MNIST的类别数,即0-9的10个数字。最后使用交叉熵函数求损失,并用Softmax分类器输出类别。

Ch02ex01

  1. 输入层$X$:MNIST数据集中的每个样本都为分辨率为$28*28$像素的二维图片,根据前馈神经网络的结构,需要将其拉伸为784维的向量,即$X=(x_0,x_1,x_2,...,x_{783})$。+1代表该层的偏置参数,值为1。
  2. 隐藏层$H_1$:100个节点的全连接层,激活函数为ReLU。
  3. 隐藏层$H_2$:100个节点的全连接层,激活函数为ReLU。
  4. 输出层$Y$:10个节点的全连接层,对应于MNIST数据集的10个类别。所有的输出节点组成一个N=10维的向量,经过Softmax分类器后,将归一化为N个$[0,1]$的实数,其值为该样本属于这N个类别的概率,并且有$\sum_{n=0}^9 y_n = 1$,即所有类别概率的和为1。

以下为该前馈神经网络(多层感知机)的网络构建函数。

def model_name(input): # 函数的输入input,一般来说表示的是样本的张量,例如image
    # 以下可以逐层定义模型,对于前馈神经网络来说,只有一种层类型,全连接层,使用如下方式定义

    # input: 定义层的输入
    # size: 神经元的数量
    # act: 激活函数
    [layer_name] = fluid.layers.fc(input=[input], size=[int], act=[relu|sigmoid|linear]) 

    return the_last_layer # 函数的返回值,通常为最后一个层的名字

2.1.2 输入层的定义

在本例中,我们输入的是图像数据,因此每个图像I都是一个元组,包含图像和标签,$[image, label]$。其中,图像是$28*28$的灰度图,所以输入的形状是$[1, 28, 28]$,如果图像是$32*32$的彩色图,那么输入的形状是$[3, 32, 32]$,因为灰度图只有一个通道,而彩色图有RGB三个通道。此外,理论上还应该有一个维度表示BatchSize,但在Paddle中,已经封装到训练过程中,因此,此处只需要关注样本本身的维度。对于标签label,它是一个一维的标量,对应于图像的类别。与图像类似,它默认也有一个BatchSize的维度被包含在训练过程中。

下面的代码主要包含三部分,定义样本、标签,以及数据提供器的feeder。

[image|label] = fluid.layers.data(name='image|label', shape=[数据的形态],dtype=['float32'|'int64'])  
feeder = fluid.DataFeeder(place=PLACE, feed_lisit=[image, label])

2.1.3 分类器及损失函数、准确率函数的定义

  1. 首先创建一个分类器,例如此处的多层感知机(前馈神经网络),简单的说是将模型类实例化。
  2. 使用分类任务中经常用到的交叉熵损失函数fluid.layers.cross_entropy(),其损失由模型的输出input=predict和真实的标签label=[]计算获得。由于该损失函数是针对整个Batch的,因此还需要对其求平均值才能获得模型的损失(相当于单个样本的损失),求损失的基本思路是Loss=总损失TotalLoss/批大小BatchSize。此处可以直接调用Paddle的函数fluid.layers.mean(loss)
  3. 最后,还可以定义一个准确率函数fluid.layers.accuracy(),用于衡量训练时输出分类的准确率,其输入依然是模型的输出input=predict和真实的标签label=[]。

2.1.4 优化方法的定义

定义优化方法,此处使用一种常用的优化方法——Adam优化方法,同时指定学习率为0.001.

在上述模型配置完毕后,得到三个fluid.Program:fluid.default_startup_program(),fluid.default_main_program(),及test_program。

  1. 所有的参数初始化操作都会被写入fluid.default_startup_program()
  2. fluid.default_main_program()用于获取默认或全局main program(主程序)。该主程序用于训练和测试模型。fluid.layers 中的所有layer函数可以向 default_main_program 中添加算子和变量。default_main_program 是fluid的许多编程接口(API)的Program参数的缺省值。例如,当用户program没有传入的时候,Executor.run() 会默认执行 default_main_program。
  3. fluid.defult_main_program有一个默认参数for_test=False,当其处于默认值False时,fluid.default_main_program用于训练,当其处于True时,fluid.default_main_program用于测试。此处,使用fluid.default_main_program().clone()操作实现了对初始配置的完全克隆。

之后需要定义一个优化器,并使用最小化的方式,依据平均损失进行优化,并得到输出:

optimizer = fluid.optimizer.优化器方法(learning_rate=[int])
opts = optimizer.minimize(avg_cost)

其中optimizer能调用的优化器非常多,常见的包括Adam, Momentum, SGD, Adagrad等。具体可以参考paddle官网,fluid.optimizer

2.1.5 Executor执行器的定义

首先定义执行训练的设备,默认情况下Paddle支持CPU和GPU两种模式。当设置fluid.CPUPlace()和 fluid.CUDAPlace(0)时,分别表示调用CPU和GPU进行训练。下面通过设置是否使用cuda来进行自动判断,并将设备信息保存到全局变量PLACE中. Executor接收传入的program,通过run()方法运行program。

use_cuda          = True  # True, False 如果设备有GPU,怎么我们可以启用GPU进行快速训练
PLACE             = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()

2.2 定义过程可视化函数

定义训练过程中用到的可视化方法, 包括训练损失, 训练集批准确率, 测试集准确率. 根据具体的需求,可以在训练后展示这些数据和迭代次数的关系. 值得注意的是, 训练过程中可以每个epoch绘制一个数据点,也可以每个batch绘制一个数据点,也可以每隔n个batch或n个epoch绘制一个数据点.

2.3 模型训练及评估

训练需要有一个训练程序和一些必要参数,并构建了一个获取训练过程中测试误差的函数。必要参数有executor,program,reader,feeder,fetch_list。

在Executor的run方法中,feed代表以字典形式定义的数据,通过fluid.DataFeeder函数的执行,会生成一个data的字典数据,其中data[0]和data[1]分别表示image和label。

值得注意的是,在每一轮的训练中,每100个batch之后会输出一次平均训练误差和准确率。每一轮训练之后,使用测试集进行一次测试,在每轮测试中,均打输出一次平均测试误差和准确率。

【注意】
在下列的代码中,我们每个TEST_EPOCH个周期都执行一次模型保存,这种方式一般应用在复杂的模型和大型数据集上。这种经常性的模型保存,有利于我们执行EarlyStopping策略,当我们发现运行曲线不再继续收敛时,就可以结束训练,并选择之前保存的最好的一个模型作为最终的模型。FinalModel (对于绘图程序矩阵,也可以进行定期保存,以便训练仍然在继续的时候,进行可视化操作,或者一定周期的训练后进行一次可视化.)

2.3.1 定义测试函数

模型测试一般包含两个类型,分别是:

  1. 在线测试,在线测试通常是针对验证集进行,并在训练过程中,每隔一定的周期输出一次精度和损失值
  2. 离线测试,指在模型训练结束之后,使用测试集进行的评估。

两种测试模式,在代码上基本上是一致的,只是所使用的数据不同。因此,我们可以定义一个eval()函数来实现代码复用,在进行评估时,分别调用val_reader()test_reader()的即可。注意,本例中只存在测试集,不存在验证集。

2.3.2 定义训练函数

2.3.3 模型训练及在线测试

2.3.4 离线测试

离线测试与在线测试基本一致,不同的是离线测试需要使用load_inference_model()先读取事先保存的模型。

【实验三】 模型评估与推理(应用)

实验目的:

  1. 学会载入已训练好的模型
  2. 掌握静态图中模型的验证与推理方法

在实际应用中,下面的工作可以视作是生产环境的具体应用。在研究中,也可以当作最终的结果对比。模型预测包含三个部分:

  1. 导入依赖库及全局参数配置
  2. 获取待预测数据
  3. 创建预测用执行器
  4. 载入模型并开始进行预测
  5. 输出结果

3.1 导入依赖库及全局参数配置

3.2 获取待预测数据

获取待测数据包含两个步骤: 1.获取图像路径和标签; 2. 根据图像路径读取图像并进行预处理。

我们可以通过data_reader()来从测试集列表中随机获取一个测试样本, 并使用line.split()函数拆分成路径和标签。但是,在实际使用中, 也可以直接指定(或从其他应用程序直接获得)待测样本。下面,我们以手动指定样本为例,对输入的图像进行预测。

  1. 使用cv2.imread(imgPath, 0)读取图像,并通过第二个参数指定使用灰度模式进行读取
  2. 首先需要将所有样本都resize到同样的尺度,MNIST数据集为28*28.
  3. 将图像数据转换为numpy.array格式,并转换为一维向量, 32bit浮点数据类型(float32)
  4. 将像素值的取值范围调整为:0-1, 原始范围为:0-255

3.3 创建预测用的Executer

通过fluid.io.load_inference_model,预测器会从params_dirname中读取已经训练好的模型,来对从未遇见过的数据进行预测。

3.4 载入模型并开始进行预测

通过fluid.io.load_inference_model,预测器会从params_dirname中读取已经训练好的模型,来对从未遇见过的数据进行预测。