Different Deep Learning Methods for Image Classification on CIFAR 10

本文是2019年4月《人工智能》专业课的大作业报告摘录

主要内容是在同一数据集(CIFAR10)上使用不同的卷积神经网络模型
进行多分类问题训练以及识别效果的横向评估

中文标题:基于不同神经网络的CIFAR10图像分类

AI Studio

图1.1 本次实验的AI Studio项目入口页面

项目地址

  1. 百度 AI Studio(需要登录AI Studio账号后访问,使用百度账号即可)
  2. 在AI Studio的“开发者共享项目”中搜索“CIFAR10图像”分类即可
  3. GitHub(待发布)

实验目的

  1. 基于百度AI Studio平台提供的paddlepaddle深度学习框架、Jupyter Notebook线上python运行环境等基础设施,编程实现包括VGG、ResNet、GoogleNet(Inception-V1)、Inception-V4等多种图像分类神经网络。在编程实现的过程中,学习深度神经网络的基本理论和实践要点,了解上述不同神经网络的具体结构设计以及体现出的优秀设计理念和不足之处。

  2. 使用平台提供的CIFAR10图像识别数据集,在相同的训练环境条件下,训练上述不同神经网络并得出数据模型。收集训练模型过程中输出的训练参数数据,绘制统计图表,比较分析不同神经网络模型的在训练过程中的性能开销、数据指标变化等特点。

  3. 通过统一的测试图像对训练得出的模型分类图像内容的准确性进行测试,从而比较分析不同神经网络模型在实际应用中的效果。

笔者注:根据最后的评估结果以及对相关论文、资料的研读,我们发现这种类似单一变量法的横向对比实验事实上是存在问题的:

不同年代的卷积神经网络模型,对于训练时最佳效果的硬件要求应该是不同的,虽然不排除存在出现轻量级框架的可能,但是主流意义上的框架对于硬件资源的需求的确是逐年上升的。不应当对每一种模型在训练过程中给出相同的硬件环境,而是给出文献或其开源代码所要求的最佳硬件环境。

因此,本次实验出现的较新版本的模型最终的识别效果较差的情况,事实上仅仅是实验平台的硬件条件不足以在短时间内训练得出最佳效果的模型。

实验仪器

  • 本地设备:华硕K550-JX笔记本电脑、macOS Mojave 10.14.4
  • 远程设备:百度AI Studio提供的通过Jupyter Notebook连接的CPU: 2 Cores 、Memory: 8GB的远程服务器(无GPU)

实验原理

实验项目概况

