赞
踩
本文介绍百度的深度学习框架PaddlePaddle的基本概念和用法。本文主要参考了官方文档
这部分通过一个线性回归的问题来感受一下PaddlePaddle的使用,读者不需要理解每一行代码(但是有必要尝试阅读和尽可能多的理解),后面的部分会详细的介绍。读者需要动手把环境搭建起来,把程序跑起来,这是最重要的第一步。
我使用的是GPU的版本,所以使用如下的命令安装:
pip install -U paddlepaddle-gpu
为了避免冲突,建议使用virtualenv。当前最新的(1.5)PaddlePaddle需要CUDA 9 ,cuDNN 7.3 ,NCCL2 等依赖,如果出现安装问题请参考安装说明。如果读者没有GPU的机器或者安装GPU的版本遇到问题,可以先安装CPU的版本跑起来,CPU版本的安装通常更简单,等了解了PaddlePaddle之后可能更容易解决安装的问题。(注:其实学习一个新的东西最大的障碍是把Hello World跑起来,很多时候放弃的原因就是第一步搞不定)
CPU版本的安装可以使用:
pip install -U paddlepaddle
我们首先需要导入paddle:
import paddle.fluid as fluid
下面是使用paddle操作tensor几个例子,请读者阅读其中的代码注释,尽量猜测它们的含义。
# 定义数组维度及数据类型,可以修改shape参数定义任意大小的数组
data = fluid.layers.ones(shape=[5], dtype='int64')
# 在CPU上执行运算
place = fluid.CPUPlace()
# 创建执行器
exe = fluid.Executor(place)
# 执行计算
ones_result = exe.run(fluid.default_main_program(),
# 获取数据data
fetch_list=[data],
return_numpy=True)
# 输出结果
print(ones_result[0])
上面代码的结果为:
[1 1 1 1 1]
和Tensorflow等框架类似,paddle也需要定义tensor,比如fluid.layers.ones会创建一个全是1的tensor。同样如果需要查看它的值,我们也需要"运行"它。paddle里需要用执行器fluid.Executor来执行各种操作,另外定义Executor时需要指定在哪个设备上运行,这里用CPUPlace()来让代码则CPU上运行。和Tensorflow不同,paddle没有session,但是有程序(Program)的概念,我们定义的操作(比如定义的data)是默认添加到默认程序(fluid.default_main_program())。执行器执行时要指定运行哪个程序,Executor.run需要传入程序、feed参数(和Tensorflow的run类似,不过这里不需要feed)、输出参数fetch_list。return_numpy告诉Executor返回numpy数组而不是paddle的tensor,这便于我们打印结果(Tensorflow的session.run返回的就是numpy数组)。
接着上面的例子,我们把data和它自己加起来:
# 调用 elementwise_op 将生成的一维数组按位相加
add = fluid.layers.elementwise_add(data,data)
# 定义运算场所
place = fluid.CPUPlace()
exe = fluid.Executor(place)
# 执行计算
add_result = exe.run(fluid.default_main_program(),
fetch_list=[add],
return_numpy=True)
# 输出结果
print (add_result[0])
结果为:
[2 2 2 2 2]
接着我们把int64的类型转换成float64:
# 将一维整型数组,转换成float64类型
cast = fluid.layers.cast(x=data, dtype='float64')
# 定义运算场所执行计算
place = fluid.CPUPlace()
exe = fluid.Executor(place)
cast_result = exe.run(fluid.default_main_program(),
fetch_list=[cast],
return_numpy=True)
# 输出结果
print(cast_result[0])
结果为:
[1. 1. 1. 1. 1.]
这是一个简单的线性回归模型,来帮助我们快速求解4元一次方程。
#加载库 import paddle.fluid as fluid import numpy as np #生成数据 np.random.seed(0) outputs = np.random.randint(5, size=(10, 4)) res = [] for i in range(10): # 假设方程式为 y=4a+6b+7c+2d y = 4*outputs[i][0]+6*outputs[i][1]+7*outputs[i][2]+2*outputs[i][3] res.append([y]) # 定义数据 train_data=np.array(outputs).astype('float32') y_true = np.array(res).astype('float32') #定义网络 x = fluid.layers.data(name="x",shape=[4],dtype='float32') y = fluid.layers.data(name="y",shape=[1],dtype='float32') y_predict = fluid.layers.fc(input=x,size=1,act=None) #定义损失函数 cost = fluid.layers.square_error_cost(input=y_predict,label=y) avg_cost = fluid.layers.mean(cost) #定义优化方法 sgd_optimizer = fluid.optimizer.SGD(learning_rate=0.05) sgd_optimizer.minimize(avg_cost) #参数初始化 cpu = fluid.CPUPlace() exe = fluid.Executor(cpu) exe.run(fluid.default_startup_program()) ##开始训练,迭代500次 for i in range(500): outs = exe.run( feed={'x':train_data,'y':y_true}, fetch_list=[y_predict.name,avg_cost.name]) if i%50==0: print ('iter={:.0f},cost={}'.format(i,outs[1][0])) #存储训练结果 params_dirname = "result" fluid.io.save_inference_model(params_dirname, ['x'], [y_predict], exe) # 开始预测 infer_exe = fluid.Executor(cpu) inference_scope = fluid.Scope() # 加载训练好的模型 with fluid.scope_guard(inference_scope): [inference_program, feed_target_names, fetch_targets] = fluid.io.load_inference_model(params_dirname, infer_exe) # 生成测试数据 test = np.array([[[9],[5],[2],[10]]]).astype('float32') # 进行预测 results = infer_exe.run(inference_program, feed={"x": test}, fetch_list=fetch_targets) # 给出题目为 【9,5,2,10】 输出y=4*9+6*5+7*2+10*2的值 print ("9a+5b+2c+10d={}".format(results[0][0]))
上面的代码就是简单的线性回归,和Tensorflow的有很多类似的地方。不同之处为:
不需要运行时指定操作
在Tensorflow里,我们通过session.run告诉引擎计算哪些操作,但是在paddlepaddle里,我们只需要告诉它执行哪个程序,我们默认的操作都是按照顺序添加到程序里的,它会自动执行最后的操作(sgd_optimizer.minimize(avg_cost))。这个操作会首先进行前向的计算,然后反向计算梯度,最后更新参数。
save_inference_model自动裁剪
训练介绍后我们需要用save_inference_model函数保存模型,我们只需要告诉它输入和预测的输出,它会自动裁剪计算图,只保留从输入到输出的子图。
fluid.scope_guard
为了避免把预测的模型也加载到默认程序里,我们用fluid.scope_guard构造一个新的Scope,然后在这里加载保存的模型来预测。如果不构造新的Scope,虽然代码依然可以运行,但是训练的模型和预测的模型混在一起很容易混淆。
大部分深度学习框架都有Tensor的概念,但是PaddlePaddle除了普通的Tensor之外还有一个特殊的LoD-Tensor,它的作用是解决变长序列。在其它的框架里我们通常使用Padding把变长的序列变成定长的序列来解决这个问题,这会导致我们的代码需要有特殊的逻辑来处理padding。而PaddlePaddle则通过Lod-Tensor来系统解决变成的问题。
大多数的深度学习框架使用Tensor表示一个mini-batch。
例如一个mini-batch中有10张图片,每幅图片大小为32x32,则这个mini-batch是一个10x32x32的 Tensor。
或者在处理NLP任务中,一个mini-batch包含N个句子,每个字都用一个D维的one-hot向量表示,假设所有句子都用相同的长度L,那这个mini-batch可以被表示为NxLxD的Tensor。
上述两个例子中序列元素都具有相同大小,但是在许多情况下,训练数据是变长序列。基于这一场景,大部分框架采取的方法是确定一个固定长度,对小于这一长度的序列数据以0填充。
在Fluid中,由于LoD-Tensor的存在,我们不要求每个mini-batch中的序列数据必须保持长度一致,因此您不需要执行填充操作,也可以满足处理NLP等具有序列要求的任务需求。
Fluid引入了一个索引数据结构(LoD)来将张量分割成序列。
为了更好的理解LoD的概念,本节提供了几个例子供您参考:
假设一个mini-batch中有3个句子,每个句子中分别包含3个、1个和2个单词。我们可以用(3+1+2)xD维Tensor 加上一些索引信息来表示这个mini-batch:
3 1 2
| | | | | |
上述表示中,每一个 | 代表一个D维的词向量,数字3,1,2构成了 1-level LoD。
让我们来看另一个2-level LoD-Tensor的例子:假设存在一个mini-batch中包含3个句子、1个句子和2个句子的文章,每个句子都由不同数量的单词组成,则这个mini-batch的样式可以看作:
3 1 2
3 2 4 1 2 3
||| || |||| | || |||
表示的LoD信息为:
[[3,1,2]/level=0/,[3,2,4,1,2,3]/level=1/]
在视觉任务中,时常需要处理视频和图像这些元素是高维的对象,假设现存的一个mini-batch包含3个视频,分别有3个,1个和2个帧,每个帧都具有相同大小:640x480,则这个mini-batch可以被表示为:
3 1 2
口口口 口 口口
最底层tensor大小为(3+1+2)x640x480,每一个 口 表示一个640x480的图像
在传统的情况下,比如有N个固定大小的图像的mini-batch,LoD-Tensor表示为:
1 1 1 1 1
口口口口 ... 口
在这种情况下,我们不会因为索引值都为1而忽略信息,仅仅把LoD-Tensor看作是一个普通的张量:
口口口口 ... 口
模型参数只是一个普通的张量,在Fluid中它们被表示为一个0-level LoD-Tensor。
为了快速访问基本序列,Fluid提供了一种偏移表示的方法——保存序列的开始和结束元素,而不是保存长度。
在上述例子中,您可以计算基本元素的长度:
3 2 4 1 2 3
将其转换为偏移表示:
0 3 5 9 10 12 15
= = = = = =
3 2+3 4+5 1+9 2+10 3+12
所以我们知道第一个句子是从单词0到单词3,第二个句子是从单词3到单词5。
类似的,LoD的顶层长度
3 1 2
可以被转化成偏移形式:
0 3 4 6
= = =
3 3+1 4+2
因此该LoD-Tensor的偏移表示为:
0 3 4 6
3 5 9 10 12 15
一个LoD-Tensor可以被看作是一个树的结构,树叶是基本的序列元素,树枝作为基本元素的标识。
在 Fluid 中 LoD-Tensor 的序列信息有两种表述形式:原始长度和偏移量。在 Paddle 内部采用偏移量的形式表述 LoD-Tensor,以获得更快的序列访问速度;在 python API中采用原始长度的形式表述 LoD-Tensor 方便用户理解和计算,并将原始长度称为: recursive_sequence_lengths 。
以上文提到的一个2-level LoD-Tensor为例:
3 1 2
3 2 4 1 2 3
||| || |||| | || |||
以偏移量表示此 LoD-Tensor:[ [0,3,4,6] , [0,3,5,9,10,12,15] ]。
以原始长度表达此 Lod-Tensor:recursive_sequence_lengths=[ [3-0 , 4-3 , 6-4] , [3-0 , 5-3 , 9-5 , 10-9 , 12-10 , 15-12] ]。
以文字序列为例: [3,1,2] 可以表示这个mini-batch中有3篇文章,每篇文章分别有3、1、2个句子,[3,2,4,1,2,3] 表示每个句子中分别含有3、2、4、1、2、3个字。
recursive_seq_lens 是一个双层嵌套列表,也就是列表的列表,最外层列表的size表示嵌套的层数,也就是lod-level的大小;内部的每个列表,对应表示每个lod-level下,每个元素的大小。
下面三段代码分别介绍如何创建一个LoD-Tensor,如何将LoD-Tensor转换成Tensor,如何将Tensor转换成LoD-Tensor:
#创建lod-tensor import paddle.fluid as fluid import numpy as np a = fluid.create_lod_tensor(np.array([[1],[1],[1], [1],[1], [1],[1],[1],[1], [1], [1],[1], [1],[1],[1]]).astype('int64') , [[3,1,2] , [3,2,4,1,2,3]], fluid.CPUPlace()) #查看lod-tensor嵌套层数 print (len(a.recursive_sequence_lengths())) # output:2 #查看最基础元素个数 print (sum(a.recursive_sequence_lengths()[-1])) # output:15 (3+2+4+1+2+3=15)
import paddle.fluid as fluid import numpy as np # 创建一个 LoD-Tensor a = fluid.create_lod_tensor(np.array([[1.1], [2.2],[3.3],[4.4]]).astype('float32'), [[1,3]], fluid.CPUPlace()) def LodTensor_to_Tensor(lod_tensor): # 获取 LoD-Tensor 的 lod 信息 lod = lod_tensor.lod() # 转换成 array array = np.array(lod_tensor) new_array = [] # 依照原LoD-Tensor的层级信息,转换成Tensor for i in range(len(lod[0]) - 1): new_array.append(array[lod[0][i]:lod[0][i + 1]]) return new_array new_array = LodTensor_to_Tensor(a) # 输出结果 print(new_array)
import paddle.fluid as fluid import numpy as np def to_lodtensor(data, place): # 存储Tensor的长度作为LoD信息 seq_lens = [len(seq) for seq in data] cur_len = 0 lod = [cur_len] for l in seq_lens: cur_len += l lod.append(cur_len) # 对待转换的 Tensor 降维 flattened_data = np.concatenate(data, axis=0).astype("int64") flattened_data = flattened_data.reshape([len(flattened_data), 1]) # 为 Tensor 数据添加lod信息 res = fluid.LoDTensor() res.set(flattened_data, place) res.set_lod([lod]) return res # new_array 为上段代码中转换的Tensor lod_tensor = to_lodtensor(new_array,fluid.CPUPlace()) # 输出 LoD 信息 print("The LoD of the result: {}.".format(lod_tensor.lod())) # 检验与原Tensor数据是否一致 print("The array : {}.".format(np.array(lod_tensor)))
LoD会让序列的处理变得简单,读者可以在下面的情感分析示例里体会到这带来的好处。如果现在还不是特别明白LoD的用处,可以在阅读后面的情感分析的代码时回过头来参考本节内容。
使用PaddlePaddle Fluid准备数据分为三个步骤:
生成的数据类型可以为Numpy Array或LoDTensor。根据Reader返回的数据形式的不同,可分为Batch级的Reader和Sample(样本)级的Reader。
Batch级的Reader每次返回一个Batch的数据,Sample级的Reader每次返回单个样本的数据
如果您的数据是Sample级的数据,我们提供了一个可以数据预处理和组建batch的工具:Python Reader 。
用户需使用 fluid.layers.data 在网络中定义数据层变量。定义数据层变量时需指明数据层的名称name、数据类型dtype和维度shape。例如:
import paddle.fluid as fluid
image = fluid.layers.data(name='image', dtype='float32', shape=[28, 28])
label = fluid.layers.data(name='label', dtype='int64', shape=[1])
需要注意的是,此处的shape是单个样本的维度,PaddlePaddle Fluid会在shape第0维位置添加-1,表示batch_size的维度,即此例中image.shape为[-1, 28, 28], label.shape为[-1, 1]。
若用户不希望框架在第0维位置添加-1,则可通过append_batch_size=False参数控制,即:
import paddle.fluid as fluid
image = fluid.layers.data(name='image', dtype='float32', shape=[28, 28], append_batch_size=False)
label = fluid.layers.data(name='label', dtype='int64', shape=[1], append_batch_size=False)
此时,image.shape为[28, 28],label.shape为[1]。
Fluid提供两种方式,分别是异步PyReader接口方式或同步Feed方式,具体介绍如下:
异步PyReader接口方式
用户需要先使用 fluid.io.PyReader 定义PyReader对象,然后通过PyReader对象的decorate方法设置数据源。 使用PyReader接口时,数据传入与模型训练/预测过程是异步进行的,效率较高,推荐使用。
同步Feed方式
用户自行构造输入数据,并在 fluid.Executor 或 fluid.ParallelExecutor 中使用 executor.run(feed=…) 传入训练数据。数据准备和模型训练/预测的过程是同步进行的, 效率较低。
这两种准备数据方法的比较如下:
对比项 | 同步Feed方式 | 异步PyReader接口方式 |
---|---|---|
API接口 | executor.run(feed=…) | fluid.io.PyReader |
数据格式 | Numpy Array或LoDTensor | Numpy Array或LoDTensor |
数据增强 | Python端使用其他库完成 | Python端使用其他库完成 |
速度 | 慢 | 快 |
推荐用途 | 调试模型 | 工业训练 |
在模型训练和预测阶段,PaddlePaddle程序需要读取训练或预测数据。为了帮助您编写数据读取的代码,我们提供了如下接口:
此外,还提供了将reader转换为batch reader的函数,会频繁用到reader creator和reader decorator。
Data read
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。