ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

深度学习高级主题--深度学习架构之飞桨框架的设计思想与二次开发

2021-07-11 16:03:48  阅读:200  来源: 互联网

标签:loss -- 深度 fluid 飞桨 program 计算 算子 二次开发


目录

前言

本章节主要介绍飞桨深度学习框架的底层设计思想,有了这些思想,或者底层运行逻辑的一些了解,这样使用飞桨更会得心应手,可以帮助我们理解飞桨框架的运作过程,以便于在实际业务需求中,更好地完成模型代码编写、调试以及基于飞桨进行二次开发。

一、框架的运行模式

设计思想:
飞桨的底层也就是PaddlePaddle的底层是怎么一个运行的逻辑呢?
我们可以认为整个神经网络是一个Program
是什么含义呢?
其实,要训练一个模型以及用这个模型去做预测,本质上来说,就是一段程序代码。换言之,无论是做训练还是预测,它背后的本质就是一段程序代码。这个Program,由多个Block(控制流结构)构成,我们都做程序的模块和设计。任何问题,其实一个大的执行的工作流,肯定是要分成很多小的一个Block,然后进行调度来形成一个大的工作流,那每个Block里在干一个什么事呢?实际上想一想,无论是我们的整个的神经网络,它的一个前向计算的过程, 还是要计算这个梯度进行传导的这个优化过程,那实际上就是要对一堆的Input,通过一堆Operator(也就是算子)进行疯狂地计算,计算完之后有一堆Output(输出)。一般这些算子用作向量计算,就是我们输入和输出其实也是向量,那这个就是基本内在的一个运行的一个基本过程,那在它底层的一个实现过程中,通常情况下用户只需要定义我们的前向网络,就是只是要告诉我们的培养框架,它大概是这样的,然后损失函数 ,想用的优化算法是这些,然后我们的框架会自动根据用户给出的信息,来生成整个前向计算的这个Program和后向传播梯度的这样的一些优化的Program的工作流程,来大大地减少代码的工作量。那实现这样一个方式,其实真正分享的背后的Program其实会有两套,一套叫startup_program,一套叫main_program, main_program负责的是整个网络的梯度计算的训练的这个过程,然后startup_program其实是做的是一开始的网络定义的声明以及参数初始化的过程一般startup_program只执行一次,main_program会随着梯度训练的过程中会执行多次

在这里插入图片描述
几个概念之间的关系
在这里插入图片描述
Program:将输入经过一堆算子的计算得到输出,这种抽象适用于网络的前向计算,也适用于梯度计算的过程。

1. 核心概念
神经网络模型是一个Program,由多个Block(控制流结构)构成,每个Block是由Operator(算子)和数据表示Variable(变量)构成,经过串联形成从输入到输出的计算流。

  • 用户只需定义前向计算网络、损失函数和优化算法,框架会自定生产前向计算和梯度优化的流程
  • 实现1的方式是初始化程序(startup_program)与主程序(main_program)
  • startup_program(初始化参数,执行一次)与main_program(m梯度更新参数,执行多次)
  • 用户指定的“数据、组网、损失和优化器”,分别会向两个Program中插入很多变量(Variable)和算子(Operator),以便执行时使用

在这里插入图片描述
代码如下:

## 每执行一行代码,飞桨都会在构建program里添加variable和opterator

# 声明数据
data = fluid.data(name='X', shape=[batch_size, 1], dtype='float32')
# 定义网络层
hidden = fluid.layers.fc(input=data, size=10)
# 计算损失函数
loss = fluid.layers.mean(hidden)
# 声明优化器
sgd_opt = fluid.optimizer.SGD(learning_rate=0.01).minimize(loss)

第一声明data,fluid.data,第二要声明这个网络结构,第三个要指定loss,要取minimize这一层,最后还得声明一个优化算法,这里用的最基本的优化算法SGD,然后learning rate是0.01。这四句话很简单,但这是神经网络要的最基本的框架。

下面打印一下内存空间,这样就可以知道这个框架到底做了什么。
在这里插入图片描述
在这里插入图片描述

