都9102年了还不懂动态图吗?一文带你了解飞桨动态图

导读:飞桨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

猜你喜欢

转载自blog.csdn.net/PaddlePaddle/article/details/100059492