马云大神开推——最擅长抓危机中的机遇,世界总是他擅长的舞台
Fluid程序代码可以分为编译时代码与运行时代码。
编译时的代码可以理解为:用户在编写一段PaddlePaddle程序时,描述模型前向计算的代码。它包括以下几个方面的内容。
- 创建变量与描述变量。
- 创建算子与描述算子。
- 创建算子的属性。
- 推断变量的类型和形状,进行静态检查。
- 规划变量的内存复用。
- 定义反向计算。
- 添加与优化相关的算子。
- 添加多机多卡相关的算子,生成在多机多卡上运行的程序。
要定义反向计算,代码如下。
x = fluid.layers.data(name=\'x\',shape=, dtype=\'float32\')
y_predict = fluid.layers.fc(input=x, size=1, act=None)
y = fluid.layers.data(name=\'y\', shape=, dtype=\'float32\')
cost = fluid.layers.square_error_cost(input=y_predict, label=y)
avg_cost = fluid.layers.mean(x=cost)要添加优化算法,代码如下。
learning_rate = 0.01
sgd_optimizer = fluid.optimizer.SGD(learning_rate)
sgd_optimizer.minimize(avg_cost)编译时的代码可以理解为计算并控制运行时计算的代码,它包括以下几个方面的内容:
- 创建框架执行器(executor)。
- 为将要执行的计算过程创建变量操作域。
- 创建块,并依次执行块。
要读入数据,代码如下。
train_reader = paddle.batch(
paddle.reader.shuffle(paddle.dataset.uci_housing.train(), buf_size=500),
batch_size=20)
feeder = fluid.DataFeeder(place=place, feed_list=)要定义执行程序的设备,代码如下。
place = fluid.CPUPlace()
feeder = fluid.DataFeeder(place=place,feed_list=)要创建执行器,并执行default_startup_program,代码如下。
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())要执行训练程序default_main_program,代码如下。
PASS_NUM = 100
for pass_id in range(PASS_NUM):
fluid.io.save_persistables(exe, \"./fit_a_line.model/\")
fluid.io.load_persistables(exe, \"./fit_a_line.model/\")
for data in train_reader():
avg_loss_value, = exe.run(fluid.default_main_program(),
feed=feeder.feed(data),
fetch_list=)
print(avg_loss_value)1.2 Fluid内部执行流程
Fluid使用一种编译器式的执行流程,分为编译时和运行时两个部分,具体包括编译器定义Program、创建框架执行器和运行Program。
本地训练任务执行流程如图1-1所示。
图1-1
(1)在编译时,用户编写一段Python程序,通过调用Fluid提供的算子,向一段Program中添加变量(在Fluid中用户输入的变量通常以张量表示)以及对变量的操作(算子或者层)。用户只需要描述核心的前向计算,不需要关心反向计算以及分布式环境下和异构设备下是如何计算的。
(2)原始的Program在平台内部转换为中间描述语言ProgramDesc。
(3)在编译期间重要的一个功能模块是编译转换器。编译转换器接受一段 ProgramDesc,输出一段变化后的 ProgramDesc,作为后端框架执行器最终需要执行的是Fluid Program。
(4)后端框架执行器接受编译转换器输出的这段Program,依次执行其中的算子(可以类比为程序语言中的指令),在执行过程中会为算子创建所需的输入/输出并进行管理。
1.3 Program设计简介
PaddlePaddle使用了一个Program的概念,那么什么是Program?
Program本质是对多个数据(通常以变量表示)和操作逻辑的描述。一个深度学习任务中的训练或者预测都可以被描述为一段Program。
多个变量和一系列的算子不仅可以组合为Program,还可以组合成Block,而多个Block可以相互嵌套、并列,最终组合为Program。
用户完成网络定义后,1个Fluid程序中通常存在两段Program。
- fluid.default_startup_program:定义了创建模型参数、输入/输出,以及模型中可学习参数的初始化等各种操作。default_startup_program可以由框架自动生成,使用时无须手动创建。如果调用修改了参数的默认初始化方式,框架会自动将相关的修改加入default_startup_program。
- fluid.default_main_program:也由框架自动生成,负责存储定义的神经网络模型、前向/反向计算,以及优化算法对网络中可学习参数的更新。如果在指定程序操作时未指定在哪个Program中操作,Fluid默认自动在fluid.default_main_program操作。
使用Fluid的核心就是构建default_main_program。
可通过图1-2所示的代码,输出ProgramDesc中的内容。
图1-2
上面框出的两行代码的输出内容如图1-3所示。
图1-3
1.4 Block简介
Block的概念与通用程序一致,例如,下面这段C++代码包含3个Block。
int main(){ //Block 0
int i = 0;
if (i<10){ //Block 1
for (int j=0;j<10;j++){ //Block 2
}
}
return 0;
}类似地,下列Fluid的Program包含3个Block。
import paddle.fluid as fluid #Block 0
limit = fluid.layers.fill_constant_batch_size_like(
input=label, dtype=\'int64\', shape=, value=5.0)
cond = fluid.layers.less_than(x=label, y=limit)
ie = fluid.layers.IfElse(cond)
with ie.true_block(): #Block 1
true_image = ie.input(image)
hidden = fluid.layers.fc(input=true_image, size=100, act=\'tanh\')
prob = fluid.layers.fc(input=hidden, size=10, act=\'softmax\')
ie.output(prob)
with ie.false_block(): #Block 2
false_image = ie.input(image)
hidden = fluid.layers.fc(
input=false_image, size=200, act=\'tanh\')
prob = fluid.layers.fc(input=hidden, size=10, act=\'softmax\')
ie.output(prob)
prob = ie()1.5 Block和Program的设计细节
用户描述的Block与Program信息在Fluid中以Protobuf 格式保存,所有的Protobuf信息定义在framework.proto中,在Fluid中称为BlockDesc和ProgramDesc。ProgramDesc和BlockDesc的概念类似于一个抽象语法树。
BlockDesc中包含一系列本地变量(在代码中用vars表示)的定义和一系列的算子(在代码中用ops表示)。
message BlockDesc {
required int32 parent = 1;
repeated VarDesc vars = 2;
repeated OpDesc ops = 3;
}parent表示父级Block,因此Block中的操作符既可以引用本地定义的变量,也可以引用祖先块中定义的变量。
Program中的每个Block都被压平并存储在数组中。Block ID是这个数组中Block的索引。
message ProgramDesc {
repeated BlockDesc blocks = 1;
}在1.4节介绍的例子中,IfElse这个算子包含了两个Block——true分支和false分支。
下述OpDesc的定义过程描述了一个算子可以包含哪些属性。
message OpDesc {
AttrDesc attrs = 1;
...
}属性可以是Block的类型,实际上就是上面描述的Block ID。
message AttrDesc {
required string name = 1;
enum AttrType {
INT = 1,
STRING = 2,
...
BLOCK = ...
}
required AttrType type = 2;
optional int32 block = 10; // 当type == BLOCK时
...
}1.6 框架执行器设计思想
在编译时,用户首先在Python前端对网络进行表述,这个表述按照Program、Block的层级关系进行定义。转换器在接收到用户的表述后,将Program编译成ProgramDesc以备运行时使用。在运行时,框架执行器将接受一个ProgramDesc、一个block_id和一个变量操作域。执行以下操作。
(1)框架执行器为每一个Block创建一个变量操作域,Block是可嵌套的,因此变量操作域也是可嵌套的。
(2)创建变量操作域中的所有变量。
(3)按顺序创建并执行所有算子。
编译、执行的具体过程如图1-4表示。
图1-4
1.6.1 代码示例
框架执行器的C++实现代码如下。
class Executor{
public:
void Run(const ProgramDesc& pdesc,
Scope* scope,
int block_id) {
auto& block = pdesc.Block(block_id);
//创建所有变量
for (auto& var : block.AllVars())
scope->Var(Var->Name());
}
//创建算子并按顺序执行
for (auto& op_desc : block.AllOps()){
auto op = CreateOp(*op_desc);
op->Run(*local_scope, place_);
}
}
};1.6.2 创建框架执行器
Fluid中使用fluid.Executor(place)创建框架执行器,place属性由用户定义,代表程序将在哪里执行。
下例代码表示创建一个框架执行器,它在CPU内运行。
cpu=fluid.CPUPlace()
exe = fluid.Executor(cpu)1.6.3 运行框架执行器
Fluid使用Executor.run来运行程序。定义中通过feed映射获取数据,通过fetch_list获取结果。
...
x = numpy.random.random(size=(10, 1)).astype(\'float32\')
outs = exe.run(
feed={\'X\': x},
fetch_list=)1.7 示例
线性回归是最简单的模型之一,可以把它作为一个优化问题来研究。该问题可通过最小化均方误差求解。
本节通过简单的线性回归例子,介绍上述内容如何在代码中实现。
1.7.1 定义Program
用户可以随意定义自己的数据和网络结构,定义的结果都将作为一段Program被Fluid接收,Program的基本结构是Block,本节的Program仅包含Block 0。
#加载函数库
import paddle.fluid as fluid #Block0
import numpy
#定义数据
train_data=numpy.array([,,,]).astype(\'float32\')
y_true = numpy.array([,,,]).astype(\'float32\')
#定义网络
x = fluid.layers.data(name=\"x\",shape=,dtype=\'float32\')
y = fluid.layers.data(name=\"y\",shape=,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.01)
sgd_optimizer.minimize(avg_cost)完成上述定义,也就是完成了fluid.default_main_program的构建过程,fluid.default_main_program负责神经网络模型、前向/反向计算,以及优化算法对网络中可学习参数的更新。
此时可以输出这段 Program,观察定义好的网络形态。
print(fluid.default_main_program().to_string(True))完整的ProgramDesc可以在本地查看,这里仅节选前3个变量。
blocks {
idx: 0
parent_idx: -1
vars {
name: \"mean_1.tmp_0\"
type {
type: LOD_TENSOR
lod_tensor {
tensor {
data_type: FP32
dims: 1
}
}
}
persistable: false
}
vars {
name: \"square_error_cost_1.tmp_1\"
type {
type: LOD_TENSOR
lod_tensor {
tensor {
data_type: FP32
dims: -1
dims: 1
}
lod_level: 0
}
}
persistable: false
}
vars {
name: \"square_error_cost_1.tmp_0\"
type {
type: LOD_TENSOR
lod_tensor {
tensor {
data_type: FP32
dims: -1
dims: 1
}
lod_level: 0
}
}
persistable: false
...从输出结果中可以看到,整个定义过程在框架内部转化为了一段ProgramDesc,以Block idx为索引。本次线性回归模型中仅有1个Block,ProgramDesc中也仅有Block 0这一段BlockDesc。
BlockDesc中包含定义的变量和一系列的算子,以输入x为例,在Python代码中定义x是一个数据类型为“float 32”的一维数据。
x = fluid.layers.data(name=\"x\",shape=,dtype=\'float32\')在BlockDesc中,变量x被描述为:
vars {
name: \"x\"
type {
type: LOD_TENSOR
lod_tensor {
tensor {
data_type: FP32
dims: -1
dims: 1
}
lod_level: 0
}
}
persistable: false在Fluid中所有的数据类型都为LoD Tensor,对于不存在序列信息的数据(如此处的变量x),其lod_level=0。
dims表示数据的维度,这里表示x的维度为,其中−1是批的维度,在无法确定具体数值时,Fluid自动用−1表示。
参数persistable表示该变量在整个训练过程中是否为持久化变量。
1.7.2 创建框架执行器
Fluid使用框架执行器来执行网络训练。作为使用者,实际上不必了解内部机制。
创建框架执行器只需要调用 fluid.Executor(place) 即可,在此之前请依据训练场所定义place变量。
#在CPU内执行训练
cpu = fluid.CPUPlace()
#创建框架执行器
exe = fluid.Executor(cpu)1.7.3 运行框架执行器
Fluid使用Executor.run来运行一段Program。
在正式进行网络训练前,要先进行参数初始化。其中 default_startup_program 中定义了创建模型参数、输入/输出,以及模型中可学习参数的初始化等操作。
#参数初始化
exe.run(fluid.default_startup_program())由于传入的数据与传出的数据存在多列,因此 Fluid 通过 feed 映射定义数据的传输数据,通过 fetch_list 取出期望结果:
#开始训练
outs = exe.run(
feed={\'x\':train_data,\'y\':y_true},
fetch_list=)上述代码段中定义了train_data用于传入x变量,y_true用于传入y变量,输出y的预测值和最后一轮样本中cost的均值。
输出结果如下。
[array([,
,
,
], dtype=float32), array(, dtype=float32)]-END-
喜欢的朋友,欢迎转发到朋友圈
关注【异步图书】微信号,回复“VIP”获取异步社区VIP会员卡,近千门电子书、专栏视频课免费学