内存空间有两部分,第一部分就是startup_program(在程序启动的时候,会执行一次的)和main_program(就是在训练的过程中,会反复执行的,每来一个mini_batch,可能就会执行一次),这两段内存空间内,我们声明了一个data变量的时候(这个是用红色的去表示),data在这里是一个持久化的,就是我们不断地在训练过程中所使用的,所以我们startup_program里就没有,因为它并不是一个初始化的东西,它是训练的时候要用的,所以main_program就出现了一个变量,这个变量就是X,这个就是我们的data的数据,因为我们指定它的名字就叫X。
然后我们在看第二句,紫色这句代码,我们声明了一个网络的结构,就是这个FC层。整个飞桨的框架,就在startup_program和main_program里都插入了一些变量和opertor(算子,就是要执行的一些操作),在初始化里面,它插入的就是FC层的初始化的变量,因为这个网络要一开始运行之前,它是有一个参数初值的,那这个初值是多少呢?其实是在这个startup_program里面是要记录得,同时呢,它在这里还加入了fill_constants的操作、random参数的这样的一些操作, 那这些操作都用于去初始化那些参数值。
在main_program,其实也是有这样一些变量的,因为实际上我们后续的训练,是要从参数的初始值来进行训练的,这些值会随着这个训练的过程中不断改变,这些参数值每训练一次,都会变好。
第三句,黄色的部分,这句是定义的loss,跟初始化没什么关系,其实一开始,程序都没有训练的时候,是不知道loss是多少的,所以它只跟main_program是有关系的,就是只跟训练过程有关系的,所以训练这个空间里面,会多了一个loss的情况,包括变量的情况以及跟它相关的计算这个函数,minimize这个函数,是在算子里面的。
最后,是声明了SGD的优化的算法,一旦这个优化算法加入,不但在startup_program有,在main_program也加了很多东西,为什么呢?是因为优化算法,一开始也是有初值的,比如说leraning_rate,这些一开始,也就是初始化的时候就需要的设置的,那另外一些在main_program里面,除了多了一些跟优化过程相关的变量之外,还多了一个专门的一些的变量,这些变量背后都打了一个标记叫grad,这些值其实是在每一次的梯度计算的过程中动态的生成的,就是每次计算反向梯度生成中间变量,用于梯度计算,所以每次,它都会生成这样的一些中间变量,同时,为了记录每次梯度的情况,还有一些固定的变量和操作的算子。
这就是飞浆框架在后台在做的一些事情。
这些可执行程序呢,其实就是用一些变量,去计算一些optertors算子,最后得到的一些结果。形成一个打的训练的和预测的程序。
在这里插入图片描述

二、动静态图详解

0、两种编程模式:静态图和动态图

在这里插入图片描述
静态图模式,类似于c++,java这些语言,一次性都写好,生成一个可执行文件,编译没问题的话。
动态图模式,类似于shell、python这种命令型的语言很像,每写一句话就可以运行了,可以实时地活的一个执行的结果,也就是用户动态编程,没必要预先定义好网络结构,只需要写一些组网的代码,每句代码都实时执行的,中间的这些结果都是可见的。
静态图的优势,在少量的一些情况下,去做全局的这种编译优化的话,其实是可以带来一定的性能的提升的,就比如说一些算子融合的一些操作,是可能提升我们的运算的速度的。另外,静态图这种模式,天然跟预测部署绑定在一起的,也就是我们要变成预测部署的模型的时候,是需要一个静态图的,这样一个模式的预测的结果和预测的格式。
这两种模式各有优缺点,但在PaddlePaddle2.0之后,有动态转静态模式的。
我们分别来看一下动静态图内部实现的一个流程。

1、静态图模式:形成完整表达后,编译执行

