【项目03】基于前馈神经网络的手势识别(动态图)

作者:欧新宇(Xinyu OU)
开发平台:Paddle 2.1
运行环境:Intel Core i7-7700K CPU 4.2GHz, nVidia GeForce GTX 1080 Ti

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

最后更新:2021年8月8日


【实验目的】

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

PS: 查看显卡利用率

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

【实验流程图】

Ch02ex01

【实验一】 数据准备

实验目的:

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

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

1.1 数据准备

1.1.1 数据集介绍

手势识别 数据集是由土耳其一所中学制作,数据集由Data文件夹中的训练验证数据Infer文件夹中的预测数据组成,包含0-9共10种数字的手势。以下为该数据集的官方描述:

This dataset is prepared by Turkey Ankara Ayrancı Anadolu high school students.
Image size: 100 x 100 pixels
Color space: RGB
Number of classes: 10 (Digits: 0-9)
Number of participant students: 218
Number of samples per student: 10
Dataset Url:https://github.com/ardamavi/Sign-Language-Digits-Dataset

Ch02ex01

数据集下载:https://aistudio.baidu.com/aistudio/datasetdetail/54000

1.1.2 生成数据列表

根据原始数据集的图片,生成列表文件。此处,按照7:1:2的比例进行训练集、验证集、测试集的划分,并将划分好的数据分别保存成四个列表文件:train.txt, val.txt, trainval.txt, test.txt文件中, 同时生成数据集的基本信息文件dataset_info.json. 数据列表文件的基本格式如下:

D:\Workspace\ExpDatasets\Gestures\Data\0\IMG_5991.JPG 0
D:\Workspace\ExpDatasets\Gestures\Data\1\IMG_1129.JPG 1
D:\Workspace\ExpDatasets\Gestures\Data\1\IMG_1139.JPG 1

其中,第一项为图片的保存路径,第二项为该图片的分类标签。中间使用一个Tab(或者1个空格)进行分隔。值得注意的是,数据(图片)保存的路径可以使用绝对路径也可以使用相对路径,但为了避免不必要的麻烦和错误,建议使用绝对路径进行索引。

为简化代码,在本例中请直接下载 Gestures数据集,并调用数据集中写好的数据集划分代码 generate_annotation.py。下面为调用数据集划分的代码,该代码只需要执行一次。

1.1.3 数据读取和预处理

数据预处理包括数据读取、数据映射和数据预处理三部分,其中:

其中,通道变化是指从原始图像通道 [高度,宽度,通道](OpenCV和PIL读取后的默认通道)转换为Paddle内置的图像形状 [通道,高度,宽度]。事实上,包括TensorFlow, PyTorch, Caffe等工具包都使用 [通道,高度,宽度] 的方式进行输入。

xmap_readers()是Paddle提供的基于CPU的多线程数据读取器,其函数说明如下:

paddle.fluid.io.xmap_readers(mapper, reader, process_num, buffer_size, order=False)
# mapper (callable): 映射 reader 数据的函数。
# reader (callable): 产生数据的 reader。
# process_num (int): 处理样本的线程数。
# buffer_size (int): 数据缓冲队列大小。
# order (bool): 是否保持原始 reader 数据顺序,默认为 False。

1.1.4 数据提供器

在本例中,所使用的数据集是外部数据,即paddlepaddle并没有内置,因此我们必须使用手动生成的样本列表来构建数据提供器。事实上,这也是最常见的方法。此处使用paddle.batch()来构建数据提供器,以下为关键参数介绍:

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

实验目的:

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

2.1 配置网络

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

Ch02ex01

  1. 输入层 $X$:手势识别数据集中的每个样本都为分辨率为 3×100×100 像素的彩色图片,根据前馈神经网络的结构。我们将其拉伸成一个向量3×100×100的向量进行输入,其维度为[N,3×100×100]。第一个维度是批大小,即batch_size为N的向量;第二个维度是将整幅彩色图像展开成向量形式,向量长度为3×100×100。
  2. 隐藏层 $H_1$:输入是输入层,即长度为3×100×100的向量,输出是长度为100的向量,激活函数为ReLU。
  3. 隐藏层 $H_2$:输入为上一层的输出,即长度为100的向量,输出是长度为256的向量,激活函数为ReLU。
  4. 隐藏层 $H_3$:输入为上一层的输出,即长度为256的向量,输出是长度为256的向量,激活函数为ReLU。
  5. 输出层$Y$:输入为上一层的输出,即长度为256的向量,输出是长度为10的向量,即10个节点的Softmax分类器层,对应于手势识别数据集的10个类别。所有的输出节点组成一个N=10维的向量,经过Softmax分类器后,将归一化为N个 [0,1] 的实数,其值为该样本属于这N个类别的概率,并且有 $\sum_{n=0}^9 y_n = 1$,即所有类别概率的和为1。对应的标签Label为正确类的One-hot向量。

