动态图机制-DyGraph

PaddlePaddle的DyGraph模式是一种动态的图执行机制,可以立即执行结果,无需构建整个图。同时,和以往静态的执行计算图不同,DyGraph模式下您的所有操作可以立即获得执行结果,而不必等待所构建的计算图全部执行完成,这样可以让您更加直观地构建PaddlePaddle下的深度学习任务,以及进行模型的调试,同时还减少了大量用于构建静态计算图的代码,使得您编写、调试网络的过程变得更加便捷。

PaddlePaddle DyGraph是一个更加灵活易用的模式,可提供:

  • 更加灵活便捷的代码组织结构: 使用python的执行控制流程和面向对象的模型设计

  • 更加便捷的调试功能: 直接调用操作从而检查正在运行的模型并且测试更改

  • 和静态执行图通用的模型代码:同样的模型代码可以使用更加便捷的DyGraph调试,执行,同时也支持使用原有的静态图模式执行

  • 支持纯Python和Numpy语法实现的layer: 支持使用Numpy相关操作直接搭建模型计算部分

设置和基本用法

  • 升级到最新的PaddlePaddle 1.4:
  1. pip install -q --upgrade paddlepaddle==1.4
  • 使用fluid.dygraph.guard(place=None) 上下文:
  1. import paddle.fluid as fluid
  2. with fluid.dygraph.guard():
  3. # write your executable dygraph code here

现在您就可以在fluid.dygraph.guard()上下文环境中使用DyGraph的模式运行网络了,DyGraph将改变以往PaddlePaddle的执行方式: 现在他们将会立即执行,并且将计算结果返回给Python。

Dygraph将非常适合和Numpy一起使用,使用fluid.dygraph.base.to_variable(x)将会将ndarray转换为fluid.Variable,而使用fluid.Variable.numpy()将可以把任意时刻获取到的计算结果转换为Numpyndarray

  1. x = np.ones([2, 2], np.float32)
  2. with fluid.dygraph.guard():
  3. inputs = []
  4. for _ in range(10):
  5. inputs.append(fluid.dygraph.base.to_variable(x))
  6. ret = fluid.layers.sums(inputs)
  7. print(ret.numpy())
  8. [[10. 10.]
  9. [10. 10.]]
  10. Process finished with exit code 0

这里创建了一系列ndarray的输入,执行了一个sum操作之后,我们可以直接将运行的结果打印出来

然后通过调用reduce_sum后使用Variable.backward()方法执行反向,使用Variable.gradient()方法即可获得反向网络执行完成后的梯度值的ndarray形式:

  1. loss = fluid.layers.reduce_sum(ret)
  2. loss.backward()
  3. print(loss.gradient())
  4. [1.]
  5. Process finished with exit code 0