静态图会形成一个整体的Program,在这个Program全部执行生成好之后,会整体的进行编译执行。我们可以看到python前端把Program生成好之后,会经过一个Transpiler,就是一个编译器,编译成Transpiler的一个Program,然后交给C++后端,(这里得说一下,整个飞桨框架其实有点类似于他的内核是C++的,但是披了一层python接口的外皮。也就是说,跟C++沟通,直接通过python沟通就可以了。这样就减少调用整个飞桨框架的一个难度。)那这个C++的内核,其实就会把刚才的编译好的IR的一个表现得形式变成一个标准的表示格式(IR graph), 然后通过一个编译优化来形成IR的Graph,这个IR graph编译优化其实可以做也可以不做。通常情况下,会做一些Operator Fusion(存储优化),是因为有一些Operator占用的内存是可以互相共享的,就比如之前有一些Operator,这些Operator,在之后Operator执行之前,它可能就废弃了,永远再不用了,那后面这些Operator其实就会占用之前的一个内存。那整个这个Transpile,还有这些编译优化的过程中,也会做一些算子融合,比如说有两个算子,它们分别去算的话,这个计算量比如说是1000的话,那它两可以合成一个算子,这个计算过程可以做一些简化,那么它计算的结果,就比如说,它消耗的性能可能只需要用300(换言之,可节省中间计算结果的存储、读取等过程,以及框架底层算子调度的开销,从而提升执行性能和效率)。这些方式,可以大大减少整个的一个计算量。那整个编译好了之后,它会交到Executer,这个Executer就会整体的执行。
这里有两点的说明一下:一个是Transpile这个过程,就是为什么要编换成一个Transpiler Program,首先来说这个过程,其实这个过程并不是必须的,在特定的情况下,这个步骤才会有用,就比如说,在进行分布式训练的时候,未经过Distributed Transpiler编译,就是转换的这种原始的Program,它里面可能只是单机要进行训练的用的一些算子(比如说前向计算,反向计算,优化器等)。但是我们都知道要进行分布式训练的话,它其实会把这个数据和程序分散到很多的机器上,然后它们的训练过程可能会采用Pserver这样一个模式的话,它会有一个主机,这个主机会收集和同步各个Trainer来得到这些梯度,进行整个的一个梯度融合,然后再下发下去。那在这个主机会有一些优化器的算子,有一些通信的算子,这个Trainer上也会加入一些通信的算子,比如说,它们做的这个Program里需要做的事情,要比单机执行时候多很多的。在这个场景下,就通过这个Distributed Transpiler把原始的Program变成可以进行分布式执行的这样一个Program。
小结上面一段话

  1. 静态图模式是形成Program的完整表达后,编译优化并交于执行器执行。
    在这里插入图片描述
  2. 原始Program经过特定的Transpiler(非必须),可以形成特定功能的Program(如用分布式的训练)
    在这里插入图片描述

2、静态图模式:IR的含义和好处

再说说为什么IR会有一个Intermediate Representation,一个中间的统用的表达形式。
第一个好处,我们可以用于编译优化,但这个实际是非必须的,多数情况下,使用编译优化,多多少少都会有性能或存储空间上的一些提升。但大家觉得这个机器自然也无所谓,也不在意这个,那其实有时候不进行编译优化,也是OK的。另外一个,我们出来这种标准的中间表达格式,是一个比较好的方式,其实就是为了适配我们不同的一个硬件。为什么这么说呢?我们知道,要进行训练所适配的这个硬件还是非常多了,如果要预测要适配的硬件就更多了,像英伟达的GPU,英特尔的CPU,ARM架构的各种移动端的这样的一些芯片,还有FPGA用于我们嵌入式编程等。在这些所有的硬件基础上,如果我们的模型要跑到这些硬件上,那就意味着我们的模型这里的这些计算逻辑,要必须能够跟这些硬件进行适配,能够调用这些硬件的一些基础的能力。那这个适配过程相对还是比较繁琐的,所以我们有这样一个标准的表达形式,不管是Paddle的框架,不管是Paddle什么模型,甚至其他的pytorch、tensorflow都可以转换成Paddle的IR的表示模式,从而运用到Paddle非常强大的这些部署工具,去达成各种各样的硬件的一个适配,所以在我们解决实际问题的时候,这个中间的表达格式还是非常有用的。

在这里插入图片描述
上面的图是下面代码案例的IR的图示。也就对应的统用逻辑结构。

import paddle.fluid as fluid

image = fluid.data(shape=[None, 3, 224, 224], name='image', dtype='float32')
label = fluid.data(shape=[None, 1], name='label', dtype='int64')

y = fluid.layers.fc(image, size=1000)

loss = fluid.layers.softmax_with_cross_entroy(y, label)

mean_loss = fluid.layers.reduce_mean(loss)

