命令式编程使用教程

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

飞桨平台中,将神经网络抽象为计算表示Operator(算子,常简称OP)和数据表示Variable(变量),如 图1 所示。神经网络的每层操作均由一个或若干Operator组成,每个Operator接受一系列的Variable作为输入,经计算后输出一系列的Variable

命令式编程使用教程 - 图1

图1 Operator和Variable关系示意图

根据**Operator**解析执行方式不同,飞桨支持如下两种编程范式: * **声明式编程模式(静态图)**:先编译后执行的方式。用户需预先定义完整的网络结构,再对网络结构进行编译优化后,才能执行获得计算结果。 * **命令式编程模式(动态图)**:解析式的执行方式。用户无需预先定义完整的网络结构,每写一行网络代码,即可同时获得计算结果。

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

为什么命令式编程模式越来越流行?

声明式编程模式作为较早提出的一种编程范式,提供丰富的 API ,能够快速的实现各种模型;并且可以利用全局的信息进行图优化,优化性能和显存占用;在预测部署方面也可以实现无缝衔接。 但具体实践中声明式编程模式存在如下问题:

  1. 采用先编译后执行的方式,组网阶段和执行阶段割裂,导致调试不方便。
  2. 属于一种符号化的编程方式,要学习新的编程方式,有一定的入门门槛。
  3. 网络结构固定,对于一些树结构的任务支持的不够好。

命令式编程模式的出现很好的解决了这些问题,存在以下优势:

  1. 代码运行完成后,可以立马获取结果,支持使用 IDE 断点调试功能,使得调试更方便。
  2. 属于命令式的编程方式,与编写Python的方式类似,更容易上手。
  3. 网络的结构在不同的层次中可以变化,使用更灵活。

综合以上优势,使得命令式编程模式越来越受开发者的青睐,本章侧重介绍在飞桨中命令式编程方法,包括如下几部分:

  1. 如何开启命令式编程模式
  2. 如何使用命令式编程进行模型训练
  3. 如何基于命令式编程进行多卡训练
  4. 如何部署命令式编程模型
  5. 命令式编程模式常见的使用技巧,如中间变量值/梯度打印、断点调试、阻断反向传递,以及某些场景下如何改写为静态图模式运行。

1. 开启命令式编程模式

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

  1. with fluid.dygraph.guard()

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

  1. import numpy as np
  2. import paddle.fluid as fluid
  3. from paddle.fluid.dygraph.base import to_variable
  4. main_program = fluid.Program()
  5. startup_program = fluid.Program()
  6. with fluid.program_guard(main_program=main_program, startup_program=startup_program):
  7. # 利用np.ones函数构造出[2*2]的二维数组,值为1
  8. data = np.ones([2, 2], np.float32)
  9. # 静态图模式下,使用layers.data构建占位符用于数据输入
  10. x = fluid.layers.data(name='x', shape=[2], dtype='float32')
  11. print('In static mode, after calling layers.data, x = ', x)
  12. # 静态图模式下,对Variable类型的数据执行x=x+10操作
  13. x += 10
  14. # 在静态图模式下,需要用户显示指定运行设备
  15. # 此处调用fluid.CPUPlace() API来指定在CPU设备上运行程序
  16. place = fluid.CPUPlace()
  17. # 创建“执行器”,并用place参数指明需要在何种设备上运行
  18. exe = fluid.Executor(place=place)
  19. # 初始化操作,包括为所有变量分配空间等,比如上面的‘x’,在下面这行代码执行后才会被分配实际的内存空间
  20. exe.run(fluid.default_startup_program())
  21. # 使用执行器“执行”已经记录的所有操作,在本例中即执行layers.data、x += 10操作
  22. # 在调用执行器的run接口时,可以通过fetch_list参数来指定要获取哪些变量的计算结果,这里我们要获取‘x += 10’执行完成后‘x’的结果;
  23. # 同时也可以通过feed参数来传入数据,这里我们将data数据传递给‘fluid.layers.data’指定的‘x’。
  24. data_after_run = exe.run(fetch_list=[x], feed={'x': data})
  25. # 此时我们打印执行器返回的结果,可以看到“执行”后,Tensor中的数据已经被赋值并进行了运算,每个元素的值都是11
  26. print('In static mode, data after run:', data_after_run)
  27. # 开启动态图模式
  28. with fluid.dygraph.guard():
  29. # 动态图模式下,将numpy的ndarray类型的数据转换为Variable类型
  30. x = fluid.dygraph.to_variable(data)
  31. print('In DyGraph mode, after calling dygraph.to_variable, x = ', x)
  32. # 动态图模式下,对Variable类型的数据执行x=x+10操作
  33. x += 10
  34. # 动态图模式下,调用Variable的numpy函数将Variable类型的数据转换为numpy的ndarray类型的数据
  35. print('In DyGraph mode, data after run:', x.numpy())
  1. In static mode, after calling layers.data, x = name: "x"
  2. type {
  3. type: LOD_TENSOR
  4. lod_tensor {
  5. tensor {
  6. data_type: FP32
  7. dims: -1
  8. dims: 2
  9. }
  10. lod_level: 0
  11. }
  12. }
  13. persistable: false
  14. In static mode, data after run: [array([[11., 11.],
  15. [11., 11.]], dtype=float32)]
  16. In DyGraph mode, after calling dygraph.to_variable, x = name generated_var_0, dtype: VarType.FP32 shape: [2, 2] lod: {}
  17. dim: 2, 2
  18. layout: NCHW
  19. dtype: float
  20. data: [1 1 1 1]
  21. In DyGraph mode, data after run: [[11. 11.]
  22. [11. 11.]]
  23. /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.
  24. warnings.warn(error_info)

