正文共:6452 字 0 图
预计阅读时间: 17 分钟
本文讨论一下PaddlePaddle框架中几个重要的概念,在使用Paddle进行开发时,弄清楚这几个概念是使用Paddle进行开发的一个前提。
Tensor张量
与当前主流框架相同,Paddle同样使用Tensor张量来表示数据,你可以将不同维度的Tensor理解成对应维度的矩阵,当然,两者是有差异的。
在Fluid中所有的数据类型都为LoD-Tensor,对于不存在序列信息的数据(如此处的变量X),其lod_level=0。
在Paddle Fluid版本中,有3个比较重要的Tensor
可学习参数
神经网络中的可学习参数(模型权重、偏置等)其生命周期与整个训练周期一样长,在训练过程中,可学习参数会被修改,最常见的就是模型权重这一可学习参数会被梯度下降算法修改,在Fluid中可以使用 fluid.layers.create_parameter
来创建可学习参数
w = fluid.layers.create_parameter(name='w', shape=[1], dtype='float32')
在日常编写模型中,该方法并不常用,因为Paddle已经为大部分常见的神经网络基本计算模型进行了封装,如创建全链接层,并不需要自己手动创建链接权重w与偏置b
y = fluid.layers.fc(input=x, size=128, bias_attr=True)
其实进入 fluid.layers.create_parameter()
方法的源码和 fluid.layers.fc()
方法的源码,可以发现两者最终都是调用 helper.create_parameter()
方法来创建可学习参数的,其中helper是LayerHelper的实例。
输入数据Tensor
Fluid使用 fluid.layers.data()
方法来接受输入数据,与TensorFlow中的placeholder占位符作用是相同的,同样,fluid.layers.data()方法需要定义好输入Tensor的形状,如果遇到无法确定形状的维度,则设置为None。
x = fluid.layers.data(name='x', shape=[3, None], dtype='int64')
# Batch size 无需显示指定, Paddle会自动填充
a = fluid.layers.data(name='a', shape=[3,6], dtype='int64')
# 输入数据为宽、高不是固定的图像, 主要图像数据类型通常为float32
img = fluid.layers.data(name='img', shape=[3, None, None], dtype='float32')
上述代码中,一个tick就是Batch size,无需自己显示的定义。(通常训练模型时,都以一Batch size大小的数据为一次训练数据的量)
可以从源码看出(如下),data()方法中使用appendbatchsize变量判断是否要为shape自动加上batch size,但正是的创建输入,使用的是
helper.create_global_variable()
方法。而helper依旧是LayerHelper的实例。
def data(name,shape,append_batch_size=True,dtype='float32',
lod_level=0,type=core.VarDesc.VarType.LOD_TENSOR,
stop_gradient=True):
helper = LayerHelper('data', **locals())
shape = list(shape)
for i in six.moves.range(len(shape)):
if shape[i] is None:
shape[i] = -1
append_batch_size = False
elif shape[i] < 0:
append_batch_size = False
if append_batch_size:
shape = [-1] + shape # append batch size as -1
data_var = helper.create_global_variable(
name=name,
shape=shape,
dtype=dtype,
type=type,
stop_gradient=stop_gradient,
lod_level=lod_level,
is_data=True)
return data_var
LayerHelper以出现多次,可以看出LayerHelper在Paddle中起着关键作用,从Paddle的设计文档中可以了解到LayerHelper,它的作用主要是在各个layers函数之间共享代码,设计LayerHelper主要考虑到如果开发全局辅助函数有几个缺点:
1.需要提供这些方法的命名空间,方便开发人员快速定位并使用
2.全局函数迫使图层开发人员需要逐个传递参数
-
为了避免以上缺点,才定义出了LayerHelper,但它通常在layers开发中使用,对应创建模型结构的搭建不会使用到LayerHelper,关于LayerHelper的更多细节可以参考Design Doc:Python API,当然后面的文章我也打算讲讲Paddle设计层面以及底层点的东西,到时也会有所提及。
常量Tensor
Fluid通过
fluid.layers.fill_constant()
实现常量Tensor,常量即常量,在模型训练过程中,其值不会改变。data = fluid.layers.fill_constant(shape=[1], value=0, dtype=‘int64’)
该方法的内部依旧是使用LayerHelper类来实现常量Tensor
helper = LayerHelper(“fill_constant”, **locals())
Paddle数据传入
Fluid版本的Paddle支持两种传入数据的方式,分别是
Python Reader同步读入数据:使用
fluid.layers.data()
定义输入层,并在fluid.Executor
或fluid.ParallelExecutor
中使用executor.run(feed=…)
来传入数据,与TensorFlow类似,定义好Placeholder占位符,再在具体训练时,将具体的数据喂养给模型py_reader异步读入数据:异步读入数据,先需要使用
fluid.layers.py_reader()
配置异步数据输入层,再使用py_reader()
的decorate_paddle_reader
或decorate_tensor_provider
方法配置数据,配置完数据后,最总通过fluid.layers.read_file
读取数据。-
对于常用模型,比较常见的事同步读入数据的方式。Paddle的数据读入会在后一章进行讲解。
Operator表示对数据的操作
Fluid版本的Paddle中,所有的数据操作都由Operator表示,在python端,Operator操作被进一步封装在
paddle.fluid.layers
与paddle.fluid.nets
等模块中,简单而言,所谓构建神经网络其实就是使用框架提供的各种Operator来操作数据,不必想的过于复杂。一个简单的加法运算如下:import paddle.fluid as fluid #输入层 a = fluid.layers.data(name='a', shape=[2], dtype='float32') b = fluid.layers.data(name='b', shape=[2], dtype='float32') result = fluid.layers.elementwise_add(a,b) cpu = fluid.CPUPlace() # 定义运算场所 exe = fluid.Executor(cpu) # 创建执行器 exe.run(fluid.default_startup_program()) # 网络参数初始化 # 准备数据 import numpy x = numpy.array([1,2]) y = numpy.array([2,3]) #执行计算 outs = exe.run( feed={'a':x,'b':y}, fetch_list=[a,b,result.name]) #查看输出结果 print(outs)
一开始使用
fluid.layers.data()
定义数据输入层,具体的数据通过numpy.array()
生成,因为fluid.layers.data()
定义了shape=[2],那么此时numpy.array()需要定义成[1,2],即第一维的形状为2,如果shape=[1,2],那么第一维的形状为1,第二维的形状为2,即numpy.array()需要定义成[[1,2]]。动态图机制
Fluid使用的是动态图机制,因为静态图机制让使用者在编写过程中丧失了对网络结构修改的灵活性,所以很多优秀的框架都引入了动态图这种设计(TensorFlow 2.0动态图机制也大幅加强了)。
动态图 vs 静态图
动态计算意味着程序将按照我们编写命令的顺序进行执行。这种机制将使得调试更加容易,并且也使得我们将大脑中的想法转化为实际代码变得更加容易。而静态计算则意味着程序在编译执行时将先生成神经网络的结构,然后再执行相应操作。从理论上讲,静态计算这样的机制允许编译器进行更大程度的优化,但是这也意味着你所期望的程序与编译器实际执行之间存在着更多的代沟。这也意味着,代码中的错误将更加难以发现(比如,如果计算图的结构出现问题,你可能只有在代码执行到相应操作的时候才能发现它)。尽管理论上而言,静态计算图比动态计算图具有更好的性能,但是在实践中我们经常发现并不是这样的。
来源:动态图 vs 静态图
Program描述模型
Fluid版本的Paddle中使用Program的形式描述计算过程,开发者所有写入在Program中的Operator操作都会自动转为ProgramDesc描述语言。
Fluid通过提供顺序、分支和循环三种执行结构的支持
其中顺序执行,写法与静态图的形式没什么差别,如下:
# 顺序执行的方式搭建网络 x = fluid.layers.data(name='x', shape=[13], dtype='float32') y_predict = fluid.layers.fc(input=x, size=1, act=None) y = fluid.layers.data(name='y', shape=[1], dtype='float32') cost = fluid.layers.square_error_cost(input=y_predict, label=y)
分支条件switch的使用如下:
# 条件分支——switch、if else lr = fluid.layers.tensor.create_global_var( shape=[1], value=0.0, dtype='float32', persistable=True, name='learning_rate' ) one_var = fluid.layers.fill_constant( shape=[1], dtype='float32', value=1.0 ) two_var = fluid.layers.fill_constant( shape=[1], dtype='float32', value=2.0 ) # switch with fluid.layers.control_flow.Switch() as switch: with switch.case(global_step == one_var): fluid.layers.tensor.assign(input=one_var, output=lr) with switch.default(): fluid.layers.tensor.assign(input=two_var, output=lr)
其中流程控制方面的内容都放在了
fluid.layers.control_flow
模块下,里面包含了While、Block、Conditional、Switch、if、ifelse等跟中操作,这样可以让在编写模型时,想编写普通的python程序一样。Executor执行Program
Program相当于你模型的整体结构,Fluid中程序执行分为编译与执行两个阶段,当你定义编写完Program后,还需要定义Executor,Executor会接收Program,然后将其转为FluidProgram,这一步称为编译,然后再使用C++编写的后端去执行它,执行的过程也由Executor完成,相关代码片段如下:
cpu = fluid.core.CPUPlace() exe = fluid.Executor(cpu) #执行器 exe.run(fluid.default_startup_program()) #初始化Program outs = exe.run( feed={'a':x, 'b':y}, fetch_list=[result.name] )
在Fluid中使用Executor.run来运行一段Program。
正式进行网络训练前,需先执行参数初始化。其中
defalut_startup_program
中定义了创建模型参数,输入输出,以及模型中可学习参数的初始化等各种操作。由于传入数据与传出数据存在多列,因此 fluid 通过 feed 映射定义数据的传输数据,通过 fetch_list 取出期望结果
整体实践
写过简单结构,预测一组数据,使用单个全连接层来实现预测,核心的逻辑就是喂养为模型一些训练数据,模型输出预测结果,该预测结果与真实结果直接会有个误差,作为损失,通过平方差损失来定义这两个损失,然后再最小化该损失,整体逻辑如下:
import paddle.fluid as fluid import numpy as np ''' 真实数据 ''' #训练数据 train_data = np.array([[1.0],[2.0],[3.0],[4.0]]).astype('float32') #真实标签 y_true = np.array([[2.0], [4.0], [6.0], [8.0]]).astype('float32') # 输入层,接受真实数据 x = fluid.layers.data(name='x', shape=[1], dtype='float32') y = fluid.layers.data(name='y', shape=[1], dtype='float32') # 模型结构,这里就一个简单的全连接层 y_predict = fluid.layers.fc(input=x, size=1, act=None) # 定义损失,真实标签值与模型预测值之间的损失 cost = fluid.layers.square_error_cost(input=y_predict, label=y) #取平均,因为通常有batch个数据,取平均值作为损失则可 avg_cost = fluid.layers.mean(cost) # 定义执行设备,CUP或GPU cpu = fluid.CPUPlace() # Executor执行,该方法只能实现单设备执行 exe = fluid.Executor(cpu) # 初始化整个结构中的节点 exe.run(fluid.default_startup_program()) for i in range(100): outs = exe.run( feed = {'x': train_data, 'y': y_true}, #将真实数据喂养给模型,注意传入数据与输入层的关系 fetch_list=[y_predict.name, avg_cost.name] #获取列表,即执行完后,outs会获得的数据 ) print(outs)