概括总结一下:
IR:Intermediate Representation,统一的中间表达
IR的概念起源于编译器,是介于程序源代码与目标代码之间的中间表达形式:
(1)便于编译优化(非必须)
(2)便于部署适配不同硬件(Nvidia GPU、Inter CPU、ARM、FPGA等),减少适配成本

3、动态图模式:Python原生控制流,实时解释执行

在这里插入图片描述
动态图和静态图最大的区别就是,它写一句代码,就执行一句,它并不是在前面有很多的声明和配置的过程,然后都整好了,最后交给我们的执行器去一次性跑,它不是这样的,每一段python代码,它都会实时的执行,那执行的过程中,我们可以看上面的代码,这是一个动态图的一个代码,我来解释一下三句话,这三句话分别是做了一个relu的计算,做了一个reduce_sum的计算,最后用backword来进行计算图的一个反向的执行,就是来进行梯度的一个反向传播。就这三句话,前两句明面上就是运算一个前向的算子(第一个是运算relu算子,第二个是运算reduce_sum算子),但真实的情况下,整个Paddle的一个后台,它实际上还做了两件事,它在进行前向计算的过程中,还记录了反向传播的一个算子的信息,同时在运行前向这个reduce_sum算子的一个过程中,它也记录了reduce_sum反向计算的这个算子的信息,这两种算子的信息一旦被记录下来之后,在backword的时候,调用这句话的时候,它就会跟我们之前已经计算好的的反向计算图来进行整体的一个梯度反向计算和传播的过程。

4、动态图模式:根据配置的前向计算,自动生成反向计算图

在这里插入图片描述
根据上面的讲述,我们可以看下上图来解读一下:进行左侧前向计算的过程,它其实反向计算这个计算图也就一点一点的生成了,当我们把整体前向计算全部算完了之后,其实,在整个飞桨框架的后台,已经把这个反向计算图生成出来了,这个时候,只要调用backword这句话,整个的反向计算图就会一次性的被执行。

三、动静态图写法区别与动转静方案

静态图模型与动态图模式的代码区别

在这里插入图片描述
首先来看一下,在一开始定义网络的时候,两者的写法其实就不太一样,在动态图中(上右图),我想大概应该比较熟悉了,首先我们会声明一个类,这个类的__init__的方法,去写一些初始化的工作,然后在forward方法,就是在定义一个前向的过程,其实就是一个组网的方式。在静态图中,使用函数式的方式来定义的,它的定义跟动态图中forward方法上略有点像。

静态图模型与动态图模式的代码在训练上的区别,如下:

在这里插入图片描述
下面一个代码,是我们相对熟悉的动态图编写模式:它其实式声明一个模型,然后定义一个优化器,接着通过双层循环来启动一个训练的过程。这个训练的过程中,内部都式这四步,一开始搞定数据,然后要进行前向的计算,接着计算损失,然后再进行反向的一个传播,最后更新参数 。每一次一个训练的过程细节都是一个Batch一个batch执行的,我们随时都可以打断去看训练和执行的一个效果。 【使用类的方式声明网络后,开启两层的训练循环,每层循环中完整的完成四个训练步骤(前向计算、计算损失,计算梯度和后向传播)】
在这里插入图片描述
上面第一个则是静态图的编写模式:一开始,有大段代码其实在进行预定义的,什么叫预定义呢?就是这些代码就像一些配置文件似的,也就是这些代码告诉了飞桨的后端了,但飞桨后端并没有真正去执行这些动作,它只是通过这些代码知道要配置的Program背后实际上是什么样形式的一个模型和一个程序,当通过这个配置,告诉飞桨大概要做什么样的一个网络,然后飞桨后端把这个program给生成了,这个program就会交给执行器。看一下上代码发现,会交入我们的执行器,也就是exe.run,然后做一个参数放进去,这个exe.run执行就会一次性地把这个program训练进行执行,这就是静态图地一个方式。这种方式没有动态图的方式好调试,因为整个训练过程,它其实是通过一个配置文件和一次性执行的执行器的方式来写的。这个还是不太好继续去调试很多细节。
【 使用函数方式声明网络,然后要编写大量预定义的配置项,如选择的损失函数,训练所在的机器环境等等。在这些训练配置定义好后,声明一个执行器exe(运行Program,调度Operator完成网络训练/预测),将数据和模型传入exe.run()函数,一次性的完成整个训练过程。】

