PaddlePaddle v2 api design
Created by: jacquesqiao
背景
- PaddlePaddle最初的使用方式是调用二进制trainer,通过解析配置文件的方式来驱动整个训练过程,例如demo/quick_start/train.sh的内容如下:
cfg=trainer_config.lr.py
paddle train \
--config=$cfg \
--save_dir=./output \
--trainer_count=4 \
--log_period=100 \
--num_passes=15 \
--use_gpu=false \
--show_parameter_stats_period=100 \
--test_all_data_in_one_period=1 \
2>&1 | tee 'train.log'
最初通过swig暴露出来了一些python接口,然后当时可以通过裸掉这些接口的方式来实现一个python驱动的训练流程,一个典型的例子见:paddle/demo/quick_start/api_train.py,这个版本的python api中直接操作paddle中的核心组件,比如gradinet_machine, trainer来实现训练流程,封装比较简陋,不便于用户使用和理解。
后来为了使用户能够在notebook这种环境中方便的使用paddle进行训练任务,启动了python api v2的设计工作。详情见:https://github.com/PaddlePaddle/Paddle/projects/10 上面这个wiki中的很多设计都是一些中间状态,最终也被抛弃掉了,最终形成设计文档design_doc(https://github.com/PaddlePaddle/Paddle/pull/1088/files)。
目标
v2 api的主要目标是: 1,用户可以方便的用python驱动整个训练流程。 2,核心概念清晰易于理解,接口易用。
重构的主要内容。
1,网络配置相关: 灵活的layer配置方式。包含 layer topology等概念。 2,数据相关:dataset api。 3,训练相关:trainer,optimizer,parameters。
网络配置的主要变化。
1,最初的网络配置是一个文件,例如paddle/demo/quick_start/trainer_config.lr.py,trainer启动之后会通过内嵌的python解释器parse这个文件,生成的结果是一个proto。
data = data_layer(name="word", size=len(word_dict))
embedding = embedding_layer(input=data, size=128)
conv = sequence_conv_pool(input=embedding, context_len=3, hidden_size=512)
output = fc_layer(input=conv, size=2, act=SoftmaxActivation())
上面是一个典型的配置,data_layer,embedding_layer等都是一些function,上面代码的每一行都会执行函数,并且生成相应的结果,返回结果一般叫做LayerOutput,然后LayerOutput作为intput输入给下一个function。
所以每次解析一个网络,需要将上述所有代码依次执行一遍。
重构的主要变化是,每个layer变成了一个class(paddle/python/paddle/v2/config_base.py#Layer),配置过程中不做解析,layer会组成一个tree的结构,在需要解析的时候,传给topology这个结构进行parse,结果依然是原来那个proto。
network解析过程:
为了避免重写太多代码,实际上v2的网络解析是封装了上面v1的那些function,每个layer对应原来的一个function,核心思想是先把用户配置网络时输入的那些参数保存在v2 layer的class中,在适当的时候,调用v1的function。
def to_proto_impl(self, **kwargs):
args = dict()
args['size'] = self.type.dim
for each in kwargs:
args[each] = kwargs[each]
for each in self.__kwargs__:
args[each] = self.__kwargs__[each]
# 调用v2 function的地方
return getattr(conf_helps, self.__method_name__)(name=self.name, **args)
调用的时机呢,因为v2是一个tree的结构,而parse需要从最初的layer开始,所以整个解析过程是一个递归的过程,其核心逻辑在Layer#to_proto这个函数里面。这个地方会先找到依赖的parent layer,先去调用他们的解析,解析的时候会把LayerOutput放到context中,这样后面的layer如果依赖这个layer的结果,就可以从context中找到并且使用了。
目前的问题:
1,核心问题在于目前v2 layer是一个递归结构,每个被配置的layer一定要在这个tree中的某个位置才能被解析到,然而有一些配置的layer只是依赖了某个layer,却没有被layer依赖,递归路径上找不到,就没有被解析。