DyGraph.md 50.3 KB
Newer Older
D
Daniel Yang 已提交
1
# 命令式编程使用教程
J
Jiabin Yang 已提交
2

J
JiabinYang 已提交
3

J
JiabinYang 已提交
4

D
Daniel Yang 已提交
5
从编程范式上说,飞桨兼容支持声明式编程和命令式编程,通俗地讲即静态图和动态图。其实飞桨本没有图的概念,在飞桨的设计中,把一个神经网络定义成一段类似程序的描述,也就是用户在写程序的过程中,就定义了模型表达及计算。在静态图的控制流实现方面,飞桨借助自己实现的控制流OP而不是python原生的if else和for循环,这使得在飞桨中的定义的program即一个网络模型,可以有一个内部的表达,是可以全局优化编译执行的。考虑对开发者来讲,更愿意使用python原生控制流,飞桨也做了支持,并通过解释方式执行,这就是命令式编程模式。但整体上,这两种编程范式是相对兼容统一的。飞桨将持续发布更完善的命令式编程功能,同时保持更强劲的性能。
J
JiabinYang 已提交
6

D
Daniel Yang 已提交
7 8 9
飞桨平台中,将神经网络抽象为计算表示**Operator**(算子,常简称OP)和数据表示**Variable**(变量),如 图1 所示。神经网络的每层操作均由一个或若干**Operator**组成,每个**Operator**接受一系列的**Variable**作为输入,经计算后输出一系列的**Variable**
<center><img src="https://ai-studio-static-online.cdn.bcebos.com/15197499f49840fcb43a38d19d9c729e19f3a7bf5ae5432a8eeca083ac4e02b7" width="600" ></center>
<br>
J
JiabinYang 已提交
10

D
Daniel Yang 已提交
11 12 13 14
<center>图1 Operator和Variable关系示意图</center>
根据**Operator**解析执行方式不同,飞桨支持如下两种编程范式:
* **声明式编程模式(静态图)**:先编译后执行的方式。用户需预先定义完整的网络结构,再对网络结构进行编译优化后,才能执行获得计算结果。
* **命令式编程模式(动态图)**:解析式的执行方式。用户无需预先定义完整的网络结构,每写一行网络代码,即可同时获得计算结果。
J
JiabinYang 已提交
15

D
Daniel Yang 已提交
16
举例来说,假设用户写了一行代码:y=x+1,在静态图模式下,运行此代码只会往计算图中插入一个Tensor加1的**Operator**,此时**Operator**并未真正执行,无法获得y的计算结果。但在动态图模式下,所有**Operator**均是即时执行的,运行完此代码后**Operator**已经执行完毕,用户可直接获得y的计算结果。
17

D
Daniel Yang 已提交
18
## 为什么命令式编程模式越来越流行?
19

D
Daniel Yang 已提交
20 21 22 23
声明式编程模式作为较早提出的一种编程范式,提供丰富的 API ,能够快速的实现各种模型;并且可以利用全局的信息进行图优化,优化性能和显存占用;在预测部署方面也可以实现无缝衔接。 但具体实践中声明式编程模式存在如下问题:
1. 采用先编译后执行的方式,组网阶段和执行阶段割裂,导致调试不方便。
2. 属于一种符号化的编程方式,要学习新的编程方式,有一定的入门门槛。
3. 网络结构固定,对于一些树结构的任务支持的不够好。
24

D
Daniel Yang 已提交
25 26 27 28
命令式编程模式的出现很好的解决了这些问题,存在以下优势:
1. 代码运行完成后,可以立马获取结果,支持使用 IDE 断点调试功能,使得调试更方便。
2. 属于命令式的编程方式,与编写Python的方式类似,更容易上手。
3. 网络的结构在不同的层次中可以变化,使用更灵活。
29

30

D
Daniel Yang 已提交
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
综合以上优势,使得命令式编程模式越来越受开发者的青睐,本章侧重介绍在飞桨中命令式编程方法,包括如下几部分:
1. 如何开启命令式编程模式
2. 如何使用命令式编程进行模型训练
3. 如何基于命令式编程进行多卡训练
4. 如何部署命令式编程模型
5. 命令式编程模式常见的使用技巧,如中间变量值/梯度打印、断点调试、阻断反向传递,以及某些场景下如何改写为静态图模式运行。


## 1. 开启命令式编程模式

目前飞桨默认的模式是静态图,采用基于 context (上下文)的管理方式开启动态图模式:
```
with fluid.dygraph.guard()
```

我们先通过一个实例,观察一下动态图模式开启前后执行方式的差别:

48

49
```python
D
Daniel Yang 已提交
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
import numpy as np
import paddle.fluid as fluid
from paddle.fluid.dygraph.base import to_variable

main_program = fluid.Program()
startup_program = fluid.Program()
with fluid.program_guard(main_program=main_program, startup_program=startup_program):
    # 利用np.ones函数构造出[2*2]的二维数组,值为1
    data = np.ones([2, 2], np.float32)

    # 静态图模式下,使用layers.data构建占位符用于数据输入
    x = fluid.layers.data(name='x', shape=[2], dtype='float32')
    print('In static mode, after calling layers.data, x = ', x)
    # 静态图模式下,对Variable类型的数据执行x=x+10操作
    x += 10
    # 在静态图模式下,需要用户显示指定运行设备
    # 此处调用fluid.CPUPlace() API来指定在CPU设备上运行程序
    place = fluid.CPUPlace()
    # 创建“执行器”,并用place参数指明需要在何种设备上运行
    exe = fluid.Executor(place=place)
    # 初始化操作,包括为所有变量分配空间等,比如上面的‘x’,在下面这行代码执行后才会被分配实际的内存空间
    exe.run(fluid.default_startup_program())
    # 使用执行器“执行”已经记录的所有操作,在本例中即执行layers.data、x += 10操作
    # 在调用执行器的run接口时,可以通过fetch_list参数来指定要获取哪些变量的计算结果,这里我们要获取‘x += 10’执行完成后‘x’的结果;
    # 同时也可以通过feed参数来传入数据,这里我们将data数据传递给‘fluid.layers.data’指定的‘x’。
    data_after_run = exe.run(fetch_list=[x], feed={'x': data})
    # 此时我们打印执行器返回的结果,可以看到“执行”后,Tensor中的数据已经被赋值并进行了运算,每个元素的值都是11
    print('In static mode, data after run:', data_after_run)

# 开启动态图模式
80
with fluid.dygraph.guard():
D
Daniel Yang 已提交
81 82 83 84 85 86 87 88
    # 动态图模式下,将numpy的ndarray类型的数据转换为Variable类型
    x = fluid.dygraph.to_variable(data)
    print('In DyGraph mode, after calling dygraph.to_variable, x = ', x)
    # 动态图模式下,对Variable类型的数据执行x=x+10操作
    x += 10
    # 动态图模式下,调用Variable的numpy函数将Variable类型的数据转换为numpy的ndarray类型的数据
    print('In DyGraph mode, data after run:', x.numpy())

89 90
```