从以上输出结果可以看出:

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

2. 使用命令式编程进行模型训练

接下来我们以一个简单的手写体识别任务为例,说明如何使用飞桨的动态图来进行模型的训练。包括如下步骤:

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

最后介绍一下:

  • 2.5 模型参数的保存和加载方法。

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

有关该任务和数据集的详细介绍,可参考:初识飞桨手写数字识别模型

2.1 定义数据读取器

飞桨提供了多个封装好的数据集API,本任务我们可以通过调用 paddle.dataset.mnist 的 train 函数和 test 函数,直接获取处理好的 MNIST 训练集和测试集;然后调用 paddle.batch 接口返回 reader 的装饰器,该 reader 将输入 reader 的数据打包成指定 BATCH_SIZE 大小的批处理数据。

  1. import paddle
  2. # 定义批大小
  3. BATCH_SIZE = 64
  4. # 通过调用paddle.dataset.mnist的train函数和test函数来构造reader
  5. train_reader = paddle.batch(
  6. paddle.dataset.mnist.train(), batch_size=BATCH_SIZE, drop_last=True)
  7. test_reader = paddle.batch(
  8. paddle.dataset.mnist.test(), batch_size=BATCH_SIZE, drop_last=True)
  1. 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
  2. Begin to download
  3. Download finished
  4. 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
  5. Begin to download
  6. ........
  7. Download finished
  8. 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
  9. Begin to download
  10. Download finished
  11. 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
  12. Begin to download
  13. ..
  14. Download finished

2.2 定义模型和优化器

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

https://ai-studio-static-online.cdn.bcebos.com/f9e59d727d68437aaaad8cee410e564c7a80063367bd4fcd9f710a1480ee338c

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

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

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

下面介绍如何通过继承 fluid.dygraph.Layers 实现一个简单的ConvPool层;该层由一个 卷积层 和一个 池化层 组成。

  1. import paddle.fluid as fluid
  2. from paddle.fluid.dygraph.nn import Conv2D, Pool2D
  3. # 定义SimpleImgConvPool网络,必须继承自fluid.dygraph.Layer
  4. # 该网络由一个卷积层和一个池化层组成
  5. class SimpleImgConvPool(fluid.dygraph.Layer):
  6. # 在__init__构造函数中会执行变量的初始化、参数初始化、子网络初始化的操作
  7. # 本例中执行了Conv2D和Pool2D网络的初始化操作
  8. def __init__(self,
  9. num_channels,
  10. num_filters,
  11. filter_size,
  12. pool_size,
  13. pool_stride,
  14. pool_padding=0,
  15. pool_type='max',
  16. global_pooling=False,
  17. conv_stride=1,
  18. conv_padding=0,
  19. conv_dilation=1,
  20. conv_groups=1,
  21. act=None,
  22. use_cudnn=False,
  23. param_attr=None,
  24. bias_attr=None):
  25. super(SimpleImgConvPool, self).__init__()
  26. # Conv2D网络的初始化
  27. self._conv2d = Conv2D(
  28. num_channels=num_channels,
  29. num_filters=num_filters,
  30. filter_size=filter_size,
  31. stride=conv_stride,
  32. padding=conv_padding,
  33. dilation=conv_dilation,
  34. groups=conv_groups,
  35. param_attr=None,
  36. bias_attr=None,
  37. act=act,
  38. use_cudnn=use_cudnn)
  39. # Pool2D网络的初始化
  40. self._pool2d = Pool2D(
  41. pool_size=pool_size,
  42. pool_type=pool_type,
  43. pool_stride=pool_stride,
  44. pool_padding=pool_padding,
  45. global_pooling=global_pooling,
  46. use_cudnn=use_cudnn)
  47. # forward函数实现了SimpleImgConvPool网络的执行逻辑
  48. def forward(self, inputs):
  49. x = self._conv2d(inputs)
  50. x = self._pool2d(x)
  51. return x

可以看出实现一个 ConvPool 层(即SimpleImgConvPool)分为两个步骤:

  1. 定义 __init__ 构造函数。

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

  1. 定义 forward 函数。

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

接下来我们介绍如何利用子网络组合出MNIST网络,该网络由两个 SimpleImgConvPool 子网络和一个全连接层组成。

  1. # 定义MNIST网络,必须继承自fluid.dygraph.Layer
  2. # 该网络由两个SimpleImgConvPool子网络、reshape层、matmul层、softmax层、accuracy层组成
  3. class MNIST(fluid.dygraph.Layer):
  4. # 在__init__构造函数中会执行变量的初始化、参数初始化、子网络初始化的操作
  5. # 本例中执行了self.pool_2_shape变量、matmul层中参数self.output_weight、SimpleImgConvPool子网络的初始化操作
  6. def __init__(self):
  7. super(MNIST, self).__init__()
  8. self._simple_img_conv_pool_1 = SimpleImgConvPool(
  9. 1, 20, 5, 2, 2, act="relu")
  10. self._simple_img_conv_pool_2 = SimpleImgConvPool(
  11. 20, 50, 5, 2, 2, act="relu")
  12. # self.pool_2_shape变量定义了经过self._simple_img_conv_pool_2层之后的数据
  13. # 除了batch_size维度之外其他维度的乘积
  14. self.pool_2_shape = 50 * 4 * 4
  15. # self.pool_2_shape、SIZE定义了self.output_weight参数的维度
  16. SIZE = 10
  17. # 定义全连接层的参数
  18. self.output_weight = self.create_parameter(
  19. [self.pool_2_shape, 10])
  20. # forward函数实现了MNIST网络的执行逻辑
  21. def forward(self, inputs, label=None):
  22. x = self._simple_img_conv_pool_1(inputs)
  23. x = self._simple_img_conv_pool_2(x)
  24. x = fluid.layers.reshape(x, shape=[-1, self.pool_2_shape])
  25. x = fluid.layers.matmul(x, self.output_weight)
  26. x = fluid.layers.softmax(x)
  27. if label is not None:
  28. acc = fluid.layers.accuracy(input=x, label=label)
  29. return x, acc
  30. else:
  31. return x

在这个复杂的 Layer 的 __init__ 构造函数中,包含了更多基础的操作:

  1. 变量的初始化:self.pool_2_shape = 50 * 4 * 4
  2. 全连接层参数的创建,通过调用 Layercreate_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

forward 函数的实现和 前面SimpleImgConvPool 类中的实现方式类似。

接下来定义MNIST类的对象,以及优化器。这里优化器我们选择 AdamOptimizer ,通过 Layerparameters 接口来读取该网络的全部参数,实现如下:

  1. import numpy as np
  2. from paddle.fluid.optimizer import AdamOptimizer
  3. from paddle.fluid.dygraph.base import to_variable
  4. with fluid.dygraph.guard():
  5. # 定义MNIST类的对象
  6. mnist = MNIST()
  7. # 定义优化器为AdamOptimizer,学习旅learning_rate为0.001
  8. # 注意动态图模式下必须传入parameter_list参数,该参数为需要优化的网络参数,本例需要优化mnist网络中的所有参数
  9. adam = AdamOptimizer(learning_rate=0.001, parameter_list=mnist.parameters())

2.3 训练

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