动态图好调试,静态图有性能的优势和部署的方便。
理想国:动态图调试,静态图训练和部署?也就是说动态图去写模型的构建代码,静态图去训练和部署。
答案:PP2.0版本后,支持一键实现动转静,动态图程序可以转为静态图模式训练和部署。
在这里插入图片描述
如果现在仅需要用静态图的方式去做训练的话,只需要写左侧这段程序就够了,这段程序会有一个@declarative,这个开关,这段代码是动态模式去写的,只要把这个开关打开,加上这么一段话,就可以转成一个静态图的方式去做训练。如果想把动态图的代码转为静态图的方式去部署,除了要写这个开关,还需要写右侧这段代码,多的东西很少的,只是说paddle首先加一个enable_imperative(),把这个静态图的模式激活,然后声明了一个imperative program的Translator,这个Translator有一个save_inference_model,这个函数去保存这个动态图的模型,它就会把动态图转为一个静态图的模式去进行优化和保存,就可以交给部署工具了。

总结:

飞桨深度学习框架内部包含四个逻辑模块,是一个包含的关系:第一个是program,我们会认为整个框架,无论是训练的过程还是预测的过程,它最后是形成一个大的程序;那第二个是block,一个大的program肯定是可以分模块,每个模块会完成一个相对独立的职能,模块之间会由program进行整体的一个调度。每个block里,其实它就是一个单独的执行单元,这个执行单元我们可以认为,它实际上就是把一堆的variable,也就是一堆的各种各样的向量的变量,通过一些列的operator,也就一些列的算子进行计算,最后输出一大堆的variable,这就是一个block基本做的一个事情。

声明式编程–静态图 – 训练和部署-- 训练性能的提升和部署的方便
命令式编程–动态图 – 构建网络结构–方便调试

基于飞桨框架的二次开发

除了直接使用飞桨的众多现成模型,还可以基于飞桨样最新或特殊领域的模型:

  1. 基于飞桨框架进行模型研发,使用底层算子搭建全新的网络结构
  2. 少量新模型涉及新算子,向框架中添加算子

飞桨有着丰富的模型资源,开发者可以使用飞桨众多现有的模型,也可以在开源模型的基础上完成二次开发。
在使用飞桨研开发最新模型或特殊领域模型的时候,极少数情况下会遇到飞桨缺少一些特殊算子(完成某个计算函数的网络层)的实现。
此时有两种解决方案

  1. 按照飞桨的OP规范,写一个C++ OP添加到飞桨框架中,重新编译飞桨框架以支持这个特殊OP。
  2. 使用飞桨的py_func功能实现特殊的API。飞桨支持使用python在框架外部实现OP,即py_func,无需更改底层框架,即可快速实现自定义的API。

前一种方案的算子性能略好,但后一种方案更加容易实现,我们优先推荐使用后一种方式。在本节,我们将以Relu函数为例,展示向飞桨框架中添加算子的方法。

添加新算子

使用py_func完成自定义API需要三个步骤

  1. 定义计算方法:前向函数和反向函数
  2. 定义算子输出变量:使用numpy库构建API计算逻辑,输出类型需要转换成飞桨的内置形式(即,创建前向输出变量,将外部API输出的Numpy类型转变成框架内置类型)
  3. 调用算子组网:使用py_func组建网。
    func:前向计算的函数名
    x:前向计算的输入,如果有多个输入,把多个输入放在一个列表里传入,如x=[input1, input2]
    out:前向计算的输出
    backward_func:反向计算的函数名

定义计算方法:前向函数和反向函数

使用深度学习框架完成算法构建时,我们只需要完成算法的前向计算即可,反向计算框架会为我们做好一切。但是自定义API时,如果需要该API参与反向计算,则完成自定义API前向计算的同时,也要完成自定义API的反向计算。实际上,框架中每个算子API均实现了前向计算和反向计算的逻辑,框架会根据用户前向计算的组网逻辑,自动生产后向计算的计算程序。