基于DyGraph构建网络

  • 编写一段用于DyGraph执行的Object-Oriented-Designed, PaddlePaddle模型代码主要由以下三个部分组成: 请注意,如果您设计的这一层结构是包含参数的,则必需要使用继承自fluid.Layer的Object-Oriented-Designed的类来描述该层的行为。

    • 建立一个可以在DyGraph模式中执行的,Object-Oriented的网络,需要继承自fluid.Layer,其中需要调用基类的init方法,并且实现带有参数namescope(用来标识本层的名字)的_init构造函数,在构造函数中,我们通常会执行一些例如参数初始化,子网络初始化的操作,执行这些操作时不依赖于输入的动态信息:
  1. class MyLayer(fluid.Layer):
  2. def __init__(self, name_scope):
  3. super(MyLayer, self).__init__(name_scope)
  • 实现一个forward(self, *inputs)的执行函数,该函数将负责执行实际运行时网络的执行逻辑, 该函数将会在每一轮训练/预测中被调用,这里我们将执行一个简单的relu -> elementwise add -> reduce sum
  1. def forward(self, inputs):
  2. x = fluid.layers.relu(inputs)
  3. self._x_for_debug = x
  4. x = fluid.layers.elementwise_mul(x, x)
  5. x = fluid.layers.reduce_sum(x)
  6. return [x]
  • (可选)实现一个build_once(self, *inputs) 方法,该方法将作为一个单次执行的函数,用于初始化一些依赖于输入信息的参数和网络信息, 例如在FC(fully connected layer)当中, 需要依赖输入的shape初始化参数, 这里我们并不需要这样的操作,仅仅为了展示,因此这个方法可以直接跳过:
  1. def build_once(self, input):
  2. pass
  • fluid.dygraph.guard()中执行:

    • 使用Numpy构建输入:
  1. np_inp = np.array([1.0, 2.0, -1.0], dtype=np.float32)
  • 输入转换并执行前向网络获取返回值: 使用fluid.dygraph.base.to_variable(np_inp)转换Numpy输入为DyGraph接收的输入,然后使用l(var_inp)[0]调用callable object并且获取了x作为返回值,利用x.numpy()方法直接获取了执行得到的xndarray返回值。
  1. with fluid.dygraph.guard():
  2. var_inp = fluid.dygraph.base.to_variable(np_inp)
  3. l = MyLayer("my_layer")
  4. x = l(var_inp)[0]
  5. dy_out = x.numpy()
  • 计算梯度:自动微分对于实现机器学习算法(例如用于训练神经网络的反向传播)来说很有用, 使用x.backward()方法可以从某个fluid.Varaible开始执行反向网络,同时利用l._x_for_debug.gradient()获取了网络中x梯度的ndarray 返回值:
  1. x.backward()
  2. dy_grad = l._x_for_debug.gradient()

使用DyGraph训练模型

接下来我们将以“手写数字识别”这个最基础的模型为例,展示如何利用DyGraph模式搭建并训练一个模型:

有关手写数字识别的相关理论知识请参考PaddleBook中的内容,我们在这里默认您已经了解了该模型所需的深度学习理论知识。

  • 准备数据,我们使用paddle.dataset.mnist作为训练所需要的数据集:
  1. train_reader = paddle.batch(
  2. paddle.dataset.mnist.train(), batch_size=BATCH_SIZE, drop_last=True)
  • 构建网络,虽然您可以根据之前的介绍自己定义所有的网络结构,但是您也可以直接使用fluid.Layer.nn当中我们为您定制好的一些基础网络结构,这里我们利用fluid.Layer.nn.Conv2d以及fluid.Layer.nn.Pool2d构建了基础的SimpleImgConvPool
  1. class SimpleImgConvPool(fluid.dygraph.Layer):
  2. def __init__(self,
  3. name_scope,
  4. num_channels,
  5. num_filters,
  6. filter_size,
  7. pool_size,
  8. pool_stride,
  9. pool_padding=0,
  10. pool_type='max',
  11. global_pooling=False,
  12. conv_stride=1,
  13. conv_padding=0,
  14. conv_dilation=1,
  15. conv_groups=1,
  16. act=None,
  17. use_cudnn=False,
  18. param_attr=None,
  19. bias_attr=None):
  20. super(SimpleImgConvPool, self).__init__(name_scope)
  21. self._conv2d = Conv2D(
  22. self.full_name(),
  23. num_channels=num_channels,
  24. num_filters=num_filters,
  25. filter_size=filter_size,
  26. stride=conv_stride,
  27. padding=conv_padding,
  28. dilation=conv_dilation,
  29. groups=conv_groups,
  30. param_attr=None,
  31. bias_attr=None,
  32. use_cudnn=use_cudnn)
  33. self._pool2d = Pool2D(
  34. self.full_name(),
  35. pool_size=pool_size,
  36. pool_type=pool_type,
  37. pool_stride=pool_stride,
  38. pool_padding=pool_padding,
  39. global_pooling=global_pooling,
  40. use_cudnn=use_cudnn)
  41. def forward(self, inputs):
  42. x = self._conv2d(inputs)
  43. x = self._pool2d(x)
  44. return x

注意: 构建网络时子网络的定义和使用请在init中进行, 而子网络的调用则在forward函数中调用

  • 利用已经构建好的SimpleImgConvPool组成最终的MNIST网络:
  1. class MNIST(fluid.dygraph.Layer):
  2. def __init__(self, name_scope):
  3. super(MNIST, self).__init__(name_scope)
  4. self._simple_img_conv_pool_1 = SimpleImgConvPool(
  5. self.full_name(), 1, 20, 5, 2, 2, act="relu")
  6. self._simple_img_conv_pool_2 = SimpleImgConvPool(
  7. self.full_name(), 20, 50, 5, 2, 2, act="relu")
  8. pool_2_shape = 50 * 4 * 4
  9. SIZE = 10
  10. scale = (2.0 / (pool_2_shape**2 * SIZE))**0.5
  11. self._fc = FC(self.full_name(),
  12. 10,
  13. param_attr=fluid.param_attr.ParamAttr(
  14. initializer=fluid.initializer.NormalInitializer(
  15. loc=0.0, scale=scale)),
  16. act="softmax")
  17. def forward(self, inputs):
  18. x = self._simple_img_conv_pool_1(inputs)
  19. x = self._simple_img_conv_pool_2(x)
  20. x = self._fc(x)
  21. return x
  • fluid.dygraph.guard()中定义配置好的MNIST网络结构,此时即使没有训练也可以在fluid.dygraph.guard()中调用模型并且检查输出:
  1. with fluid.dygraph.guard():
  2. mnist = MNIST("mnist")
  3. id, data = list(enumerate(train_reader()))[0]
  4. dy_x_data = np.array(
  5. [x[0].reshape(1, 28, 28)
  6. for x in data]).astype('float32')
  7. img = to_variable(dy_x_data)
  8. print("cost is: {}".format(mnist(img).numpy()))
  9. cost is: [[0.10135901 0.1051138 0.1027941 ... 0.0972859 0.10221873 0.10165327]
  10. [0.09735426 0.09970362 0.10198303 ... 0.10134517 0.10179105 0.10025002]
  11. [0.09539858 0.10213123 0.09543551 ... 0.10613529 0.10535969 0.097991 ]
  12. ...
  13. [0.10120598 0.0996111 0.10512722 ... 0.10067689 0.10088114 0.10071224]
  14. [0.09889644 0.10033772 0.10151272 ... 0.10245881 0.09878646 0.101483 ]
  15. [0.09097178 0.10078511 0.10198414 ... 0.10317434 0.10087223 0.09816764]]
  16. Process finished with exit code 0
  • 构建训练循环,在每一轮参数更新完成后我们调用mnist.clear_gradients()来重置梯度:
  1. for epoch in range(epoch_num):
  2. for batch_id, data in enumerate(train_reader()):
  3. dy_x_data = np.array(
  4. [x[0].reshape(1, 28, 28)
  5. for x in data]).astype('float32')
  6. y_data = np.array(
  7. [x[1] for x in data]).astype('int64').reshape(BATCH_SIZE, 1)
  8. img = to_variable(dy_x_data)
  9. label = to_variable(y_data)
  10. label.stop_gradient = True
  11. cost = mnist(img)
  12. loss = fluid.layers.cross_entropy(cost, label)
  13. avg_loss = fluid.layers.mean(loss)
  14. dy_out = avg_loss.numpy()
  15. avg_loss.backward()
  16. sgd.minimize(avg_loss)
  17. mnist.clear_gradients()
  • 变量及优化器

模型的参数或者任何您希望检测的值可以作为变量封装在类中,并且通过对象获取并使用numpy()方法获取其ndarray的输出, 在训练过程中您可以使用mnist.parameters()来获取到网络中所有的参数,也可以指定某一个Layer的某个参数或者parameters()来获取该层的所有参数,使用numpy()方法随时查看参数的值

