在前面章节,我们已经熟悉了动态图的编程方法,并通过一个实例对比,初步了解了两者的差异。接下来侧重于介绍静态图的编程方法。 首先回顾一下前面章节给出的对比实例,如下所示:
import paddle.fluid as fluid
# 在动态图模式下调用fill_constant操作
with fluid.dygraph.guard():
data = fluid.layers.fill_constant(shape=[3, 4], value=16, dtype='int64')
# 在上面代码运行后,‘data’中已经存储了一个shape为[3, 4]的Tensor,并且每个元素的值都是16
# 对‘data’进行print操作,可以看到其中具体的数据值
print('In DyGraph mode, after calling layers.fill_constant, data = ', data)
print('-------------------------------------------------')
# 在静态图模式下调用fill_constant操作
data = fluid.layers.fill_constant(shape=[3, 4], value=16, dtype='int64')
# 在上面代码运行后,‘data’中并没有实际数据,
# 仅仅是“记录”了在此处有一个Tensor,其shape应该为[3, 4],但并没有为其赋值,也没有为其分配空间
# 此时对‘data’进行print操作,仅能看到一个对Tensor的结构描述信息
print('In static mode, after calling layers.fill_constant, data = ', data)
# 下面代码用来实际“执行”操作
# 与动态图的细节区别:
# 在动态图模式下,默认会自动选择运行设备(CPU或者GPU),但在静态图模式下,需要用户指定运行设备
# 此处调用fluid.CPUPlace() API来指定在CPU设备上运行程序
place = fluid.CPUPlace()
# 创建“执行器”,并用place参数指明需要在何种设备上运行
exe = fluid.Executor(place=place)
# 初始化操作,包括为所有变量分配空间等,比如上面的‘data’,在下面这行代码执行后才会被分配实际的内存空间
exe.run(fluid.default_startup_program())
# 使用执行器“执行”已经记录的所有操作,在本例中即执行fill_constant操作为data变量赋值
# 在调用执行器的run接口时,可以通过fetch_list参数来指定要获取哪些变量的计算结果,这里我们要获取‘data’的结果
data_after_run = exe.run(fetch_list=[data])
# 此时我们打印执行器返回的结果,可以看到“执行”后,Tensor中的数据已经被赋值,每个元素的值都是16
print('data after run:', data_after_run)
从上面的例子中我们可以看出:
另一方面,对于一些复杂的使用场景(比如模型中需要用到与数据相关的控制流操作,可参见下文组建复杂网络的示例),目前动态图模型还无法很好的支持使用飞桨高性能预测库进行预测部署,这对性能有较高要求的任务来说是不可接受的。在这种场景下,我们使用动态图模型完成模型的调试和训练之后,就需要使用静态图方式来重新实现该模型。这样做能够最大化的利用动态图和静态图的优势:在模型开发调试阶段使用动态图,以获得更好的调试能力和更优的编程体验;在模型开发完成后,利用静态图获得高性能预测引擎的部署能力,以提高在线预测的性能,缩短在线响应时间或节约在线计算资源。
比较典型的如NLP领域应用比较多的Transformer模型,在预测(解码)阶段的Beam Search算法中,需要使用循环操作来进行候选的选取与生成,在动态图实现中使用了Python的for语句来完成该操作,但目前飞桨的高性能预测库还不支持对这类控制流的转化,所以需要使用静态图实现Transformer,再进行预测部署的工作。大家在看完本篇内容后,也可以对比Transformer的动态图实现和静态图实现中的Beam Search部分,作为进阶学习的例子。
接下来开始介绍静态图的编程方法,包括如下几部分:
import paddle.fluid as fluid
# 定义一个数据类型为int64的二维数据变量x,x第一维的维度为3,第二个维度未知,要在程序执行过程中才能确定,
# 因此x的形状可以指定为[3, None]
x = fluid.data(name="x", shape=[3, None], dtype="int64")
print('x =', x)
# 大多数网络都会采用batch方式进行数据组织,batch大小在定义时不确定,
# 因此batch所在维度(通常是第一维)可以指定为None
batched_x = fluid.data(name="batched_x", shape=[None, 3, None], dtype='int64')
print('batched_x =', batched_x)
x = name: "x" type { type: LOD_TENSOR lod_tensor { tensor { data_type: INT64 dims: 3 dims: -1 } lod_level: 0 } } persistable: false need_check_feed: true batched_x = name: "batched_x" type { type: LOD_TENSOR lod_tensor { tensor { data_type: INT64 dims: -1 dims: 3 dims: -1 } lod_level: 0 } } persistable: false need_check_feed: true
import paddle.fluid as fluid
# 定义输入变量,即“占位符”,输入变量的第一维是batch大小,在运行期读入数据时才会确定,因此在这里以None表示
a = fluid.data(name="a", shape=[None, 1], dtype='int64')
b = fluid.data(name="b", shape=[None, 1], dtype='int64')
# 组建网络(此处网络仅由一个操作构成,即elementwise_add)
result = fluid.layers.elementwise_add(a,b)
# 准备运行网络
cpu = fluid.CPUPlace() # 定义运算设备,这里选择在CPU下训练
exe = fluid.Executor(cpu) # 创建执行器
exe.run(fluid.default_startup_program()) # 网络参数初始化
# 读取输入数据
import numpy
data_1 = int(input("Please enter an integer: a="))
data_2 = int(input("Please enter an integer: b="))
print('---------------------------------')
x = numpy.array([[data_1]])
y = numpy.array([[data_2]])
# 运行网络
outs = exe.run(
feed={'a':x, 'b':y}, # 将输入数据x, y分别赋值给变量a,b
fetch_list=[result] # 通过fetch_list参数指定需要获取的变量结果
)
# 输出计算结果
print("%d+%d=%d" % (data_1, data_2, outs[0][0]))
在动态图中,可以方便的使用Python的控制流语句(如for,if-else等)来进行条件判断,但是在静态图中,由于组网阶段并没有实际执行操作,也没有产生中间计算结果,因此无法使用Python的控制流语句来进行条件判断,为此静态图提供了多个控制流OP来实现条件判断。这里以fluid.layers.while_loop为例来说明如何在静态图中实现条件循环的操作。
while_loop API用于实现类似while/for的循环控制功能,使用一个callable的方法cond作为参数来表示循环的条件,只要cond的返回值为True,while_loop就会循环执行循环体body(也是一个callable的方法),直到 cond 的返回值为False。对于while_loop API的详细定义和具体说明请参考文档fluid.layers.while_loop。
下面的例子中,使用while_loop API进行条件循环操作,其实现的功能相当于在python中实现如下代码:
i = 0
ten = 10
while i < ten:
i = i + 1
print('i =', i)
在静态图中使用while_loop API实现以上代码的逻辑:
# 该示例代码展示整数循环+1,循环10次,输出计数结果
import paddle.fluid as fluid
import paddle.fluid.layers as layers
# 定义cond方法,作为while_loop的判断条件
def cond(i, ten):
return i < ten
# 定义body方法,作为while_loop的执行体,只要cond返回值为True,while_loop就会一直调用该方法进行计算
# 由于在使用while_loop OP时,cond和body的参数都是由while_loop的loop_vars参数指定的,所以cond和body必须有相同数量的参数列表,因此body中虽然只需要i这个参数,但是仍然要保持参数列表个数为2,此处添加了一个dummy参数来进行"占位"
def body(i, dummy):
# 计算过程是对输入参数i进行自增操作,即 i = i + 1
i = i + 1
return i, dummy
i = layers.fill_constant(shape=[1], dtype='int64', value=0) # 循环计数器
ten = layers.fill_constant(shape=[1], dtype='int64', value=10) # 循环次数
out, ten = layers.while_loop(cond, body, [i, ten]) # while_loop的返回值是一个tensor列表,其长度,结构,类型与loop_vars相同
exe = fluid.Executor(fluid.CPUPlace())
res = exe.run(fluid.default_main_program(), feed={}, fetch_list=out)
print(res) #[array([10])]
限于篇幅,上面仅仅用一个最简单的例子来说明如何在静态图中实现循环操作,循环操作在很多应用中都有着重要作用,比如NLP中常用的Transformer模型,在解码(生成)阶段的Beam Search算法中,需要使用循环操作来进行候选的选取与生成,可以参考Transformer模型的实现来进一步学习while_loop在复杂场景下的用法。
除while_loop之外,飞桨还提供fluid.layers.cond API来实现条件分支的操作,以及fluid.layers.switch_case和fluid.layers.case API来实现分支控制功能,具体用法请参考文档:cond,switch_case和case
下面以一个经典的任务“波士顿房价预测”为例,来说明如何根据动态图代码来转写静态图代码。我们回顾一下这个任务:基于13种可能影响房价的因素X来预计房价Y,共有500多条数据样本,每个数据样本包含13个X(影响房价的特征)和一个Y(该类型房屋的均价)。
关于“波士顿房价预测”任务以及相关数据集的详细描述,可以参考文档使用飞桨重写【房价预测】模型。
下面是一个使用动态图进行“波士顿房价预测”的例子:
import numpy
import paddle
import paddle.fluid as fluid
# 定义网络结构,该任务中使用线性回归模型,网络由一个FC层构成
class LinearRegression(fluid.dygraph.Layer):
def __init__(self, input_dim, hidden):
super(LinearRegression, self).__init__()
self.linear = fluid.dygraph.Linear(input_dim, hidden)
def forward(self, x):
x = self.linear(x)
return x
# 训练和预测的数据读取处理,这部分的用法动态图和静态图是一致的
batch_size = 20
train_reader = fluid.io.batch(fluid.io.shuffle(paddle.dataset.uci_housing.train(), 500), batch_size)
test_reader = fluid.io.batch(paddle.dataset.uci_housing.test(), batch_size)
# 波士顿房价预测任务中,共有13个特征
input_feature = 13
max_epoch_num = 100
with fluid.dygraph.guard():
# 定义网络
model = LinearRegression(input_dim=input_feature, hidden=1)
#定义优化器
sgd = fluid.optimizer.SGD(learning_rate=0.001, parameter_list=model.parameters() )
# 执行max_epoch_num次训练
for epoch in range(max_epoch_num):
# 读取训练数据进行训练
for batch_id, data in enumerate(train_reader()):
x_data = numpy.array([x[0] for x in data], dtype=numpy.float32)
y_data = numpy.array([x[1] for x in data], dtype=numpy.float32)
# 将numpy数据转换为网络训练所需的Tensor数据
x_var = fluid.dygraph.to_variable(x_data)
y_var = fluid.dygraph.to_variable(y_data)
# 调用网络,执行前向计算
prediction = model(x_var)
# 计算损失值
loss = fluid.layers.square_error_cost(prediction, y_var)
avg_loss = fluid.layers.mean(loss)
if batch_id % 10 == 0 and batch_id is not 0:
print("epoch: {}, batch_id: {}, loss is: {}".format(epoch, batch_id, avg_loss.numpy()))
# 执行反向计算,并调用minimize接口计算和更新梯度
avg_loss.backward()
sgd.minimize(avg_loss)
# 将本次计算的梯度值清零,以便进行下一次迭代和梯度更新
model.clear_gradients()
# 训练结束,保存训练好的模型
fluid.dygraph.save_dygraph(model.state_dict(), 'dygraph_linear')
with fluid.dygraph.guard():
linear_infer = LinearRegression(input_dim=input_feature, hidden=1)
# 加载之前已经训练好的模型准备进行预测
model_dict, _ = fluid.dygraph.load_dygraph("dygraph_linear")
linear_infer.set_dict(model_dict)
print("checkpoint loaded.")
# 开启评估测试模式(区别于训练模式)
linear_infer.eval()
infer_data = next(test_reader())
infer_x_data = numpy.array([x[0] for x in infer_data], dtype=numpy.float32)
infer_y_data = numpy.array([x[1] for x in infer_data], dtype=numpy.float32)
infer_x_var = fluid.dygraph.to_variable(infer_x_data)
infer_y_var = fluid.dygraph.to_variable(infer_y_data)
infer_result = linear_infer(infer_x_var)
print("id: prediction ground_truth")
for idx, val in enumerate(infer_result.numpy()):
print("%d: %.2f %.2f" % (idx, val, infer_y_data[idx]))
对应的静态图实现方式如下:
import paddle
import paddle.fluid as fluid
import numpy
# 定义网络结构,该任务中使用线性回归模型,网络由一个fc层构成
def linear_regression_net(input, hidden):
#区别1:在静态图中要使用静态图对应的OP,与fluid.dygraph.Linear对应的是fluid.layers.fc,其用法如下,更详细的OP使用方法说明可以参考飞桨官网的API文档
out = fluid.layers.fc(input, hidden)
return out
# 训练和预测的数据读取处理与动态图一致
batch_size = 20
train_reader = fluid.io.batch(fluid.io.shuffle(paddle.dataset.uci_housing.train(), 500), batch_size)
test_reader = fluid.io.batch(paddle.dataset.uci_housing.test(), batch_size)
max_epoch_num = 100
# 区别2:在静态图中需要明确定义输入变量,即“占位符”,在静态图组网阶段并没有读入数据,所以需要使用占位符指明输入数据的类型,shape等信息
# 波士顿房价预测任务中,共有13个特征,数据以batch形式组织,batch大小在定义时可以不确定,用None表示,因此shape=[None, 13]
x = fluid.data(name='x', shape=[None, 13], dtype='float32')
y = fluid.data(name='y', shape=[None, 1], dtype='float32')
# 调用网络,执行前向计算
prediction = linear_regression_net(x, 1)
# 计算损失值
loss = fluid.layers.square_error_cost(input=prediction, label=y)
avg_loss = fluid.layers.mean(loss)
# 定义优化器,并调用minimize接口计算和更新梯度
sgd_optimizer = fluid.optimizer.SGD(learning_rate=0.001)
sgd_optimizer.minimize(avg_loss)
# 区别3:在静态图中需要用户显示指定运行设备,此处指定运行设备为CPU
place = fluid.CPUPlace()
# 区别4:静态图中需要使用执行器执行之前已经定义好的网络
exe = fluid.Executor(place)
num_epochs = 100
# 区别5:静态图中数据要经过DataFeeder转换才能在执行器中使用
# 也可以使用其他数据读取API如DataLoader,具体用法请参考DataLoader API的文档
feeder = fluid.DataFeeder(place=place, feed_list=[x, y])
# 区别6:静态图中需要显示对网络进行初始化操作
exe.run(fluid.default_startup_program())
for epoch in range(num_epochs):
for batch_id, data_train in enumerate(train_reader()):
# 区别7:静态图中需要调用执行器的run方法执行计算过程,需要获取的计算结果(如avg_loss)需要通过fetch_list指定
avg_loss_value, = exe.run(feed=feeder.feed(data_train), fetch_list=[avg_loss])
if batch_id % 10 == 0 and batch_id is not 0:
print("epoch: {}, batch_id: {}, loss is: {}".format(epoch, batch_id, avg_loss_value))
# 区别8:静态图中需要使用save_inference_model来保存模型,以供预测使用
fluid.io.save_inference_model('static_linear', ['x'], [prediction], exe)
infer_exe = fluid.Executor(place)
inference_scope = fluid.core.Scope()
# 使用训练好的模型做预测
with fluid.scope_guard(inference_scope):
# 区别9:静态图中需要使用load_inference_model来加载之前保存的模型
[inference_program, feed_target_names, fetch_targets
] = fluid.io.load_inference_model('static_linear', infer_exe)
infer_data = next(test_reader())
infer_x = numpy.array([data[0] for data in infer_data]).astype("float32")
infer_y = numpy.array([data[1] for data in infer_data]).astype("float32")
# 区别10:静态图中预测时也需要调用执行器的run方法执行计算过程,并指定之前加载的inference_program
results = infer_exe.run(
inference_program,
feed={feed_target_names[0]: numpy.array(infer_x)},
fetch_list=fetch_targets)
print("id: prediction ground_truth")
for idx, val in enumerate(results[0]):
print("%d: %.2f %.2f" % (idx, val, infer_y[idx]))
总结一下,动态图改写成静态图的时候,主要有以下几点区别: