号召一次Paddle避坑指南!
fiyen123_ 发布于2020-07 浏览:2964 回复:1
0
收藏

写这个主要是想分享一下自己平时基于Paddle框架编程时候遇到的坑以及解决的思路。也号召大神们分享一下自己平时遇到的坑。我总结了一个赏坑的项目,链接:[当你使用Paddle时你可能会踩到这些坑](https://aistudio.baidu.com/aistudio/projectdetail/616307)

# 静态图模式

## 我的静态图编程的步骤

我习惯于把自己想要实现的程序尽量整合到一个类里进行封装,这样在用的时候会方便一些。我用静态图编程一般是:

0. \_\_init\_\_ 自己定义

1. 定义模型训练参数的核心逻辑,即接收某个变量,然后输出想要的结果。这个单独写成一个函数;

2. 定义训练模型和测试模型,分别单独写成函数,接收main_program和starup_program,返回结果(loss,accuracy等)在函数中用fluid.layers.data定义需要模型接收的参数;

3. 定义一个数据batch生成器,把数据一个batch一个batch地送入1中的函数;

4. 定义一个训练函数和测试函数,两个函数的主要区别是训练函数有损失后向传播,定义优化器等操作,而测试函数不需要这一部分;

5. 调用这个类,用类的实例进行训练和测试

## 静态图的坑
以下是我在静态图编程过程中遇到的坑以及暂时的解决方案。仅供参考。

(1)第一个问题我觉得很多人可能都要遇到,就是用fluid.layers.data定义数据类型时,如果自己需要定义了batch的大小,会莫名其妙出现数据格式不匹配的错误。这个错误虽然很容易debug出来,但是还是费了我很久的时间,因为压根就没往这上面考虑,一直以为是上述步骤1出了错(张量变换最容易出错这个大家没意见吧哈哈哈)。

这个问题出现的原因是官方在fluid.layers.data中有个append_batch_size的选项,这个选项默认为True。当它检测不到有个维度值是-1的话,它就觉得好像没有定义batch这一维,然后它就“贴心地”给你加了一个batch_size=-1。但是batch_size已经给出来了啊。。。解决方法很简单,如果用这个函数定义数据,就记着定义append_batch_size。值得注意的是,fluid.data也可以完成相应的功能,但是没有append_batch_size这个选项。

(2)如果你用静态图,并且有查询词向量的需求,那么你大概率会碰到这个问题。当我把期盼着期盼着把embedding训练完以后,想要看看自己训练embedding的结果,我调用embedding[0],然后报错:请在fluid.dygraph.guard()的环境下运行这个。搞毛?我用的静态图,干嘛要用动态图保护啊。这个问题困扰我了好久,那时已经有“这还玩个毛线”的感觉,离弃坑就差那么一毫米的距离了。后来仔细研读了官方的实例教程,也没发现有相应的应用(这里吐槽一下官方的案例,一言难尽。。。官方教你画一匹马:我们先画一个嘴巴,再画一个鼻子,然后勾勒一下,好了,一匹马就完成了!)

后来。。。我不得不承认自己是个傻子,这是静态图啊!取参数的时候是不是要run一下呢?一试果然行。后来才明白fetch_list真的可以包罗万象,你想要什么参数,尽管去试一试,只要有名字,就可能给你fetch出来。不过fetch会大大拖慢运行速度,如果是最终要得到某个参数,不要在train的过程中频繁的fetch,等到模型训练完,单独run一下去fetch就行了。

(3)关于fluid.Program()和fluid.default_main/startup-program()的使用问题,这里有很多坑。主要是因为这个Program()的官方实例让人有点摸不着头脑。首先,官方例子中定义main=fluid.Program(), startup=fluid.Program(),应该是可以跑通的,但是我尝试的时候却出现了错误,提示没有初始化参数,但是Program不就是干这事的吗?于是我用main=fluid.default_main_program(), startup=fluid.default_startup_program(),没问题。需要注意的是,我定义了测试模块。而当我把test_main_program=fluid.default_main_program(), test_startup_program=test_startup_program(),完了,又错了,重复定义了某个参数。而我把test_main_program和test_startup_program=Program()时,问题又消失了。这个问题可以看我的项目[NLP经典之七:理解LSTM和GRU - Paddle实现](https://aistudio.baidu.com/aistudio/projectdetail/592636) 。但是问题又出来了,我又写了一个项目,相同的套路,不灵了。我调来调去,把test_main_program和test_startup_program又写成default_main/startup_program(),又行了。。。我。。。

不得不说,我现在也没搞懂为什么,有懂的大哥请提示一下我,万分感谢。

(4)这次是test_main_program.clone(for_test=True)这个命令该放到哪里合适,官方文档中的建议是放在exe.run(startup_program)之前,但是我刚开始并没有看官方文件,直接把这个和test_main_program以及test_startup_program都放在了测试函数中,也就是说等train运行完了,才开始创建这几个量。也安安静静用了好几天,并没有发现什么不对劲的问题。后来看了官方文档,给改回去了,不知道是不是错觉,改回去后测试的loss增加了那么一丢丢,不知道是不是心理原因,难道后定义,会继续训练?我测试模块甚至都没定义误差反向传播啊。。。这个也是个迷,望懂的大哥提示我一下,万分感谢。

# 动态图模式

## 我的静态图编程的步骤
跟上边说的一样,我喜欢封装成类哈哈哈。。。

0. \_\_init\_\_,注意继承fluid.dygraph.Layer

1. 定义forward函数,类似于静态图中的步骤1;

2. 定义训练函数和测试函数

3. 定义一个数据batch生成器,把数据一个batch一个batch地送入1中的函数;

4. 调用实例,进行训练

## 动态图的坑
要说动态图的坑,那可真是。。。。。。。。。。。。

(1)自定义的层需要继承fluid.dygraph.Layer吗?不用。如果你定义了一个类来写你的模型,你模型中有一个层是自己定义的,那么你只要在模型中调用自定义的这个层就行了,模型需要继承dygraph.Layer;

(2)with fluid.dygraph.guard()用在哪里?因为这个问题,我曾经一度怀疑动态图无法把训练和测试直接写到定义的模型中,因为我直接在train和test(valuate)函数中用with fluid.dygraph.guard(),调用类实例,用train训练后,各种报错,各式各样的错,然后我把train和test单独写出来,嘛事没有。后来我突然想试试with fluid.dygraph.guard()是不是要用在类实例上,于是我把train和test重新写入类中,然后先写with fluid.dygraph.guard(),然后调用类实例,用train训练,用test(evaluate)测试,跑通了。

这样的问题是,如果你想把所有的工具都封装在一个类中,那么你要提醒用这个工具的人,在跑的时候莫忘了加上一句with fluid.dygraph.guard(),就像你去郊游带上自煮火锅,封面上还给你备注了请使用前带上煤气罐烧火。个人认为还有另外一个解决方法,就是麻烦点,把模型写到一个类里,然后把各种工具比如train,evaluate,predict等写到另外一个类里,然后这个类调用模型那个类,用with fluid.dygraph.guard(),完美!就是有点费手![](https://ai-studio-static-online.cdn.bcebos.com/88f6cb42e7cf4204ad7242d0b5dd2fd0fe45a18eb79648b9a369183abb46cf6f)

(3)接下来就是这个梯度传播的问题了。首先,动态图中各个层的定义,用fluid.XXX而不是fluid.layers.XXX,这个最典型的就是全连接层fc,我在项目[Paddle实践之搭建文本CNN网络](https://aistudio.baidu.com/aistudio/projectdetail/550438) 中就因为这个layers.fc,在动态图中loss一直不收敛。后来在高人指点下,意识到了问题所在。动态图中请使用Linear来做全连接层。但是这个Linear有一个问题就是,在定义的时候必须指定input_dim这个参数,也就是说,输入的这个被拉通的向量有多大。这就很难受了,因为一般batch_size是不指定的,在运行的时候才知道有多大,那么这个被拉通的向量有多大事先是不知道的,又怎么给出呢?我在上述项目中给出了一个临时的解决方法,就是Linear这个层在训练时送入数据后才确定,然后就可以知道batch_size了。具体请看上述项目。这只是个临时的解决方式,也是被逼无奈。希望官方可以想办法去掉Linear对input_dim的要求,这样就方便多了。

(4)还是梯度传播问题。我在[NLP经典之四:考虑子结构的词嵌入模型 - 基于Paddle动态图模式](https://aistudio.baidu.com/aistudio/projectdetail/519590) 中遇到了一个问题,没有用全连接层,但是loss依然不收敛,看着明显就是没有被训练的样子。这是为什么呢?找了好久bug,最后在一个交流群里有个高手提醒我了一下,把所有训练参数打印出来看看,我才突然意识到,我是不是训练了所有参数。打印一看,果然,很多层的参数都没有。后来才发现,问题出在优化器定义的时机不对。我在with fluid.dygraph.guard()命令后调用模型的实例,调用后为了方便直接定义了优化器,因为优化器要用模型的parameters()去填充parameter_list。但是问题是,由于某些原因,一些没有在\_\_init\_\_中定义的参数,此时可能还没有定义,那么这时候调用parameters(),就不能得到所有的可训练参数,自然也就不会训练到所有参数。所以,为了避免这个问题,优化器的定义最好在forward被调用了一次后。我定义在loss的后向传播之后,因为这个时候要用到优化器了。

另外,个人感觉优化器被调用一次,还是每个batch都调用一次的差别不大。所以优化器的定义可以直接写,也可以用判定条件让它只被定义一次。具体可以参考上述项目。

# 总结
从两个月前首次接触AI Studio以来,从陌生到熟悉,感觉是又爱又恨。每个工具都有一个从入门到放弃的艰难历程,只是我们作为Paddle这个新生框架的早期用户,难免要摸着石头过河。这个历程虽然历尽艰辛,但是也不乏各种乐趣。希望飞桨这个平台越办越好,也希望更多的人能够加入到这个生态的建设中。更希望有朝一日,当我们使用这个框架时,遇到某个问题,只要一百度,就会有各种解决方法等着我们。这才是我理想的Paddle应该有的样子。

收藏
点赞
0
个赞
共1条回复 最后由用户已被禁言回复于2022-04
#2fiyen123_回复于2020-07

自顶一下

0
TOP
切换版块