100行代码入门PaddlePaddle图像识别(无痛看代码)

  导语:PaddlePaddle是由百度研发,国内首个开源的深度学习框架。你在学了N多机器学习课程后,发现要手写一个深度学习程序的时候仍会无从下手。本文目的是解决这种入门问题,适合有深度学习基础但不会写程序,或者会使用其他深度学习框架但想学习PaddlePaddle使用方式的人群。本文将带领大家将大脑中的想法及模型用PaddlePaddle框架实现出来。

设想一下,如果要实现一个图像分类的深度学习程序,有哪些必要的模块?首先想到的是他一定要有一个描述、定义网络结构的模块。在本文中我们就用VGG来描述网络结构,那第一个模块就是Vgg_bn_drop。有了这个网络模块我们可以推想出一定需要一个推理程序,这个程序会驱动网络模块产生一个输出,我们就叫这个输出为Predict。那第二个模块就是推理程序(Inference_Program)。我们有了Predict之后,在训练过程中自然需要将Predict与数据集中的Label进行比较,并通过损失函数来计算比较的差值。那第三个模块就是将Predict实例、Label定义、cost函数计算整合在一起的程序,它在PaddlePaddle里我们将它成为train_func,那在这里我们将第三个模块起名为train_program。在第三个模块里我们定义了cost,cost存在的意义是计算当前参数的推理与label的差值,从而调整网络中的参数,那我们就需要定义一个优化器来调整网络中的参数。所以第四个模块就是Optimizer。有了以上的四个模块,就将整个网络运转的流程(从推理到反向调整)都定义好了。
上述结构图

我们将框架定义好之后,需要一个程序将这个框架给驱动起来。这个驱动程序还有一个作用是数据灌入框架中,让数据再里面流动起来(这也是Fluid这个词的由来)。在PaddlePaddle中,可以使用Trainer这个方法来实现这个功能。之后我们只需要将数据准备好,做成reader的格式,就可以使用Trainer中的train函数来执行训练啦。
千里之行,至于足下。我们来看一下第一步代码该怎么写。
一、第一步除了导入各种库之外,

1
2
3
4
5
import paddle
import paddle.fluid as fluid
import numpy
import sys
from __future__ import print_function #用于提供Python3标准的print特性

自然是要将我们的第一个模块——网络结构定义给实现出来。所以我们定义一个vgg_bn_drop的函数:

1
def vgg_bn_drop(input):

  我们观察一下VGG的网络结构
vgg16

  可以发现VGG网络中有很多重复的部分,如果我们把这些重复的卷积操作化为一组,那么VGG中卷积的部分可以分为五组。在PaddlePaddle中对于这种连续的卷积操作可以用img_conv_group函数来实现。

1
2
····def conv_block(参数先空着):
········return fluid.nets.img_conv_group()

  img_conv_group是整合了卷积层、池化层、BatchNorm和DropOut的复合函数,并且可以很方便的支持连续卷及操作。我们想一下,对于每组连续卷积,我们需要定义哪些内容呢?首先它必须接受一个数据输入input。在卷积层方面,我们可以想到的是要定义卷积核大小、卷积核数量、卷积层激活函数;在池化层方面我们可以想到要定义池化区域的大小、池化窗口的步长以及池化的方法。那关于DropOut的功能我们需要提供一个DropOut的概率,在img_conv_group的参数中还有一个是否打开batchnorm的开关,需要指定一下。那么我们关img_conv_group的参数定义如下:

1
2
3
4
5
6
7
8
9
10
········return fluid.nets.img_conv_group(
input=ipt,
conv_filter_size=3,
conv_num_filter=[num_filter] * groups,
conv_act='relu',
pool_size=2,
pool_stride=2,
pool_type='max'
conv_with_batchnorm=True,
conv_batchnorm_drop_rate=dropouts)