实现如下:

  • 数据读取:读取每批数据,通过 to_variable 接口将 numpy.ndarray 对象转换为 Variable 类型的对象。
  • 网络正向执行:在正向执行时,用户构造出img和label之后,可利用类似函数调用的方式(如:mnist(img, label))传递参数执行对应网络的 forward 函数。
  • 计算损失值:根据网络返回的计算结果,计算损失值,便于后续执行反向计算。
  • 执行反向计算:需要用户主动调用 backward 接口来执行反向计算。
  • 参数更新:调用优化器的 minimize 接口对参数进行更新。
  • 梯度重置:将本次计算的梯度值清零,以便进行下一次迭代和梯度更新。
  • 保存训练好的模型:通过 Layerstate_dict 获取模型的参数;通过 save_dygraph 对模型参数进行保存。
  1. import numpy as np
  2. from paddle.fluid.optimizer import AdamOptimizer
  3. from paddle.fluid.dygraph.base import to_variable
  4. with fluid.dygraph.guard():
  5. # 定义MNIST类的对象
  6. mnist = MNIST()
  7. # 定义优化器为AdamOptimizer,学习旅learning_rate为0.001
  8. # 注意动态图模式下必须传入parameter_list参数,该参数为需要优化的网络参数,本例需要优化mnist网络中的所有参数
  9. adam = AdamOptimizer(learning_rate=0.001, parameter_list=mnist.parameters())
  10. # 设置全部样本的训练次数
  11. epoch_num = 5
  12. # 执行epoch_num次训练
  13. for epoch in range(epoch_num):
  14. # 读取训练数据进行训练
  15. for batch_id, data in enumerate(train_reader()):
  16. dy_x_data = np.array([x[0].reshape(1, 28, 28) for x in data]).astype('float32')
  17. y_data = np.array([x[1] for x in data]).astype('int64').reshape(-1, 1)
  18. #将ndarray类型的数据转换为Variable类型
  19. img = to_variable(dy_x_data)
  20. label = to_variable(y_data)
  21. # 网络正向执行
  22. cost, acc = mnist(img, label)
  23. # 计算损失值
  24. loss = fluid.layers.cross_entropy(cost, label)
  25. avg_loss = fluid.layers.mean(loss)
  26. # 执行反向计算
  27. avg_loss.backward()
  28. # 参数更新
  29. adam.minimize(avg_loss)
  30. # 将本次计算的梯度值清零,以便进行下一次迭代和梯度更新
  31. mnist.clear_gradients()
  32. # 输出对应epoch、batch_id下的损失值
  33. if batch_id % 100 == 0:
  34. print("Loss at epoch {} step {}: {:}".format(
  35. epoch, batch_id, avg_loss.numpy()))
  36. # 保存训练好的模型
  37. model_dict = mnist.state_dict()
  38. fluid.save_dygraph(model_dict, "save_temp")
  1. Loss at epoch 0 step 0: [3.362183]
  2. Loss at epoch 0 step 100: [0.20108832]
  3. Loss at epoch 0 step 200: [0.1681692]
  4. Loss at epoch 0 step 300: [0.11894853]
  5. Loss at epoch 0 step 400: [0.13005154]
  6. Loss at epoch 0 step 500: [0.10004535]
  7. Loss at epoch 0 step 600: [0.11465541]
  8. Loss at epoch 0 step 700: [0.14584845]
  9. Loss at epoch 0 step 800: [0.21515566]
  10. Loss at epoch 0 step 900: [0.13847716]
  11. Loss at epoch 1 step 0: [0.03004131]
  12. Loss at epoch 1 step 100: [0.1855965]
  13. Loss at epoch 1 step 200: [0.07302501]
  14. Loss at epoch 1 step 300: [0.02016284]
  15. Loss at epoch 1 step 400: [0.03899964]
  16. Loss at epoch 1 step 500: [0.05415711]
  17. Loss at epoch 1 step 600: [0.09633664]
  18. Loss at epoch 1 step 700: [0.07155745]
  19. Loss at epoch 1 step 800: [0.13023862]
  20. Loss at epoch 1 step 900: [0.09051394]
  21. Loss at epoch 2 step 0: [0.00580437]
  22. Loss at epoch 2 step 100: [0.1506507]
  23. Loss at epoch 2 step 200: [0.03713503]
  24. Loss at epoch 2 step 300: [0.01145383]
  25. Loss at epoch 2 step 400: [0.0497771]

2.4 评估测试

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

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

模型评估测试的实现如下:

  • 首先定义 MNIST 类的对象 mnist_eval,然后通过 load_dygraph 接口加载保存好的模型参数,通过 Layerset_dict 接口将参数导入到模型中,通过 Layer 的 eval 接口切换到预测评估模式。
  • 读取测试数据执行网络正向计算,进行评估测试,输出不同 batch 数据下损失值和准确率的平均值。
  1. with fluid.dygraph.guard():
  2. # 定义MNIST类的对象
  3. mnist_eval = MNIST()
  4. # 加载保存的模型
  5. model_dict, _ = fluid.load_dygraph("save_temp")
  6. mnist_eval.set_dict(model_dict)
  7. print("checkpoint loaded")
  8. # 切换到预测评估模式
  9. mnist_eval.eval()
  10. acc_set = []
  11. avg_loss_set = []
  12. # 读取测试数据进行评估测试
  13. for batch_id, data in enumerate(test_reader()):
  14. dy_x_data = np.array([x[0].reshape(1, 28, 28)
  15. for x in data]).astype('float32')
  16. y_data = np.array(
  17. [x[1] for x in data]).astype('int64').reshape(-1, 1)
  18. # 将ndarray类型的数据转换为Variable类型
  19. img = to_variable(dy_x_data)
  20. label = to_variable(y_data)
  21. # 网络正向执行
  22. prediction, acc = mnist_eval(img, label)
  23. # 计算损失值
  24. loss = fluid.layers.cross_entropy(input=prediction, label=label)
  25. avg_loss = fluid.layers.mean(loss)
  26. acc_set.append(float(acc.numpy()))
  27. avg_loss_set.append(float(avg_loss.numpy()))
  28. # 输出不同 batch 数据下损失值和准确率的平均值
  29. acc_val_mean = np.array(acc_set).mean()
  30. avg_loss_val_mean = np.array(avg_loss_set).mean()
  31. print("Eval avg_loss is: {}, acc is: {}".format(avg_loss_val_mean, acc_val_mean))

2.5 模型参数的保存和加载

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

保存模型 :

  1. 保存模型参数:首先通过 minist.state_dict 函数获取 mnist 网络的所有参数,然后通过 fluid.save_dygraph 函数将获得的参数保存至以 save_path 为前缀的文件中。
  2. 保存优化器信息:首先通过 adam.state_dict 函数获取 adam 优化器的信息,然后通过 fluid.save_dygraph 函数将获得的参数保存至以 save_path 为前缀的文件中。
    • Layerstate_dict 接口:该接口可以获取当前层及其子层的所有参数,并将参数存放在 dict 结构中。
    • Optimizer 的 state_dict 接口:该接口可以获取优化器的信息,并将信息存放在 dict 结构中。其中包含优化器使用的所有变量,例如对于 Adam 优化器,包括 beta1、beta2、momentum 等信息。注意如果该优化器的 minimize 函数没有被调用过,则优化器的信息为空。
    • save_dygraph 接口:该接口将传入的参数或优化器的 dict 保存到磁盘上。
  1. # 保存模型参数
  2. 1. fluid.save_dygraph(minist.state_dict(), save_path”)
  3. # 保存优化器信息
  4. 2. fluid.save_dygraph(adam.state_dict(), save_path”)