反向运行后调用之前定义的SGD优化器对象的minimize方法进行参数更新:

  1. with fluid.dygraph.guard():
  2. fluid.default_startup_program().random_seed = seed
  3. fluid.default_main_program().random_seed = seed
  4. mnist = MNIST("mnist")
  5. sgd = SGDOptimizer(learning_rate=1e-3)
  6. train_reader = paddle.batch(
  7. paddle.dataset.mnist.train(), batch_size= BATCH_SIZE, drop_last=True)
  8. np.set_printoptions(precision=3, suppress=True)
  9. for epoch in range(epoch_num):
  10. for batch_id, data in enumerate(train_reader()):
  11. dy_x_data = np.array(
  12. [x[0].reshape(1, 28, 28)
  13. for x in data]).astype('float32')
  14. y_data = np.array(
  15. [x[1] for x in data]).astype('int64').reshape(BATCH_SIZE, 1)
  16. img = to_variable(dy_x_data)
  17. label = to_variable(y_data)
  18. label.stop_gradient = True
  19. cost = mnist(img)
  20. loss = fluid.layers.cross_entropy(cost, label)
  21. avg_loss = fluid.layers.mean(loss)
  22. dy_out = avg_loss.numpy()
  23. avg_loss.backward()
  24. sgd.minimize(avg_loss)
  25. mnist.clear_gradients()
  26. dy_param_value = {}
  27. for param in mnist.parameters():
  28. dy_param_value[param.name] = param.numpy()
  29. if batch_id % 20 == 0:
  30. print("Loss at step {}: {:.7}".format(batch_id, avg_loss.numpy()))
  31. print("Final loss: {:.7}".format(avg_loss.numpy()))
  32. print("_simple_img_conv_pool_1_conv2d W's mean is: {}".format(mnist._simple_img_conv_pool_1._conv2d._filter_param.numpy().mean()))
  33. print("_simple_img_conv_pool_1_conv2d Bias's mean is: {}".format(mnist._simple_img_conv_pool_1._conv2d._bias_param.numpy().mean()))
  34. Loss at step 0: [2.302]
  35. Loss at step 20: [1.616]
  36. Loss at step 40: [1.244]
  37. Loss at step 60: [1.142]
  38. Loss at step 80: [0.911]
  39. Loss at step 100: [0.824]
  40. Loss at step 120: [0.774]
  41. Loss at step 140: [0.626]
  42. Loss at step 160: [0.609]
  43. Loss at step 180: [0.627]
  44. Loss at step 200: [0.466]
  45. Loss at step 220: [0.499]
  46. Loss at step 240: [0.614]
  47. Loss at step 260: [0.585]
  48. Loss at step 280: [0.503]
  49. Loss at step 300: [0.423]
  50. Loss at step 320: [0.509]
  51. Loss at step 340: [0.348]
  52. Loss at step 360: [0.452]
  53. Loss at step 380: [0.397]
  54. Loss at step 400: [0.54]
  55. Loss at step 420: [0.341]
  56. Loss at step 440: [0.337]
  57. Loss at step 460: [0.155]
  58. Final loss: [0.164]
  59. _simple_img_conv_pool_1_conv2d W's mean is: 0.00606656912714
  60. _simple_img_conv_pool_1_conv2d Bias's mean is: -3.4576318285e-05
  • 性能

在使用fluid.dygraph.guard()可以通过传入fluid.CUDAPlace(0)或者fluid.CPUPlace()来选择执行DyGraph的设备,通常如果不做任何处理将会自动适配您的设备。

模型参数的保存

在模型训练中可以使用fluid.dygraph.save_persistables(your_model_object.state_dict(), "save_dir")来保存your_model_object中所有的模型参数。也可以自定义需要保存的“参数名” - “参数对象”的Python Dictionary传入。

同样可以使用your_modle_object.load_dict(fluid.dygraph.load_persistables("save_dir"))接口来恢复保存的模型参数从而达到继续训练的目的。