根据VGG的网络图我们发现所有卷积层的卷积核都为3*3,那么我们在参数中就直接指定一个参数3,如果在此处给定两个参数,PaddlePaddle会认为这是个WH格式的矩形卷积核。conv_num_filte参数需要给定这组连续卷积操作中所有的卷积核数量,以用来统一初始化,所以这里需要在num_filter后乘上group的数量。根据论文我们可以知道激活函数为’relu’。根据VGG的网络图我们看到是二分之一池化,所以pool_size和pool_stride都定义为2。之后使用最大池化方法、打开batchnorm选项、指定dropout的概率。需要注意的是这里给到的dropout需要以Python中List数据结构给出,这个list存放的是连续卷积中每一层卷积的dropout概率。到这里img_conv_group的定义就完成。根据这些参数,去除硬编码的参数我们发现input、num_filter、groups、dropouts需要在上层函数中传递。所以conv_block参数如下:

1
····def conv_block(ipt, num_filter, groups, dropouts):

所以在这一步我们的连续卷积定义就完成啦。但是我们的conv_block不能只有连续卷积的定义,还需要将他按照VGG模型的样子给组装起来。那么卷积层的组装代码为:

1
2
3
4
5
····conv1 = conv_block(input, 64, 2, [0.3, 0])
conv2 = conv_block(conv1, 128, 2, [0.4, 0])
conv3 = conv_block(conv2, 256, 3, [0.4, 0.4, 0])
conv4 = conv_block(conv3, 512, 3, [0.4, 0.4, 0])
conv5 = conv_block(conv4, 512, 3, [0.4, 0.4, 0])

从第二层开始,每一层接受上一层的输出,第二个参数根据VGG结构定义每一层输出的维度,第三个参数定义连续卷积的次数,第四个参数定义dropout的概率,最后一层不进行dropout操作。根据网络结构,后面需要做三层全连接操作,定义如下:

1
2
3
4
5
6
····drop = fluid.layers.dropout(x=conv5, dropout_prob=0.5)
fc1 = fluid.layers.fc(input=drop, size=512, act=None)
bn = fluid.layers.batch_norm(input=fc1, act='relu')
drop2 = fluid.layers.dropout(x=bn, dropout_prob=0.5)
fc2 = fluid.layers.fc(input=drop2, size=512, act=None)
predict = fluid.layers.fc(input=fc2, size=10, act='softmax')

这里用到了PaddlePaddle内置的算子,有全连接layers.fc,batch_norm和dropout。所以整个vgg_bn_drop代码如下:

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
def vgg_bn_drop(input):
def conv_block(ipt, num_filter, groups, dropouts):
return fluid.nets.img_conv_group(
input=ipt,
pool_size=2,
pool_stride=2,
conv_num_filter=[num_filter] * groups,
conv_filter_size=3,
conv_act='relu',
conv_with_batchnorm=True,
conv_batchnorm_drop_rate=dropouts,
pool_type='max')

conv1 = conv_block(input, 64, 2, [0.3, 0])
conv2 = conv_block(conv1, 128, 2, [0.4, 0])
conv3 = conv_block(conv2, 256, 3, [0.4, 0.4, 0])
conv4 = conv_block(conv3, 512, 3, [0.4, 0.4, 0])
conv5 = conv_block(conv4, 512, 3, [0.4, 0.4, 0])

drop = fluid.layers.dropout(x=conv5, dropout_prob=0.5)
fc1 = fluid.layers.fc(input=drop, size=512, act=None)
bn = fluid.layers.batch_norm(input=fc1, act='relu')
drop2 = fluid.layers.dropout(x=bn, dropout_prob=0.5)
fc2 = fluid.layers.fc(input=drop2, size=512, act=None)
predict = fluid.layers.fc(input=fc2, size=10, act='softmax')
return predict

二、定义好网络结构以后,我们需要将网络的输出Predict给接住,并且将它的奶嘴(输入格式)给备好,所以我们定义

1
def inference_program():

方法时要先接住输出:

1
predict = vgg_bn_drop(images)

再喂上奶嘴:

1
2
3
data_shape = [3, 32, 32]
images = fluid.layers.data(name='pixel', shape=data_shape, dtype='float32')
predict = vgg_bn_drop(images) # un-comment to use vgg net

