导读:飞桨PaddlePaddle致力于让深度学习技术的创新与应用更简单。飞桨核心框架已提供了动态图(DyGraph)相关的API和文档,并且还附有Language model、Sentiment Classification、OCR、ResNet等模型的动态图版本官方实现。飞桨目前兼具了动态图和静态图的优势,同时具备灵活性和高效性。
飞桨动态图&静态图整体结构如下:
1. 动态图与静态图
目前深度学习框架主要有声明式编程和命令式编程两种编程方式。声明式编程,代码先描述要做的事情但不立即执行,对深度学习任务建模,需要事先定义神经网络的结构,然后再执行整个图结构,这一般称为静态图模式。而命令式编程对应的动态图模式,代码直接返回运算的结果,神经网络结构的定义和执行同步。通常来说,静态图模式能够对整体性做编译优化,更有利于性能的提升,而动态图则非常便于用户对程序进行调试。
2. 飞桨动态图的三大特色
飞桨的DyGraph模式是一种动态的图执行机制。与静态计算图的执行机制不同,DyGraph模式下的操作可以立即获得执行结果,而不必等待计算图全部构建完成。这样可以让开发者更加直观地构建深度学习任务并进行模型的调试,同时还减少了大量用于构建静态计算图的代码,使得编写、调试网络的过程变得非常便捷。
飞桨DyGraph动态图模式,主要有三大特色:
-
灵活便捷的代码书写方式:能够使用Python的控制流(for,if…else..等)进行编程。
-
便捷的调试功能:直接使用Python的打印方法即时打印所需要的结果,从而检查正在运行的模型结果便于调试。
-
和静态执行图通用的模型代码:对于没有使用Python控制流的网络,动态图的代码可以直接在静态图模式下执行,提升执行的效率。
3. 飞桨动态图与静态图的直观对比
让我们通过一个实际例子,直观地感受一下动态图与静态图在使用过程中的差异。
想要实现如下的功能:
(1) 如果inp1各元素之和小于inp2各元素之和,那么执行inp1与 inp2各元素对应相加。
(2) 如果inp1各元素之和大于等于inp2各元素之和,那么执行inp1与 inp2各元素对应相减。
如果使用飞桨动态图来实现的话,代码如下:
import paddle.fluid asfluid
import numpy as np
inp1 = np.random.rand(4, 3, 3)
inp2 = np.random.rand(4, 3, 3)
# dynamic graph
with fluid.dygraph.guard():
if np.sum(inp1) <np.sum(inp2):
x =fluid.layers.elementwise_add(inp1, inp2)
else:
x =fluid.layers.elementwise_sub(inp1, inp2)
dygraph_result = x.numpy()
在飞桨动态图的模式下,可以灵活复用(if…else…)等Python控制流操作,关键代码只需要短短6行,非常简单。
而如果换用静态图方式来实现的话,代码可就复杂多了。具体如下:
import paddle.fluid asfluid
import numpy as np
inp1 = np.random.rand(4, 3, 3)
inp2 = np.random.rand(4, 3, 3)
# static graph
with new_program_scope():
inp_data1 =fluid.layers.data(name='inp1', shape=[3, 3], dtype=np.float32)
inp_data2 =fluid.layers.data(name='inp2', shape=[3, 3], dtype=np.float32)
a =fluid.layers.expand(fluid.layers.reshape(fluid.layers.reduce_sum(inp_data1),[1, 1]), [4, 1])
b =fluid.layers.expand(fluid.layers.reshape(fluid.layers.reduce_sum(inp_data2),[1, 1]), [4, 1])
cond =fluid.layers.less_than(x=a, y=b)
ie =fluid.layers.IfElse(cond)
with ie.true_block():
d1 =ie.input(inp_data1)
d2 =ie.input(inp_data2)
d3 =fluid.layers.elementwise_add(d1, d2)
ie.output(d3)
with ie.false_block():
d1 =ie.input(inp_data1)
d2 =ie.input(inp_data2)
d3 =fluid.layers.elementwise_sub(d1, d2)
ie.output(d3)
out = ie()
exe =fluid.Executor(fluid.CPUPlace() if not core.is_compiled_with_cuda() elsefluid.CUDAPlace(0))
static_result =exe.run(fluid.default_main_program(),feed={'inp1': inp1,'inp2':inp2},fetch_list=out)[0]
怎么样?感受到差异了吗?
直观一点,直接看代码行数。
关键代码部分,静态图方式的代码行数有20行,而动态图方式仅需要短短的6行代码。代码量减少到1/3,逻辑复杂程度也大大简化。
这就是飞桨动态图在Python控制流操作复用和代码简洁性方面的优势。
除此之外,飞桨动态图还提供了非常便捷的调试功能,直接使用Python的打印方法,就可以即时打印出所需要的结果,从而检查正在运行的模型结果,非常方便调试。
4. 飞桨动态图的基本用法
飞桨动态图具有如此多的优势,下面讲述最基本的一些用法。
(1) 动态图与静态图的最大区别是采用了命令式的编程方式,任务不用在区分组网阶段和执行阶段。代码运行完成之后,可以立马获取结果。由于采用与我们书写大部分Python和c++的方式是一致的命令式编程方式,程序的编写和调试会非常的容易。
(2) 同时动态图能够使用Python的控制流,例如for,if else, switch等,对于rnn等任务的支持更方便。
(3) 动态图能够与numpy更好的交互。
使用飞桨动态图,首先需要将PaddlePaddle升级到最新的1.5.1版本,使用以下命令即可。
pip install -q --upgrade paddlepaddle==1.5.1
import paddle.fluid as fluid
with fluid.dygraph.guard():
这样就可以在fluid.dygraph.guard()上下文环境中使用动态图DyGraph的模式运行网络了。DyGraph将改变以往静态图的执行方式,开始运行之后会立即执行,并且将计算结果返回给Python。
Dygraph非常适合和Numpy一起使用,使用fluid.dygraph.to_variable(x)将会将Numpy的ndarray转换为fluid.Variable,而使用fluid.Variable.numpy()将可以把任意时刻获取到的计算结果转换为Numpy ndarray,举例如下:
import paddle.fluid asfluid
import numpy as np
x = np.ones([10, 2, 2], np.float32)
with fluid.dygraph.guard():
inputs = []
seq_len = x.shape[0]
for i in range(seq_len):
inputs.append(fluid.dygraph.to_variable(x[i]))
ret =fluid.layers.sums(inputs)
print(ret.numpy())
得到输出:
[[10. 10.]
[10. 10.]]
以上代码根据输入x的第0维的长度、将x拆分为多个ndarray的输入,执行了一个sum操作之后,可以直接将运行的结果打印出来。然后通过调用reduce_sum后使用Variable.backward()方法执行反向,使用Variable.gradient()方法即可获得反向网络执行完成后的梯度值的ndarray形式:
loss =fluid.layers.reduce_sum(ret)
loss.backward()
print(loss.gradient())
得到输出 :
[1.]
5. 飞桨动态图的项目实战
下面以“手写数字识别”为例讲解一个动态图实战案例,手写体识别是一个非常经典的图像识别任务,任务中的图片如下图所示,根据一个28 * 28像素的图像,识别图片中的数字。
MNIST示例代码地址:
https://github.com/PaddlePaddle/models/tree/develop/dygraph/mnist
介绍网络训练的基本结构,也比较简单,两组conv2d和pool2d层,最后一个输出的全连接层。
飞桨动态图模式下搭建网络并训练模型的全过程主要包含以下内容:
5.1 数据准备
首先使用paddle.dataset.mnist作为训练所需要的数据集:飞桨把一些公开的数据集进行了封装,用户可以通过dataset.mnist接口直接调用mnist数据集,train()返回训练数据的reader,test()接口返回测试的数据的reader。
train_reader = paddle.batch(paddle.dataset.mnist.train(),batch_size=BATCH_SIZE, drop_last=True)
5.2 Layer定义
为了能够支持更复杂的网络搭建,动态图引入了Layer模块,每个Layer是一个独立的模块,Layer之间又可以互相嵌套。
用户需要关注的是,a)Layer存储的状态,包含一些隐层维度、需要学习的参数等;b)包含的sub Layer,为了方便大家使用,飞桨提供了一些定制好的Layer结构,如果Conv2D,Pool2D,FC等。c) 前向传播的函数,这个函数中定义了图的运行结构,这个函数与静态图的网络搭建是完全不一样的概念,函数只是描述了运行结构,在函数被调用的时候代码才执行,静态图的网络搭建是代码真正在执行。
Conv2D是飞桨提供的卷积运算的Layer,Pool2D是池化操作的Layer。
1)定义SimpleImgConvPool 子Layer:SimpleImgConvPool把网络中循环使用的部分进行整合,其中包含包含了两个子Layer,Conv2D和Pool2D,forward函数定义了前向运行时的结构。
class SimpleImgConvPool(fluid.dygraph.Layer)
def __init__(self,name_scope, num_filters, filter_size, pool_size, pool_stride, pool_padding=0, pool_type='max',global_pooling=False, conv_stride=1, conv_padding=0, conv_dilation=1, conv_groups=1,act=None, use_cudnn=False, param_attr=None, bias_attr=None):
super(SimpleImgConvPool,self).__init__(name_scope)
self._conv2d =fluid.dygraph.Conv2D(self.full_name(), num_filters=num_filters, filter_size=filter_size,stride=conv_stride,padding=conv_padding, dilation=conv_dilation, groups=conv_groups,aram_attr=None, bias_attr=None, act=act, use_cudnn=use_cudnn)
self._pool2d =fluid.dygraph.Pool2D(self.full_name(), pool_size=pool_size, pool_type=pool_type,pool_stride=pool_stride, pool_padding=pool_padding, global_pooling=global_pooling,use_cudnn=use_cudnn)
def forward(self,inputs):
x =self._conv2d(inputs)
x = self._pool2d(x)
return x
2)构建MNIST Layer,MNIST Layes包含了两个SimpleImgConvPool子Layer,以及一个FC(全连接层),forward函数定义了如图2所示得网络结构
class MNIST(fluid.dygraph.Layer):
def __init__(self,name_scope):
super(MNIST,self).__init__(name_scope)
self._simple_img_conv_pool_1 = SimpleImgConvPool(self.full_name(), 20,5, 2, 2, act="relu")
self._simple_img_conv_pool_2 = SimpleImgConvPool(self.full_name(), 50,5, 2, 2, act="relu")
pool_2_shape = 50 *4 * 4
SIZE = 10
scale = (2.0 / (pool_2_shape**2 *SIZE))**0.5
self._fc =fluid.dygraph.FC(self.full_name(),10, param_attr=fluid.param_attr.ParamAttr(initializer=fluid.initializer.NormalInitializer(loc=0.0, scale=scale)),act="softmax")
def forward(self, inputs,label=None):
x =self._simple_img_conv_pool_1(inputs)
x =self._simple_img_conv_pool_2(x)
x = self._fc(x)
if label is notNone:
acc =fluid.layers.accuracy(input=x, label=label)
return x, acc
else:
return x
5.3 优化器定义
使用经典的Adam优化算法:
adam =fluid.optimizer.AdamOptimizer(learning_rate=0.001)
5.4 训练
构建训练循环,顺序为:1).从reader读取数据 2).调用MNIST Layer 前向网络3).利用cross_entropy计算loss 4)调用backward计算梯度 5)调用adam.minimize更新梯度,6) clear_gradients()将梯度设置为0(这种方案是为了支持backward of backward功能,如果系统自动将梯度置为0,则无法使用backward of backward功能)
with fluid.dygraph.guard():
epoch_num = 5
BATCH_SIZE = 64
mnist =MNIST("mnist")
adam =fluid.optimizer.AdamOptimizer(learning_rate=0.001)
train_reader =paddle.batch(paddle.dataset.mnist.train(), batch_size= BATCH_SIZE,drop_last=True)
np.set_printoptions(precision=3,suppress=True)
for epoch inrange(epoch_num):
for batch_id, data inenumerate(train_reader()):
dy_x_data = np.array(
[x[0].reshape(1, 28, 28)
for x indata]).astype('float32')
y_data =np.array(
[x[1] for xin data]).astype('int64').reshape(BATCH_SIZE, 1)
img =fluid.dygraph.to_variable(dy_x_data)
label =fluid.dygraph.to_variable(y_data)
label.stop_gradient = True
cost =mnist(img)
loss =fluid.layers.cross_entropy(cost, label)
avg_loss =fluid.layers.mean(loss)
dy_out =avg_loss.numpy()
avg_loss.backward()
adam.minimize(avg_loss)
mnist.clear_gradients()
dy_param_value ={}
for param inmnist.parameters():
dy_param_value[param.name] = param.numpy()
if batch_id % 20== 0:
print("Loss at step {}: {}".format(batch_id,avg_loss.numpy()))
5.5 预测
预测的目标是为了在训练的同时,了解一下在开发集上模型的表现情况,由于动态图的训练和预测使用同一个Layer,有一些op(比如dropout)在训练和预测时表现不一样,用户需要切换到预测的模式,通过 .eval()接口进行切换(注:训练的时候需要切回到训练的模式)
预测代码如下图所示:
def test_mnist(reader, model, batch_size):
acc_set = []
avg_loss_set = []
for batch_id, data in enumerate(reader()):
dy_x_data = np.array([x[0].reshape(1, 28, 28) for x indata]).astype('float32')
y_data = np.array([x[1] for x indata]).astype('int64').reshape(batch_size, 1)
img = to_variable(dy_x_data)
label = to_variable(y_data)
label.stop_gradient = True
prediction, acc = model(img, label)
loss = fluid.layers.cross_entropy(input=prediction, label=label)
avg_loss = fluid.layers.mean(loss)
acc_set.append(float(acc.numpy()))
avg_loss_set.append(float(avg_loss.numpy()))
# get test acc and loss
acc_val_mean = np.array(acc_set).mean()
avg_loss_val_mean = np.array(avg_loss_set).mean()
return avg_loss_val_mean, acc_val_mean
最终可以通过打印数据自行绘制Loss曲线:
5.6 调试
调试是我们在搭建网络时候非常重要的功能,动态图由于是命令式编程,用户可以直接利用python的print打印变量,通过print( tensor.numpy() ) 直接打印tensor的值。在执行了backward之后,用户可以通过print(tensor.gradient) 打印反向的梯度值。
这样,一个简单的动态图的实例就完成了,亲爱的开发者们,你们学会了么?
想与更多的深度学习开发者交流,请加入飞桨官方QQ群:432676488。
想了解更多内容:
-
官网地址:https://www.paddlepaddle.org.cn/
-
动态图代码地址:
https://github.com/PaddlePaddle/models/tree/v1.5.1/dygraph
来源:oschina
链接:https://my.oschina.net/u/4067628/blog/3206759