前向计算和反向计算函数有固定的格式,其格式如下所示。 需注意,前向计算函数只需要输入值作为参数。但由于求导的链式法则,反向计算函数除了输出的梯度值外,还需要输出值本身,所以有两个参数。

# 前向计算函数,function是前向计算函数的实现过程
def API(x):
	y = function(x)
	return y

# 反向计算函数,funtion_backward是反向计算函数的实现过程
# y是反向函数的输入数据,dy是y的梯度
def API_backward(y, dy):
	res = function_backward(y, dy)
	return res

def create_tmp_var(program, name, dtype, shape):
    return program.current_block().create_var(name=name, dtype=dtype, shape=shape)

# 手动创建前向输出变量
y_var = create_tmp_var(fluid.default_main_program(), 'output', 'float32', [-1, 4])
print(y_var)
 x = fluid.data(name='x', shape=[-1, 1, 28, 28], dtype='int32')
 fluid.layers.py_func(func=relu, x=x, out=y_var, backward_func=relu_grad, skip_var_in_backward_input=x)

API()这个函数是前向计算,API_backward()是反向计算,
create_tmp_var():因为api内部其实是拿numpy实现的,所以它实际上是numpy的一个数据结构,就在算子内部,但是飞桨执行的过程实际上使用的tensor。这个函数声明出来,是为了将我们编写的这个算子的输出(numpy类型)转为飞桨的内部的一个类型,也就是variable的类型。
构造一个data,这个data是内置的一个类型,然后我们就用layers.py_func(),这个函数可以将外部研发的算子挂接到飞桨的框架里。这个函数的参数就是:第一个是要指定这个api和反向计算的api这个函数,然后还要指定算子的输入和输出,这个输入和输出的格式肯定是要经过刚才create_tmp_var()那个函数进行转换,从numpy的操作类型变成飞桨内置的操作类型。就可以把这个算子挂接上来。

添加新算子案例

以Relu为加入新算子的案例:前向计算和反向梯度的函数
在这里插入图片描述
在这里插入图片描述

import numpy as np

# 定义前向计算
def relu(x):
    y = np.maximum(x, 0.0)
    return y

# 定义反向计算函数
def relu_backward(y, dy):
    dx = np.zeros_like(y)
    dx[y > 0] = 1.0
    res = np.array(dy) * dx
    return res

def test_relu():
    x = np.random.uniform(-1, 1, [2, 4]).astype(np.float32)
    y = relu(x)
    print("relu的输入数据:{} \nrelu的输出:{}".format(x, y))
    dy = np.random.uniform(-1, 1, [2, 4]).astype(np.float32)
    dydx = relu_backward(y, dy)
    
    print("\n假设的y的梯度是:{} \n通过relu_backward计算得到的x的梯度是:{}".format(dy, dydx))

    
test_relu()

运行的结果为:

relu的输入数据:[[-0.39115584 -0.7299341  -0.12647586 -0.5624508 ]
 [-0.8664403  -0.70393366  0.04075145 -0.4767929 ]] 
relu的输出:[[0.         0.         0.         0.        ]
 [0.         0.         0.04075145 0.        ]]

假设的y的梯度是:[[ 0.15134676  0.45530593  0.54943573  0.10149622]
 [-0.9668464  -0.48074073  0.34972975 -0.5144964 ]] 
通过relu_backward计算得到的x的梯度是:[[ 0.          0.          0.          0.        ]
 [-0.         -0.          0.34972975 -0.        ]]

梯度的链式法则:当前这个函数的梯度,它等于当前这个函数对目前的这个输入变量的梯度乘以它上一步骤的传过来之前的梯度,那这个y的梯度,其实就是它后面一层给它传来的梯度,这个y其实会用于它本层的一个梯度的计算。注意传入和传出,强制的类型转换。

定义算子输出变量

外部API都是通过Numpy实现的,飞桨网络需要的数据类型是tensor,因此,需要将外部API的输出转换成tensor。

飞桨通过 Program.current_block().create_var 创建前向输出变量,其中变量的名称name、数据类型dtype和维度shape为必选参数,格式如下:

import paddle.fluid as fluid

def create_tmp_var(program, name, dtype, shape):
    return program.current_block().create_var(name=name, dtype=dtype, shape=shape)
    