在PaddlePaddle中,无论是图像数据,张量数据还是标签数据,都可以用layers.data容器来存放。在data函数中,’name’参数是可以自定义指定的。因为本实验是使用cifar10的数据,是3通道32x32的图片。所以inference_program的代码如下:

1
2
3
4
5
6
7
8
def inference_program():
# The image is 32 * 32 with RGB representation.
data_shape = [3, 32, 32]
images = fluid.layers.data(name='pixel', shape=data_shape, dtype='float32')

#predict = resnet_cifar10(images, 32)
predict = vgg_bn_drop(images) # un-comment to use vgg net
return predict

三、根据开头讲的思路,我们有了推理模块后,需要将Predict和label进行交叉对比计算,所以我们需要一个train_program。train_program起到的作用是定义label、计算损失函数、计算准确率。他需要将每一批的平均cost和准确率转给下一步的优化器。所以train_program的定义如下:

1
2
3
4
5
6
7
def train_program():
predict = inference_program()
label = fluid.layers.data(name='label', shape=[1], dtype='int64')
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]

我们得到cost之后,在训练过程需要根据cost返回的数据来反向调整神经网络中的参数,那反向调整参数的模块就叫optimizer_program,我们对optimizer_program的定义只需要返回指定的optimizer即可(在这里指定学习率超参数):

1
2
def optimizer_program():
return fluid.optimizer.Adam(learning_rate=0.001)

四、有了以上三个模块,训练-推理-调整这一个有向循环图就构成了。现在我们就像已经将自来水管道修好,需要往里通水的状态。那我们整个循环系统的中控程序是什么呢?是fluid.Trainer。在PaddlePaddle中fluid.Trainer是一个较高层的API,使用时只需将fluid.Trainer这个类实例化即可,启动实例化对象中的.train()方法即可启动网络训练。那这就涉及到了两个步骤:实例化对象和启动训练。

五、在实例化对象时需要指定3个参数:train_func、optimizer和place。train_func就是我们刚才定义的train_program,它包含了网络正向推理及cost的所有信息,只需将train_program传递给train_func参数就好。optimizer如同一辙。place的含义是整个训练程序在哪个设备上运行,不用多说,计算机中进行大规模计算的硬件只有CPU和GPU。为了程序设计规范,我们设置一个指定设备的开关:

1
2
use_cuda = False
place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()

这样我们就可以将Trainer实例化部分给写出来了:

1
2
3
4
5
6
use_cuda = False
place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
trainer = fluid.Trainer(
train_func=train_program,
optimizer_func=optimizer_program,
place=place)

六、读到这里是不是想赶紧运行一下trainer.train()赶紧让他先跑起来?且慢,我们发现,我们只定义了数据的容器(杯子),且从没有处理让真正数据读进网络的代码(水)。所以我们还需要写一个数据读取、预处理的模块。因为图像识别的网络都是将图片一批一批训练的,所以显而易见我们需要数据读入和分批这两个操作。PaddlePaddle在daraset包里存放个各种各样公开数据库的API,一句话就可以调用这些数据包,并返回Python reader格式的数据。所以我们的流程是先将数据从API中读出来,然后做乱序处理,之后用batch函数进行分批操作(在这里指定BATCH_SIZE超参数):

1
2
3
4
5
6
7
8
9
10
11
# Each batch will yield 128 images
BATCH_SIZE = 128

# Reader for training
train_reader = paddle.batch(
paddle.reader.shuffle(paddle.dataset.cifar.train10(), buf_size=50000),
batch_size=BATCH_SIZE)

# Reader for testing. A separated data set for testing.
test_reader = paddle.batch(
paddle.dataset.cifar.test10(), batch_size=BATCH_SIZE)

七、是不是觉得有了数据就可以跑起来了?Naive~ 在trainer中,必须指定一个事件处理函数才可以运行。这个函数的作用是观察、调试参数,保存参数模型。这里我们用画图的方式来观察网络中cost参数的变化过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
params_dirname = "image_classification_resnet.inference.model"

from paddle.v2.plot import Ploter

train_title = "Train cost"
test_title = "Test cost"
cost_ploter = Ploter(train_title, test_title)