下面的代码展示了如何在“手写数字识别”任务中保存参数并且读取已经保存的参数来继续训练。

  1. dy_param_init_value={}
  2. for epoch in range(epoch_num):
  3. for batch_id, data in enumerate(train_reader()):
  4. dy_x_data = np.array(
  5. [x[0].reshape(1, 28, 28)
  6. for x in data]).astype('float32')
  7. y_data = np.array(
  8. [x[1] for x in data]).astype('int64').reshape(BATCH_SIZE, 1)
  9. img = to_variable(dy_x_data)
  10. label = to_variable(y_data)
  11. label.stop_gradient = True
  12. cost = mnist(img)
  13. loss = fluid.layers.cross_entropy(cost, label)
  14. avg_loss = fluid.layers.mean(loss)
  15. dy_out = avg_loss.numpy()
  16. avg_loss.backward()
  17. sgd.minimize(avg_loss)
  18. fluid.dygraph.save_persistables(mnist.state_dict(), "save_dir")
  19. mnist.clear_gradients()
  20. for param in mnist.parameters():
  21. dy_param_init_value[param.name] = param.numpy()
  22. mnist.load_dict(fluid.dygraph.load_persistables("save_dir"))
  23. restore = mnist.parameters()
  24. # check save and load
  25. success = True
  26. for value in restore:
  27. if (not np.allclose(value.numpy(), dy_param_init_value[value.name])) or (not np.isfinite(value.numpy().all())) or (np.isnan(value.numpy().any())):
  28. success = False
  29. print("model save and load success? {}".format(success))

模型评估

当我们需要在DyGraph模式下利用搭建的模型进行预测任务,可以使用YourModel.eval()接口,在之前的手写数字识别模型中我们使用mnist.eval()来启动预测模式(我们默认在fluid.dygraph.guard()上下文中是训练模式),在预测的模式下,DyGraph将只会执行前向的预测网络,而不会进行自动求导并执行反向网络:

下面的代码展示了如何使用DyGraph模式训练一个用于执行“手写数字识别”任务的模型并保存,并且利用已经保存好的模型进行预测。

我们在第一个fluid.dygraph.guard()上下文中进行了模型的保存和训练,值得注意的是,当我们需要在训练的过程中进行预测时需要使用YourModel.eval()切换到预测模式,并且在预测完成后使用YourModel.train()切换回训练模式继续训练。