使用动态图模式比静态图要简单很多,只需要定义模型结构即可。模型定义需要使用Object-Oriented-Designed面向对象的类进行定义。以下为该前馈神经网络(多层感知机)的网络构建函数。此处我们使用动态图模式进行网络结构的定义。首先定义了一个多层感知机的类class MLP(fluid.dygraph.Layer)。在该类别使用__inin__(self)对参数进行初始化,并定义前向传播forward()方法。

class paddle.fluid.dygraph.Linear(input_dim, output_dim, param_attr=None, bias_attr=None, act=None, dtype='float32')

以上是线性核Linear的说明,其中第一项为输入维度,第二项为输出维度,act为激活函数。对应于公式 $Out=Act(XW+b)$. 其中,X 为输入的 Tensor, W 和 b 分别为权重和偏置。

2.2 定义过程可视化函数

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

2.3 模型训练及评估

在Paddle 1.8的动态图模式下,所有的训练测试代码都需要基于动态图守护进程fluid.dygraph.guard(PLACE)

2.3.1 定义测试函数

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

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

两种测试模式,在代码上基本上是一致的,只是所使用的数据不同。因此,我们可以定义一个eval()函数来实现代码复用,在进行评估时,分别调用val_reader()test_reader()的即可。在Paddle 1.8的动态图模式下,测试进程同样要基于动态守护框架fluid.dygraph.guard()

  1. 设置模型运行模式为验证模式model.eval()
  2. 基于周期epoch-批次batch的结构进行两层循环训练,具体包括:
    1). 定义输入层(image,label)
    2). 定义输出层,包括前向传播的输出predict=model(image)及精度accuracy。如果需要,还可以输出针对测试集的损失loss。

    值得注意的,在计算测试集精度的时候,应该是针对所有测试样本的平均精度。在获得每个批次的精度test_accuracy时,通常情况下,有以下两种处理方法:

# 方法一:计算每个批次的平均精度
   accs = []
   accs.append(test_accuracy.numpy()[0])
   avg_acc = np.mean(accs)

# 方法二:计算每个样本的平局精度
   accs = []
   accs.append(test_accuracy.numpy()[0]*num_current_batch)) 
   # 当前批次的样本数 num_current_batch = len(labels)
   avg_acc = sum(accs)/NUM_TEST

不难发现,方法二是最标准的计算方法,但相对繁琐。方法一是一种常见的方法啊,更为简单快速,但存在一定的误差。主要原因来自于最后一个批次的样本数并不等于前面的批次时,因此这种简化会带来一定的误差。

2.3.2 定义训练函数

在动态图模式下,所有的训练测试代码都需要基于动态图守护进程fluid.dygraph.guard(PLACE)

训练部分的具体流程包括:

  1. 模型实例化,并设置为训练模式model.train()
  2. 定义优化器optimizer
  3. 基于周期epoch-批次batch的结构进行两层循环训练,具体包括:
    1). 定义输入层(image,label),图像输入维度 [batch, channel, Width, Height] (-1,3,32,32),标签输入维度 [batch, 1] (-1,1)
    2). 定义输出层,包括前向传播的输出predict=model(image),损失loss及平均损失,精度accuracy。
    3). 执行反向传播,并将损失最小化,清除梯度

在训练过程中,可以将周期,批次,损失及精度等信息打印到屏幕。

【注意】

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

  2. 在下列的代码中,我们每个(若干个)epoch都执行一次模型保存,这种方式一般应用在复杂的模型和大型数据集上。这种经常性的模型保存,有利于我们执行EarlyStopping策略,当我们发现运行曲线不再继续收敛时,就可以结束训练,并选择之前保存的最好的一个模型作为最终的模型。

  3. 在下面的代码中,我们设置了一个最优精度,并将每次对验证集的评估与最优精度进行对比,若当前的验证精度比最优精度更好,则将当前轮次的模型保存为最终模型;若当前验证精度比最优精度差,则跳过,继续进行训练。

2.3.3 模型训练及在线测试

2.3.4 离线测试

离线测试同样要基于动态守护框架fluid.dygraph.guard()。测试过程与训练过程中的在线测试流程基本一致。只是需要实现载入已保存的模型,可以使用fluid.load_dygraph()方法。 观察训练过程,我们可以使用np.argmax()获取测试性能最好的模型作为最终模型进行使用。

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

实验目的:

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

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

  1. 导入依赖库及全局参数配置
  2. 获取待预测数据,并进行初始化
  3. 载入模型并开始进行预测

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

3.2 获取待预测数据

获取待测数据包含两个步骤: 1.获取图像路径和标签; 2. 根据图像路径读取图像并进行预处理。两个功能分别由下面两个函数实现:

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