加载模型:

  1. 通过 fluid.load_dygraph 函数获取模型参数信息 model_state 和优化器信息 opt_state;
  2. 通过 mnist.set_dict 函数用获取的模型参数信息设置 mnist 网络的参数
  3. 通过 adam.set_dict 函数用获取的优化器信息设置 adam 优化器信息。
    • Layerset_dict 接口:该接口根据传入的 dict 结构设置参数,所有参数将由 dict 结构中的 Tensor 设置。
    • Optimizer 的 set_dict 接口:该接口根据传入的 dict 结构设置优化器信息,例如对于 Adam 优化器,包括 beta1、beta2、momentum 等信息。如果使用了 LearningRateDecay ,则 global_step 信息也将会被设置。
    • load_dygraph 接口:该接口尝试从磁盘中加载参数或优化器的 dict 。
  1. # 获取模型参数和优化器信息
  2. 1. model_state, opt_state= fluid.load_dygraph(“save_path”)
  3. # 加载模型参数
  4. 2. mnist.set_dict(model_state)
  5. # 加载优化器信息
  6. 3. adam.set_dict(opt_state)

3. 多卡训练

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

我们通过一个实例了解如何进行多卡训练:

由于AI Studio上未配置多卡环境,所以本实例需在本地构建多卡环境后运行。

  1. 本实例仍然采用前面定义的 MNIST 网络,可将前面定义的 SimpleImgConvPool、MNIST 网络结构、相关的库导入代码、以及下面多卡训练的示例代码拷贝至本地文件 train.py 中。
  1. import numpy as np
  2. from paddle.fluid.optimizer import AdamOptimizer
  3. from paddle.fluid.dygraph.base import to_variable
  4. # 通过 Env() 的 dev_id 设置程序运行的设备
  5. place = fluid.CUDAPlace(fluid.dygraph.parallel.Env().dev_id)
  6. with fluid.dygraph.guard(place):
  7. # 准备多卡环境
  8. strategy = fluid.dygraph.parallel.prepare_context()
  9. epoch_num = 5
  10. BATCH_SIZE = 64
  11. mnist = MNIST()
  12. adam = fluid.optimizer.AdamOptimizer(learning_rate=0.001, parameter_list=mnist.parameters())
  13. # 数据并行模块
  14. mnist = fluid.dygraph.parallel.DataParallel(mnist, strategy)
  15. train_reader = paddle.batch(
  16. paddle.dataset.mnist.train(), batch_size=BATCH_SIZE, drop_last=True)
  17. # 数据切分
  18. train_reader = fluid.contrib.reader.distributed_batch_reader(
  19. train_reader)
  20. for epoch in range(epoch_num):
  21. for batch_id, data in enumerate(train_reader()):
  22. dy_x_data = np.array([x[0].reshape(1, 28, 28)
  23. for x in data]).astype('float32')
  24. y_data = np.array(
  25. [x[1] for x in data]).astype('int64').reshape(-1, 1)
  26. img = fluid.dygraph.to_variable(dy_x_data)
  27. label = fluid.dygraph.to_variable(y_data)
  28. label.stop_gradient = True
  29. # 网络正向执行
  30. cost, acc = mnist(img, label)
  31. # 计算损失值
  32. loss = fluid.layers.cross_entropy(cost, label)
  33. avg_loss = fluid.layers.mean(loss)
  34. # 单步训练:首先对 loss 进行归一化,然后计算单卡的梯度,最终将所有的梯度聚合
  35. avg_loss = mnist.scale_loss(avg_loss)
  36. avg_loss.backward()
  37. mnist.apply_collective_grads()
  38. # 参数更新
  39. adam.minimize(avg_loss)
  40. # 将本次计算的梯度值清零,以便进行下一次迭代和梯度更新
  41. mnist.clear_gradients()
  42. # 输出对应epoch、batch_id下的损失值
  43. if batch_id % 100 == 0 and batch_id is not 0:
  44. print("epoch: {}, batch_id: {}, loss is: {}".format(epoch, batch_id, avg_loss.numpy()))

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

  1. CUDA_VISIBLE_DEVICES=0,1 python -m paddle.distributed.launch --log_dir ./mylog train.py

其中 log_dir 为存放 log 的地址,train.py 为程序名。 执行结果如下:

  1. ----------- Configuration Arguments -----------
  2. cluster_node_ips: 127.0.0.1
  3. log_dir: ./mylog
  4. node_ip: 127.0.0.1
  5. print_config: True
  6. selected_gpus: 0,1
  7. started_port: 6170
  8. training_script: train.py
  9. training_script_args: []
  10. use_paddlecloud: False
  11. ------------------------------------------------
  12. 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

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

  1. .
  2. ├── mylog
  3. ├── workerlog.0
  4. └── workerlog.1
  5. └── train.py