step = 0
def event_handler_plot(event):
global step
if isinstance(event, fluid.EndStepEvent):
cost_ploter.append(train_title, step, event.metrics[0])
cost_ploter.plot()
step += 1
if isinstance(event, fluid.EndEpochEvent):
avg_cost, accuracy = trainer.test(
reader=test_reader,
feed_order=['pixel', 'label'])
cost_ploter.append(test_title, step, avg_cost)

# save parameters
if params_dirname is not None:
trainer.save_params(params_dirname)

使用paddle.v2.plot可以轻松在ipython notebook中将参数点在图像中画出来,核心代码只有一个

1
2
3
4
5
6
7
八、现在可以执行训练了,启动trainer.train方法需要指定4个必要参数:reader(Python reader格式的数据流)、num_epochs(对数据集训练轮次的超参数)、event_handler(事件处理函数)、feed_order(存放训练数据和标签的容器)。所以代码如下:
```Python
trainer.train(
reader=train_reader,
num_epochs=2,
event_handler=event_handler_plot,
feed_order=['pixel', 'label'])

等待一会就可以看到输出不断变化的cost值了:
横坐标为训练批次数,纵坐标为cost的批次均值

九、在训练一轮数据集之后,模型便保存在我们制定的路径中了。那如何使用这个模型来进行预测呢?PaddlePaddle的预测代码很简单,先实例化一个预测引擎

1
2
3
```Python
inferencer = fluid.Inferencer(infer_func=inference_program, param_path=params_dirname, place=place)
results = inferencer.infer({'pixel': img})

我们从results中取到的是由每一分类的似然值构成的List。ifar.train10是10分类数据,所以我们将这个分类的名称用人类的语言来描述一下:

1
label_list = ["airplane", "automobile", "bird", "cat", "deer", "dog", "frog", "horse", "ship", "truck"]

然后result这个List中用np.argmax取最大概率的位置值,将其对应的label输出就得到了我们想要的分类了:

1
print("infer results: %s" % label_list[np.argmax(results[0])])