本次实验的基本框架来自于paddlepaddle官网教程中的《深度学习基础教程》的《图像分类》章节(网页链接:http://paddlepaddle.org/documentation/docs/zh/1.4/beginners_guide/basics/image_classification/index.html)。

该章节介绍了图像识别分类领域中包括VGG、ResNet和GoogleNet等常用模型的基本原理,并给出了paddlepaddle使用其框架自带的CIFAR10数据集以及VGG、ResNet训练模型并进行图像分类的基本步骤和代码实现。

我们在研读了该教程中的相关理论知识、各行代码实现的前提下,将该教程所述的数据预处理、训练模型、图像识别等完整的流程代码,移植到了同样搭载了最新版本的paddlepaddle的AI Studio在线项目环境中。该项目为新建的项目,而非直接fork在AI Studio上现有的项目,因此能够使用最新版本的paddlepaddle,避免了fork使用早期paddlepaddle版本项目所带来的一系列问题。

除此之外,我们也对代码进行了逐行的注释解读工作,来帮助使用者理解代码的基本含义和相关的理论知识。我们添加了训练过程中的数据统计图表绘制功能代码,能够在训练结束后将收集到的训练数据绘制成形象的图表并输出,有助于使用者对不同模型的性能进行综合的判断。

在此基础之上,我们更进一步,参考网络上的相关资料,将该教程中仅给出理论知识而无代码实践的GoogleNet(Inception-V1)、以及其同一系列的最新版本Inception-V4的模型代码移植实现到了百度AI Studio在线项目环境上(由于部分代码存在版本过低等问题,我们进行了相应的修改以确保代码能够正常运行),同样给出了详尽的代码注释解读。

现在,本项目已经公开在了百度AI Studio的“开发者共享项目”栏目中,欢迎大家fork本项目,也欢迎大家联系我们(邮箱:lmy98129@163.com)提出建议。

CIFAR数据集介绍

注:以下实验原理介绍部分摘录自paddlepaddle官方教程以及其他网络资料,同时也添加了我们在理论学习和实践过程中对于数据集使用、神经网络模型设计的优缺点等方面的思考和理解,能力有限,如有偏差,敬请谅解。

cifar

图1.2 CIFAR数据集局部
(图片摘自paddlepaddle官方教程)

CIFAR10数据集是主要用于通用图像分类而公开的标准数据集CIFAR的一个子集,包含60,000张32x32的彩色图片,10个类别(分别为:飞机airplane、轿车automobile、鸟类bird、猫cat、鹿deer、狗dog、蛙frog、马horse、船ship、卡车truck),每个类包含6,000张。其中50,000张图片作为训练集,10000张作为测试集。

之所以选用CIFAR而不是大量学术研究成果所基于的ImageNet,我们主要考虑到其体积的问题,在AI Studio的在线项目环境中使用的是CPU训练,而CPU的训练速度由于其核心数量、并行计算能力等原因一般要远远慢于GPU,因此选择一个较小的数据集能够较好地节省训练的时间,但也因此对模型的在小数据集条件下的训练效果提出了考验。

关于下载速度,由于AI Studio提供了可动态加载的数据集仓库,能够通过创建项目时进行设置、或者创建后修改项目设置等方式动态加载到项目中,因此不存在联网下载的问题。

VGG基本介绍

vgg

图1.3 VGG模型结构
(图片摘自paddlepaddle官方教程)

相比以往的神经网络模型(例如CNN等),由牛津大学于2014年提出的VGG模型在神经网络的层数(深度)和卷积层的卷积核数目(宽度)上进行了增加。其核心结构是:五组不同卷积核数目的卷积层,以及每两组卷积层之间的max-pooling最大池化的降维操作,最后是全连接层和分类预测层。

关于VGG网络的设计,我们认为,加深神经网络能够进行更多次的特征提取,提高神经网络的表达能力,但是也增加了训练神经网络的时间和成本,过深的神经网络往往会因为带来梯度的损失而无法找到最优解,从而导致过拟合、准确度下降等一系列问题;加宽的神经网络能够输入更多的细节特征,但也导致了需要输入的参数过多,而同等深度下的神经网络,参数的个数对训练的结果没有明显的影响。

ResNet基本介绍

resnet

图1.4 残差模块示意图
(图片摘自paddlepaddle官方教程)

为了解决随着网络层数加深而导致准确度下降的问题,ResNet提出了残差学习方法来减轻训练深层网络的困难,在添加batchnorm、小卷积核、全卷积网络等特性基础上,引入了残差模块。

残差模块的其中一条路径是输入特征的直连通道(可以认为是输入特征中的普遍特征),另一条经过多次卷积的到特征的残差(可以认为是输入特征中的显著特征),最后将以上两条结果相加得到输出。通过这种输出的叠加,残差模块很好地提升了深层次网络训练结果的准确度和收敛速度。

我们对于以上提到的一些现有特性概念的理解是:batchnorm能够将每次输入的数据分布进行规范化,让其均匀分布在当前层上,从而加速神经网络的训练速度、防止过拟合。小卷积核的意思是指单个卷积核的长宽尺寸减小,能够减少训练参数,从而降低训练模型的性能开销。全卷积网络是指整个模型的主体部分完全使用卷积网络,全连接层使用增加步长的特定卷积层替换,这种替换在功能上是等价的。

GoogleNet(Inception-V1)基本介绍

googlenet

图1.5 Inception模块示意图
右图为添加1*1卷积层进行降维之后的模块
(图片摘自paddlepaddle官方教程)

GoogleNet由多组Inception模块组成,Inception模块的主要特点是在同一层级上并行设置了多个不同尺寸的卷积层和一个最大池化层,根据资料以及我们的理解总结,这一特性解决了多个问题:

  1. 卷积层的不同尺寸消除了信息分布的均匀程度对卷积核大小的选取影响
  2. 并行的卷积层减缓了网络层数过深导致的梯度损失以及过拟合
  3. 并行的最大池化层对输入尺寸进行压缩并提取主要特征,也缓解了简单堆叠多层网络导致的计算资源的消耗

但是这个特点同样带来了缺陷:并行的池化层并不会改变整个Inception模块的通道数量,并行卷积层构成的Inception在将各个并行层结果拼接后,特征的通道数较大,经过几层这样的模块堆积后,通道数会越来越大,导致参数和计算量也随之增大。因此,Inception还在每一个并行分支上引入了1*1卷积层进行降维操作,减少通道数,解决了这一问题。

除此之外,GoogleNet的另一个显著特征就是采用了三个子网络,可以得到3个网络的损失率进行加权求和得出整个网络的损失,从而有利于使用优化器(optimizer)的训练程序计算更准确的梯度,加快收敛速度。

Inception-V4基本介绍

inception-v4

图1.6 inception-sterm模块示意图
(图片摘自论文《Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning》)

Inception-V4是Inception系列中的最新版本,经过V2版本添加batchnorm,V3版本对卷积层的调整,在Inception-V4中加入了同样基于卷积+池化并行理念的inception-sterm模块,并分化出了inception-A、B、C三种不同的模块类型。其设计的理念是要与添加了残差模块的Inception-ResNet具有相同的性能,因此使用了大量的经验性的结构设计,其对应的论文中没有对这些结构设计的由来做出进一步的解释说明。

此外,该模型还添加了reduction模块,起到了之前版本中的一层单层池化层的作用,同样采用了卷积+池化并行的结构设计。

实验内容与步骤

项目初始化

登录AI Studio平台

登录百度AI Studio首页并登录AI Studio账号,选择顶部导航栏中的“项目”,进入项目页面

登录 AI Studio

图2.1 登录百度AI Studio进入AI Studio的项目页面
创建项目

点击“创建项目”,输入项目名称、描述并添加数据集。在数据集添加界面中搜索并选中“cifar10数据集”。这里之所以选择这一项“cifar10数据集”是因为该数据集与在调用paddlepaddle自带的cifar10数据集时需要自动联网下载的cifar10数据集格式相同,可以在项目建立后通过在Jupyter Notebook中执行shell命令的方式,将数据集自行放入paddlepaddle的缓存目录中,节省其下载时间。

创建项目

图2.2 创建项目界面

选择数据集

图2.3 选择“cifar10数据集”
运行项目

创建项目之后,进入项目界面,点击“运行项目”,进入Jupyter Notebook界面

Jupyter Notebook

图2.4 Jupyter Notebook界面
加载数据集

在第一个cell中输入将当前自动载入到项目当中的数据集cifar-10-python.tar.gz拷贝到paddlepaddle缓存目录的shell命令,如下所示

1
2
!cp data/data5752/cifar-10-python.tar.gz /home/aistudio/.cache/paddle/dataset/cifar/cifar-10-python.tar.gz
!ls -l /home/aistudio/.cache/paddle/dataset/cifar/

执行该cell,若得到如下输出,则拷贝成功。

拷贝数据集

图2.5 拷贝数据集成功的输出

至此,项目初始化完成。

编写项目主体代码

导入系统模块代码
1
2
3
4
5
6
7
8
9
#导入paddle模块以及一些系统模块
import paddle
import paddle.fluid as fluid
import numpy
import sys
import os
import math
from __future__ import print_function
from paddle.fluid.param_attr import ParamAttr

如上所示,这些代码的主要导入了包括paddlepaddle、numpy、sys、math等运行环境内置的python库。

训练模型所需的模块函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 预测程序
def inference_network(model):
# 图像是32 * 32的rgb格式,rgb格式每个像素应该是3位
data_shape = [3, 32, 32]
# 设置图片格式
images = fluid.layers.data(name='pixel', shape=data_shape, dtype='float32')
if model == 'vgg':
# 使用vgg模型进行预测
predict = vgg_bn_drop(images)
elif model == 'resnet':
# 使用resnet模型进行预测
predict = resnet_cifar10(images, 32)
elif model == 'googlenet':
# 使用googlenet模型进行预测
predict = googlenet(images, 10)
elif model == 'inception_v4':
# 使用inception_v4模型进行预测
inception_v4 = InceptionV4()
predict = inception_v4.net(images, 10)

return predict

预测程序是在训练或预测过程中实际调用各神经网络模型的最底层函数,这里可以看到不同的模型要求输入的参数类型、调用方式都各有不同。这些模型的具体实现代码在下文会详细给出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 训练程序
def train_network(predict, model=None):
# 首先从预测程序中获取预测结果

# 设置图片类别标签格式
label = fluid.layers.data(name='label', shape=[1], dtype='int64')
if model == 'googlenet':
# 若为googlenet
out, out1, out2 = predict
# 分别采用多类交叉熵作为损失函数
cost0 = fluid.layers.cross_entropy(input=out, label=label)
cost1 = fluid.layers.cross_entropy(input=out1, label=label)
cost2 = fluid.layers.cross_entropy(input=out2, label=label)
# 得到的平均损失用于在上一层中的训练主函数中计算梯度
avg_cost0 = fluid.layers.mean(x=cost0)
avg_cost1 = fluid.layers.mean(x=cost1)
avg_cost2 = fluid.layers.mean(x=cost2)
# 最后加权求和
avg_cost = avg_cost0 + 0.3 * avg_cost1 + 0.3 * avg_cost2
# 预测精度看第一个输出即可
accuracy = fluid.layers.accuracy(input=out, label=label)
else:
# 对于其他模型
# 在训练中采用多类交叉熵作为损失函数
cost = fluid.layers.cross_entropy(input=predict, label=label)
# 得到的平均损失用于在上一层中的训练主函数中计算梯度
avg_cost = fluid.layers.mean(cost)
# 计算当前预测精度
accuracy = fluid.layers.accuracy(input=predict, label=label)

# 返回平均损失和预测精度
return [avg_cost, accuracy]

训练程序是在训练过程中通过模型返回的predict结果来计算损失率和预测精度的函数。这里特别处理了GoogleNet的三个损失率分量的加权求和计算。

1
2
3
4
# 优化器程序
def optimizer_program():
# 输入学习率,也就是训练的速度,这里与网络的训练收敛速度有关
return fluid.optimizer.Adam(learning_rate=0.001)

优化器程序是在训练过程中通过设置学习率、也就是训练的速度后返回一个特定的Adam优化器实例的函数,这是python类的用法。Adam优化器是优化器的一种,对梯度的一阶矩估计和二阶矩估计进行综合考虑,计算出当前神经网络中各个神经元的参数更新的步长,以加快梯度下降速度。Adam优化器在当前深度学习优化器中被默认是相当优异的

训练主函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# 训练主函数
def train(use_cuda, model, params_dirname=None):
# 事实上本次训练使用的是CPU,所以use_cuda应当固定为False
place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
# 每次训练所选取的样本数量,适当的batch_size可以使得数据并行化处理且梯度下降的方向更加明确
if model == 'inception_v4':
# 针对inception_v4调整batch_size
BATCH_SIZE = 256
else:
BATCH_SIZE = 128

# 训练集数据输入,这里使用了shuffle,是用来将读入的数据进行打乱操作的
# 所以需要定义一个打乱缓冲区的大小buf_size
train_reader = paddle.batch(
paddle.reader.shuffle(
paddle.dataset.cifar.train10(), buf_size=128*100),
batch_size=BATCH_SIZE);

# 测试集数据输入
test_reader = paddle.batch(
paddle.dataset.cifar.test10(), batch_size=BATCH_SIZE)

print("\nstart training");

# 输入数据的先后顺序格式
feed_order = ['pixel', 'label']

# 生成默认的训练主程序和启动程序
main_program = fluid.default_main_program()
star_program = fluid.default_startup_program()

# 输出预测结果,这里没有传入数据是因为数据传入操作是之后的训练过程中设置的
predict = inference_network(model)
# 获取训练结果
avg_cost, acc = train_network(predict, model)

# 此处开始是测试程序
test_program = main_program.clone(for_test=True)
# 优化器
optimizer = optimizer_program()
# 告诉优化器在当前平均损失的基础上计算梯度以减少损失
optimizer.minimize(avg_cost)

# 执行器,将以上操作放入CPU执行
exe = fluid.Executor(place)

# epoch意思为所有数据项目完成一次前向运算和反向传播的次数
# 这里因为我们训练时间有限,还是1~3次就够了
EPOCH_NUM = 3

# 统计图横纵坐标的列表
train_steps=[]
train_costs=[]
test_steps=[]
test_costs=[]

# 对训练结果进行损失率检测的函数
def train_test(program, reader):
# 检测次数count
count = 0
# 输入数据的变量名列表,这里应该就是feed_order中的‘pixel’和‘label’
# global_block经查应该是fluid的全局作用域
feed_var_list = [
program.global_block().var(var_name) for var_name in feed_order
]
# 数据喂入器DataFeeder负责将数据读取器的输入转换成一种特殊的数据结构中去
# 从而能够将该数据结构的数据输入到执行器中
feeder_test = fluid.DataFeeder(feed_list=feed_var_list, place=place)
test_exe = fluid.Executor(place);

# 这个变量是记录包括数据变量名在内的所有数据个数以及对应的损失率的
accumulated = len([avg_cost, acc]) * [0];
# 将数据读取器reader中获取到的输入数据通过enumerate转换为索引序列
for tid, test_data in enumerate(reader()):
# 执行训练结果损失率检测的执行器test_exe,喂入测试集数据test_data,得到当前的平均损失率avg_cost_np
avg_cost_np = test_exe.run(
program=program,
feed=feeder_test.feed(test_data),
fetch_list=[avg_cost, acc])
# 记录当前的数据个数,这里使用的zip函数将accumulate和avg_cost_np打包成了一个元组进行记录
# 其中x[1][0]应该是avg_cost_np中的第一项,也就是损失率loss
accumulated = [x[0] + x[1][0] for x in zip(accumulated, avg_cost_np)]
count += 1

# 返回的是accumulated中每一条记录中的x与count相除的结果,为平均每次检测得到的损失率
return [x/count for x in accumulated]

# 训练循环函数
def train_loop():
# 同样是输入数据的变量名列表,应该就是feed_order中的‘pixel’和‘label’
feed_var_list_loop = [
main_program.global_block().var(var_name) for var_name in feed_order
]
feeder = fluid.DataFeeder(feed_list=feed_var_list_loop, place=place)
# 开始运行启动程序
exe.run(star_program)

# 记录训练次数
step = 0;

# 训练次数id为pass_id,range生成了一个以epoch次数的
for pass_id in range(EPOCH_NUM):
# 每次训练中的分组训练次数step_id,
for step_id, train_data in enumerate(train_reader()):
# 执行训练执行器,喂入训练集数据train_data,得到当前的平均损失率avg_lost_value
avg_loss_value = exe.run(
main_program,
feed=feeder.feed(train_data),
fetch_list=[avg_cost, acc])
# 每50次输出一次训练结果,分别是训练次数,分组训练次数,损失率,预测精度
if step_id % 50 == 0:
print("\nPass %d, Batch %d, Cost %f, Acc %f" % (
step_id, pass_id, avg_loss_value[0], avg_loss_value[1]))
else:
# 否则单纯输出点表示正在训练
sys.stdout.write('.')
sys.stdout.flush()
# 并更新一次训练损失率统计图
train_steps.append(step)
train_costs.append(avg_loss_value[0])
step += 1

# 每次训练的全部分组训练结束后,进行一次损失率检测,
avg_cost_test, accuracy_test = train_test(test_program, reader=test_reader)
# 输出损失率检测结果
print('\nTest with Pass {0}, Loss {1:2.2}, Acc {2:2.2}'.format(
pass_id, avg_cost_test, accuracy_test))
# 并更新一次检测损失率统计图
test_steps.append(step)
test_costs.append(avg_cost_test)

# 若模型保存地址为有效,则自动保存本次训练的模型结果
# 其中从第二个开始的变量意思为:
# 喂入数据的基本格式(pixel)
# 保存预测结果所使用的变量组(predict)
# 执行预测程序(exe=fluid.Executor(place))
if params_dirname is not None:
if model == 'googlenet':
model_out, _, _ = predict
else:
model_out = predict
fluid.io.save_inference_model(params_dirname, ["pixel"], [model_out], exe)


# 在以上函数和变量定义全部结束后,即可开始训练
train_loop();

# 训练结束后绘制损失率统计图
%matplotlib inline
import matplotlib.pyplot as plt

train_title = "Train cost"
test_title = "Test cost"
title = "Train cost/Test cost"
# 标题,横纵坐标
plt.title(title, fontsize=24)
plt.xlabel("step", fontsize=14)
plt.ylabel("cost", fontsize=14)
# 设置图例
plt.plot(train_steps, train_costs, color='blue', label=train_title)
plt.plot(test_steps, test_costs, color='red', label=test_title)
plt.legend()
# 显示统计图
plt.show()

训练主函数train相对较长,而且还内部声明了对训练结果进行损失率检测的train_test、训练循环函数train_loop几个函数,这里绘制了程序流程图以方便理解,如下图所示:

训练主函数

从代码和流程图中,我们可以看出训练主函数的主要工作是:

  • 对训练进行一系列的函数调用关系的绑定、变量的声明和初始化以及训练所需的主要元器件实例(执行器、启动函数、主函数、优化器、数据集)的生成
  • 进行实际训练过程中的执行、模型生成、数据生成
  • 训练结束后图表的绘制

在这里需要说明的有以下几点:

  • 在paddlepaddle中损失率为均方差函数得出的,故没有固定单位,但是一般在训练过程中是呈现总体下降的趋势,损失率越低,模型的效果越好。

  • 准确度较容易理解,就是当前模型能够准确识别的样本个数占当前训练样本或测试样本的百分比。

  • batch_size是指每次训练时输入的样本个数,合理的batch_size设置能够减缓在训练过程中的损失率上下震荡的趋势,使得模型的损失率下降速度更快,精确率提升更加明显。根据经验,过大的batch_size可能会导致损失率下降或精确度提升到某一点后停滞,并且导致每次训练的时间和性能开销增大,过小的batch_size则会导致损失率上下震荡,下降速度减慢。

  • epoch是指所有样本完成一次前向运算和反向传播的次数,也就是所有样本都参与过训练的次数。epoch决定了整个训练的总时长,如果使用的是GPU,则可以因为并行处理性能高、训练速度较快而将epoch定在30~50甚至更多,而使用CPU则建议1~5,否则将导致训练时间过长,无法及时生成模型文件。

预测主函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# 预测主程序
def infer(use_cuda, params_dirname=None):
from PIL import Image
# 事实上本次训练使用的是CPU,所以use_cuda应当固定为False
place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
# 创建执行器
exe = fluid.Executor(place)
# 创建用于预测的局部作用域
inference_scope = fluid.core.Scope()

# 用于装载需要预测的图片的子函数
def load_image(infer_file):
# 打开图片
im = Image.open(infer_file)

%matplotlib inline
import matplotlib.pyplot as plt
# 清空plt输出
plt.close()
# 输出当前图片
plt.imshow(im)
plt.show()
# 将图片拉伸为32 * 32,与训练图片相同的大小
im = im.resize((32, 32), Image.ANTIALIAS)
# 将图片转换为像素数组
im = numpy.array(im).astype(numpy.float32)
# 注意,一般存储图片的像素数组格式为W(宽度)、H(高度)、C(像素通道)
# 但是paddlepaddle需要将格式转换为CHW格式,所以使用了transpose函数
im = im.transpose((2, 0, 1))
# 过滤值为255以上的颜色,也就是进行灰度变换
im = im/255.0
# 向图片添加一个维度用来模拟为列表结构,事实上该维度只有这张图片一个元素
im = numpy.expand_dims(im, axis=0)
# 返回处理好的图片
return im

# 获取程序所在的当前位置
cur_dir = os.path.dirname(os.path.realpath('__file__'))
# 设置预测图片
img = load_image(cur_dir + '/image/dog.png');

# 进入当前的局部作用域
with fluid.scope_guard(inference_scope):
# 使用fluid.io.load_inference_model去获取以下的信息
# inference_program:当前的预测程序
# feed_target_names:喂入数据需要的变量名称
# fetch_targets:获取数据的目标,通过使用这个目标从而在exe.run中输入fetch_list
[inference_program, feed_target_names, fetch_targets] = fluid.io.load_inference_model(params_dirname, exe)

# 输入到神经网络的维度数目一般为4D或5D,使用trainpiler这种编译方式可以将输入的数据结构进行转译
# 转译的目的主要是能够将fluid生成的对应自有fluid解释器、
# 而非Python解释器(这样速度更快)的protobuf message表示的程序翻译成 C++ 或其他语言的程序
inference_transpiler_program = inference_program.clone()
t = fluid.transpiler.InferenceTranspiler()
t.transpile(inference_transpiler_program, place)

# 将喂入得数据构造成如下结构{feed_target_name: feed_target_data}
# 预测的结构带有与fetch_targets对应的一系列数据
# 这里分别使用带有trainpiler转译和不带有转译的程序进行预测
results = exe.run(inference_program,
feed={feed_target_names[0]: img},
fetch_list=fetch_targets)

transpiler_results = exe.run(inference_transpiler_program,
feed={feed_target_names[0]: img},
fetch_list=fetch_targets)

# 断言,确定以上两次预测的结果(就1个,所以是[0])的长度是否相同
# 若相同则继续比对结果中的各个项目是否相同
# 总之,就是在比对转译前后结果是否能够相同
assert len(results[0]) == len(transpiler_results[0])
for i in range(len(results[0])):
numpy.testing.assert_almost_equal(
results[0][i], transpiler_results[0][i], decimal=5)

# 预测标签,这个顺序一般是和训练的模型数据的顺序相同的
label_list = [
"airplane", "automobile", "bird", "cat", "deer", "dog", "frog",
"horse", "ship", "truck"
]

# 输出预测结果
print("infer results: %s" % label_list[numpy.argmax(results[0])])

预测主函数

图2.7 预测主函数predict的程序流程图
程序主函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def main(use_cuda):
# 如果需要使用GPU的CUDA函数库,则需要判断fluid是否根据cuda进行了编译
if use_cuda and not fluid.core.is_compiled_with_cuda():
return

# 注意:更改训练模型请更改此处的model变量
model='googlenet'
# 模型文件保存路径
save_path = 'image_classification_'+model+'.inference.model'

# 训练
train(use_cuda=use_cuda, model=model, params_dirname=save_path)
# 注意:如果报出optimzer相关的错误,可以尝试在kernel操作中“重启”,之后再从头开始重新运行
# 这一错误可能是在对部分函数中途修改并重新运行后optimizer不再识别其输入导致的问题

# 预测,如果训练已经得出了save_path指定的模型文件
# 预测程序则可以独立运行,否则不可以运行
infer(use_cuda=use_cuda, params_dirname=save_path)

if __name__ == '__main__':
# 根据当前测试环境,使用CPU
main(use_cuda=False)

程序主函数main的主要功能是先确定环境变量:使用CPU/GPU、当前使用的模型名称、模型文件保存路径等,再执行训练主函数和预测主函数,是整个程序的最顶层模块。

编写神经网络模型代码

注:由于在“实验原理”章节中,对于各神经网络模型的关键技术原理和关键模块结构已经进行了说明,此处代码部分对于这些内容不再重复解释。

在整体上,神经网络模型的实现主要是基于paddlepaddle提供的卷积层、池化层、全连接层等函数API以及层与层之间的连接来实现的,各神经网络的共性的地方在于以下2点:

  • 经过若干个卷积、池化层结构之后,在最后输出结果之前的一层全连接层中都要经历一次softmax归一化,通过softmax归一化得到每个类别的概率,softmax能够将输入映射为0-1之间的实数,作为取到某个分类的概率,作为最终的输出结果。
  • 在每一组神经网络之间,常用dropout层对结果按照一定概率随机丢弃一些特征,以防止过拟合;同时也常用batchnorm,将每次输入的数据分布进行规范化,让其均匀分布在当前层上,从而加速神经网络的训练速度、同样防止过拟合。
VGG模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# vgg模型定义

# 代码来自:http://paddlepaddle.org/documentation/docs/zh/1.4/beginners_guide/basics/image_classification/index.html

def vgg_bn_drop(input):
# 创建神经网络的公用函数
def conv_block(ipt, num_filter, groups, dropouts):
# 返回根据传入参数创建的神经网络
return fluid.nets.img_conv_group(
# 图像输入
input=ipt,
# 池化窗口大小为2*2
pool_size=2,
# 池化窗口移动的步长
pool_stride=2,
# 该神经网络层组的过滤器数量,可以认为是神经元个数
conv_num_filter=[num_filter] * groups,
# 过滤器大小,默认值为3
conv_filter_size=3,
# 激活函数的类型,这里选用RELU
conv_act='relu',
# 在每一层后使用batchnorm以加速神经网络训练速度
# batchnorm能够将每次输入的数据分布进行规范化
conv_with_batchnorm=True,
# 对于每一层进行batchnorm后的dropout概率
# dropout是避免过拟合的手段,按照一定概率随机丢弃一些特征
conv_batchnorm_drop_rate=dropouts,
# 池化类型为最大池化,提取每个池化窗口中的最显著特征
pool_type='max')

# 以下各函数中最后一个列表为每一层结束后dropout的概率
# 一般在两组卷积层之间不使用dropout
# 第1组卷积层,2次连续卷积,卷积核数目64
conv1 = conv_block(input, 64, 2, [0.3, 0])
# 第2组卷积层,2次连续卷积,卷积核数目128
conv2 = conv_block(conv1, 128, 2, [0.4, 0])
# 第3组卷积层,3次连续卷积,卷积核数目为256
conv3 = conv_block(conv2, 256, 3, [0.4, 0.4, 0])
# 第4组卷积层,3次连续卷积,卷积核数目为512
conv4 = conv_block(conv3, 512, 3, [0.4, 0.4, 0])
# 第5组卷积层,3次连续卷积,卷积核数目为512
conv5 = conv_block(conv4, 512, 3, [0.4, 0.4, 0])

# 最后一层结束后添加一层概率为0.5的dropout层
drop = fluid.layers.dropout(x=conv5, dropout_prob=0.5)
# 添加全连接层,维度数为512
fc1 = fluid.layers.fc(input=drop, size=512, act=None)
# 在全连接层结束后添加batchnorm防止过拟合
bn = fluid.layers.batch_norm(input=fc1, act='relu')
# 添加概率为0.5的dropout层
drop2 = fluid.layers.dropout(x=bn, dropout_prob=0.5)
# 添加全连接层,维度数为512
fc2 = fluid.layers.fc(input=drop2, size=512, act=None)

# 最后,添加预测用的全连接层,映射到类别维度大小的向量,本次数据类别一共10种
# 通过softmax归一化得到每个类别的概率,softmax是将输入映射为0-1之间的实数,作为取到某个分类的概率
# 可以认为是一个分类器
predict = fluid.layers.fc(input=fc2, size=10, act='softmax')

# 输出最终的结果
return predict
ResNet模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# resnet模型定义

# 代码来自:http://paddlepaddle.org/documentation/docs/zh/1.4/beginners_guide/basics/image_classification/index.html

# 以下为resnet_cifar10需要用到的工具函数

# conv_bn_layer为自带batchnorm的神经网络层
# input为输入,ch_out为滤波器个数,该个数与输出图像通道相同,故赋值为channel_out=ch_out
# filter_size为过滤器大小,stride为窗口移动的步长
# padding为填充格式,VALID对于多出来的数据直接丢弃,SAME将多出来的数据继续填充到下一层的额外行和列
# act为激活函数,这里使用RELU函数
# bias_attr为False,说明不需要得到单个卷积核卷积图片的结果
def conv_bn_layer(input,
ch_out,
filter_size,
stride,
padding,
act='relu',
bias_attr=False):
tmp = fluid.layers.conv2d(
input=input,
filter_size=filter_size,
num_filters=ch_out,
stride=stride,
padding=padding,
act=None,
bias_attr=bias_attr)
return fluid.layers.batch_norm(input=tmp, act=act)

# shortcut为残差模块的“直连路径”
# 在resnet中引入残差模块后,解决了网络层数加深导致准确度下降的问题
def shortcut(input, ch_in, ch_out, stride):
# 残差模块输入和输出特征通道数不等时,采用1x1卷积的升维操作
if ch_in != ch_out:
return conv_bn_layer(input, ch_out, 1, stride, 0, None)
else:
# 残差模块输入和输出通道相等时,采用直连操作
return input

# basicblock为基础残差模块,由两组3x3卷积组成的路径和一条"直连"路径组成
def basicblock(input, ch_in, ch_out, stride):
# 由两组3x3卷积组成的路径
tmp = conv_bn_layer(input, ch_out, 3, stride, 1)
tmp = conv_bn_layer(tmp, ch_out, 3, 1, 1, act=None, bias_attr=True)
# 一条“直连”路径
short = shortcut(input, ch_in, ch_out, stride)
# 使用fluid自动在每一层后添加这一残差模块的输入
return fluid.layers.elementwise_add(x=tmp, y=short, act='relu')

# layer_warp为一组残差模块,由若干个残差模块堆积而成
# 这里的block_func事实上指的就是basicblock
# ch_in和ch_out分别为输入输出通道
# count为残差模块的个数,stride为窗口移动步长
def layer_warp(block_func, input, ch_in, ch_out, count, stride):
tmp = block_func(input, ch_in, ch_out, stride)
# 每组中第一个残差模块滑动窗口大小与其他可以不同,以用来减少特征图在垂直和水平方向的大小
for i in range(1, count):
tmp = block_func(tmp, ch_out, ch_out, 1)
return tmp

# resnet_cifar10模型主函数

def resnet_cifar10(ipt, depth=32):
# 除第一层卷积层和最后一层全连接层之外
# 要求三组 layer_warp 总的含参层数能够被6整除
# 即 resnet_cifar10 的 depth 要满足 (depth−2)

# 因此深度的可能取值: 20, 32, 44, 56, 110, 1202
assert (depth - 2) % 6 == 0
n = (depth - 2) // 6
nStages = {16, 64, 128}

# 底层输入连接一层带batchnorm的卷积层
conv1 = conv_bn_layer(ipt, ch_out=16, filter_size=3, stride=1, padding=1)
# 连接3组残差模块
res1 = layer_warp(basicblock, conv1, 16, 16, n, 1)
res2 = layer_warp(basicblock, res1, 16, 32, n, 2)
res3 = layer_warp(basicblock, res2, 32, 64, n, 2)
# 对网络做均值池化,可以看到pool_type=‘avg’表示均值池化
pool = fluid.layers.pool2d(
input=res3, pool_size=8, pool_type='avg', pool_stride=1)
# 添加全连接层作为预测层,通过softmax归一化得到每个类别的概率
predict = fluid.layers.fc(input=pool, size=10, act='softmax')
return predict
GoogleNet模型

我们获得的初始GoogleNet模型代码使用的是早期的paddlepaddle版本,因此我们花费了一些时间查阅了paddlepaddle官网的API文档,研究了不同版本之间的API对应关系和调用方式上的差异。最终,我们成功地将该模型代码移植到了AI Studio在线项目平台上的paddlepaddle V1.4版本上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# googlenet模型定义

# 代码来自:https://www.cnblogs.com/charlotte77/p/8066867.html

# 以下为googlenet需要用到的工具函数

# inception为一个inceoption网络,目前已经发展到了inceptionV4和inception-resnet
# inception主要的特点是在同一层级上运行多个不同尺寸的卷积层,这一特性解决了多个问题
# 1. 消除了信息分布的均匀程度对卷积核大小的选取影响
# 2. 减缓了网络层数过深导致的梯度损失以及过拟合
# 3. 缓解了简单堆叠多层网络导致的计算资源的消耗
# 但是这个特点同样带来了缺陷:
# 池化层不会改变特征通道数,拼接后会导致特征的通道数较大,经过几层这样的模块堆积后,通道数会越来越大,导致参数和计算量也随之增大
# 因此,inception还通过引入3个1*1卷积层进行降维,减少通道数
# 下面的版本是inceptionv1版本,v2引入batchnorm,v3对卷积层进一步分解,v4引入了res-net

# 这些参数的意思为:
# name:整个inception的名称
# channels:通道个数
# filter1、filter3R、filter3、filter5R、filter5、proj:各个卷积层的过滤器数量
def inception(name, input, channels, filter1, filter3R, filter3, filter5R,
filter5, proj):
# 1*1卷积层_1
cov1 = fluid.layers.conv2d(
input=input,
filter_size=1,
num_filters=filter1,
stride=1,
padding=0)

# 1*1卷积层_3r
cov3r = fluid.layers.conv2d(
input=input,
filter_size=1,
num_filters=filter3R,
stride=1,
padding=0)
# 1*1卷积层_3r的下一层3*3卷积层
cov3 = fluid.layers.conv2d(
input=cov3r,
filter_size=3,
num_filters=filter3,
stride=1,
padding=1)

# 1*1卷积层_5r
cov5r = fluid.layers.conv2d(
input=input,
filter_size=1,
num_filters=filter5R,
stride=1,
padding=0)
# 1*1卷积层_5r的下一层5*5卷积层
cov5 = fluid.layers.conv2d(
input=cov5r,
filter_size=5,
num_filters=filter5,
stride=1,
padding=2)

# 3*3最大池化层
pool1 = fluid.layers.pool2d(
input=input,
pool_size=3,
pool_type="max",
pool_stride=1,
pool_padding=1)
# 3*3最大池化层的下一层1*1卷积层
covprj = fluid.layers.conv2d(
input=pool1,
filter_size=1,
num_filters=proj,
stride=1,
padding=0)

# 全连接层将以上的结果汇总处理
cat = fluid.layers.concat(input=[cov1, cov3, cov5, covprj], axis=1)
return cat

# googlenet模型主函数
# class_dim为当前类别的维度个数,这里一共有10个类,因此填10
def googlenet(input, class_dim):
# stage 1
# 7*7卷积层
conv1 = fluid.layers.conv2d(
input=input,
filter_size=7,
num_filters=64,
stride=2,
padding=3)
# 3*3最大池化层
pool1 = fluid.layers.pool2d(
input=conv1, pool_size=3, pool_type="max", pool_stride=2)

# stage 2
# 1*1卷积层
conv2_1 = fluid.layers.conv2d(
input=pool1,
filter_size=1,
num_filters=64,
stride=1,
padding=0)
# 3*3卷积层
conv2_2 = fluid.layers.conv2d(
input=conv2_1,
filter_size=3,
num_filters=192,
stride=1,
padding=1)
# 3*3最大池化层
pool2 = fluid.layers.pool2d(
input=conv2_2, pool_size=3, pool_type='max', pool_stride=2)

# stage 3
# 2组inception+1个3*3最大池化层
ince3a = inception("ince3a", pool2, 192, 64, 96, 128, 16, 32, 32)
ince3b = inception("ince3b", ince3a, 256, 128, 128, 192, 32, 96, 64)
pool3 = fluid.layers.pool2d(
input=ince3b, pool_size=3, pool_type='max', pool_stride=2)

# stage 4
# 5组inception+1个3*3最大池化层
ince4a = inception("ince4a", pool3, 480, 192, 96, 208, 16, 48, 64)
ince4b = inception("ince4b", ince4a, 512, 160, 112, 224, 24, 64, 64)
ince4c = inception("ince4c", ince4b, 512, 128, 128, 256, 24, 64, 64)
ince4d = inception("ince4d", ince4c, 512, 112, 144, 288, 32, 64, 64)
ince4e = inception("ince4e", ince4d, 528, 256, 160, 320, 32, 128, 128)
pool4 = fluid.layers.pool2d(
input=ince4e, pool_size=3, pool_type='max', pool_stride=2, pool_padding=1)

# stage 5
# 2组inception+1个7*7最大池化层
ince5a = inception("ince5a", pool4, 832, 256, 160, 320, 32, 128, 128)
ince5b = inception("ince5b", ince5a, 832, 384, 192, 384, 48, 128, 128)
pool5 = fluid.layers.pool2d(
input=ince5b,
pool_size=7,
pool_stride=7,
pool_type="avg")
# 添加丢弃概率为0.4的dropout层避免过拟合
drop1 = fluid.layers.dropout(x=pool5, dropout_prob=0.5)
# 最后一层全连接层进行主损失率out的输出,softmax归一化每个类别的概率
out = fluid.layers.fc(
input=drop1, size=class_dim, act='softmax')

# 用于计算损失率分量out1的第一个辅助的分类器
# 5*5均值池化,注意这里的输入为ince4a的输出,也就是在生成out中途的输出
pool_o1 = fluid.layers.pool2d(
input=ince4a,
pool_size=5,
pool_stride=3,
pool_type="avg",
pool_padding=1)
# 1*1卷积
conv_o1 = fluid.layers.conv2d(
input=pool_o1,
filter_size=1,
num_filters=128,
stride=1,
padding=0)
# 带有激活函数RELU的全连接
fc_o1 = fluid.layers.fc(
input=conv_o1,
size=1024,
act="relu")
# 添加丢弃概率为0.4的dropout层避免过拟合
drop2 = fluid.layers.dropout(x=fc_o1, dropout_prob=0.7)
# 最后一层全连接层softmax归一化后输出的out1
out1 = fluid.layers.fc(
input=drop2, size=class_dim, act='softmax')

# 用于计算损失率分量out2的第二个辅助的分类器
# 5*5均值池化,这里的输入为ince4d的输出,同样是在生成out中途的输出
pool_o2 = fluid.layers.pool2d(
input=ince4d,
pool_size=5,
pool_stride=3,
pool_type="avg",
pool_padding=1)
# 1*1卷积
conv_o2 = fluid.layers.conv2d(
input=pool_o2,
filter_size=1,
num_filters=128,
stride=1,
padding=0)
# 带有激活函数RELU的全连接
fc_o2 = fluid.layers.fc(
input=conv_o2,
size=1024,
act="relu")
# 添加丢弃概率为0.4的dropout层避免过拟合
drop3 = fluid.layers.dropout(x=fc_o2, dropout_prob=0.7)
# 最后一层全连接层softmax归一化后输出的out1
out2 = fluid.layers.fc(
input=drop3, size=class_dim, act='softmax')

# 输出损失率的三个分量
return out, out1, out2

Inception-V4模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# inception_v4模型定义

# 代码来自:https://github.com/PaddlePaddle/models/blob/43cdafbb97e52e6d93cc5bbdc6e7486f27665fc8/PaddleCV/image_classification/models/inception_v4.py

# 这里使用了面向对象的封装
# 因为不同的inception版本中的同名函数例如conv_bn_layer(带batchnorm的卷积层)的具体实现是不同的
# 所以为了防止同名函数定义的互相覆盖,使用类的封装思想比较合理

# 类
class InceptionV4():
def __init__(self):
print("using inception v4.")

# 模型主函数
def net(self, input, class_dim=1000):
# STEP 1 inception_sterm模块
# stem模块其实就是多次卷积+2次池化,采用了Inception论文里提到的卷积+池化并行的结构
# 在同时也使用了多个1*1卷积,之前的googlenet(inception_v1)中也提到过
# 这是一种降维操作,能够通过减少通道数从而减少因为并行结构带来的巨大计算量
x = self.inception_stem(input)

# STEP 2 4层inception_A模块+1层reduction模块
# inception_A、B、C模块之间内在结构各有不同,在该算法论文中没有详细的解答,应该是一种经验性的结构
# reduction起到了作为之前版本中的一层单层池化层的作用,同样采用了卷积+池化并行的结构
for i in range(4):
x = self.inceptionA(x, name=str(i + 1))
x = self.reductionA(x)

# STEP 3 7层inception_B模块+1层reduction模块
for i in range(7):
x = self.inceptionB(x, name=str(i + 1))
x = self.reductionB(x)

# STEP 4 3层inception_C模块+1层reduction模块
for i in range(3):
x = self.inceptionC(x, name=str(i + 1))

# 平均池化,不同于最大池化提取特征,这是在保留背景信息
pool = fluid.layers.pool2d(
input=x, pool_size=8, pool_type='avg', global_pooling=True)

# dropout操作用来减少过拟合
drop = fluid.layers.dropout(x=pool, dropout_prob=0.2)

# 这里做了一次运算,stdv是标准差的意思
stdv = 1.0 / math.sqrt(drop.shape[1] * 1.0)
# 这里传入的initializer.Uniform是随机均匀分布初始化器的意思
# 这里的全连接层的各个神经元权重使用了随机均匀分布初始化的方式
# 一般的初始化方式有正态分布和随机均匀分布两种,两者优劣没有定论,但经验上看,均匀分布的随机数能够让更多的权重接近于0
out = fluid.layers.fc(
input=drop,
size=class_dim,
param_attr=ParamAttr(
initializer=fluid.initializer.Uniform(-stdv, stdv),
name="final_fc_weights"),
bias_attr=ParamAttr(
initializer=fluid.initializer.Uniform(-stdv, stdv),
name="final_fc_offset"),
act='softmax')
return out

# 带batchnorm的卷积层函数
def conv_bn_layer(self,
data,
num_filters,
filter_size,
stride=1,
padding=0,
groups=1,
act='relu',
name=None):
# 和之前resnet使用的卷积层定义基本一致,只是添加了一些name名称,此处不再赘述
conv = fluid.layers.conv2d(
input=data,
num_filters=num_filters,
filter_size=filter_size,
stride=stride,
padding=padding,
groups=groups,
act=None,
param_attr=ParamAttr(name=name + "_weights"),
bias_attr=False,
name=name)
bn_name = name + "_bn"
# batchnorm也是如此,基本一致
return fluid.layers.batch_norm(
input=conv,
act=act,
name=bn_name,
param_attr=ParamAttr(name=bn_name + "_scale"),
bias_attr=ParamAttr(name=bn_name + "_offset"),
moving_mean_name=bn_name + '_mean',
moving_variance_name=bn_name + '_variance')

# inception_stem层,具体结构可以参考论文中的图像,
# 基本原理还是和googlenet一样,并行处理的卷积+池化以及1*1卷积降维
# 具体到结构为什么这么设计可以认为是经验性的,论文没有深入讨论
def inception_stem(self, data, name=None):
conv = self.conv_bn_layer(
data, 32, 3, stride=2, act='relu', name="conv1_3x3_s2")
conv = self.conv_bn_layer(conv, 32, 3, act='relu', name="conv2_3x3_s1")
conv = self.conv_bn_layer(
conv, 64, 3, padding=1, act='relu', name="conv3_3x3_s1")

pool1 = fluid.layers.pool2d(
input=conv, pool_size=3, pool_stride=2, pool_type='max')
conv2 = self.conv_bn_layer(
conv, 96, 3, stride=2, act='relu', name="inception_stem1_3x3_s2")
concat = fluid.layers.concat([pool1, conv2], axis=1)

conv1 = self.conv_bn_layer(
concat, 64, 1, act='relu', name="inception_stem2_3x3_reduce")
conv1 = self.conv_bn_layer(
conv1, 96, 3, act='relu', name="inception_stem2_3x3")

conv2 = self.conv_bn_layer(
concat, 64, 1, act='relu', name="inception_stem2_1x7_reduce")
conv2 = self.conv_bn_layer(
conv2,
64, (7, 1),
padding=(3, 0),
act='relu',
name="inception_stem2_1x7")
conv2 = self.conv_bn_layer(
conv2,
64, (1, 7),
padding=(0, 3),
act='relu',
name="inception_stem2_7x1")
conv2 = self.conv_bn_layer(
conv2, 96, 3, act='relu', name="inception_stem2_3x3_2")

concat = fluid.layers.concat([conv1, conv2], axis=1)

conv1 = self.conv_bn_layer(
concat, 192, 3, stride=2, act='relu', name="inception_stem3_3x3_s2")
pool1 = fluid.layers.pool2d(
input=concat, pool_size=3, pool_stride=2, pool_type='max')

concat = fluid.layers.concat([conv1, pool1], axis=1)

return concat

# inception_A模块,同样不再赘述
def inceptionA(self, data, name=None):
pool1 = fluid.layers.pool2d(
input=data, pool_size=3, pool_padding=1, pool_type='avg')
conv1 = self.conv_bn_layer(
pool1, 96, 1, act='relu', name="inception_a" + name + "_1x1")

conv2 = self.conv_bn_layer(
data, 96, 1, act='relu', name="inception_a" + name + "_1x1_2")

conv3 = self.conv_bn_layer(
data, 64, 1, act='relu', name="inception_a" + name + "_3x3_reduce")
conv3 = self.conv_bn_layer(
conv3,
96,
3,
padding=1,
act='relu',
name="inception_a" + name + "_3x3")

conv4 = self.conv_bn_layer(
data,
64,
1,
act='relu',
name="inception_a" + name + "_3x3_2_reduce")
conv4 = self.conv_bn_layer(
conv4,
96,
3,
padding=1,
act='relu',
name="inception_a" + name + "_3x3_2")
conv4 = self.conv_bn_layer(
conv4,
96,
3,
padding=1,
act='relu',
name="inception_a" + name + "_3x3_3")

concat = fluid.layers.concat([conv1, conv2, conv3, conv4], axis=1)

return concat

# reduction_A模块
def reductionA(self, data, name=None):
pool1 = fluid.layers.pool2d(
input=data, pool_size=3, pool_stride=2, pool_type='max', pool_padding=1)

conv2 = self.conv_bn_layer(
data, 384, 3, stride=2, padding=1, act='relu', name="reduction_a_3x3")

conv3 = self.conv_bn_layer(
data, 192, 1, act='relu', name="reduction_a_3x3_2_reduce")
conv3 = self.conv_bn_layer(
conv3, 224, 3, padding=1, act='relu', name="reduction_a_3x3_2")
conv3 = self.conv_bn_layer(
conv3, 256, 3, stride=2, padding=1, act='relu', name="reduction_a_3x3_3")

concat = fluid.layers.concat([pool1, conv2, conv3], axis=1)

return concat

# inception_B模块
def inceptionB(self, data, name=None):
pool1 = fluid.layers.pool2d(
input=data, pool_size=3, pool_padding=1, pool_type='avg')
conv1 = self.conv_bn_layer(
pool1, 128, 1, act='relu', name="inception_b" + name + "_1x1")

conv2 = self.conv_bn_layer(
data, 384, 1, act='relu', name="inception_b" + name + "_1x1_2")

conv3 = self.conv_bn_layer(
data, 192, 1, act='relu', name="inception_b" + name + "_1x7_reduce")
conv3 = self.conv_bn_layer(
conv3,
224, (1, 7),
padding=(0, 3),
act='relu',
name="inception_b" + name + "_1x7")
conv3 = self.conv_bn_layer(
conv3,
256, (7, 1),
padding=(3, 0),
act='relu',
name="inception_b" + name + "_7x1")

conv4 = self.conv_bn_layer(
data,
192,
1,
act='relu',
name="inception_b" + name + "_7x1_2_reduce")
conv4 = self.conv_bn_layer(
conv4,
192, (1, 7),
padding=(0, 3),
act='relu',
name="inception_b" + name + "_1x7_2")
conv4 = self.conv_bn_layer(
conv4,
224, (7, 1),
padding=(3, 0),
act='relu',
name="inception_b" + name + "_7x1_2")
conv4 = self.conv_bn_layer(
conv4,
224, (1, 7),
padding=(0, 3),
act='relu',
name="inception_b" + name + "_1x7_3")
conv4 = self.conv_bn_layer(
conv4,
256, (7, 1),
padding=(3, 0),
act='relu',
name="inception_b" + name + "_7x1_3")

concat = fluid.layers.concat([conv1, conv2, conv3, conv4], axis=1)

return concat

# reduction_B模块
def reductionB(self, data, name=None):
pool1 = fluid.layers.pool2d(
input=data, pool_size=3, pool_stride=2, pool_type='max', pool_padding=1)

conv2 = self.conv_bn_layer(
data, 192, 1, act='relu', name="reduction_b_3x3_reduce")
conv2 = self.conv_bn_layer(
conv2, 192, 3, stride=2, padding=1, act='relu', name="reduction_b_3x3")

conv3 = self.conv_bn_layer(
data, 256, 1, act='relu', padding=1,name="reduction_b_1x7_reduce")
conv3 = self.conv_bn_layer(
conv3,
256, (1, 7),
padding=(0, 3),
act='relu',
name="reduction_b_1x7")
conv3 = self.conv_bn_layer(
conv3,
320, (7, 1),
padding=(3, 0),
act='relu',
name="reduction_b_7x1")
conv3 = self.conv_bn_layer(
conv3, 320, 3, stride=2, act='relu', name="reduction_b_3x3_2")

concat = fluid.layers.concat([pool1, conv2, conv3], axis=1)

return concat

# inception_C模块
def inceptionC(self, data, name=None):
pool1 = fluid.layers.pool2d(
input=data, pool_size=3, pool_padding=1, pool_type='avg')
conv1 = self.conv_bn_layer(
pool1, 256, 1, act='relu', name="inception_c" + name + "_1x1")

conv2 = self.conv_bn_layer(
data, 256, 1, act='relu', name="inception_c" + name + "_1x1_2")

conv3 = self.conv_bn_layer(
data, 384, 1, act='relu', name="inception_c" + name + "_1x1_3")
conv3_1 = self.conv_bn_layer(
conv3,
256, (1, 3),
padding=(0, 1),
act='relu',
name="inception_c" + name + "_1x3")
conv3_2 = self.conv_bn_layer(
conv3,
256, (3, 1),
padding=(1, 0),
act='relu',
name="inception_c" + name + "_3x1")

conv4 = self.conv_bn_layer(
data, 384, 1, act='relu', name="inception_c" + name + "_1x1_4")
conv4 = self.conv_bn_layer(
conv4,
448, (1, 3),
padding=(0, 1),
act='relu',
name="inception_c" + name + "_1x3_2")
conv4 = self.conv_bn_layer(
conv4,
512, (3, 1),
padding=(1, 0),
act='relu',
name="inception_c" + name + "_3x1_2")
conv4_1 = self.conv_bn_layer(
conv4,
256, (1, 3),
padding=(0, 1),
act='relu',
name="inception_c" + name + "_1x3_3")
conv4_2 = self.conv_bn_layer(
conv4,
256, (3, 1),
padding=(1, 0),
act='relu',
name="inception_c" + name + "_3x1_3")

concat = fluid.layers.concat(
[conv1, conv2, conv3_1, conv3_2, conv4_1, conv4_2], axis=1)

return concat

训练和预测

为了能够更好地评价不同神经网络模型的训练性能、实际预测效果等,我们采用控制变量法,使用相同的训练和预测流程设计,训练使用的参数统一为batch_size=128、epoch=3,预测使用的待预测图像为一张狗的照片。运行程序、进行训练和预测的主要流程如下所示:

  • 首先,我们需要保证项目之前的输出被全部清空,且在“Kernel操作”中进行过至少一次的“重启”操作。
  • 之后,在最后一个cell的程序主函数中的model变量中确定对应模型的名称,若只需要使用已生成的模型文件进行预测而不需要再次训练,可以注释掉train训练主函数,只运行predict预测主函数。
  • 最后,选中第一个cell,点击“Notebook操作”中的“运行当前及下方所有”,开始程序的运行。

各模型的具体运行结果截图可以参见下一章节“实验数据”。

实验数据

在训练和预测流程执行完毕后,对于各个模型程序输出的原始数据结果截图如下所示:

VGG模型

VGG 模型的运行结果

图 3.1 VGG模型的运行结果(仅训练数据输出)

可能是由于最终训练结果的精确度过低,在预测过程中出现了报错的情况,因此此处没有预测结果。

ResNet模型

ResNet 模型的运行结果

图3.2 ResNet模型的训练和预测结果

GoogleNet模型

GoogleNet 模型的运行结果

图3.3 GoogleNet模型的训练和预测结果

Inception-V4模型

由于在理论上Inception-V4模型应当是GoogleNet(Inception-V1)的改进,但是首次训练和预测后的结果都完全差于GoogleNet,于是我们查询了该模型代码来源的GitHub仓库上的参数设置,发现batch_size应当由128改为256。

在针对该模型设置该特有参数值之后,我们进行了第二次的额外训练和预测。两次训练和预测的原始数据如下所示:

Inception-v4 模型的首次运行结果

图3.4 Inception-V4模型首次运行时的训练和预测结果

Inception-v4 模型修改后的运行结果

图3.5 Inception-V4模型修改batch_size之后
再次运行时的训练和预测结果

实验数据处理

VGG模型数据图表

由于在“实验数据”环节所述的程序报错的关系,未能够通过python代码自动生成损失率图表,此处使用Excel生成相关图表:

vgg 训练数据图表

图4.1 VGG训练数据图表

VGG 测试数据图表

图4.2 VGG测试数据图表

可以看出,VGG模型在当前训练环境下,训练过程中损失率震荡较大,下降速率较慢,准确率同样在上下波动且上升速率较慢,而使用测试数据集生成的测试数据基本保持不变。而且准确率相当低,在10%左右徘徊,说明VGG模型在当前环境下的综合性能较差。

ResNet模型数据图表

vgg 损失率数据图表

图4.3 实验程序生成的ResNet的训练和测试损失率图表

vgg 准确率数据图表

图4.4 ResNet的训练和测试准确率图表

ResNet模型在当前训练环境下,训练过程中损失率震荡较小,下降速率在训练初期较快,之后趋于平缓。虽然测试过程中的损失率虽然震荡较大,但是参照训练过程,确实维持在一个合理的区间内。

在训练和测试过程中,ResNet模型的准确率都保持着不断升高的趋势,最终的准确率接近70%。

但是,在实际的预测过程中,ResNet模型却将带预测的图片分类为了horse马,说明在实际应用过程中,该模型仍存在可以提升的空间。

restnet 实际预测结果

图4.5 ResNet模型实际预测结果

GoogleNet模型数据图表

googlenet 损失率数据图表

图4.6 实验程序生成的GoogleNet的训练和测试损失率图表

googlenet 准确率数据图表

图4.7 GoogleNet的训练和测试准确率图表

GoogleNet模型在当前训练环境下,训练过程中损失率几乎没有震荡,下降速率在训练初期极快,之后趋于平缓且不断逼近0。测试过程中的损失率曲线与训练过程曲线近乎重合。以上现象说明了在当前环境下,该模型的训练效果相当出色。

在训练和测试过程中,GoogleNet模型的准确率都保持着不断升高的趋势,最终的准确率在60%左右。

除了下降速率曲线之外,该模型还有如下2点令人印象深刻之处:

  • 训练速度快:相比其他模型,该模型的训练耗时相当少,当其他模型需要5~10秒才能训练完一个pass时,该模型只需1秒左右的时间,因此训练速度极快。我们认为训练速度快的主要原因是:该模型的网络层数相较于其他模型更少,且3个子网络分别输出损失率并加权求和的操作有助于优化器更加精确地计算出当前梯度,从而更准确地调整网络中各层神经元的权重参数。
  • 模型实际预测结果精准:如下图所示,在实际预测的过程中,该模型是唯一一个将该图片正确分类为dog狗的,可以看出该模型在实际应用方面的准确度相当高,虽然数据层面的准确率60%略逊于ResNet的70%。

GoogleNet 模型实际预测结果

图4.8 GoogleNet模型实际预测结果

Inception-V4模型数据图表

inception-v4 损失率数据图表

图4.9 实验程序生成的Inception-V4的训练和测试损失率图表

inception-v4 损失率数据图表

图4.10 Inception-V4的训练和测试准确率图表

Inception-V4模型在当前训练环境下,训练过程中损失率震荡严重,下降速率缓慢。测试过程中的损失率曲线与训练过程曲线同样近乎重合。以上现象说明了在当前环境下该模型训练效果较差。

在训练和测试过程中,Inception-V4模型的准确率都保持着不断升高的趋势,但是最终的准确率在30%左右。

在实际预测中,该模型将带预测图片分类为了truck卡车,说明其模型准确度确实不高。

Inception-v4 模型实际预测结果

图4.11 Inception-v4 模型实际预测结果

我们在“实验数据”环节就已经根据程序输出的实验数据提出了“为何作为GoogleNet的迭代版本,Inception-V4反而在性能和实际效果上不如GoogleNet”的疑问并根据相关资料修改了batch_size为256,并进行了第二次的训练和预测。实验数据处理如下所示:

修改后inception-v4 损失率数据图表

图4.12 调整参数第二次训练后

实验程序生成的Inception-V4的训练和测试损失率图表

修改后inception-v4 准确率数据图表

图4.13 调整参数第二次训练后

Inception-V4的训练和测试准确率图表

可以看出,Inception-V4模型在修改参数后的训练环境下,训练过程中损失率震荡有所收敛,但下降速率依旧缓慢。测试过程中的损失率曲线与训练过程曲线同样近乎重合。以上现象说明了在当前环境下该模型训练效果仍然较差。

在训练和测试过程中,Inception-V4模型的准确率都保持着不断升高的趋势,但是最终的准确率还是在30%左右。
在实际预测中,该模型将带预测图片分类为了automobile轿车,说明其模型准确度仍然不高

修改后Inception-v4 模型实际预测结果

图4.11 调整参数第二次训练后

GoogleNet模型实际预测结果

我们初步怀疑调整参数后效果仍然较差的原因有如下两点:

  • epoch的次数仍然偏少,因为Inception_V4的网络层数明显多于GoogleNet,因此需要更长时间的训练才能够获得一个较好的模型,但由于使用CPU训练速度较慢,实验时间有限,暂时不考虑进行更多次的实验。
  • 据Inception_V4对应的论文《Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning》中的描述,该模型的设计主要考虑到了TensorFlow等框架在内存分配等方面的优化设计,因此也存在着paddlepaddle不支持这些特性导致的模型性能表现的不佳。

实验结果与分析

结果分析

针对在“实验数据处理”章节中对实验数据的图表绘制处理和初步分析,我们能够得出以下综合分析结果:

  • 使用控制变量法在相同环境(尤其是采用相同参数)下的不同模型进行的性能评价结果,这一方法存在着很大的局限性。本次实验尤为突出的表现正是Inception系列的V1(GoogleNet)和V4之间的可以称之为逆转性的结果。在batch_size=128、epoch=3的条件下,V1在训练速度、训练数据展示的效果、实际预测效果上都优于V4。而出于对这一反常问题的好奇,我们将batch_size按照V4代码的初始来源处的参数改为了batch_size=256,结果效果改善程度有限。这些说明了控制变量法并不能够全面地衡量不同模型之间的性能和实际效果。
  • 在当前环境下,GoogleNet和ResNet在各项实验数据指标上优于其他模型,而GoogleNet为最优。ResNet仅在准确率的数据层面上的70%略胜于GoogleNet的60%,而GoogleNet无论是在训练所需时间、损失率曲线的震荡程度、损失率曲线的下降速率、以及实际预测的准确程度都明显优于ResNet,且是唯一一个正确分类了带预测图片的模型。
  • 但是这并不意味着Inception系列的Inception模块设计不存在缺陷,也并不意味着Inception系列不应该引入ResNet的结构设计,相反,ResNet的残差模块的结构设计在实际研究和应用过程中,确实有其“提升深层次网络训练结果的准确度和收敛速度”的独到之处。只不过由于实验时间的关系,我们并未继续引入Inception-ResNet-V1、Inception-ResNet-V2等两者相结合的模型,并进行进一步的实验和分析。

结论

实验结论

本次实验基于百度AI Stuido平台的在线项目平台,使用了包括VGG、ResNet、GoogleNet(Inception-V1)、Inception-V4等图像分类神经网络模型以及CIFAR10数据集,对相同环境条件下的不同模型在训练和预测过程中的性能开销、数据指标变化情况、实际预测情况等进行了详细的分析讨论。尽管通过实验表明,本次实验所使用的控制变量法存在着一定的局限性,但是本次实验仍然得出了GoogleNet(代表Inception系列)、ResNet在性能指标和实际效果上较为优秀的结论,这肯定了Inception模块、残差模块的结构设计在模型训练、实际预测等多方面相较于传统的多层神经网络存在着相当大的优势。

本次实验目的步骤明确、实验过程较为顺利、对实验结果的也进行了较为细致的处理和分析,是一次虽然存在问题,但在一定程度上较为成功的人工智能课程实验。

成果收获

经过本次实验,我们团队成员收获了以下成果:

  • 通过研读paddlepaddle官方教程和文档以及其他网上相关资料、编写、移植以及逐行注释不同模型代码、处理分析实验数据等方式,我们锻炼了团队合作完成“查阅人工智能相关文献、理解相关基本概念、使用代码实现相应模型的结构设计、对实验数据进行处理和分析”的一整套人工智能领域研究流程的实战能力。
  • 通过对paddlepaddle框架的学习,我们初步掌握了深度学习框架、以及其他辅助用途的python库的基本使用方式,了解了使用深度学习框架的需要进行的“数据处理、参数设置、模型训练、测试集测试、结果输出”等一般流程。
  • 通过这次实验,我们也巩固了团队合作的情况下完成实验的任务分配、进度协调、成员沟通等综合能力。
待改进的地方

经过本次实验,我们认为仍然存在以下待改进的地方:

  • 实验对不同模型的评估比较方法存在问题。控制单一变量法并不能够全面地让不同模型发挥出应有的性能效果,应当考虑给予不同模型以其目前研究水平下最佳的环境配置,通过基于单一测试数据的多次实际预测效果测试来衡量不同模型的性能,其结果会更好。
  • 未能引入更多较为新型的图像分类模型例如Inception-ResNet系列模型等,进行范围更加广泛的比较和分析。

鸣谢

最后,在整篇文章的结尾,我还是要一如既往地感谢本次与我合作完成这一项目的搭档:Jet Lian,他主要负责本次项目的相关文献资料的查阅和汇总,VGG、ResNet、Inception-V4模型代码的编写、注释,实验程序执行和实验数据结果的处理、分析,“实验内容与步骤”之后的实验报告的撰写。

在他的合作之下,我才能够完成我自己的工作内容:实验方案选取、实验环境初始化、项目训练和预测函数等模块结构的搭建,基于paddlepaddle早期版本GoogleNet模型代码的移植、编写和注释,“实验内容与步骤”及之前的实验报告撰写。

作为室友兼搭档,我个人是十分敬佩他分析问题、解决问题和实际编码的强大综合能力的,像他这样成绩优秀且技术能力过硬的同学,在USTB的CS专业中乃至SCCE学院中都是罕见的。真的十分荣幸,能够在这三年的时光中与他为友,在技术成长的道路上并肩前行。

同时,我也十分感谢《人工智能》专业选修课的任课老师王睿老师、以及本次和AI专选课合作的百度AI Studio在线实验平台,正是老师和工作人员们的通力合作和不懈努力,为我们本届CS学生创造了一次实际体验深度学习训练到预测全过程的宝贵机会。希望这样的机会在未来的SCCE学院乃至整个行业会越来越多,再次感谢这些为技术知识的传播做出贡献的人们!

参考资料

  1. 百度paddlepaddle官网教程《深度学习基础教程》的《图像分类》章节:
    http://paddlepaddle.org/documentation/docs/zh/1.4/beginners_guide/basics/image_classification/index.html
  2. 基于paddlepaddle的inception-v4模型代码:
    https://github.com/PaddlePaddle/models/blob/43cdafbb97e52e6d93cc5bbdc6e7486f27665fc8/PaddleCV/image_classification/models/inception_v4.py
  3. 基于paddlepaddle旧版的googlenet模型(本文中展示的是基于该项目移植到新版paddlepaddle后的代码):
    https://www.cnblogs.com/charlotte77/p/8066867.html
  4. Google Inception系列论文《Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning》:
    https://www.aaai.org/ocs/index.php/AAAI/AAAI17/paper/viewPDFInterstitial/14806/14311