总结一下,多卡训练相比单卡训练,有如下步骤不同:

  1. 通过 Env() 的 dev_id 设置程序运行的设备。
  1. place = fluid.CUDAPlace(fluid.dygraph.parallel.Env().dev_id)
  2. with fluid.dygraph.guard(place):
  1. 准备多卡环境。
  1. strategy = fluid.dygraph.parallel.prepare_context()
  1. 数据并行模块。

在数据并行的时候,我们需要存储和初始化一些多卡相关的信息,这些信息和操作放在 DataParallel 类中,使用的时候,我们需要利用 model(定义的模型) 和 strategy(第二步得到的多卡环境) 信息初始化 DataParallel。

  1. mnist = fluid.dygraph.parallel.DataParallel(mnist, strategy)
  1. 数据切分。

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

  1. train_reader = fluid.contrib.reader.distributed_batch_reader(train_reader)
  1. 单步训练。

首先对 loss 进行归一化,然后计算单卡的梯度,最终将所有的梯度聚合。

  1. avg_loss = mnist.scale_loss(avg_loss)
  2. avg_loss.backward()
  3. mnist.apply_collective_grads()
  1. 模型保存。

和单卡不同,多卡训练时需逐个进程执行保存操作,多个进程同时保存会使模型文件格式出错。

  1. if fluid.dygraph.parallel.Env().local_rank == 0
  2. fluid.save_dygraph(mnist.state_dict, path”)
  1. 评估测试。

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

4. 模型部署

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

  • 如果用户使用的 if/else、switch、for/while 与输入(包括输入的值和 shape )无关,则可以使用如下动态图模型部署方案:
    • 使用 TracedLayer 将前向动态图模型转换为静态图模型。可以将动态图保存后做在线C++预测;除此以外,用户也可使用转换后的静态图模型在Python端做预测,通常比原先的动态图性能更好。
    • 所有的TracedLayer对象均不应通过构造函数创建,而需通过调用静态方法 TracedLayer.trace(layer, inputs) 创建。
    • TracedLayer使用 Executor 和 CompiledProgram 运行静态图模型。
  1. from paddle.fluid.dygraph import TracedLayer
  2. with fluid.dygraph.guard():
  3. # 定义MNIST类的对象
  4. mnist = MNIST()
  5. in_np = np.random.random([10, 1, 28, 28]).astype('float32')
  6. # 将numpy的ndarray类型的数据转换为Variable类型
  7. input_var = fluid.dygraph.to_variable(in_np)
  8. # 通过 TracerLayer.trace 接口将动态图模型转换为静态图模型
  9. out_dygraph, static_layer = TracedLayer.trace(mnist, inputs=[input_var])
  10. save_dirname = './saved_infer_model'
  11. # 将转换后的模型保存
  12. static_layer.save_inference_model(save_dirname, feed=[0], fetch=[0])
  1. # 静态图中需要使用执行器执行之前已经定义好的网络
  2. place = fluid.CPUPlace()
  3. exe = fluid.Executor(place)
  4. program, feed_vars, fetch_vars = fluid.io.load_inference_model(save_dirname, exe)
  5. # 静态图中需要调用执行器的run方法执行计算过程
  6. fetch, = exe.run(program, feed={feed_vars[0]: in_np}, fetch_list=fetch_vars)

以上示例中,通过 TracerLayer.trace 接口来运行动态图模型并将其转换为静态图模型,该接口需要传入动态图的网络模型 mnist 和输入变量列表 [input_var];然后调用 save_inference_model 接口将静态图模型保存为用于预测部署的模型,之后利用 load_inference_model 接口将保存的模型加载,并使用 Executor 执行,检查结果是否正确。

save_inference_model 保存的下来的模型,同样可以使用 C++ 加载部署,具体的操作请参考:C++ 预测 API介绍

  • 如果任务中包含了依赖数据的控制流,比如下面这个示例中if条件的判断依赖输入的shape。针对这种场景,可以使用基于ProgramTranslator的方式转成静态图的program,通过save_inference_model 接口将静态图模型保存为用于预测部署的模型,之后利用 load_inference_model 接口将保存的模型加载,并使用 Executor 执行,检查结果是否正确。

