2.9 手写数字识别之恢复训练
在上一节中,我们已经介绍了将训练好的模型保存到磁盘文件的方法。应用程序可以随时加载模型,完成预测任务。但是在日常训练工作中我们会遇到一些突发情况,导致训练过程主动或被动的中断。如果训练一个模型需要花费几天的训练时间,中断后从初始状态重新训练是不可接受的。万幸的是,飞桨支持从上一次保存状态开始训练,只要我们随时保存训练过程中的模型状态,就不用从初始状态重新训练。
2.9.1 构建并训练网络
下面介绍恢复训练的实现方法,依然使用手写数字识别的案例,网络定义的部分保持不变,直接调用封装好的代码。
In [1]
import paddle import os from data_process import get_MNIST_dataloader from MNIST_network import MNIST import paddle.nn.functional as F train_loader, test_loader = get_MNIST_dataloader()
定义训练Trainer,包含训练过程和模型保存
In [2]
class Trainer(object): def __init__(self, model_path, model, optimizer): self.model_path = model_path # 模型存放路径 self.model = model # 定义的模型 self.optimizer = optimizer # 优化器 def save(self): # 保存模型 paddle.save(self.model.state_dict(), self.model_path) def train_step(self, data): images, labels = data # 前向计算的过程 predicts = self.model(images) # 计算损失 loss = F.cross_entropy(predicts, labels) avg_loss = paddle.mean(loss) # 后向传播,更新参数的过程 avg_loss.backward() self.optimizer.step() self.optimizer.clear_grad() return avg_loss def train_epoch(self, datasets, epoch): self.model.train() for batch_id, data in enumerate(datasets()): loss = self.train_step(data) # 每训练了1000批次的数据,打印下当前Loss的情况 if batch_id % 500 == 0: print("epoch_id: {}, batch_id: {}, loss is: {}".format(epoch, batch_id, loss.numpy())) def train(self, train_datasets, start_epoch, end_epoch, save_path): if not os.path.exists(save_path): os.makedirs(save_path) for i in range(start_epoch, end_epoch): self.train_epoch(train_datasets, i) paddle.save(self.optimizer.state_dict(), './{}/mnist_epoch{}'.format(save_path,i)+'.pdopt') paddle.save(self.model.state_dict(), './{}/mnist_epoch{}'.format(save_path,i)+'.pdparams') self.save()
在开始介绍使用飞桨恢复训练前,先正常训练一个模型,优化器使用Adam,使用动态变化的学习率,学习率从0.01衰减到0.001。每训练一轮后保存一次模型,之后将采用其中某一轮的模型参数进行恢复训练,验证一次性训练和中断再恢复训练的模型表现是否相差不多(训练loss的变化)。
注意进行恢复训练的程序不仅要保存模型参数,还要保存优化器参数。这是因为某些优化器含有一些随着训练过程变换的参数,例如Adam、Adagrad等优化器采用可变学习率的策略,随着训练进行会逐渐减少学习率。这些优化器的参数对于恢复训练至关重要。
为了演示这个特性,训练程序使用Adam优化器,PolynomialDecay的变化策略将学习率从0.01衰减到0.001。
classpaddle.optimizer.lr.PolynomialDecay (learningrate, decaysteps, endlr=0.0001, power=1.0, cycle=False, lastepoch=- 1, verbose=False)
参数说明如下:
- learning_rate (float):初始学习率,数据类型为Python float。
- decay_steps (int):进行衰减的步长,这个决定了衰减周期。
- end_lr (float,可选):最小的最终学习率,默认值为0.0001。
- power (float,可选):多项式的幂,默认值为1.0。
- last_epoch (int,可选):上一轮的轮数,重启训练时设置为上一轮的epoch数。默认值为-1,则为初始学习率。
- verbose (bool,可选):如果是 True,则在每一轮更新时在标准输出stdout输出一条信息,默认值为False。
- cycle (bool,可选):学习率下降后是否重新上升。若为True,则学习率衰减到最低学习率值时会重新上升。若为False,则学习率单调递减。默认值为False。
PolynomialDecay的变化曲线下图所示:
In [ ]
#在使用GPU机器时,可以将use_gpu变量设置成True use_gpu = True paddle.set_device('gpu:0') if use_gpu else paddle.set_device('cpu') paddle.seed(1024) epochs = 3 BATCH_SIZE = 32 model_path = './mnist.pdparams' model = MNIST() total_steps = (int(50000//BATCH_SIZE) + 1) * epochs lr = paddle.optimizer.lr.PolynomialDecay(learning_rate=0.01, decay_steps=total_steps, end_lr=0.001) optimizer = paddle.optimizer.Adam(learning_rate=lr, parameters=model.parameters()) trainer = Trainer( model_path=model_path, model=model, optimizer=optimizer ) trainer.train(train_datasets=train_loader, start_epoch = 0, end_epoch = epochs, save_path='checkpoint')
W0901 17:29:43.145830 192 device_context.cc:447] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 11.2, Runtime API Version: 10.1
W0901 17:29:43.152843 192 device_context.cc:465] device: 0, cuDNN Version: 7.6.
epoch_id: 0, batch_id: 0, loss is: [3.900196]
epoch_id: 0, batch_id: 500, loss is: [0.1180672]
epoch_id: 1, batch_id: 0, loss is: [0.05891193]
epoch_id: 1, batch_id: 500, loss is: [0.16080402]
epoch_id: 2, batch_id: 0, loss is: [0.05444674]
epoch_id: 2, batch_id: 500, loss is: [0.02639692]
2.9.2 恢复训练
模型恢复训练,需要重新组网,所以我们需要重启AI Studio,运行数据处理和
MNIST
网络定义、Trainer
部分代码,再执行模型恢复代码。
在上述训练代码中,我们训练了3轮(epoch)。在每轮结束时,我们均保存了模型参数和优化器相关的参数。
- 使用
model.state_dict()
获取模型参数。 - 使用
opt.state_dict
获取优化器和学习率相关的参数。 - 调用
paddle.save
将参数保存到本地。
比如第一轮训练保存的文件是mnist_epoch0.pdparams,mnist_epoch0.pdopt,分别存储了模型参数和优化器参数。
使用paddle.load
分别加载模型参数和优化器参数,如下代码所示。
paddle.load(params_path+'.pdparams') paddle.load(params_path+'.pdopt')
如何判断模型是否准确的恢复训练呢?
理想的恢复训练是模型状态回到训练中断的时刻,恢复训练之后的梯度更新走向是和恢复训练前的梯度走向完全相同的。基于此,我们可以通过恢复训练后的损失变化,判断上述方法是否能准确的恢复训练。即从epoch 0结束时保存的模型参数和优化器状态恢复训练,校验其后训练的损失变化(epoch 1)是否和不中断时的训练相差不多。
说明:
恢复训练有如下两个要点:
- 保存模型时分别保存模型参数和优化器参数。
- 恢复参数时分别恢复模型参数和优化器参数。
下面的代码将展示恢复训练的过程,并验证恢复训练是否成功。加载模型参数并从第一个epoch开始训练,以便读者可以校验恢复训练后的损失变化。
In [3]
# MLP继续训练 paddle.seed(1024) epochs = 3 BATCH_SIZE = 32 model_path = './mnist_retrain.pdparams' model = MNIST() # lr = 0.01 total_steps = (int(50000//BATCH_SIZE) + 1) * epochs lr = paddle.optimizer.lr.PolynomialDecay(learning_rate=0.01, decay_steps=total_steps, end_lr=0.001) optimizer = paddle.optimizer.Adam(learning_rate=lr, parameters=model.parameters()) params_dict = paddle.load('./checkpoint/mnist_epoch0.pdparams') opt_dict = paddle.load('./checkpoint/mnist_epoch0.pdopt') # 加载参数到模型 model.set_state_dict(params_dict) optimizer.set_state_dict(opt_dict) trainer = Trainer( model_path=model_path, model=model, optimizer=optimizer ) # 前面训练模型都保存了,这里save_path设置为新路径,实际训练中保存在同一目录就可以 trainer.train(train_datasets=train_loader,start_epoch = 1, end_epoch = epochs, save_path='checkpoint_con')
W0901 17:31:41.696164 509 device_context.cc:447] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 11.2, Runtime API Version: 10.1
W0901 17:31:41.700848 509 device_context.cc:465] device: 0, cuDNN Version: 7.6.
epoch_id: 1, batch_id: 0, loss is: [0.03602091]
epoch_id: 1, batch_id: 500, loss is: [0.4263561]
epoch_id: 2, batch_id: 0, loss is: [0.0733113]
epoch_id: 2, batch_id: 500, loss is: [0.13029066]
我们可以将模型参数和优化器参数分别保存,也可以同时保存,具体见下面的例子:
In [4]
import paddle from paddle import nn from paddle.optimizer import Adam layer = paddle.nn.Linear(3, 4) adam = Adam(learning_rate=0.001, parameters=layer.parameters()) obj = {'model': layer.state_dict(), 'opt': adam.state_dict(), 'epoch': 100} path = 'example/model.pdparams' paddle.save(obj, path)
从恢复训练的损失变化来看,加载模型参数继续训练的损失函数值和正常训练损失函数值是一致,可见使用飞桨实现恢复训练是极其简单的。 总结一下:
- 保存模型时同时保存模型参数和优化器参数;
paddle.save(opt.state_dict(), 'model.pdopt') paddle.save(model.state_dict(), 'model.pdparams')
- 恢复参数时同时恢复模型参数和优化器参数。
model_dict = paddle.load("model.pdparams") opt_dict = paddle.load("model.pdopt") model.set_state_dict(model_dict) opt.set_state_dict(opt_dict)