将训练好的模型保存到磁盘之后,应用程序可以随时加载模型,完成预测任务。但是在日常训练工作中我们会遇到一些突发情况,导致训练过程主动或被动的中断。如果训练一个模型需要花费几天的训练时间,中断后从初始状态重新训练是不可接受的。
飞桨支持从上一次保存状态开始训练,只要我们随时保存训练过程中的模型状态,就不用从初始状态重新训练。
下面介绍恢复训练的代码实现,依然使用手写数字识别的案例,在网络定义的部分保持不变。
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 9 import gzip 10 import json 11 12 # 定义数据集读取器 13 def load_data(mode='train'): 14 15 # 数据文件 16 datafile = './work/mnist.json.gz' 17 print('loading mnist dataset from {} ......'.format(datafile)) 18 data = json.load(gzip.open(datafile)) 19 train_set, val_set, eval_set = data 20 21 # 数据集相关参数,图片高度IMG_ROWS, 图片宽度IMG_COLS 22 IMG_ROWS = 28 23 IMG_COLS = 28 24 25 if mode == 'train': 26 imgs = train_set[0] 27 labels = train_set[1] 28 elif mode == 'valid': 29 imgs = val_set[0] 30 labels = val_set[1] 31 elif mode == 'eval': 32 imgs = eval_set[0] 33 labels = eval_set[1] 34 35 imgs_length = len(imgs) 36 37 assert len(imgs) == len(labels), 38 "length of train_imgs({}) should be the same as train_labels({})".format( 39 len(imgs), len(labels)) 40 41 index_list = list(range(imgs_length)) 42 43 # 读入数据时用到的batchsize 44 BATCHSIZE = 100 45 46 # 定义数据生成器 47 def data_generator(): 48 #if mode == 'train': 49 # random.shuffle(index_list) 50 imgs_list = [] 51 labels_list = [] 52 for i in index_list: 53 img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype('float32') 54 label = np.reshape(labels[i], [1]).astype('int64') 55 imgs_list.append(img) 56 labels_list.append(label) 57 if len(imgs_list) == BATCHSIZE: 58 yield np.array(imgs_list), np.array(labels_list) 59 imgs_list = [] 60 labels_list = [] 61 62 # 如果剩余数据的数目小于BATCHSIZE, 63 # 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch 64 if len(imgs_list) > 0: 65 yield np.array(imgs_list), np.array(labels_list) 66 67 return data_generator 68 69 #调用加载数据的函数 70 train_loader = load_data('train') 71 72 # 定义模型结构 73 class MNIST(fluid.dygraph.Layer): 74 def __init__(self, name_scope): 75 super(MNIST, self).__init__(name_scope) 76 name_scope = self.full_name() 77 self.conv1 = Conv2D(name_scope, num_filters=20, filter_size=5, stride=1, padding=2, act="relu") 78 self.pool1 = Pool2D(name_scope, pool_size=2, pool_stride=2, pool_type='max') 79 self.conv2 = Conv2D(name_scope, num_filters=20, filter_size=5, stride=1, padding=2, act="relu") 80 self.pool2 = Pool2D(name_scope, pool_size=2, pool_stride=2, pool_type='max') 81 self.fc = FC(name_scope, size=10, act='softmax') 82 83 #加入分类准确率的评估指标 84 def forward(self, inputs, label=None): 85 x = self.conv1(inputs) 86 x = self.pool1(x) 87 x = self.conv2(x) 88 x = self.pool2(x) 89 x = self.fc(x) 90 if label is not None: 91 acc = fluid.layers.accuracy(input=x, label=label) 92 return x, acc 93 else: 94 return x
loading mnist dataset from ./work/mnist.json.gz ......
这是上面打印输出的一句话。
在开始使用飞桨恢复训练前,先正常训练一个模型,优化器使用Adam,使用动态变化的学习率,学习率从0.01衰减到0.001(12-16行)。每训练一轮后保存一次模型,之后将采用训练中的模型参数作为恢复训练的模型参数继续训练。
说明:
本次训练不仅保存模型参数,而且保存优化器、学习率有关的参数,比如Adam, Adagrad优化器在训练时会创建一些新的变量辅助训练;动态变化的学习率需要训练停止时的训练步数。这些参数对于恢复训练至关重要。
保存模型参数见43行,保存优化器参数见44行。
1 #在使用GPU机器时,可以将use_gpu变量设置成True 2 use_gpu = True 3 place = fluid.CUDAPlace(0) if use_gpu else fluid.CPUPlace() 4 5 with fluid.dygraph.guard(place): 6 model = MNIST("mnist") 7 model.train() 8 9 EPOCH_NUM = 5 10 BATCH_SIZE = 100 11 # 定义学习率,并加载优化器参数到模型中 12 total_steps = (int(60000//BATCH_SIZE) + 1) * EPOCH_NUM 13 lr = fluid.dygraph.PolynomialDecay(0.01, total_steps, 0.001) 14 15 # 使用Adam优化器 16 optimizer = fluid.optimizer.AdamOptimizer(learning_rate=lr) 17 18 for epoch_id in range(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 #前向计算的过程,同时拿到模型输出值和分类准确率 26 predict, acc = model(image, label) 27 avg_acc = fluid.layers.mean(acc) 28 29 #计算损失,取一个批次样本损失的平均值 30 loss = fluid.layers.cross_entropy(predict, label) 31 avg_loss = fluid.layers.mean(loss) 32 33 #每训练了200批次的数据,打印下当前Loss的情况 34 if batch_id % 200 == 0: 35 print("epoch: {}, batch: {}, loss is: {}, acc is {}".format(epoch_id, batch_id, avg_loss.numpy(),avg_acc.numpy())) 36 37 #后向传播,更新参数的过程 38 avg_loss.backward() 39 optimizer.minimize(avg_loss) 40 model.clear_gradients() 41 42 # 保存模型参数和优化器的参数 43 fluid.save_dygraph(model.state_dict(), './checkpoint/mnist_epoch{}'.format(epoch_id)) 44 fluid.save_dygraph(optimizer.state_dict(), './checkpoint/mnist_epoch{}'.format(epoch_id))
epoch: 0, batch: 0, loss is: [2.8080773], acc is [0.16] epoch: 0, batch: 200, loss is: [0.12834078], acc is [0.95] epoch: 0, batch: 400, loss is: [0.10862964], acc is [0.97] epoch: 1, batch: 0, loss is: [0.09647215], acc is [0.98] epoch: 1, batch: 200, loss is: [0.06849223], acc is [0.97] epoch: 1, batch: 400, loss is: [0.06437591], acc is [0.98] epoch: 2, batch: 0, loss is: [0.0582899], acc is [0.99] epoch: 2, batch: 200, loss is: [0.03881769], acc is [0.99] epoch: 2, batch: 400, loss is: [0.02290947], acc is [0.99] epoch: 3, batch: 0, loss is: [0.08704039], acc is [0.98] epoch: 3, batch: 200, loss is: [0.04802512], acc is [0.99] epoch: 3, batch: 400, loss is: [0.01132718], acc is [1.] epoch: 4, batch: 0, loss is: [0.07382318], acc is [0.99] epoch: 4, batch: 200, loss is: [0.05015907], acc is [0.98] epoch: 4, batch: 400, loss is: [0.0086437], acc is [1.]
恢复训练
在上述训练代码中,我们训练了五轮(epoch)。在每轮结束时,我们均保存了模型参数和优化器相关的参数。
- 使用model.state_dict()获取模型参数。
- 使用optimizer.state_dict()获取优化器和学习率相关的参数。
- 调用paddle的save_dygraph API将参数保存到本地。
比如第一轮训练保存的文件是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_epoch2" 2 #在使用GPU机器时,可以将use_gpu变量设置成True 3 use_gpu = True 4 place = fluid.CUDAPlace(0) if use_gpu else fluid.CPUPlace() 5 6 with fluid.dygraph.guard(place): 7 # 加载模型参数到模型中 8 params_dict, opt_dict = fluid.load_dygraph(params_path) 9 model = MNIST("mnist") 10 model.load_dict(params_dict) 11 12 EPOCH_NUM = 5 13 BATCH_SIZE = 100 14 # 定义学习率,并加载优化器参数到模型中 15 total_steps = (int(60000//BATCH_SIZE) + 1) * EPOCH_NUM 16 lr = fluid.dygraph.PolynomialDecay(0.01, total_steps, 0.001) 17 18 # 使用Adam优化器 19 optimizer = fluid.optimizer.AdamOptimizer(learning_rate=lr) 20 optimizer.set_dict(opt_dict) 21 22 for epoch_id in range(3, EPOCH_NUM): 23 for batch_id, data in enumerate(train_loader()): 24 #准备数据,变得更加简洁 25 image_data, label_data = data 26 image = fluid.dygraph.to_variable(image_data) 27 label = fluid.dygraph.to_variable(label_data) 28 29 #前向计算的过程,同时拿到模型输出值和分类准确率 30 predict, acc = model(image, label) 31 avg_acc = fluid.layers.mean(acc) 32 33 #计算损失,取一个批次样本损失的平均值 34 loss = fluid.layers.cross_entropy(predict, label) 35 avg_loss = fluid.layers.mean(loss) 36 37 #每训练了200批次的数据,打印下当前Loss的情况 38 if batch_id % 200 == 0: 39 print("epoch: {}, batch: {}, loss is: {}, acc is {}".format(epoch_id, batch_id, avg_loss.numpy(),avg_acc.numpy())) 40 41 #后向传播,更新参数的过程 42 avg_loss.backward() 43 optimizer.minimize(avg_loss) 44 model.clear_gradients()
epoch: 3, batch: 0, loss is: [0.08704039], acc is [0.98] epoch: 3, batch: 200, loss is: [0.03669801], acc is [0.99] epoch: 3, batch: 400, loss is: [0.0107842], acc is [1.] epoch: 4, batch: 0, loss is: [0.06170184], acc is [0.99] epoch: 4, batch: 200, loss is: [0.0378595], acc is [0.99] epoch: 4, batch: 400, loss is: [0.01447322], acc is [1.]
我更改了第1行和第22行,使系统从第3个epoch开始训练。
如果从第一个epoch开始重新训练,则1和22行改为*_epoch0,1这两个地方。
从恢复训练的损失变化来看,加载模型参数继续训练的损失函数值和正常训练损失函数值是完全一致的,可见使用飞桨实现恢复训练是极其简单的。