保存的下来的模型,同样可以使用 C++ 加载部署,具体的操作请参考:C++ 预测 API介绍

  1. with fluid.dygraph.guard():
  2. in_np = np.array([-2]).astype('int')
  3. # 将numpy的ndarray类型的数据转换为Variable类型
  4. input_var = fluid.dygraph.to_variable(in_np)
  5. # if判断与输入input_var的shape有关
  6. if input_var.shape[0] > 1:
  7. print("input_var's shape[0] > 1")
  8. else:
  9. print("input_var's shape[1] < 1")
  • 针对依赖数据的控制流,解决流程如下 1. 添加declarative装饰器; 2. 利用ProgramTranslator进行转换
  1. 添加declarative装饰器 首先需要对给MNist类的forward函数添加一个declarative 装饰器,来标记需要转换的代码块,(注:需要在最外层的class的forward函数中添加)
  1. from paddle.fluid.dygraph.jit import declarative
  2. # 定义MNIST网络,必须继承自fluid.dygraph.Layer
  3. # 该网络由两个SimpleImgConvPool子网络、reshape层、matmul层、softmax层、accuracy层组成
  4. class MNIST(fluid.dygraph.Layer):
  5. # 在__init__构造函数中会执行变量的初始化、参数初始化、子网络初始化的操作
  6. # 本例中执行了self.pool_2_shape变量、matmul层中参数self.output_weight、SimpleImgConvPool子网络的初始化操作
  7. def __init__(self):
  8. super(MNIST, self).__init__()
  9. self._simple_img_conv_pool_1 = SimpleImgConvPool(
  10. 1, 20, 5, 2, 2, act="relu")
  11. self._simple_img_conv_pool_2 = SimpleImgConvPool(
  12. 20, 50, 5, 2, 2, act="relu")
  13. # self.pool_2_shape变量定义了经过self._simple_img_conv_pool_2层之后的数据
  14. # 除了batch_size维度之外其他维度的乘积
  15. self.pool_2_shape = 50 * 4 * 4
  16. # self.pool_2_shape、SIZE定义了self.output_weight参数的维度
  17. SIZE = 10
  18. # 定义全连接层的参数
  19. self.output_weight = self.create_parameter(
  20. [self.pool_2_shape, 10])
  21. # forward函数实现了MNIST网络的执行逻辑
  22. @declarative
  23. def forward(self, inputs, label=None):
  24. x = self._simple_img_conv_pool_1(inputs)
  25. x = self._simple_img_conv_pool_2(x)
  26. x = fluid.layers.reshape(x, shape=[-1, self.pool_2_shape])
  27. x = fluid.layers.matmul(x, self.output_weight)
  28. x = fluid.layers.softmax(x)
  29. if label is not None:
  30. acc = fluid.layers.accuracy(input=x, label=label)
  31. return x, acc
  32. else:
  33. return x
  1. File "<ipython-input-1-b7b25c28bae2>", line 25
  2. @declarative
  3. ^
  4. TabError: inconsistent use of tabs and spaces in indentation

2) 利用ProgramTranslator进行转换

  1. from paddle.fluid.dygraph.dygraph_to_static import ProgramTranslator
  2. with fluid.dygraph.guard():
  3. prog_trans = fluid.dygraph.ProgramTranslator()
  4. mnist = MNIST()
  5. in_np = np.random.random([10, 1, 28, 28]).astype('float32')
  6. label_np = np.random.randint(0, 10, size=(10,1)).astype( "int64")
  7. input_var = fluid.dygraph.to_variable(in_np)
  8. label_var = flui.dyraph.to_variable(label_np)
  9. out = mnist( input_var, label_var)
  10. prog_trans.save_inference_model("./mnist_dy2stat", fetch=[0,1])

5. 使用技巧

5.1 中间变量值、梯度打印

  1. 用户想要查看任意变量的值,可以使用 numpy 接口。
  1. x = y * 10
  2. print(x.numpy())

来直接打印变量的值

  1. 查看反向的值 可以在执行了 backward 之后,可以通过 gradient 接口来看任意变量的梯度
  1. x = y * 10
  2. x.backward()
  3. 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

  1. 同时可以观察 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小节仅预测部署改为了静态图模式。
    1. 如果使用了与输入相关的控制流,请参照如何把动态图转写成静态图章节,将动态图代码进行转写。

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

  1. # 设置全部样本训练次数(epoch_num)、批大小(BATCH_SIZE)。
  2. epoch_num = 1
  3. BATCH_SIZE = 64
  4. main_program = fluid.Program()
  5. startup_program = fluid.Program()
  6. with fluid.program_guard(main_program=main_program, startup_program=startup_program):
  7. # 静态图中需要使用执行器执行之前已经定义好的网络
  8. exe = fluid.Executor(fluid.CPUPlace())
  9. # 定义MNIST类的对象,可以使用动态图定义好的网络结构
  10. mnist_static = MNIST()
  11. # 定义优化器对象,静态图模式下不需要传入parameter_list参数
  12. sgd_static = fluid.optimizer.SGDOptimizer(learning_rate=1e-3)
  13. # 通过调用paddle.dataset.mnist的train函数,直接获取处理好的MNIST训练集
  14. train_reader = paddle.batch(
  15. paddle.dataset.mnist.train(), batch_size=BATCH_SIZE, drop_last=True)
  16. # 静态图需要明确定义输入变量,即“占位符”,在静态图组网阶段并没有读入数据,所以需要使用占位符指明输入数据的类型,shape等信息
  17. img_static = fluid.data(
  18. name='pixel', shape=[None, 1, 28, 28], dtype='float32')
  19. label_static = fluid.data(name='label', shape=[None, 1], dtype='int64')
  20. # 调用网络,执行前向计算
  21. cost_static = mnist_static(img_static)
  22. # 计算损失值
  23. loss_static = fluid.layers.cross_entropy(cost_static, label_static)
  24. avg_loss_static = fluid.layers.mean(loss_static)
  25. # 调用优化器的minimize接口计算和更新梯度
  26. sgd_static.minimize(avg_loss_static)
  27. # 静态图中需要显示对网络进行初始化操作
  28. exe.run(fluid.default_startup_program())
  29. for epoch in range(epoch_num):
  30. for batch_id, data in enumerate(train_reader()):
  31. x_data_static = np.array(
  32. [x[0].reshape(1, 28, 28)
  33. for x in data]).astype('float32')
  34. y_data_static = np.array(
  35. [x[1] for x in data]).astype('int64').reshape([BATCH_SIZE, 1])
  36. fetch_list = [avg_loss_static.name]
  37. # 静态图中需要调用执行器的run方法执行计算过程,需要获取的计算结果(如avg_loss)需要通过fetch_list指定
  38. out = exe.run(
  39. fluid.default_main_program(),
  40. feed={"pixel": x_data_static,
  41. "label": y_data_static},
  42. fetch_list=fetch_list)
  43. static_out = out[0]
  44. if batch_id % 100 == 0 and batch_id is not 0:
  45. print("epoch: {}, batch_id: {}, loss: {}".format(epoch, batch_id, static_out))

