转载自AI Studio某网友
前言
paddle 静态图的开发结构可以看成“八股文”。一般进来,按照简单例子就直接组网训练。示例代码中会看到如下语句:
fluid.default_main_program()
with fluid.program_guard(main_prog, startup_prog)
with fluid.unique_name.guard()
如果完全照着来,能跑通。但是想要实现自己的东西来点变化,不理解这块儿可能容易碰到问题。不同版本 paddle 的具体实现会有区别,但大致相似,此处以 paddle 1.6 为例子讲解这部分知识点,总的一句话:
program 只存储网络结构(比如参数的描述,算子的描述),但不存储实际参数的值,真正的参数保存在 scope 中,并且按照参数名字去重。
scope
scope:作用域。为了解释清楚 program,先介绍一个新概念:scope。
scope 在 paddle 里可以看作变量空间,存储fluid创建的变量。
在 Paddle/blob/develop/paddle/fluid/framework/scope.h 中可以看到存储的数据结构的定义:
mutable std::unordered_map<std::string, std::unique_ptr<Variable>, KeyHasher> vars_;
unordered_map
数据结构类似于python中的dict,键是变量的名字,值是变量的指针。
类似 fluid.default_main_program()
,**默认有一个全局的 fluid.global_scope()
**。如果我们没有主动创建 scope
并且通过 fluid.scope_guard()
替换当前 scope
,那么所有参数都在全局 scope
中。
参数创建的时机不是在组网时,而是在 executor.run()
执行时。可以从 executor.run()
的注释得到印证:
And you could specify the scope to store the `Variables` during the executor running if the scope is not set,
the executor will use the global scope, i.e. `fluid.global_scope()`
program
program 的作用是存储网络结构,但不存储实际参数的值。
网络结构通过输入数据在各个操作(operator,即常说的OP)之间的流动来定义。我们以卷积为例,看看参数和op是如何组合到一起,并且加入到 prgram 中。在 fluid.layers.nn.py
文件里,可以看到 conv2d
的函数定义。其中有类似这样的代码:
helper = LayerHelper(l_type, **locals())
...
filter_param = helper.create_parameter(...)
helper.append_op(...)
这表示,通过 helper
实例创建参数,然后将 op
添加到当前 program
中。
create_parameter()
在 helper 的 create_parameter
函数里,静态图模式会运行如下代码:
self.startup_program.global_block().create_parameter(
dtype=dtype,
shape=shape,
**attr._to_kwargs(with_initializer=True))
return self.main_program.global_block().create_parameter(
dtype=dtype, shape=shape, **attr._to_kwargs())
这表示参数会在初始化参数的 startup_program
和训练主体的 main_program
中创建。这也侧面支持我们需要通过 startup_program
来初始化网络参数。
append_op
在 append_op 函数里,执行部分只有一行:
def append_op(self, *args, **kwargs):
return self.main_program.current_block().append_op(*args, **kwargs)
这表示把 op
加入到训练主体的 main_program
中。
继续跟踪,可以找到 Block 的 append_op()
函数在 fluid.framework.py
文件中的实现,部分如下:
op_desc = self.desc.append_op()
op = Operator(
block=self,
desc=op_desc,
type=kwargs.get("type", None),
inputs=kwargs.get("inputs", None),
outputs=kwargs.get("outputs", None),
attrs=kwargs.get("attrs", None))
self.ops.append(op)
这表示创建了一个操作 op,然后加入自己的一个集合中。再查看 fluid.framework.py 文件中 Operator 类的定义:
class Operator(object):
def __init__(self, block, desc, type=None, inputs=None,
outputs=None, attrs=None):
可以看到,op
会记录输入和输出。换个角度,把 op
想像成水管的各种转接头,定义流入的水和流出的水,同时对流经的水做处理。
只有 program
和 scope
配合,才能表达完整模型(模型=网络结构+参数)。
unique_name
前面讲了 scope 中的变量名不能重复,但是Program中声明变量的时候可能没有指定变量名,那怎么生成名字的呢?还是以 conv2d 为例子,打开一个训练好的模型,可以看到参数名形如:conv2d_0.b_0 和 conv2d_0.w_0。因为变量是在 helper.create_parameter()
中创建,所以跟踪进去看看里面和名字相关的部分:
suffix = 'b' if is_bias else 'w'
if attr.name is None:
attr.name = unique_name.generate(".".join([self.name, suffix]))
首先可以看到先判断变量的尾缀是 w 还是 b。然后涉及到两个部分,一个是 helper 自己的 name;一个是 unique_name.generate() 方法。
先关注 helper 的 name。在创建 helper 实例之前有这么一小段:
# conv2d 的 l_type 默认是 conv2d,在特殊条件下是 depthwise_conv2d
l_type = 'conv2d'
if (num_channels == groups and num_filters % num_channels == 0 and
not use_cudnn):
l_type = 'depthwise_conv2d'
helper = LayerHelper(l_type, **locals())
l_type 是操作类型。在 fluid.layer_helper.py 中可以看到 LayerHelper 类的定义:
def __init__(self, layer_type, **kwargs):
self.kwargs = kwargs
# 在没有设置 name 的情况下,name 为 None
name = self.kwargs.get('name', None)
if name is None:
self.kwargs['name'] = unique_name.generate(layer_type)
super(LayerHelper, self).__init__(
self.kwargs['name'], layer_type=layer_type)
当没有指定 name 的情况下,也会通过 unique_name.generate() 产生名字。重点关注一下这个函数的实现。在 fluid.unique_name.py 中可以看到 generate 函数的定义,其中有一段注释:
def generate(key):
"""
Examples:
import paddle.fluid as fluid
name1 = fluid.unique_name.generate('fc')
name2 = fluid.unique_name.generate('fc')
print(name1, name2) # fc_0, fc_1
"""
简单来说,通过对每一个名字产生当前的计数,从而生成全局唯一的名字。
把 conv2d_0.w_0 的过程串起来:首先生成 helper,用 conv2d 的类型产生了 conv2d_0 的 helper.name。然后生成具体参数的时候,根据类型,以 conv2d_0.w 和 conv2d_0.d 为参数送入 unique_name.generate(),产生 conv2d_0.b_0 和 conv2d_0.w_0。
以上就是这个部分的知识点总结。如果有什么问题、建议或者觉得内容有误,欢迎留言。
这个部分还可以参考:
如果喜欢,可以在AI Studio 关注我
还可以关注我的微信公众号:程序员宇波没有智。分享平凡程序员生活的点滴
关于train_program和test_program变量共享问题
在这份PGL图神经网络课程中,有这样一段代码:
train_program = fluid.default_main_program()
startup_program = fluid.default_startup_program()
with fluid.program_guard(train_program, startup_program):
with fluid.unique_name.guard():
gw, loss, acc, pred = build_model(dataset,
config=config,
phase="train",
main_prog=train_program)
test_program = fluid.Program()
with fluid.program_guard(test_program, startup_program):
with fluid.unique_name.guard():
_gw, v_loss, v_acc, v_pred = build_model(dataset,
config=config,
phase="test",
main_prog=test_program)
通过两个with fluid.program_guard()
语句,我们定义了两段局部的Program——train_program
和test_program
,它们是Program
类型的,里面包含了网络框架描述信息。同时,startup_program
同时参与了两个with fluid.program_guard()
语句,所以它包含两者的所有信息。这里有一个非常有趣的现象就是,由于Scope
是同名的变量是共享的内容,所以,当train_program
训练完后test_program
在运行时,发现自己的变量早已经被train_program
创建了,于是它就会直接使用已有的参数值,这也是PaddlePaddle
里面能够同时训练和测试的原因,属于一个极小的细节,如下图:
for epoch in range(epoch):
# Full Batch 训练
# 设定图上面那些节点要获取
# node_index: 训练节点的nid
# node_label: 训练节点对应的标签
feed_dict["node_index"] = np.array(train_index, dtype="int64")
feed_dict["node_label"] = np.array(train_label, dtype="int64")
train_loss, train_acc = exe.run(train_program,
feed=feed_dict,
fetch_list=[loss, acc],
return_numpy=True)
# Full Batch 验证
# 设定图上面那些节点要获取
# node_index: 训练节点的nid
# node_label: 训练节点对应的标签
feed_dict["node_index"] = np.array(val_index, dtype="int64")
feed_dict["node_label"] = np.array(val_label, dtype="int64")
val_loss, val_acc = exe.run(test_program,
feed=feed_dict,
fetch_list=[v_loss, v_acc],
return_numpy=True)
print("Epoch", epoch, "Train Acc", train_acc[0], "Valid Acc", val_acc[0])