D
Daniel Yang 已提交
91 92 93 94 95 96 97 98 99 100 101 102 103
    In static mode, after calling layers.data, x =  name: "x"
    type {
      type: LOD_TENSOR
      lod_tensor {
        tensor {
          data_type: FP32
          dims: -1
          dims: 2
        }
        lod_level: 0
      }
    }
    persistable: false
104

D
Daniel Yang 已提交
105 106 107 108 109 110 111
    In static mode, data after run: [array([[11., 11.],
           [11., 11.]], dtype=float32)]
    In DyGraph mode, after calling dygraph.to_variable, x =  name generated_var_0, dtype: VarType.FP32 shape: [2, 2] 	lod: {}
    	dim: 2, 2
    	layout: NCHW
    	dtype: float
    	data: [1 1 1 1]
112

D
Daniel Yang 已提交
113 114
    In DyGraph mode, data after run: [[11. 11.]
     [11. 11.]]
115 116


D
Daniel Yang 已提交
117 118
    /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/fluid/executor.py:804: UserWarning: There are no operators in the program to be executed. If you pass Program manually, please use fluid.program_guard to ensure the current Program is being used.
      warnings.warn(error_info)
119 120


D
Daniel Yang 已提交
121 122 123
从以上输出结果可以看出:
* 动态图模式下,所有操作在运行时就已经完成,更接近我们平时的编程方式,可以随时获取每一个操作的执行结果。
* 静态图模式下,过程中并没有实际执行操作,上述例子中可以看到只能打印声明的类型,最后需要调用执行器来统一执行所有操作,计算结果需要通过执行器统一返回。
124

D
Daniel Yang 已提交
125 126
##  2. 使用命令式编程进行模型训练
接下来我们以一个简单的手写体识别任务为例,说明如何使用飞桨的动态图来进行模型的训练。包括如下步骤:
127

D
Daniel Yang 已提交
128 129 130 131
* 2.1 定义数据读取器:读取数据和预处理操作。
* 2.2 定义模型和优化器:搭建神经网络结构。
* 2.3 训练:配置优化器、学习率、训练参数。循环调用训练过程,循环执行“前向计算 + 损失函数 + 反向传播”。
* 2.4 评估测试:将训练好的模型保存并评估测试。
J
JiabinYang 已提交
132

D
Daniel Yang 已提交
133 134
最后介绍一下:
* 2.5 模型参数的保存和加载方法。
J
JiabinYang 已提交
135

