模型加载及恢复训练

在快速入门中,我们已经介绍了将训练好的模型保存到磁盘文件的方法。应用程序可以随时加载模型,完成预测任务。但是在日常训练工作中我们会遇到一些突发情况,导致训练过程主动或被动的中断。如果训练一个模型需要花费几天的训练时间,中断后从初始状态重新训练是不可接受的。

万幸的是,飞桨支持从上一次保存状态开始训练,只要我们随时保存训练过程中的模型状态,就不用从初始状态重新训练。

下面介绍恢复训练的代码实现,依然使用手写数字识别的案例,在网络定义的部分保持不变。

  1. import os
  2. import random
  3. import paddle
  4. import paddle.fluid as fluid
  5. from paddle.fluid.dygraph.nn import Conv2D, Pool2D, FC
  6. import numpy as np
  7. from PIL import Image
  8. import gzip
  9. import json
  10. # 定义数据集读取器
  11. def load_data(mode='train'):
  12. # 数据文件
  13. datafile = './work/mnist.json.gz'
  14. print('loading mnist dataset from {} ......'.format(datafile))
  15. data = json.load(gzip.open(datafile))
  16. train_set, val_set, eval_set = data
  17. # 数据集相关参数,图片高度IMG_ROWS, 图片宽度IMG_COLS
  18. IMG_ROWS = 28
  19. IMG_COLS = 28
  20. if mode == 'train':
  21. imgs = train_set[0]
  22. labels = train_set[1]
  23. elif mode == 'valid':
  24. imgs = val_set[0]
  25. labels = val_set[1]
  26. elif mode == 'eval':
  27. imgs = eval_set[0]
  28. labels = eval_set[1]
  29. imgs_length = len(imgs)
  30. assert len(imgs) == len(labels), \
  31. "length of train_imgs({}) should be the same as train_labels({})".format(
  32. len(imgs), len(labels))
  33. index_list = list(range(imgs_length))
  34. # 读入数据时用到的batchsize
  35. BATCHSIZE = 100
  36. # 定义数据生成器
  37. def data_generator():
  38. #if mode == 'train':
  39. # random.shuffle(index_list)
  40. imgs_list = []
  41. labels_list = []
  42. for i in index_list:
  43. img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype('float32')
  44. label = np.reshape(labels[i], [1]).astype('int64')
  45. imgs_list.append(img)
  46. labels_list.append(label)
  47. if len(imgs_list) == BATCHSIZE:
  48. yield np.array(imgs_list), np.array(labels_list)
  49. imgs_list = []
  50. labels_list = []
  51. # 如果剩余数据的数目小于BATCHSIZE,
  52. # 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch
  53. if len(imgs_list) > 0:
  54. yield np.array(imgs_list), np.array(labels_list)
  55. return data_generator
  56. #调用加载数据的函数
  57. train_loader = load_data('train')
  58. # 定义模型结构
  59. class MNIST(fluid.dygraph.Layer):
  60. def __init__(self, name_scope):
  61. super(MNIST, self).__init__(name_scope)
  62. name_scope = self.full_name()
  63. self.conv1 = Conv2D(name_scope, num_filters=20, filter_size=5, stride=1, padding=2, act="relu")
  64. self.pool1 = Pool2D(name_scope, pool_size=2, pool_stride=2, pool_type='max')
  65. self.conv2 = Conv2D(name_scope, num_filters=20, filter_size=5, stride=1, padding=2, act="relu")
  66. self.pool2 = Pool2D(name_scope, pool_size=2, pool_stride=2, pool_type='max')
  67. self.fc = FC(name_scope, size=10, act='softmax')
  68. #加入分类准确率的评估指标
  69. def forward(self, inputs, label=None):
  70. x = self.conv1(inputs)
  71. x = self.pool1(x)
  72. x = self.conv2(x)
  73. x = self.pool2(x)
  74. x = self.fc(x)
  75. if label is not None:
  76. acc = fluid.layers.accuracy(input=x, label=label)
  77. return x, acc
  78. else:
  79. return x
  1. loading mnist dataset from ./work/mnist.json.gz ......

在开始介绍使用飞桨恢复训练前,先正常训练一个模型,优化器使用Adam,使用动态变化的学习率,学习率从0.01衰减到0.001。每训练一轮后保存一次模型,之后将采用其中某一轮的模型参数进行恢复训练,验证一次性训练和中断再恢复训练的模型表现是否一致(训练loss的变化)。

注意进行恢复训练的程序不仅要保存模型参数,还要保存优化器参数。这是因为某些优化器含有一些随着训练过程变换的参数,例如Adam, Adagrad等优化器采用可变学习率的策略,随着训练进行会逐渐减少学习率。这些优化器的参数对于恢复训练至关重要。