我们在第二个fluid.dygraph.guard()上下文中利用之前保存的checkpoint进行预测,同样的在执行预测前需要使用YourModel.eval()来切换的预测模式。

  1. with fluid.dygraph.guard():
  2. fluid.default_startup_program().random_seed = seed
  3. fluid.default_main_program().random_seed = seed
  4. mnist = MNIST("mnist")
  5. adam = AdamOptimizer(learning_rate=0.001)
  6. train_reader = paddle.batch(
  7. paddle.dataset.mnist.train(), batch_size=BATCH_SIZE, drop_last=True)
  8. test_reader = paddle.batch(
  9. paddle.dataset.mnist.test(), batch_size=BATCH_SIZE, drop_last=True)
  10. for epoch in range(epoch_num):
  11. for batch_id, data in enumerate(train_reader()):
  12. dy_x_data = np.array(
  13. [x[0].reshape(1, 28, 28)
  14. for x in data]).astype('float32')
  15. y_data = np.array(
  16. [x[1] for x in data]).astype('int64').reshape(BATCH_SIZE, 1)
  17. img = to_variable(dy_x_data)
  18. label = to_variable(y_data)
  19. label.stop_gradient = True
  20. cost, acc = mnist(img, label)
  21. loss = fluid.layers.cross_entropy(cost, label)
  22. avg_loss = fluid.layers.mean(loss)
  23. avg_loss.backward()
  24. adam.minimize(avg_loss)
  25. # save checkpoint
  26. mnist.clear_gradients()
  27. if batch_id % 100 == 0:
  28. print("Loss at epoch {} step {}: {:}".format(epoch, batch_id, avg_loss.numpy()))
  29. mnist.eval()
  30. test_cost, test_acc = self._test_train(test_reader, mnist, BATCH_SIZE)
  31. mnist.train()
  32. print("Loss at epoch {} , Test avg_loss is: {}, acc is: {}".format(epoch, test_cost, test_acc))
  33. fluid.dygraph.save_persistables(mnist.state_dict(), "save_dir")
  34. print("checkpoint saved")
  35. with fluid.dygraph.guard():
  36. fluid.default_startup_program().random_seed = seed
  37. fluid.default_main_program().random_seed = seed
  38. mnist_infer = MNIST("mnist")
  39. # load checkpoint
  40. mnist_infer.load_dict(
  41. fluid.dygraph.load_persistables("save_dir"))
  42. print("checkpoint loaded")
  43. # start evaluate mode
  44. mnist_infer.eval()
  45. def load_image(file):
  46. im = Image.open(file).convert('L')
  47. im = im.resize((28, 28), Image.ANTIALIAS)
  48. im = np.array(im).reshape(1, 1, 28, 28).astype(np.float32)
  49. im = im / 255.0 * 2.0 - 1.0
  50. return im
  51. cur_dir = os.path.dirname(os.path.realpath(__file__))
  52. tensor_img = load_image(cur_dir + '/image/infer_3.png')
  53. results = mnist_infer(to_variable(tensor_img))
  54. lab = np.argsort(results.numpy())
  55. print("Inference result of image/infer_3.png is: %d" % lab[0][-1])
  56. Loss at epoch 3 , Test avg_loss is: 0.0721620170576, acc is: 0.97796474359
  57. Loss at epoch 4 step 0: [0.01078923]
  58. Loss at epoch 4 step 100: [0.10447877]
  59. Loss at epoch 4 step 200: [0.05149534]
  60. Loss at epoch 4 step 300: [0.0122997]
  61. Loss at epoch 4 step 400: [0.0281883]
  62. Loss at epoch 4 step 500: [0.10709661]
  63. Loss at epoch 4 step 600: [0.1306036]
  64. Loss at epoch 4 step 700: [0.01628026]
  65. Loss at epoch 4 step 800: [0.07947419]
  66. Loss at epoch 4 step 900: [0.02067161]
  67. Loss at epoch 4 , Test avg_loss is: 0.0802323290939, acc is: 0.976963141026
  68. checkpoint saved
  69. checkpoint loaded
  70. Ran 1 test in 208.017s
  71. Inference result of image/infer_3.png is: 3

编写兼容的模型

以上一步中手写数字识别的例子为例,相同的模型代码可以直接在PaddlePaddle的Executor中执行:

  1. exe = fluid.Executor(fluid.CPUPlace(
  2. ) if not core.is_compiled_with_cuda() else fluid.CUDAPlace(0))
  3. mnist = MNIST("mnist")
  4. sgd = SGDOptimizer(learning_rate=1e-3)
  5. train_reader = paddle.batch(
  6. paddle.dataset.mnist.train(), batch_size= BATCH_SIZE, drop_last=True)
  7. img = fluid.layers.data(
  8. name='pixel', shape=[1, 28, 28], dtype='float32')
  9. label = fluid.layers.data(name='label', shape=[1], dtype='int64')
  10. cost = mnist(img)
  11. loss = fluid.layers.cross_entropy(cost, label)
  12. avg_loss = fluid.layers.mean(loss)
  13. sgd.minimize(avg_loss)
  14. out = exe.run(fluid.default_startup_program())
  15. for epoch in range(epoch_num):
  16. for batch_id, data in enumerate(train_reader()):
  17. static_x_data = np.array(
  18. [x[0].reshape(1, 28, 28)
  19. for x in data]).astype('float32')
  20. y_data = np.array(
  21. [x[1] for x in data]).astype('int64').reshape([BATCH_SIZE, 1])
  22. fetch_list = [avg_loss.name]
  23. out = exe.run(
  24. fluid.default_main_program(),
  25. feed={"pixel": static_x_data,
  26. "label": y_data},
  27. fetch_list=fetch_list)
  28. static_out = out[0]