动态图改写成静态图涉及如下改动:

  1. 定义占位符
  • 利用fluid.data 定义占位符,在静态图表示会在执行器执行时才会提供数据。
  1. 组网
  • 优化器对象在静态图模式下不需要传入parameter_list参数。
  • 将定义的占位符,输入给模型执行正向,然后计算损失值,最后利用优化器将损失值做最小化优化,得到要训练的网络。
  1. 执行
  • 需要对网络进行初始化操作。
  • 需要使用执行器执行之前已经定义好的网络,需要调用执行器的run方法执行计算过程。

5.4 阻断反向传递

在一些任务中,只希望拿到正向预测的值,但是不希望更新参数,或者在反向的时候剪枝,减少计算量,阻断反向的传播, Paddle提供了两种解决方案: detach 接口和 stop_gradient 接口,建议用户使用 detach 接口。

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

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

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

  1. import paddle.fluid as fluid
  2. import numpy as np
  3. with fluid.dygraph.guard():
  4. value0 = np.arange(26).reshape(2, 13).astype("float32")
  5. value1 = np.arange(6).reshape(2, 3).astype("float32")
  6. value2 = np.arange(10).reshape(2, 5).astype("float32")
  7. # 将ndarray类型的数据转换为Variable类型
  8. a = fluid.dygraph.to_variable(value0)
  9. b = fluid.dygraph.to_variable(value1)
  10. c = fluid.dygraph.to_variable(value2)
  11. # 构造fc、fc2层
  12. fc = fluid.Linear(13, 5, dtype="float32")
  13. fc2 = fluid.Linear(3, 3, dtype="float32")
  14. # 对fc、fc2层执行前向计算
  15. out1 = fc(a)
  16. out2 = fc2(b)
  17. # 将不会对out1这部分子图做反向计算
  18. out1 = out1.detach()
  19. out = fluid.layers.concat(input=[out1, out2, c], axis=1)
  20. out.backward()
  21. # 可以发现这里out1.gradient()的值都为0,同时使得fc.weight的grad没有初始化
  22. assert (out1.gradient() == 0).all()
  1. stop_gradient 接口

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

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

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

使用方式如下:

  1. fw_out.stop_gradient = True

通过将 Variable 的 stop_gradient 属性设置为 True,当 stop_gradient 设置为 True 时,梯度在反向传播时,遇到该 Variable,就不会继续传递。

  1. import paddle.fluid as fluid
  2. import numpy as np
  3. with fluid.dygraph.guard():
  4. value0 = np.arange(26).reshape(2, 13).astype("float32")
  5. value1 = np.arange(6).reshape(2, 3).astype("float32")
  6. value2 = np.arange(10).reshape(2, 5).astype("float32")
  7. # 将ndarray类型的数据转换为Variable类型
  8. a = fluid.dygraph.to_variable(value0)
  9. b = fluid.dygraph.to_variable(value1)
  10. c = fluid.dygraph.to_variable(value2)
  11. # 构造fc、fc2层
  12. fc = fluid.Linear(13, 5, dtype="float32")
  13. fc2 = fluid.Linear(3, 3, dtype="float32")
  14. # 对fc、fc2层执行前向计算
  15. out1 = fc(a)
  16. out2 = fc2(b)
  17. # 相当于不会对out1这部分子图做反向计算
  18. out1.stop_gradient = True
  19. out = fluid.layers.concat(input=[out1, out2, c], axis=1)
  20. out.backward()
  21. # 可以发现这里fc参数的梯度都为0
  22. assert (fc.weight.gradient() == 0).all()
  23. assert (out1.gradient() == 0).all()