D
Daniel Yang 已提交
136 137
在前面章节我们已经了解到,“手写数字识别”的任务是:根据一个28 * 28像素的图像,识别图片中的数字。可采用MNIST数据集进行训练。
![](https://ai-studio-static-online.cdn.bcebos.com/f8ffb092f6354d8c9c0219224db0e87b5490c5715cc346cf87b7098b2c3c2069)
138

D
Daniel Yang 已提交
139
有关该任务和数据集的详细介绍,可参考:[初识飞桨手写数字识别模型](https://aistudio.baidu.com/aistudio/projectdetail/224342)
J
JiabinYang 已提交
140

D
Daniel Yang 已提交
141
### 2.1 定义数据读取器
142

D
Daniel Yang 已提交
143
飞桨提供了多个封装好的数据集API,本任务我们可以通过调用 [paddle.dataset.mnist](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/data/dataset_cn.html) 的 train 函数和 test 函数,直接获取处理好的 MNIST 训练集和测试集;然后调用 [paddle.batch](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/io_cn/batch_cn.html#batch) 接口返回 reader 的装饰器,该 reader 将输入 reader 的数据打包成指定 BATCH_SIZE 大小的批处理数据。
144

J
JiabinYang 已提交
145

D
Daniel Yang 已提交
146 147
```python
import paddle
J
JiabinYang 已提交
148

D
Daniel Yang 已提交
149 150
# 定义批大小
BATCH_SIZE = 64
151

D
Daniel Yang 已提交
152 153 154 155 156 157
# 通过调用paddle.dataset.mnist的train函数和test函数来构造reader
train_reader = paddle.batch(
    paddle.dataset.mnist.train(), batch_size=BATCH_SIZE, drop_last=True)
test_reader = paddle.batch(
    paddle.dataset.mnist.test(), batch_size=BATCH_SIZE, drop_last=True)
```
158

D
Daniel Yang 已提交
159 160
    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
J
JiabinYang 已提交
161

D
Daniel Yang 已提交
162 163 164 165 166 167 168
    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
169

D
Daniel Yang 已提交
170 171 172 173 174
    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
175

J
JiabinYang 已提交
176

D
Daniel Yang 已提交
177 178 179 180 181 182 183 184 185 186 187 188 189 190
### 2.2 定义模型和优化器

本节我们采用如下网络模型,该模型可以很好的完成“手写数字识别”的任务。模型由卷积层 -> 池化层 -> 卷积层 -> 池化层 -> 全连接层组成,池化层即降采样层。

![](https://ai-studio-static-online.cdn.bcebos.com/f9e59d727d68437aaaad8cee410e564c7a80063367bd4fcd9f710a1480ee338c)


在开始构建网络模型前,需要了解如下信息:

> <font size=2>在动态图模式中,参数和变量的存储管理方式与静态图不同。动态图模式下,网络中学习的参数和中间变量,生命周期和 Python 对象的生命周期是一致的。简单来说,一个 Python 对象的生命周期结束,相应的存储空间就会释放。</font>

对于一个网络模型,在模型学习的过程中参数会不断更新,所以参数需要在整个学习周期内一直保持存在,因此需要一个机制来保持网络的所有的参数不被释放,飞桨的动态图模式采用了继承自 [fluid.dygraph.Layer](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Layer_cn.html#layer) 的面向对象设计的方法来管理所有的参数,该方法也更容易模块化组织代码。

下面介绍如何通过继承 fluid.dygraph.Layers 实现一个简单的ConvPool层;该层由一个 [卷积层](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Conv2D_cn.html#conv2d) 和一个 [池化层](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Pool2D_cn.html#pool2d) 组成。
J
JiabinYang 已提交
191 192


193 194
```python
import paddle.fluid as fluid
D
Daniel Yang 已提交
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
from paddle.fluid.dygraph.nn import Conv2D, Pool2D

# 定义SimpleImgConvPool网络,必须继承自fluid.dygraph.Layer
# 该网络由一个卷积层和一个池化层组成
class SimpleImgConvPool(fluid.dygraph.Layer):
    # 在__init__构造函数中会执行变量的初始化、参数初始化、子网络初始化的操作
    # 本例中执行了Conv2D和Pool2D网络的初始化操作
    def __init__(self,
                 num_channels,
                 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__()

        # Conv2D网络的初始化
        self._conv2d = Conv2D(
            num_channels=num_channels,
            num_filters=num_filters,
            filter_size=filter_size,
            stride=conv_stride,
            padding=conv_padding,
            dilation=conv_dilation,
            groups=conv_groups,
            param_attr=None,
            bias_attr=None,
            act=act,
            use_cudnn=use_cudnn)

        # Pool2D网络的初始化
        self._pool2d = Pool2D(
            pool_size=pool_size,
            pool_type=pool_type,
            pool_stride=pool_stride,
            pool_padding=pool_padding,
            global_pooling=global_pooling,
            use_cudnn=use_cudnn)

    # forward函数实现了SimpleImgConvPool网络的执行逻辑
    def forward(self, inputs):
        x = self._conv2d(inputs)
        x = self._pool2d(x)
        return x
```
J
JiabinYang 已提交
250

D
Daniel Yang 已提交
251 252
可以看出实现一个 ConvPool 层(即SimpleImgConvPool)分为两个步骤:
1. 定义 \_\_init\_\_ 构造函数。
253

D
Daniel Yang 已提交
254
\_\_init\_\_ 构造函数中,通常会执行变量初始化、参数初始化、子网络初始化等操作,执行这些操作时不依赖于输入的动态信息。这里我们对子网络(卷积层和池化层)做了初始化操作。
255

D
Daniel Yang 已提交
256
2. 定义 forward 函数。
257

D
Daniel Yang 已提交
258
该函数负责定义网络运行时的执行逻辑,将会在每一轮训练/预测中被调用。上述示例中,forward 函数的逻辑是先执行一个卷积操作,然后执行一个池化操作。
259

260

D
Daniel Yang 已提交
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
接下来我们介绍如何利用子网络组合出MNIST网络,该网络由两个 SimpleImgConvPool 子网络和一个全连接层组成。


```python
# 定义MNIST网络,必须继承自fluid.dygraph.Layer
# 该网络由两个SimpleImgConvPool子网络、reshape层、matmul层、softmax层、accuracy层组成
class MNIST(fluid.dygraph.Layer):
    # 在__init__构造函数中会执行变量的初始化、参数初始化、子网络初始化的操作
    # 本例中执行了self.pool_2_shape变量、matmul层中参数self.output_weight、SimpleImgConvPool子网络的初始化操作
    def __init__(self):
        super(MNIST, self).__init__()
        self._simple_img_conv_pool_1 = SimpleImgConvPool(
            1, 20, 5, 2, 2, act="relu")
        self._simple_img_conv_pool_2 = SimpleImgConvPool(
            20, 50, 5, 2, 2, act="relu")

        # self.pool_2_shape变量定义了经过self._simple_img_conv_pool_2层之后的数据
        # 除了batch_size维度之外其他维度的乘积
        self.pool_2_shape = 50 * 4 * 4
        # self.pool_2_shape、SIZE定义了self.output_weight参数的维度
        SIZE = 10
        # 定义全连接层的参数
        self.output_weight = self.create_parameter(
            [self.pool_2_shape, 10])

    # forward函数实现了MNIST网络的执行逻辑
    def forward(self, inputs, label=None):
        x = self._simple_img_conv_pool_1(inputs)
        x = self._simple_img_conv_pool_2(x)
        x = fluid.layers.reshape(x, shape=[-1, self.pool_2_shape])
        x = fluid.layers.matmul(x, self.output_weight)
        x = fluid.layers.softmax(x)
        if label is not None:
            acc = fluid.layers.accuracy(input=x, label=label)
            return x, acc
        else:
            return x
```
299

D
Daniel Yang 已提交
300 301 302 303
在这个复杂的 Layer 的 \_\_init\_\_ 构造函数中,包含了更多基础的操作:
1. 变量的初始化:self.pool_2_shape = 50 * 4 * 4
2. 全连接层参数的创建,通过调用 [Layer](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Layer_cn.html#layer)[create_parameter](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Layer_cn.html#create_parameter) 接口:self.output_weight = self.create_parameter( [ self.pool_2_shape, 10])
3. 子 Layer 的构造:self._simple_img_conv_pool_1、self._simple_img_conv_pool_2
304

D
Daniel Yang 已提交
305
forward 函数的实现和 前面SimpleImgConvPool 类中的实现方式类似。
306

D
Daniel Yang 已提交
307
接下来定义MNIST类的对象,以及优化器。这里优化器我们选择 [AdamOptimizer](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/optimizer_cn/AdamOptimizer_cn.html#adamoptimizer) ,通过 [Layer](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Layer_cn.html#layer)[parameters](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Layer_cn.html#parameters) 接口来读取该网络的全部参数,实现如下:
308 309 310 311


```python
import numpy as np
D
Daniel Yang 已提交
312 313
from paddle.fluid.optimizer import AdamOptimizer
from paddle.fluid.dygraph.base import to_variable
314 315

with fluid.dygraph.guard():
D
Daniel Yang 已提交
316 317 318 319 320
    # 定义MNIST类的对象
    mnist = MNIST()
    # 定义优化器为AdamOptimizer,学习旅learning_rate为0.001
    # 注意动态图模式下必须传入parameter_list参数,该参数为需要优化的网络参数,本例需要优化mnist网络中的所有参数
    adam = AdamOptimizer(learning_rate=0.001, parameter_list=mnist.parameters())
321 322
```

D
Daniel Yang 已提交
323 324 325 326 327 328 329 330 331 332 333 334
### 2.3 训练

当我们定义好上述网络结构之后,就可以进行训练了。

实现如下:
* 数据读取:读取每批数据,通过 [to_variable](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/to_variable_cn.html#to-variable) 接口将 numpy.ndarray 对象转换为 [Variable](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/fluid_cn/Variable_cn.html#variable) 类型的对象。
* 网络正向执行:在正向执行时,用户构造出img和label之后,可利用类似函数调用的方式(如:mnist(img, label))传递参数执行对应网络的 forward 函数。
* 计算损失值:根据网络返回的计算结果,计算损失值,便于后续执行反向计算。
* 执行反向计算:需要用户主动调用 [backward](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/fluid_cn/Variable_cn.html#backward) 接口来执行反向计算。
* 参数更新:调用优化器的 [minimize](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/optimizer_cn/AdamOptimizer_cn.html#minimize) 接口对参数进行更新。
* 梯度重置:将本次计算的梯度值清零,以便进行下一次迭代和梯度更新。
* 保存训练好的模型:通过 [Layer](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Layer_cn.html#layer)[state_dict](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Layer_cn.html#state_dict) 获取模型的参数;通过 [save_dygraph](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/save_dygraph_cn.html#save-dygraph) 对模型参数进行保存。
335 336 337 338


```python
import numpy as np
D
Daniel Yang 已提交
339 340
from paddle.fluid.optimizer import AdamOptimizer
from paddle.fluid.dygraph.base import to_variable
341 342

with fluid.dygraph.guard():
D
Daniel Yang 已提交
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466
    # 定义MNIST类的对象
    mnist = MNIST()
    # 定义优化器为AdamOptimizer,学习旅learning_rate为0.001
    # 注意动态图模式下必须传入parameter_list参数,该参数为需要优化的网络参数,本例需要优化mnist网络中的所有参数
    adam = AdamOptimizer(learning_rate=0.001, parameter_list=mnist.parameters())

    # 设置全部样本的训练次数
    epoch_num = 5

    # 执行epoch_num次训练
    for epoch in range(epoch_num):
        # 读取训练数据进行训练
        for batch_id, data in enumerate(train_reader()):
            dy_x_data = np.array([x[0].reshape(1, 28, 28) for x in data]).astype('float32')
            y_data = np.array([x[1] for x in data]).astype('int64').reshape(-1, 1)

            #将ndarray类型的数据转换为Variable类型
            img = to_variable(dy_x_data)
            label = to_variable(y_data)

            # 网络正向执行
            cost, acc = mnist(img, label)

            # 计算损失值
            loss = fluid.layers.cross_entropy(cost, label)
            avg_loss = fluid.layers.mean(loss)
            # 执行反向计算
            avg_loss.backward()
            # 参数更新
            adam.minimize(avg_loss)
            # 将本次计算的梯度值清零,以便进行下一次迭代和梯度更新
            mnist.clear_gradients()

            # 输出对应epoch、batch_id下的损失值
            if batch_id % 100 == 0:
                print("Loss at epoch {} step {}: {:}".format(
                    epoch, batch_id, avg_loss.numpy()))

    # 保存训练好的模型
    model_dict = mnist.state_dict()
    fluid.save_dygraph(model_dict, "save_temp")
```

    Loss at epoch 0 step 0: [3.362183]
    Loss at epoch 0 step 100: [0.20108832]
    Loss at epoch 0 step 200: [0.1681692]
    Loss at epoch 0 step 300: [0.11894853]
    Loss at epoch 0 step 400: [0.13005154]
    Loss at epoch 0 step 500: [0.10004535]
    Loss at epoch 0 step 600: [0.11465541]
    Loss at epoch 0 step 700: [0.14584845]
    Loss at epoch 0 step 800: [0.21515566]
    Loss at epoch 0 step 900: [0.13847716]
    Loss at epoch 1 step 0: [0.03004131]
    Loss at epoch 1 step 100: [0.1855965]
    Loss at epoch 1 step 200: [0.07302501]
    Loss at epoch 1 step 300: [0.02016284]
    Loss at epoch 1 step 400: [0.03899964]
    Loss at epoch 1 step 500: [0.05415711]
    Loss at epoch 1 step 600: [0.09633664]
    Loss at epoch 1 step 700: [0.07155745]
    Loss at epoch 1 step 800: [0.13023862]
    Loss at epoch 1 step 900: [0.09051394]
    Loss at epoch 2 step 0: [0.00580437]
    Loss at epoch 2 step 100: [0.1506507]
    Loss at epoch 2 step 200: [0.03713503]
    Loss at epoch 2 step 300: [0.01145383]
    Loss at epoch 2 step 400: [0.0497771]


### 2.4 评估测试

模型训练完成,我们已经保存了训练好的模型,接下来进行评估测试。某些OP(如 dropout、batch_norm)需要区分训练模式和评估模式,以标识不同的执行状态。飞桨中OP默认采用的是训练模式(train mode),可通过如下方法切换:

 ```
model.eval()      #切换到评估模式
model.train()     #切换到训练模式
 ```


模型评估测试的实现如下:
* 首先定义 MNIST 类的对象 mnist_eval,然后通过 [load_dygraph](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/load_dygraph_cn.html#load-dygraph) 接口加载保存好的模型参数,通过 [Layer](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Layer_cn.html#layer)[set_dict](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Layer_cn.html#set_dict) 接口将参数导入到模型中,通过 [Layer](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Layer_cn.html#layer) 的 eval 接口切换到预测评估模式。
* 读取测试数据执行网络正向计算,进行评估测试,输出不同 batch 数据下损失值和准确率的平均值。


```python
with fluid.dygraph.guard():
    # 定义MNIST类的对象
    mnist_eval = MNIST()

    # 加载保存的模型
    model_dict, _ = fluid.load_dygraph("save_temp")
    mnist_eval.set_dict(model_dict)
    print("checkpoint loaded")

    # 切换到预测评估模式
    mnist_eval.eval()

    acc_set = []
    avg_loss_set = []
    # 读取测试数据进行评估测试
    for batch_id, data in enumerate(test_reader()):
        dy_x_data = np.array([x[0].reshape(1, 28, 28)
                              for x in data]).astype('float32')
        y_data = np.array(
            [x[1] for x in data]).astype('int64').reshape(-1, 1)

        # 将ndarray类型的数据转换为Variable类型
        img = to_variable(dy_x_data)
        label = to_variable(y_data)

        # 网络正向执行
        prediction, acc = mnist_eval(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()))

    # 输出不同 batch 数据下损失值和准确率的平均值
    acc_val_mean = np.array(acc_set).mean()
    avg_loss_val_mean = np.array(avg_loss_set).mean()
    print("Eval avg_loss is: {}, acc is: {}".format(avg_loss_val_mean, acc_val_mean))
467 468
```

D
Daniel Yang 已提交
469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500
### 2.5 模型参数的保存和加载

在动态图模式下,模型和优化器在不同的模块中,所以模型和优化器分别在不同的对象中存储,使得模型参数和优化器信息需分别存储。
因此模型的保存需要单独调用模型和优化器中的 state_dict() 接口,同样模型的加载也需要单独进行处理。

保存模型 :
1. 保存模型参数:首先通过 minist.state_dict 函数获取 mnist 网络的所有参数,然后通过 fluid.save_dygraph 函数将获得的参数保存至以 save_path 为前缀的文件中。
1. 保存优化器信息:首先通过 adam.state_dict 函数获取 adam 优化器的信息,然后通过  fluid.save_dygraph 函数将获得的参数保存至以 save_path 为前缀的文件中。
   * [Layer](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Layer_cn.html#layer)[state_dict](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Layer_cn.html#state_dict) 接口:该接口可以获取当前层及其子层的所有参数,并将参数存放在 dict 结构中。
   * [Optimizer](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/optimizer_cn/AdamOptimizer_cn.html#adamoptimizer) 的 state_dict 接口:该接口可以获取优化器的信息,并将信息存放在 dict 结构中。其中包含优化器使用的所有变量,例如对于 Adam 优化器,包括 beta1、beta2、momentum 等信息。注意如果该优化器的 minimize 函数没有被调用过,则优化器的信息为空。
   * [save_dygraph](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/save_dygraph_cn.html#save-dygraph) 接口:该接口将传入的参数或优化器的 dict 保存到磁盘上。
```
# 保存模型参数
1. fluid.save_dygraph(minist.state_dict(), “save_path”)
# 保存优化器信息
2. fluid.save_dygraph(adam.state_dict(), “save_path”)
```
加载模型:
1. 通过 fluid.load_dygraph 函数获取模型参数信息 model_state 和优化器信息 opt_state;
1. 通过 mnist.set_dict 函数用获取的模型参数信息设置 mnist 网络的参数
1. 通过 adam.set_dict 函数用获取的优化器信息设置 adam 优化器信息。
   * [Layer](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Layer_cn.html#layer)[set_dict](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Layer_cn.html#set_dict) 接口:该接口根据传入的 dict 结构设置参数,所有参数将由 dict 结构中的 Tensor 设置。
   * [Optimizer](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/optimizer_cn/AdamOptimizer_cn.html#adamoptimizer) 的 set_dict 接口:该接口根据传入的 dict 结构设置优化器信息,例如对于 Adam 优化器,包括 beta1、beta2、momentum 等信息。如果使用了 LearningRateDecay ,则 global_step 信息也将会被设置。
   * [load_dygraph](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/load_dygraph_cn.html#load-dygraph) 接口:该接口尝试从磁盘中加载参数或优化器的 dict 。
```
# 获取模型参数和优化器信息
1. model_state, opt_state= fluid.load_dygraph(“save_path”)
# 加载模型参数
2. mnist.set_dict(model_state)
# 加载优化器信息
3. adam.set_dict(opt_state)
```
501

J
JiabinYang 已提交
502

D
Daniel Yang 已提交
503
## 3. 多卡训练
504

D
Daniel Yang 已提交
505 506 507 508 509 510
针对数据量、计算量较大的任务,我们需要多卡并行训练,以提高训练效率。目前动态图模式可支持GPU的单机多卡训练方式,在动态图中多卡的启动和单卡略有不同,动态图多卡通过 Python 基础库 subprocess.Popen 在每一张 GPU 上启动单独的 Python 程序的方式,每张卡的程序独立运行,只是在每一轮梯度计算完成之后,所有的程序进行梯度的同步,然后更新训练的参数。

我们通过一个实例了解如何进行多卡训练:
><font size=2>由于AI Studio上未配置多卡环境,所以本实例需在本地构建多卡环境后运行。</font>

1. 本实例仍然采用前面定义的 MNIST 网络,可将前面定义的 SimpleImgConvPool、MNIST 网络结构、相关的库导入代码、以及下面多卡训练的示例代码拷贝至本地文件 train.py 中。
511

512 513

```python
D
Daniel Yang 已提交
514 515 516 517 518
import numpy as np
from paddle.fluid.optimizer import AdamOptimizer
from paddle.fluid.dygraph.base import to_variable

# 通过 Env() 的 dev_id 设置程序运行的设备
519 520
place = fluid.CUDAPlace(fluid.dygraph.parallel.Env().dev_id)
with fluid.dygraph.guard(place):
D
Daniel Yang 已提交
521
    # 准备多卡环境
522
    strategy = fluid.dygraph.parallel.prepare_context()
523 524 525 526
    epoch_num = 5
    BATCH_SIZE = 64
    mnist = MNIST()
    adam = fluid.optimizer.AdamOptimizer(learning_rate=0.001, parameter_list=mnist.parameters())
D
Daniel Yang 已提交
527
    # 数据并行模块
528 529 530 531
    mnist = fluid.dygraph.parallel.DataParallel(mnist, strategy)

    train_reader = paddle.batch(
        paddle.dataset.mnist.train(), batch_size=BATCH_SIZE, drop_last=True)
D
Daniel Yang 已提交
532
    # 数据切分
533
    train_reader = fluid.contrib.reader.distributed_batch_reader(
534
        train_reader)
535 536 537 538 539 540 541 542

    for epoch in range(epoch_num):
        for batch_id, data in enumerate(train_reader()):
            dy_x_data = np.array([x[0].reshape(1, 28, 28)
                                  for x in data]).astype('float32')
            y_data = np.array(
                [x[1] for x in data]).astype('int64').reshape(-1, 1)

543 544
            img = fluid.dygraph.to_variable(dy_x_data)
            label = fluid.dygraph.to_variable(y_data)
545 546
            label.stop_gradient = True

D
Daniel Yang 已提交
547
            # 网络正向执行
548 549
            cost, acc = mnist(img, label)

D
Daniel Yang 已提交
550
            # 计算损失值
551 552 553
            loss = fluid.layers.cross_entropy(cost, label)
            avg_loss = fluid.layers.mean(loss)

D
Daniel Yang 已提交
554
            # 单步训练:首先对 loss 进行归一化,然后计算单卡的梯度,最终将所有的梯度聚合
555 556 557
            avg_loss = mnist.scale_loss(avg_loss)
            avg_loss.backward()
            mnist.apply_collective_grads()
558

D
Daniel Yang 已提交
559
            # 参数更新
560
            adam.minimize(avg_loss)
D
Daniel Yang 已提交
561
            # 将本次计算的梯度值清零,以便进行下一次迭代和梯度更新
562
            mnist.clear_gradients()
D
Daniel Yang 已提交
563 564

            # 输出对应epoch、batch_id下的损失值
565 566 567
            if batch_id % 100 == 0 and batch_id is not 0:
                print("epoch: {}, batch_id: {}, loss is: {}".format(epoch, batch_id, avg_loss.numpy()))
```
568

D
Daniel Yang 已提交
569
2、飞桨动态图多进程多卡模型训练启动时,需要指定使用的 GPU,比如使用 0,1 卡,可执行如下命令启动训练:
570 571


572
```
D
Daniel Yang 已提交
573
CUDA_VISIBLE_DEVICES=0,1 python -m paddle.distributed.launch --log_dir ./mylog train.py
574
```
D
Daniel Yang 已提交
575 576
其中 log_dir 为存放 log 的地址,train.py 为程序名。
执行结果如下:
577

578 579 580 581 582 583
```
-----------  Configuration Arguments -----------
cluster_node_ips: 127.0.0.1
log_dir: ./mylog
node_ip: 127.0.0.1
print_config: True
D
Daniel Yang 已提交
584
selected_gpus: 0,1
585 586
started_port: 6170
training_script: train.py
D
Daniel Yang 已提交
587 588
training_script_args: []
use_paddlecloud: False
589
------------------------------------------------
D
Daniel Yang 已提交
590
trainers_endpoints: 127.0.0.1:6170,127.0.0.1:6171 , node_id: 0 , current_node_ip: 127.0.0.1 , num_nodes: 1 , node_ips: ['127.0.0.1'] , nranks: 2
591
```
592

D
Daniel Yang 已提交
593
此时,程序会将每个进程的输出 log 导出到 ./mylog 路径下,可以打开 workerlog.0 和 workerlog.1 来查看结果:
594

595 596 597 598
```
.
├── mylog
│   ├── workerlog.0
D
Daniel Yang 已提交
599
│   └── workerlog.1
600 601
└── train.py
```
602

D
Daniel Yang 已提交
603 604 605 606 607 608 609 610 611 612 613
总结一下,多卡训练相比单卡训练,有如下步骤不同:
1. 通过 Env() 的 dev_id 设置程序运行的设备。
```
place = fluid.CUDAPlace(fluid.dygraph.parallel.Env().dev_id)
with fluid.dygraph.guard(place):
```
2. 准备多卡环境。
```
strategy = fluid.dygraph.parallel.prepare_context()
```
3. 数据并行模块。
614

D
Daniel Yang 已提交
615
在数据并行的时候,我们需要存储和初始化一些多卡相关的信息,这些信息和操作放在 DataParallel 类中,使用的时候,我们需要利用 model(定义的模型) 和 strategy(第二步得到的多卡环境) 信息初始化 DataParallel。
616
```
D
Daniel Yang 已提交
617 618 619
mnist = fluid.dygraph.parallel.DataParallel(mnist, strategy)
```
4. 数据切分。
620

D
Daniel Yang 已提交
621
数据切分是一个非常重要的流程,是为了防止每张卡在每一轮训练见到的数据都一样,可以使用 distributed_batch_reader 对单卡的 reader 进行进行切分处理。 用户也可以其他的策略来达到数据切分的目的,比如事先分配好每张卡的数据,这样就可以使用单卡的 reader ,不使用 distributed_batch_reader。
622

D
Daniel Yang 已提交
623 624 625
```
train_reader = fluid.contrib.reader.distributed_batch_reader(train_reader)
```
626

D
Daniel Yang 已提交
627
5. 单步训练。
628

D
Daniel Yang 已提交
629 630 631 632 633 634 635
首先对 loss 进行归一化,然后计算单卡的梯度,最终将所有的梯度聚合。
```
avg_loss = mnist.scale_loss(avg_loss)
avg_loss.backward()
mnist.apply_collective_grads()
```
6. 模型保存。
636

D
Daniel Yang 已提交
637 638 639 640 641 642
和单卡不同,多卡训练时需逐个进程执行保存操作,多个进程同时保存会使模型文件格式出错。
```
if fluid.dygraph.parallel.Env().local_rank == 0:
	fluid.save_dygraph(mnist.state_dict, “path”)
```
7. 评估测试。
643

D
Daniel Yang 已提交
644
对模型进行评估测试时,如果需要加载模型,须确保评估和保存的操作在同一个进程中,否则可能出现模型尚未保存完成,即启动评估,造成加载出错的问题。如果不需要加载模型,则没有这个问题,在一个进程或多个进程中评估均可。
645

D
Daniel Yang 已提交
646
## 4. 模型部署
647

D
Daniel Yang 已提交
648 649 650 651 652 653 654 655
动态图虽然有非常多的优点,但是如果用户希望使用 C++ 部署已经训练好的模型,会存在一些不便利。比如,动态图中可使用 Python 原生的控制流,包含 if/else、switch、for/while,这些控制流需要通过一定的机制才能映射到 C++ 端,实现在 C++ 端的部署。

<ul><li>如果用户使用的 if/else、switch、for/while 与输入(包括输入的值和 shape )无关,则可以使用如下动态图模型部署方案:
<ul><li>使用 TracedLayer 将前向动态图模型转换为静态图模型。可以将动态图保存后做在线C++预测;除此以外,用户也可使用转换后的静态图模型在Python端做预测,通常比原先的动态图性能更好。</li>
<li>所有的TracedLayer对象均不应通过构造函数创建,而需通过调用静态方法 TracedLayer.trace(layer, inputs) 创建。</li>
<li>TracedLayer使用 Executor 和 CompiledProgram 运行静态图模型。</li></ul></li>

</ul>
656 657


D
Daniel Yang 已提交
658 659 660 661 662 663 664 665 666 667 668 669 670 671 672

```python
from paddle.fluid.dygraph import TracedLayer

with fluid.dygraph.guard():
    # 定义MNIST类的对象
    mnist = MNIST()
    in_np = np.random.random([10, 1, 28, 28]).astype('float32')
    # 将numpy的ndarray类型的数据转换为Variable类型
    input_var = fluid.dygraph.to_variable(in_np)
    # 通过 TracerLayer.trace 接口将动态图模型转换为静态图模型
    out_dygraph, static_layer = TracedLayer.trace(mnist, inputs=[input_var])
    save_dirname = './saved_infer_model'
    # 将转换后的模型保存
    static_layer.save_inference_model(save_dirname, feed=[0], fetch=[0])
673
```
J
JiabinYang 已提交
674

675

676
```python
D
Daniel Yang 已提交
677 678 679 680 681 682
# 静态图中需要使用执行器执行之前已经定义好的网络
place = fluid.CPUPlace()
exe = fluid.Executor(place)
program, feed_vars, fetch_vars = fluid.io.load_inference_model(save_dirname, exe)
# 静态图中需要调用执行器的run方法执行计算过程
fetch, = exe.run(program, feed={feed_vars[0]: in_np}, fetch_list=fetch_vars)
683
```
J
JiabinYang 已提交
684

D
Daniel Yang 已提交
685
以上示例中,通过 [TracerLayer.trace](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/TracedLayer_cn.html#trace) 接口来运行动态图模型并将其转换为静态图模型,该接口需要传入动态图的网络模型 mnist 和输入变量列表 [input_var];然后调用 [save_inference_model](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/TracedLayer_cn.html#save_inference_model) 接口将静态图模型保存为用于预测部署的模型,之后利用 [load_inference_model](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/io_cn/load_inference_model_cn.html) 接口将保存的模型加载,并使用 [Executor](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/executor_cn/Executor_cn.html#executor) 执行,检查结果是否正确。
J
JiabinYang 已提交
686

D
Daniel Yang 已提交
687
[save_inference_model](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/api_cn/dygraph_cn/TracedLayer_cn.html#save_inference_model) 保存的下来的模型,同样可以使用 C++ 加载部署,具体的操作请参考:[C++ 预测 API介绍](https://www.paddlepaddle.org.cn/documentation/docs/zh/advanced_guide/inference_deployment/inference/native_infer.html)
688

D
Daniel Yang 已提交
689
* 如果任务中包含了依赖数据的控制流,比如下面这个示例中if条件的判断依赖输入的shape。针对这种场景,可以使用基于ProgramTranslator的方式转成静态图的program,通过save_inference_model 接口将静态图模型保存为用于预测部署的模型,之后利用 [load_inference_model](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/io_cn/load_inference_model_cn.html) 接口将保存的模型加载,并使用 [Executor](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/executor_cn/Executor_cn.html#executor) 执行,检查结果是否正确。
J
JiabinYang 已提交
690

D
Daniel Yang 已提交
691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709
保存的下来的模型,同样可以使用 C++ 加载部署,具体的操作请参考:[C++ 预测 API介绍](https://www.paddlepaddle.org.cn/documentation/docs/zh/advanced_guide/inference_deployment/inference/native_infer.html)


```python
with fluid.dygraph.guard():
    in_np = np.array([-2]).astype('int')
    # 将numpy的ndarray类型的数据转换为Variable类型
    input_var = fluid.dygraph.to_variable(in_np)
    # if判断与输入input_var的shape有关
    if input_var.shape[0] > 1:
        print("input_var's shape[0] > 1")
    else:
        print("input_var's shape[1] < 1")
```

* 针对依赖数据的控制流,解决流程如下 1. 添加declarative装饰器; 2. 利用ProgramTranslator进行转换

1) 添加declarative装饰器
首先需要对给MNist类的forward函数添加一个declarative 装饰器,来标记需要转换的代码块,(注:需要在最外层的class的forward函数中添加)
J
JiabinYang 已提交
710 711


712

713
```python
D
Daniel Yang 已提交
714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749
from paddle.fluid.dygraph.jit import declarative

# 定义MNIST网络,必须继承自fluid.dygraph.Layer
# 该网络由两个SimpleImgConvPool子网络、reshape层、matmul层、softmax层、accuracy层组成
class MNIST(fluid.dygraph.Layer):
    # 在__init__构造函数中会执行变量的初始化、参数初始化、子网络初始化的操作
    # 本例中执行了self.pool_2_shape变量、matmul层中参数self.output_weight、SimpleImgConvPool子网络的初始化操作
    def __init__(self):
        super(MNIST, self).__init__()
        self._simple_img_conv_pool_1 = SimpleImgConvPool(
            1, 20, 5, 2, 2, act="relu")
        self._simple_img_conv_pool_2 = SimpleImgConvPool(
            20, 50, 5, 2, 2, act="relu")

        # self.pool_2_shape变量定义了经过self._simple_img_conv_pool_2层之后的数据
        # 除了batch_size维度之外其他维度的乘积
        self.pool_2_shape = 50 * 4 * 4
        # self.pool_2_shape、SIZE定义了self.output_weight参数的维度
        SIZE = 10
        # 定义全连接层的参数
        self.output_weight = self.create_parameter(
            [self.pool_2_shape, 10])

    # forward函数实现了MNIST网络的执行逻辑
	@declarative
    def forward(self, inputs, label=None):
        x = self._simple_img_conv_pool_1(inputs)
        x = self._simple_img_conv_pool_2(x)
        x = fluid.layers.reshape(x, shape=[-1, self.pool_2_shape])
        x = fluid.layers.matmul(x, self.output_weight)
        x = fluid.layers.softmax(x)
        if label is not None:
            acc = fluid.layers.accuracy(input=x, label=label)
            return x, acc
        else:
            return x
750

D
Daniel Yang 已提交
751
```
752 753 754



D
Daniel Yang 已提交
755
2) 利用ProgramTranslator进行转换
756 757 758



D
Daniel Yang 已提交
759 760
```python
from paddle.fluid.dygraph.dygraph_to_static import ProgramTranslator
761 762

with fluid.dygraph.guard():
D
Daniel Yang 已提交
763
    prog_trans = fluid.dygraph.ProgramTranslator()
764
    mnist = MNIST()
765

D
Daniel Yang 已提交
766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846
    in_np = np.random.random([10, 1, 28, 28]).astype('float32')
    label_np = np.random.randint(0, 10, size=(10,1)).astype( "int64")
    input_var = fluid.dygraph.to_variable(in_np)
    label_var = flui.dyraph.to_variable(label_np)

    out = mnist( input_var, label_var)

    prog_trans.save_inference_model("./mnist_dy2stat", fetch=[0,1])
```

## 5. 使用技巧

### 5.1 中间变量值、梯度打印

1. 用户想要查看任意变量的值,可以使用 [numpy](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/fluid_cn/Variable_cn.html#numpy) 接口。

```
x = y * 10
print(x.numpy())
```

来直接打印变量的值

2. 查看反向的值
可以在执行了 backward 之后,可以通过 [gradient](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/fluid_cn/Variable_cn.html#gradient) 接口来看任意变量的梯度

```
x = y * 10
x.backward()
print(y.gradient())
```

可以直接打印反向梯度的值

### 5.2 断点调试

因为动态图采用了命令似的编程方式,程序在执行之后,可以立马获取到执行的结果,因此在动态图中,用户可以利用IDE提供的断点调试功能,通过查 Variable 的 shape、真实值等信息,有助于发现程序中的问题。

1. 如下图所示,在示例程序中设置两个断点,执行到第一个断点的位置,我们可以观察变量 x 和 linear1 的信息。

![](https://ai-studio-static-online.cdn.bcebos.com/b9bade026bea4ae797d26dcd4590452d0d563574df6b4e1cbedd0645dcbcb349)
![](https://ai-studio-static-online.cdn.bcebos.com/c2a9096e653044849b98d94758a4ac3a77025351c1134453b2c8d18dc8ad8a73)

2. 同时可以观察 linear1 中的权重值。

![](https://ai-studio-static-online.cdn.bcebos.com/e46576c64de84fa780830e1146afda0acc67fb20ea43452dadfc4949a3aad684)
![](https://ai-studio-static-online.cdn.bcebos.com/c00a6152805a492485ba0bdde773b2ac7f544f56a0364038aa2d0681ed8d0483)
![](https://ai-studio-static-online.cdn.bcebos.com/f9bc8a52eaa24181a6a6832e992feb9e726afa17764146c38fd69e8d008e7994)



### 5.3 使用声明式编程模式运行

动态图虽然有友好编写、易于调试等功能,但是动态图中需要频繁进行 Python 与 C++ 交互,会导致一些任务在动态图中运行比静态图慢,根据经验,这类任务中包含了很多小粒度的 OP(指运算量相对比较小的 OP,如加减乘除、sigmoid 等,像 conv、matmul 等属于大粒度的 OP不在此列 )。

在实际任务中,如果发现这类任务运行较慢,有以下两种处理方式:
* 1. 当用户使用的 if/else、switch、for/while 与输入(包括输入的值和 shape )无关时,可以在不改动模型定义的情况下使用静态图的模式运行。该方法将模型训练改为了静态图模式,区别于第4小节仅预测部署改为了静态图模式。
* 2. 如果使用了与输入相关的控制流,请参照[如何把动态图转写成静态图](https://www.paddlepaddle.org.cn/tutorials/projectdetail/360460#anchor-3)章节,将动态图代码进行转写。


下面我们介绍上面的第一种方案,仍然以手写字体识别任务为例,在静态图模式下的实现如下:


```python
# 设置全部样本训练次数(epoch_num)、批大小(BATCH_SIZE)。
epoch_num = 1
BATCH_SIZE = 64

main_program = fluid.Program()
startup_program = fluid.Program()

with fluid.program_guard(main_program=main_program, startup_program=startup_program):
    # 静态图中需要使用执行器执行之前已经定义好的网络
    exe = fluid.Executor(fluid.CPUPlace())

    # 定义MNIST类的对象,可以使用动态图定义好的网络结构
    mnist_static = MNIST()
    # 定义优化器对象,静态图模式下不需要传入parameter_list参数
    sgd_static = fluid.optimizer.SGDOptimizer(learning_rate=1e-3)

    # 通过调用paddle.dataset.mnist的train函数,直接获取处理好的MNIST训练集
847
    train_reader = paddle.batch(
D
Daniel Yang 已提交
848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865
        paddle.dataset.mnist.train(), batch_size=BATCH_SIZE, drop_last=True)

    # 静态图需要明确定义输入变量,即“占位符”,在静态图组网阶段并没有读入数据,所以需要使用占位符指明输入数据的类型,shape等信息
    img_static = fluid.data(
        name='pixel', shape=[None, 1, 28, 28], dtype='float32')
    label_static = fluid.data(name='label', shape=[None, 1], dtype='int64')

    # 调用网络,执行前向计算
    cost_static = mnist_static(img_static)
    # 计算损失值
    loss_static = fluid.layers.cross_entropy(cost_static, label_static)
    avg_loss_static = fluid.layers.mean(loss_static)

    # 调用优化器的minimize接口计算和更新梯度
    sgd_static.minimize(avg_loss_static)

    # 静态图中需要显示对网络进行初始化操作
    exe.run(fluid.default_startup_program())
866 867 868

    for epoch in range(epoch_num):
        for batch_id, data in enumerate(train_reader()):
D
Daniel Yang 已提交
869 870 871 872 873
            x_data_static = np.array(
                [x[0].reshape(1, 28, 28)
                for x in data]).astype('float32')
            y_data_static = np.array(
                [x[1] for x in data]).astype('int64').reshape([BATCH_SIZE, 1])
874

D
Daniel Yang 已提交
875 876 877 878 879 880 881
            fetch_list = [avg_loss_static.name]
            # 静态图中需要调用执行器的run方法执行计算过程,需要获取的计算结果(如avg_loss)需要通过fetch_list指定
            out = exe.run(
                fluid.default_main_program(),
                feed={"pixel": x_data_static,
                    "label": y_data_static},
                fetch_list=fetch_list)
882

D
Daniel Yang 已提交
883
            static_out = out[0]
884

D
Daniel Yang 已提交
885 886 887
            if batch_id % 100 == 0 and batch_id is not 0:
                print("epoch: {}, batch_id: {}, loss: {}".format(epoch, batch_id, static_out))
```
888

D
Daniel Yang 已提交
889 890
动态图改写成静态图涉及如下改动:
1. 定义占位符
891

D
Daniel Yang 已提交
892
* 利用fluid.data 定义占位符,在静态图表示会在执行器执行时才会提供数据。
893

D
Daniel Yang 已提交
894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932
2. 组网

* 优化器对象在静态图模式下不需要传入parameter_list参数。
* 将定义的占位符,输入给模型执行正向,然后计算损失值,最后利用优化器将损失值做最小化优化,得到要训练的网络。

3. 执行

* 需要对网络进行初始化操作。
* 需要使用执行器执行之前已经定义好的网络,需要调用执行器的run方法执行计算过程。

### 5.4 阻断反向传递

在一些任务中,只希望拿到正向预测的值,但是不希望更新参数,或者在反向的时候剪枝,减少计算量,阻断反向的传播, Paddle提供了两种解决方案: [detach](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/fluid_cn/Variable_cn.html#detach) 接口和 [stop_gradient](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/fluid_cn/Variable_cn.html#stop_gradient) 接口,建议用户使用 detach 接口。

1. detach接口(建议用法)
使用方式如下:

```
fw_out = fw_out.detach()
```

detach() 接口会产生一个新的、和当前计算图分离的,但是拥有当前变量内容的临时变量。

通过该接口可以阻断反向的梯度传递。


```python
import paddle.fluid as fluid
import numpy as np

with fluid.dygraph.guard():
    value0 = np.arange(26).reshape(2, 13).astype("float32")
    value1 = np.arange(6).reshape(2, 3).astype("float32")
    value2 = np.arange(10).reshape(2, 5).astype("float32")

    # 将ndarray类型的数据转换为Variable类型
    a = fluid.dygraph.to_variable(value0)
    b = fluid.dygraph.to_variable(value1)
    c = fluid.dygraph.to_variable(value2)
933

D
Daniel Yang 已提交
934 935 936
    # 构造fc、fc2层
    fc = fluid.Linear(13, 5, dtype="float32")
    fc2 = fluid.Linear(3, 3, dtype="float32")
937

D
Daniel Yang 已提交
938 939 940
    # 对fc、fc2层执行前向计算
    out1 = fc(a)
    out2 = fc2(b)
941

D
Daniel Yang 已提交
942 943 944 945 946 947 948 949
    # 将不会对out1这部分子图做反向计算
    out1 = out1.detach()

    out = fluid.layers.concat(input=[out1, out2, c], axis=1)
    out.backward()

    # 可以发现这里out1.gradient()的值都为0,同时使得fc.weight的grad没有初始化
    assert (out1.gradient() == 0).all()
950 951
```

D
Daniel Yang 已提交
952 953 954 955 956 957 958 959 960
2. stop_gradient 接口

每个 Variable 都有一个 stop_gradient 属性,可以用于细粒度地在反向梯度计算时排除部分子图,以提高效率。

如果OP只要有一个输入需要梯度,那么该OP的输出也需要梯度。相反,只有当OP的所有输入都不需要梯度时,该OP的输出也不需要梯度。在所有的 Variable 都不需要梯度的子图中,反向计算就不会进行计算了。

在动态图模式下,除参数以外的所有 Variable 的 stop_gradient 属性默认值都为 True,而参数的 stop_gradient 属性默认值为 False。 该属性用于自动剪枝,避免不必要的反向运算。

使用方式如下:
961 962

```
D
Daniel Yang 已提交
963
fw_out.stop_gradient = True
964
```
J
JiabinYang 已提交
965

D
Daniel Yang 已提交
966
通过将 Variable 的 stop_gradient 属性设置为 True,当 stop_gradient 设置为 True 时,梯度在反向传播时,遇到该 Variable,就不会继续传递。
J
JiabinYang 已提交
967

968

969
```python
D
Daniel Yang 已提交
970 971
import paddle.fluid as fluid
import numpy as np
972

D
Daniel Yang 已提交
973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989
with fluid.dygraph.guard():
    value0 = np.arange(26).reshape(2, 13).astype("float32")
    value1 = np.arange(6).reshape(2, 3).astype("float32")
    value2 = np.arange(10).reshape(2, 5).astype("float32")

    # 将ndarray类型的数据转换为Variable类型
    a = fluid.dygraph.to_variable(value0)
    b = fluid.dygraph.to_variable(value1)
    c = fluid.dygraph.to_variable(value2)

    # 构造fc、fc2层
    fc = fluid.Linear(13, 5, dtype="float32")
    fc2 = fluid.Linear(3, 3, dtype="float32")

    # 对fc、fc2层执行前向计算
    out1 = fc(a)
    out2 = fc2(b)
990

D
Daniel Yang 已提交
991 992
    # 相当于不会对out1这部分子图做反向计算
    out1.stop_gradient = True
993

D
Daniel Yang 已提交
994 995
    out = fluid.layers.concat(input=[out1, out2, c], axis=1)
    out.backward()
996

D
Daniel Yang 已提交
997 998 999
    # 可以发现这里fc参数的梯度都为0
    assert (fc.weight.gradient() == 0).all()
    assert (out1.gradient() == 0).all()
1000
```