# 手动创建前向输出变量
y_var = create_tmp_var(fluid.default_main_program(), 'output', 'float32', [-1, 4])
print(y_var)

调用 py_func 组建网络

定义好前向反向计算函数,并且有了创建输出变量的函数,就可以使用py_func API在代码中使用自定义的函数了。

py_func的使用方法如下:

fluid.layers.py_func(func=relu, x=in_var, out=out_var, backward_func=relu_backward)

py_func有四个输入参数,分别是前向计算函数,输出,输出和反向计算函数。

  • func:接收自定义API的前向输入函数名;
  • x:自定义API的前向输入数据,如果有多个输入,把多个输入放在一个列表里传给x, 如 x=[inputs1, inputs2];
  • out:自定义API的输出;
  • backward_func: 自定义API的反向计算函数。

以Relu为加入新算子的完整案例

下面提供一个完整的案例,使用两层全连接层构建MNIST识别网络,第一层全连接的激活函数使用自定义的ReLU激活函数。

实现Relu算子的完整步骤:

  1. 前向函数relu()和反向函数relu_grad()
  2. 将前向函数的输出转变为框架内置的数据格式create_var()
  3. 使用py_func()挂接算子到网络中

完整程序代码如下:

import paddle 
import paddle.fluid as fluid

import numpy as np

#第一步: 定义前向和反向函数
# 定义ReLU函数的前向计算过程
def relu(x):
    return np.maximum(x, 0.0)
  
#定义反向计算过程:y是前向函数的输出,dy是y的梯度
def relu_grad(y, dy):
    dx = np.zeros_like(y)
    dx[y > 0] = 1.0
    return np.array(dy) * dx

def create_tmp_var(name, dtype, shape):
    return fluid.default_main_program().current_block().create_var(
        name=name, dtype=dtype, shape=shape)

BATCH_SIZE = 256

def mnist(x):
    x = fluid.layers.fc(x, size=100)
    #创建relu激活函数的前向输出的变量
    output = create_tmp_var(name='relu', dtype=x.dtype, shape=x.shape)

    #使用py_func组建网络,设置前向、反向计算函数,输入是x,输出是定义好的输出变量output
    x = fluid.layers.py_func(func=relu, x=x,
                out=output, backward_func=relu_grad,
                skip_vars_in_backward_input=x)
    prediction = fluid.layers.fc(x, size=10, act='softmax')
    return prediction

在这里插入图片描述

设置数据读取器,使用飞桨自带的MNIST数据读取函数

# 设置数据读取器,读取MNIST数据训练集
trainset = paddle.dataset.mnist.train()
testset = paddle.dataset.mnist.test()
# 包装数据读取器,每次读取的数据数量设置为batch_size=BATCH_SIZE
train_reader = paddle.batch(trainset, batch_size=BATCH_SIZE)
test_reader = paddle.batch(testset, batch_size=BATCH_SIZE)
Cache file /home/aistudio/.cache/paddle/dataset/mnist/train-images-idx3-ubyte.gz not found, downloading https://dataset.bj.bcebos.com/mnist/train-images-idx3-ubyte.gz 
Begin to download
....................
Download finished
Cache file /home/aistudio/.cache/paddle/dataset/mnist/train-labels-idx1-ubyte.gz not found, downloading https://dataset.bj.bcebos.com/mnist/train-labels-idx1-ubyte.gz 
Begin to download
........
Download finished
Cache file /home/aistudio/.cache/paddle/dataset/mnist/t10k-images-idx3-ubyte.gz not found, downloading https://dataset.bj.bcebos.com/mnist/t10k-images-idx3-ubyte.gz 
Begin to download
....................
Download finished
Cache file /home/aistudio/.cache/paddle/dataset/mnist/t10k-labels-idx1-ubyte.gz not found, downloading https://dataset.bj.bcebos.com/mnist/t10k-labels-idx1-ubyte.gz 
Begin to download
..
Download finished

设置训练过程,使用自定义的py_func时,不影响算法的训练部分,因此训练部分的代码与一般的静态图训练算法相同,启动训练代码如下:

