专栏首页PaddlePaddle都9102年了还不懂动态图吗?一文带你了解飞桨动态图

都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) 打印反向的梯度值。

这样,一个简单的动态图的实例就完成了,亲爱的开发者们,你们学会了么?

本文分享自微信公众号 - PaddlePaddle(PaddleOpenSource)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-08-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 数据可视化-Matplotlib使用子图绘制数据

    今天我们将学习如何在Matplotlib中使用子图。使用子图,以便我们可以以更面向对象的方式使用Matplotlib。我们将学习如何使用子图来绘制我们在之前的文...

    亚乐记
  • 最赚钱的程序语言揭晓:Clojure工程师领高薪,区块链反被看衰?

    工程师至今依是众人心中的明星职业,不过工程师也有许多方向,在加入这个行业前,了解当前趋势与业界人士想法可说是重要的前提功课。汇集全球5千万名开发者的问答网站St...

    用户5997198
  • ​测试开发进阶(二)

    https://pythonguidecn.readthedocs.io/zh/latest/writing/structure.html

    zx钟
  • Python之pygame学习(2)

    这个函数创建一个显示画面,参数用来指定请求display的类型。实际创建的display会是系统支持的类型中与请求的尽可能一致的类型。

    萌海无涯
  • Python高手必修课:如何让 Python 代码更易读,推荐收藏

    我们知道 Python 是一种动态语言,在声明一个变量时我们不需要显式地声明它的类型,例如下面的例子:

    叫我龙总
  • 懒人福音——渗透测试单行化

    One-Lin3r是一个简单的轻量级框架,它的设计灵感来源于Metasploit中的web-delivery模块。

    网e渗透安全部
  • 测试开发进阶(一)

    PEP257:https://github.com/qiuxiang/pep/blob/master/peps/257.md

    zx钟
  • Python urllib HTTP头注入漏洞

    Python的urllib库(在Python2中为urllib2,在Python3中为urllib)有一个HTTP协议下的协议流注入漏洞。如果攻击者可以控制Py...

    网e渗透安全部
  • Python入门教程:超详细1小时学会Python

    安装完Python之后,打开IDLE(Python GUI) ,该程序是Python语言解释器,你写的语句能够立即运行。

    马哥linux运维
  • 用Python给女友 准备个绝对甜蜜的七夕礼物

    如果我们把这些甜言蜜语都收集起来,做成一个心爱的甜心,在七夕节的当天送给自己的另一半。这样既用心,而且还很甜蜜的操作,是不是很酷!说干就干,接下来,小安就带领大...

    叫我龙总

扫码关注云+社区

领取腾讯云代金券