你以为大功告成了吗?仔细看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
```Python
# Prepare testing data.
from PIL import Image
import numpy as np
import os

def load_image(file):
im = Image.open(file)
im = im.resize((32, 32), Image.ANTIALIAS)

im = np.array(im).astype(np.float32)#浮点精度转换
im = im.transpose((2, 0, 1)) # 转为CHW顺序
im = im / 255.0 #归一化在[-1,1]的区间内

# Add one dimension to mimic the list format.
im = numpy.expand_dims(im, axis=0)
return im

cur_dir = os.getcwd()#拼接为绝对地址
img = load_image( './image/dog.png')#要预测图像的地址

所以我们的整段预测代码为:

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
# Prepare testing data.
from PIL import Image
import numpy as np
import os

def load_image(file):
im = Image.open(file)
im = im.resize((32, 32), Image.ANTIALIAS)

im = np.array(im).astype(np.float32)
# The storage order of the loaded image is W(width),
# H(height), C(channel). PaddlePaddle requires
# the CHW order, so transpose them.
im = im.transpose((2, 0, 1)) # CHW
im = im / 255.0 #-1 - 1

# Add one dimension to mimic the list format.
im = numpy.expand_dims(im, axis=0)
return im

cur_dir = os.getcwd()
img = load_image(cur_dir + '/03.image_classification/image/dog.png')
#img = load_image( './image/dog.png')

inferencer = fluid.Inferencer(infer_func=inference_program, param_path=params_dirname, place=place)

label_list = ["airplane", "automobile", "bird", "cat", "deer", "dog", "frog", "horse", "ship", "truck"]
# inference
results = inferencer.infer({'pixel': img})
#print(results[0])
print("infer results: %s" % label_list[np.argmax(results[0])+1])

十、附整个过程的完整代码:

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
import paddle
import paddle.fluid as fluid
import numpy
import sys
from __future__ import print_function

def vgg_bn_drop(input):
def conv_block(ipt, num_filter, groups, dropouts):
return fluid.nets.img_conv_group(
input=ipt,
pool_size=2,
pool_stride=2,
conv_num_filter=[num_filter] * groups,
conv_filter_size=3,
conv_act='relu',
conv_with_batchnorm=True,
conv_batchnorm_drop_rate=dropouts,
pool_type='max')

conv1 = conv_block(input, 64, 2, [0.3, 0])
conv2 = conv_block(conv1, 128, 2, [0.4, 0])
conv3 = conv_block(conv2, 256, 3, [0.4, 0.4, 0])
conv4 = conv_block(conv3, 512, 3, [0.4, 0.4, 0])
conv5 = conv_block(conv4, 512, 3, [0.4, 0.4, 0])

drop = fluid.layers.dropout(x=conv5, dropout_prob=0.5)
fc1 = fluid.layers.fc(input=drop, size=512, act=None)
bn = fluid.layers.batch_norm(input=fc1, act='relu')
drop2 = fluid.layers.dropout(x=bn, dropout_prob=0.5)
fc2 = fluid.layers.fc(input=drop2, size=512, act=None)
predict = fluid.layers.fc(input=fc2, size=10, act='softmax')
return predict

def inference_program():
# The image is 32 * 32 with RGB representation.
data_shape = [3, 32, 32]
images = fluid.layers.data(name='pixel', shape=data_shape, dtype='float32')

#predict = resnet_cifar10(images, 32)
predict = vgg_bn_drop(images) # un-comment to use vgg net
return predict

def train_program():
predict = inference_program()

label = fluid.layers.data(name='label', shape=[1], dtype='int64')
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]

def optimizer_program():
return fluid.optimizer.Adam(learning_rate=0.001)

use_cuda = False
place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
trainer = fluid.Trainer(
train_func=train_program,
optimizer_func=optimizer_program,
place=place)

# Each batch will yield 128 images
BATCH_SIZE = 128

# Reader for training
train_reader = paddle.batch(
paddle.reader.shuffle(paddle.dataset.cifar.train10(), buf_size=50000),
batch_size=BATCH_SIZE)

# Reader for testing. A separated data set for testing.
test_reader = paddle.batch(
paddle.dataset.cifar.test10(), batch_size=BATCH_SIZE)


params_dirname = "image_classification_resnet.inference.model"

from paddle.v2.plot import Ploter

train_title = "Train cost"
test_title = "Test cost"
cost_ploter = Ploter(train_title, test_title)

step = 0
def event_handler_plot(event):
global step
if isinstance(event, fluid.EndStepEvent):
cost_ploter.append(train_title, step, event.metrics[0])
cost_ploter.plot()
step += 1
if isinstance(event, fluid.EndEpochEvent):
avg_cost, accuracy = trainer.test(
reader=test_reader,
feed_order=['pixel', 'label'])
cost_ploter.append(test_title, step, avg_cost)

# save parameters
if params_dirname is not None:
trainer.save_params(params_dirname)

trainer.train(
reader=train_reader,
num_epochs=2,
event_handler=event_handler_plot,
feed_order=['pixel', 'label'])

# Prepare testing data.
from PIL import Image
import numpy as np
import os

def load_image(file):
im = Image.open(file)
im = im.resize((32, 32), Image.ANTIALIAS)

im = np.array(im).astype(np.float32)
# The storage order of the loaded image is W(width),
# H(height), C(channel). PaddlePaddle requires
# the CHW order, so transpose them.
im = im.transpose((2, 0, 1)) # CHW
im = im / 255.0 #-1 - 1

# Add one dimension to mimic the list format.
im = numpy.expand_dims(im, axis=0)
return im

cur_dir = os.getcwd()
img = load_image(cur_dir + '/03.image_classification/image/dog.png')
#img = load_image( './image/dog.png')

inferencer = fluid.Inferencer(infer_func=inference_program, param_path=params_dirname, place=place)

label_list = ["airplane", "automobile", "bird", "cat", "deer", "dog", "frog", "horse", "ship", "truck"]
# inference
results = inferencer.infer({'pixel': img})
#print(results[0])
print("infer results: %s" % label_list[np.argmax(results[0])+1])