5分钟PaddlePaddle知识点——program和它的朋友们


转载自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 想像成水管的各种转接头,定义流入的水和流出的水,同时对流经的水做处理。

只有 programscope 配合,才能表达完整模型(模型=网络结构+参数)。

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。

以上就是这个部分的知识点总结。如果有什么问题、建议或者觉得内容有误,欢迎留言。

这个部分还可以参考:

https://www.paddlepaddle.org.cn/documentation/docs/zh/advanced_guide/addon_development/design_idea/fluid_design_idea.html#id2


如果喜欢,可以在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_programtest_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])

文章作者: CarlYoung
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 CarlYoung !
 上一篇
test test
squares = [] for x in range(10): squares.append(x**2) squares #include <iostream> using namespace std; int
2020-11-29 CarlYoung
下一篇 
PaddlePaddle设计思想 PaddlePaddle设计思想
转载自PaddlePaddle官方文档 简介本篇文档主要介绍飞桨(PaddlePaddle,以下简称Paddle)底层的设计思想,帮助用户更好的理解框架运作过程。 阅读本文档,您将了解: Paddle 内部的执行流程 Program
2020-11-28
  目录