为了演示这个特性,下面训练程序使用adam优化器,学习率以多项式曲线从0.01衰减到0.001(polynomial decay)。

  1. lr = fluid.dygraph.PolynomialDecay(0.01, total_steps, 0.001)
  • learning_rate:初始学习率
  • decay_steps:衰减步数
  • end_learning_rate:最终学习率
  • power:多项式的幂,默认值为1.0
  • cycle:下降后是否重新上升,polynomial decay的变化曲线下图所示。

    【手写数字识别】之恢复训练 - 图1

  1. #在使用GPU机器时,可以将use_gpu变量设置成True
  2. use_gpu = False
  3. place = fluid.CUDAPlace(0) if use_gpu else fluid.CPUPlace()
  4. with fluid.dygraph.guard(place):
  5. model = MNIST("mnist")
  6. model.train()
  7. EPOCH_NUM = 5
  8. BATCH_SIZE = 100
  9. # 定义学习率,并加载优化器参数到模型中
  10. total_steps = (int(60000//BATCH_SIZE) + 1) * EPOCH_NUM
  11. lr = fluid.dygraph.PolynomialDecay(0.01, total_steps, 0.001)
  12. # 使用Adam优化器
  13. optimizer = fluid.optimizer.AdamOptimizer(learning_rate=lr)
  14. for epoch_id in range(EPOCH_NUM):
  15. for batch_id, data in enumerate(train_loader()):
  16. #准备数据,变得更加简洁
  17. image_data, label_data = data
  18. image = fluid.dygraph.to_variable(image_data)
  19. label = fluid.dygraph.to_variable(label_data)
  20. #前向计算的过程,同时拿到模型输出值和分类准确率
  21. predict, acc = model(image, label)
  22. avg_acc = fluid.layers.mean(acc)
  23. #计算损失,取一个批次样本损失的平均值
  24. loss = fluid.layers.cross_entropy(predict, label)
  25. avg_loss = fluid.layers.mean(loss)
  26. #每训练了200批次的数据,打印下当前Loss的情况
  27. if batch_id % 200 == 0:
  28. print("epoch: {}, batch: {}, loss is: {}, acc is {}".format(epoch_id, batch_id, avg_loss.numpy(),avg_acc.numpy()))
  29. #后向传播,更新参数的过程
  30. avg_loss.backward()
  31. optimizer.minimize(avg_loss)
  32. model.clear_gradients()
  33. # 保存模型参数和优化器的参数
  34. fluid.save_dygraph(model.state_dict(), './checkpoint/mnist_epoch{}'.format(epoch_id))
  35. fluid.save_dygraph(optimizer.state_dict(), './checkpoint/mnist_epoch{}'.format(epoch_id))
  1. epoch: 0, batch: 0, loss is: [2.4541051], acc is [0.12]
  2. epoch: 0, batch: 200, loss is: [0.14235404], acc is [0.95]
  3. epoch: 0, batch: 400, loss is: [0.04791263], acc is [0.99]
  4. epoch: 1, batch: 0, loss is: [0.04537241], acc is [0.98]
  5. epoch: 1, batch: 200, loss is: [0.04753707], acc is [0.98]
  6. epoch: 1, batch: 400, loss is: [0.02443215], acc is [0.99]
  7. epoch: 2, batch: 0, loss is: [0.0282321], acc is [0.99]
  8. epoch: 2, batch: 200, loss is: [0.04439911], acc is [0.99]
  9. epoch: 2, batch: 400, loss is: [0.03719958], acc is [0.98]
  10. epoch: 3, batch: 0, loss is: [0.04289422], acc is [0.99]
  11. epoch: 3, batch: 200, loss is: [0.02114591], acc is [0.99]
  12. epoch: 3, batch: 400, loss is: [0.00777181], acc is [1.]
  13. epoch: 4, batch: 0, loss is: [0.01535593], acc is [0.99]
  14. epoch: 4, batch: 200, loss is: [0.00227343], acc is [1.]
  15. epoch: 4, batch: 400, loss is: [0.0032079], acc is [1.]

恢复训练

在上述训练代码中,我们训练了五轮(epoch)。在每轮结束时,我们均保存了模型参数和优化器相关的参数。

  • 使用model.state_dict()获取模型参数。
  • 使用optimizer.state_dict()获取优化器和学习率相关的参数。
  • 调用fluid.save_dygraph()将参数保存到本地。

比如第一轮训练保存的文件是mnist_epoch0.pdparams,mnist_epoch0.pdopt,分别存储了模型参数和优化器参数。

当加载模型时,如果模型参数文件和优化器参数文件是相同的,我们可以使用load_dygraph同时加载这两个文件,如下代码所示。

  1. params_dict, opt_dict = fluid.load_dygraph(params_path)

如果模型参数文件和优化器参数文件的名字不同,需要调用两次load_dygraph分别获得模型参数和优化器参数。

如何判断是模型是否准确的恢复训练呢?

理想的恢复训练是模型状态回到训练中断的时刻,恢复训练之后的梯度更新走向是和恢复训练前的梯度走向是完全相同的。基于此,我们可以通过恢复训练后的损失变化,判断上述方法是否能准确的恢复训练。即从epoch 0结束时保存的模型参数和优化器状态恢复训练,校验其后训练的损失变化(epoch 1)是否和不中断时的训练完全一致。


说明:

恢复训练有如下两个要点:

  • 保存模型时同时保存模型参数和优化器参数。
  • 恢复参数时同时恢复模型参数和优化器参数。

下面的代码将展示恢复训练的过程,并验证恢复训练是否成功。其中,我们重新定义一个train_again()训练函数,加载模型参数并从第一个epoch开始训练,以便读者可以校验恢复训练后的损失变化。

  1. params_path = "./checkpoint/mnist_epoch0"
  2. #在使用GPU机器时,可以将use_gpu变量设置成True
  3. use_gpu = False
  4. place = fluid.CUDAPlace(0) if use_gpu else fluid.CPUPlace()
  5. with fluid.dygraph.guard(place):
  6. # 加载模型参数到模型中
  7. params_dict, opt_dict = fluid.load_dygraph(params_path)
  8. model = MNIST("mnist")
  9. model.load_dict(params_dict)
  10. EPOCH_NUM = 5
  11. BATCH_SIZE = 100
  12. # 定义学习率,并加载优化器参数到模型中
  13. total_steps = (int(60000//BATCH_SIZE) + 1) * EPOCH_NUM
  14. lr = fluid.dygraph.PolynomialDecay(0.01, total_steps, 0.001)
  15. # 使用Adam优化器
  16. optimizer = fluid.optimizer.AdamOptimizer(learning_rate=lr)
  17. optimizer.set_dict(opt_dict)
  18. for epoch_id in range(1, EPOCH_NUM):
  19. for batch_id, data in enumerate(train_loader()):
  20. #准备数据,变得更加简洁
  21. image_data, label_data = data
  22. image = fluid.dygraph.to_variable(image_data)
  23. label = fluid.dygraph.to_variable(label_data)
  24. #前向计算的过程,同时拿到模型输出值和分类准确率
  25. predict, acc = model(image, label)
  26. avg_acc = fluid.layers.mean(acc)
  27. #计算损失,取一个批次样本损失的平均值
  28. loss = fluid.layers.cross_entropy(predict, label)
  29. avg_loss = fluid.layers.mean(loss)
  30. #每训练了200批次的数据,打印下当前Loss的情况
  31. if batch_id % 200 == 0:
  32. print("epoch: {}, batch: {}, loss is: {}, acc is {}".format(epoch_id, batch_id, avg_loss.numpy(),avg_acc.numpy()))
  33. #后向传播,更新参数的过程
  34. avg_loss.backward()
  35. optimizer.minimize(avg_loss)
  36. model.clear_gradients()
  1. epoch: 1, batch: 0, loss is: [0.04537241], acc is [0.98]
  2. epoch: 1, batch: 200, loss is: [0.04753707], acc is [0.98]
  3. epoch: 1, batch: 400, loss is: [0.02443215], acc is [0.99]
  4. epoch: 2, batch: 0, loss is: [0.0282321], acc is [0.99]
  5. epoch: 2, batch: 200, loss is: [0.04439911], acc is [0.99]
  6. epoch: 2, batch: 400, loss is: [0.03719958], acc is [0.98]
  7. epoch: 3, batch: 0, loss is: [0.04289422], acc is [0.99]
  8. epoch: 3, batch: 200, loss is: [0.02114591], acc is [0.99]
  9. epoch: 3, batch: 400, loss is: [0.00777181], acc is [1.]
  10. epoch: 4, batch: 0, loss is: [0.01535593], acc is [0.99]
  11. epoch: 4, batch: 200, loss is: [0.00227343], acc is [1.]
  12. epoch: 4, batch: 400, loss is: [0.0032079], acc is [1.]

从恢复训练的损失变化来看,加载模型参数继续训练的损失函数值和正常训练损失函数值是完全一致的,可见使用飞桨实现恢复训练是极其简单的。

epoch: 1, batch: 0, loss is: [0.04537241], acc is [0.98] epoch: 1, batch: 200, loss is: [0.04753707], acc is [0.98] epoch: 1, batch: 400, loss is: [0.02443215], acc is [0.99]