import paddle.fluid as fluid
# 定义program
train_program = fluid.Program()
start_program = fluid.Program()

place = fluid.CPUPlace()
with fluid.program_guard(train_program, start_program):
    # 声明输入数据
    data = fluid.data(name="X", shape=[None, 784], dtype="float32")
    label = fluid.data(name="label", shape=[None, 1], dtype="int64")
    # 定义优化器
    sgd = fluid.optimizer.SGD(learning_rate=0.01)

    # 运行网络的前向计算,计算损失函数,计算精度
    res = mnist(data)
    loss= fluid.layers.cross_entropy(res, label)
    loss = fluid.layers.mean(loss)
    acc = fluid.layers.accuracy(res, fluid.layers.reshape(label, [-1, 1]))

    # 优化器最小化loss
    sgd.minimize(loss)
    # 定义执行器
    exe=fluid.Executor(fluid.CPUPlace())
    exe.run(fluid.default_startup_program())
    
    # 启动训练
    EPOCHS = 5
    for epoch in range(EPOCHS):

        for batch_id, data in enumerate(train_reader()):
            # 获得图像数据,并转为float32类型的数组
            img_data = np.array([x[0] for x in data]).astype('float32')
            # 获得图像标签数据,并转为float32类型的数组
            label_data = np.array([[x[1]] for x in data]).astype('int64')

            loss_acc = exe.run(feed={'X':img_data, "label":label_data}, fetch_list=[loss.name, acc.name])
            if batch_id %100==0:
                print("iter [{}]/[{}], loss:{}, acc:{}".format(batch_id, int(60000/BATCH_SIZE), loss_acc[0], loss_acc[1]))
    
        # 训练一个epoch后启动测试
        test_program = fluid.default_main_program().clone(for_test=True)
        ACC = []

        for batch_id, data in enumerate(test_reader()):
            # 获得图像数据,并转为float32类型的数组
            img_data = np.array([x[0] for x in data]).astype('float32')
            # 获得图像标签数据,并转为float32类型的数组
            label_data = np.array([[x[1]] for x in data]).astype('int64')

            _acc = exe.run(test_program, feed={'X':img_data, "label":label_data}, fetch_list=[acc.name])
            ACC.append(_acc)
        print("\nEpoch:{}, Test Done!, The accuracy is :{} \n".format(epoch, np.mean(ACC)))
iter [0]/[234], loss:[2.7249632], acc:[0.07421875]
iter [100]/[234], loss:[0.86871946], acc:[0.80078125]
iter [200]/[234], loss:[0.65437365], acc:[0.84375]

Epoch:0, Test Done!, The accuracy is :0.838085949421 

iter [0]/[234], loss:[0.598769], acc:[0.84765625]
iter [100]/[234], loss:[0.49178913], acc:[0.8671875]
iter [200]/[234], loss:[0.4901981], acc:[0.87890625]

Epoch:1, Test Done!, The accuracy is :0.8720703125 

iter [0]/[234], loss:[0.47113904], acc:[0.86328125]
iter [100]/[234], loss:[0.4032817], acc:[0.890625]
iter [200]/[234], loss:[0.43236786], acc:[0.8828125]

Epoch:2, Test Done!, The accuracy is :0.885546863079 

iter [0]/[234], loss:[0.41707835], acc:[0.8671875]
iter [100]/[234], loss:[0.36154997], acc:[0.90234375]
iter [200]/[234], loss:[0.4007422], acc:[0.89453125]

Epoch:3, Test Done!, The accuracy is :0.892871081829 

iter [0]/[234], loss:[0.38527423], acc:[0.87109375]
iter [100]/[234], loss:[0.3367114], acc:[0.91015625]
iter [200]/[234], loss:[0.37993705], acc:[0.8984375]

Epoch:4, Test Done!, The accuracy is :0.896386742592

自创算子的表现
与原生算子相比,性能略有降低

这就是向模型添加算子的过程!
从上述完整案例可见,使用Py_func加入一个新的算子是相当容易的,仅需要在组网的部分做好新算子的定义和使用,模型的训练代码并无变化。

标签:loss,--,深度,fluid,飞桨,program,计算,算子,二次开发
来源: https://blog.51cto.com/u_11495341/3036174

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有