模型训练、评估与推理

在准备好数据集和模型后,就可以将数据送入模型中启动训练评估了,概括地讲包括如下几步:

  1. 模型训练:训练包括多轮迭代(epoch),每轮迭代遍历一次训练数据集,并且每次从中获取一小批(mini-batch)样本,送入模型执行前向计算得到预测值,并计算预测值(predict_label)与真实值(true_label)之间的损失函数值(loss)。执行梯度反向传播,并根据设置的优化算法(optimizer)更新模型的参数。观察每轮迭代的 loss 值减小趋势,可判断模型训练效果。

  2. 模型评估:将测试数据集送入训练好的模型进行评估,得到预测值,计算预测值与真实值之间的损失函数值(loss),并计算评价指标值(metric),便于评估模型效果。

  3. 模型推理:将待验证的数据(样本)送入训练好的模型执行推理,观察并验证推理结果(标签)是否符合预期。

飞桨框架提供了两种训练、评估与推理的方法:

  • 使用飞桨高层 API:先用 paddle.Model 对模型进行封装,然后通过 Model.fit 、 Model.evaluate 、 Model.predict 等完成模型的训练、评估与推理。该方式代码量少,适合快速上手。

  • 使用飞桨基础 API:提供了损失函数、优化器、评价指标、更新参数、反向传播等基础组件的实现,可以更灵活地应用到模型训练、评估与推理任务中,当然也可以很方便地自定义一些组件用于相关任务中。

高层 API 如 Model.fit 、 Model.evaluate 、 Model.predict 等都可以通过基础 API 实现,本文先介绍高层 API 的使用方式,然后将高层 API 拆解为基础 API 介绍,方便对比学习。

一、训练前准备

开始之前,需要使用下面的命令安装 Python 的 matplotlib 库,用于可视化图片。

# 使用 pip 工具安装 matplotlib
! python3 -m pip install matplotlib -i https://mirror.baidu.com/pypi/simple

1.1 (可选)指定训练的硬件

模型训练时,需要用到 CPU、 GPU 等计算处理器资源,由于飞桨框架的安装包是区分处理器类型的,默认情况下飞桨框架会根据所安装的版本自动选择对应硬件,比如安装的 GPU 版本的飞桨,则自动使用 GPU 训练模型,无需手动指定。因此一般情况下,无需执行此步骤。

但是如果安装的 GPU 版本的飞桨框架,想切换到 CPU 上训练,则可通过 paddle.device.set_device 修改。如果本机有多个 GPU 卡,也可以通过该 API 选择指定的卡进行训练,不指定的情况下则默认使用 'gpu:0'。

import paddle

# 指定在 CPU 上训练
paddle.device.set_device('cpu')

# 指定在 GPU 第 0 号卡上训练
# paddle.device.set_device('gpu:0')
Place(cpu)

需要注意的是,使用 paddle.device.set_device 时,只能使用 CUDA_VISIBLE_DEVICES 设置范围内的显卡,例如可以设置export CUDA_VISIBLE_DEVICES=0,1,2 和 paddle.device.set_device('gpu:0'),但是设置 export CUDA_VISIBLE_DEVICES=1 和 paddle.device.set_device('gpu:0') 时会冲突报错。

注:

  • 本文仅以单机单卡场景为例,介绍模型训练的方法,如果需要使用单机多卡、多机多卡训练,请参考如下章节:分布式训练。

  • 飞桨框架除了支持在 CPU、GPU 上训练,还支持在百度昆仑 XPU、华为昇腾 NPU 等 AI 计算处理器上训练,对应的训练指导请参考 硬件支持 章节。

1.2 准备训练用的数据集和模型

模型训练前,需要先完成数据集的加载和模型组网,以 MNIST 手写数字识别任务为例,代码示例如下:

from paddle.vision.transforms import Normalize

transform = Normalize(mean=[127.5], std=[127.5], data_format='CHW')
# 加载 MNIST 训练集和测试集
train_dataset = paddle.vision.datasets.MNIST(mode='train', transform=transform)
test_dataset = paddle.vision.datasets.MNIST(mode='test', transform=transform)

# 模型组网,构建并初始化一个模型 mnist
mnist = paddle.nn.Sequential(
    paddle.nn.Flatten(1, -1), 
    paddle.nn.Linear(784, 512), 
    paddle.nn.ReLU(), 
    paddle.nn.Dropout(0.2), 
    paddle.nn.Linear(512, 10)
)

二、使用 paddle.Model 高层 API 训练、评估与推理

以手写数字识别任务为例,使用高层 API 进行模型训练、评估与推理的步骤如下:

2.1 使用 paddle.Model 封装模型

使用高层 API 训练模型前,可使用 paddle.Model 将模型封装为一个实例,方便后续进行训练、评估与推理。代码如下:

# 封装模型为一个 model 实例,便于进行后续的训练、评估和推理
model = paddle.Model(mnist)

2.2 使用 Model.prepare 配置训练准备参数

用 paddle.Model 完成模型的封装后,需通过 Model.prepare 进行训练前的配置准备工作,包括设置优化算法、Loss 计算方法、评价指标计算方法:

  • 优化器(optimizer):即寻找最优解的方法,可计算和更新梯度,并根据梯度更新模型参数。飞桨框架在 paddle.optimizer 下提供了优化器相关 API。并且需要为优化器设置合适的学习率,或者指定合适的学习率策略,飞桨框架在 paddle.optimizer.lr 下提供了学习率策略相关的 API。

  • 损失函数(loss):用于评估模型的预测值和真实值的差距,模型训练过程即取得尽可能小的 loss 的过程。飞桨框架在 paddle.nn Loss层 提供了适用不同深度学习任务的损失函数相关 API。

  • 评价指标(metrics):用于评估模型的好坏,不同的任务通常有不同的评价指标。飞桨框架在 paddle.metric 下提供了评价指标相关 API。

# 为模型训练做准备,设置优化器及其学习率,并将网络的参数传入优化器,设置损失函数和精度计算方式
model.prepare(optimizer=paddle.optimizer.Adam(learning_rate=0.001, parameters=model.parameters()), 
              loss=paddle.nn.CrossEntropyLoss(), 
              metrics=paddle.metric.Accuracy())

示例中使用 Adam 优化器,设置优化器的学习率 learning_rate=0.001,并传入封装好的全部模型参数 model.parameters 用于后续更新;使用交叉熵损失函数 CrossEntropyLoss 用于分类任务评估;使用分类任务常用的准确率指标 Accuracy 计算模型在训练集上的精度。

2.3 使用 Model.fit 训练模型

做好模型训练的前期准备工作后,调用 Model.fit 接口来启动训练。 训练过程采用二层循环嵌套方式:内层循环完成整个数据集的一次遍历,采用分批次方式;外层循环根据设置的训练轮次完成数据集的多次遍历。因此需要指定至少三个关键参数:训练数据集,训练轮次和每批次大小:

  • 训练数据集:传入之前定义好的训练数据集。

  • 训练轮次(epoch):训练时遍历数据集的次数,即外循环轮次。

  • 批次大小(batch_size):内循环中每个批次的训练样本数。

除此之外,还可以设置样本乱序(shuffle)、丢弃不完整的批次样本(drop_last)、同步/异步读取数据(num_workers) 等参数,另外可通过 Callback 参数传入回调函数,在模型训练的各个阶段进行一些自定义操作,比如收集训练过程中的一些数据和参数,详细介绍可参见 自定义 Callback 章节。

# 启动模型训练,指定训练数据集,设置训练轮次,设置每次数据集计算的批次大小,设置日志格式
model.fit(train_dataset, 
          epochs=5, 
          batch_size=64,
          verbose=1)
The loss value printed in the log is the current step, and the metric is the average value of previous steps.
Epoch 1/5
step  10/938 [..............................] - loss: 0.9679 - acc: 0.4109 - ETA: 13s - 14ms/stepstep 938/938 [==============================] - loss: 0.1158 - acc: 0.9020 - 10ms/step          
Epoch 2/5
step 938/938 [==============================] - loss: 0.0981 - acc: 0.9504 - 10ms/step          
Epoch 3/5
step 938/938 [==============================] - loss: 0.0215 - acc: 0.9588 - 10ms/step          
Epoch 4/5
step 938/938 [==============================] - loss: 0.0134 - acc: 0.9643 - 10ms/step          
Epoch 5/5
step 938/938 [==============================] - loss: 0.3371 - acc: 0.9681 - 11ms/step          

示例中传入数据集 train_dataset 进行迭代训练,共遍历 5 轮(epochs=5),每轮迭代中分批次取数据训练,每批次 64 个样本(batch_size=64),并打印训练过程中的日志(verbose=1)。 从打印日志中可观察到损失函数 loss 值减小,精度指标 acc 值提高的趋势,说明模型训练取得了成效。

2.4 使用 Model.evaluate 评估模型

训练好模型后,可在事先定义好的测试数据集上,使用 Model.evaluate 接口完成模型评估操作,结束后根据在 Model.prepare 中定义的 loss 和 metric 计算并返回相关评估结果。

返回格式是一个字典:

  • 只包含loss, {'loss': xxx}

  • 包含loss和一个评估指标, {'loss': xxx, 'metric name': xxx}

  • 包含loss和多个评估指标, {'loss': xxx, 'metric name1': xxx, 'metric name2': xxx}

# 用 evaluate 在测试集上对模型进行验证
eval_result = model.evaluate(test_dataset, verbose=1)
print(eval_result)
Eval begin...
step 10000/10000 [==============================] - loss: 2.3842e-07 - acc: 0.9714 - 2ms/step          
Eval samples: 10000
{'loss': [2.384186e-07], 'acc': 0.9714}

示例中返回一个 loss 和 一个 acc 准确率指标的结果。在模型之前未"见过"的测试集上,评估出仍然有 98.1% 的准确率,验证了模型在该任务上取得不错的效果。

2.5 使用 Model.predict 执行推理

高层 API 中提供了 Model.predict 接口,可对训练好的模型进行推理验证。只需传入待执行推理验证的样本数据,即可计算并返回推理结果。

返回格式是一个列表:

  • 模型是单一输出:[(numpy_ndarray_1, numpy_ndarray_2, …, numpy_ndarray_n)]

  • 模型是多输出:[(numpy_ndarray_1, numpy_ndarray_2, …, numpy_ndarray_n), (numpy_ndarray_1, numpy_ndarray_2, …, numpy_ndarray_n), …]

如果模型是单一输出,则输出的形状为 [1, n],n 表示数据集的样本数。其中每个 numpy_ndarray_n 是对应原始数据经过模型计算后得到的预测结果,类型为 numpy 数组,例如 mnist 分类任务中,每个 numpy_ndarray_n 是长度为 10 的 numpy 数组。

如果模型是多输出,则输出的形状为[m, n],m 表示标签的种类数,在多标签分类任务中,m 会根据标签的数目而定。


# 用 predict 在测试集上对模型进行推理
test_result = model.predict(test_dataset)
# 由于模型是单一输出,test_result的形状为[1, 10000],10000是测试数据集的数据量。这里打印第一个数据的结果,这个数组表示每个数字的预测概率
print(len(test_result))
print(test_result[0][0])

# 从测试集中取出一张图片
img, label = test_dataset[0]

# 打印推理结果,这里的argmax函数用于取出预测值中概率最高的一个的下标,作为预测标签
pred_label = test_result[0][0].argmax()
print('true label: {}, pred label: {}'.format(label[0], pred_label))
# 使用matplotlib库,可视化图片
from matplotlib import pyplot as plt
plt.imshow(img[0])
Predict begin...
step 10000/10000 [==============================] - 2ms/step          
Predict samples: 10000
1
[[ -6.512169   -6.7076845   0.5048795   1.6733919  -9.670526   -1.6352568
  -15.833721   13.87411    -8.215239    1.5966017]]
true label: 7, pred label: 7





<matplotlib.image.AxesImage at 0x7f95f014f4d0>

png

示例中对测试集 test_dataset 中每一个样本执行预测,测试数据集中包含 10000 个数据,因此将取得 10000 个预测输出。

打印第一个样本数据的预测输出,可以看到,在手写数字识别任务中,经过模型的计算得到一个数组 [[ -6.5593615 -6.4680595 -1.4708003 2.1043894 -11.743436 -4.4516582 -14.733968 12.036645 -6.582403 -1.8672216]],取其中最大的值(12.036645)的下标(对应 label 7),即得到该样本数据的预测结果(pred label: 7),可视化该样本图像(true label: 7),与预测结果一致,说明模型准确预测了样本图像上的数字。

除了上面介绍的三个 API 之外, paddle.Model 类也提供了其他与训练、评估与推理相关的 API:

这三个 API 与上面介绍的三个 API 的输入数据的维度有所不同,详细介绍可参考对应 API 文档。

三、使用基础 API 训练、评估与推理

除了通过高层 API 实现模型的训练、评估与推理,飞桨框架也同样支持通过基础 API。简单来说, Model.prepare 、 Model.fit 、 Model.evaluate 、 Model.predict 都是由基础 API 封装而来。下面通过拆解高层 API 到基础 API 的方式,来了解如何用基础 API 完成模型训练、评估与推理。

3.1 模型训练(拆解 Model.prepare、Model.fit)

飞桨框架通过基础 API 对模型进行训练,对应高层 API 的 Model.prepare 与 Model.fit ,一般包括如下几个步骤:

  1. 加载训练数据集、声明模型、设置模型实例为 train 模式

  2. 设置优化器、损失函数与各个超参数

  3. 设置模型训练的二层循环嵌套,并在内层循环嵌套中设置如下内容

    • 3.1 从数据读取器 DataLoader 获取一批次训练数据

    • 3.2 执行一次预测,即经过模型计算获得输入数据的预测值

    • 3.3 计算预测值与数据集标签的损失

    • 3.4 计算预测值与数据集标签的准确率

    • 3.5 将损失进行反向传播

    • 3.6 打印模型的轮数、批次、损失值、准确率等信息

    • 3.7 执行一次优化器步骤,即按照选择的优化算法,根据当前批次数据的梯度更新传入优化器的参数

    • 3.8 将优化器的梯度进行清零

# dataset与mnist的定义与使用高层API的内容一致
# 用 DataLoader 实现数据加载
train_loader = paddle.io.DataLoader(train_dataset, batch_size=64, shuffle=True)

# 将mnist模型及其所有子层设置为训练模式。这只会影响某些模块,如Dropout和BatchNorm。
mnist.train()

# 设置迭代次数
epochs = 5

# 设置优化器
optim = paddle.optimizer.Adam(parameters=mnist.parameters())
# 设置损失函数
loss_fn = paddle.nn.CrossEntropyLoss()
for epoch in range(epochs):
    for batch_id, data in enumerate(train_loader()):
        
        x_data = data[0]            # 训练数据
        y_data = data[1]            # 训练数据标签
        predicts = mnist(x_data)    # 预测结果  
        
        # 计算损失 等价于 prepare 中loss的设置
        loss = loss_fn(predicts, y_data)
        
        # 计算准确率 等价于 prepare 中metrics的设置
        acc = paddle.metric.accuracy(predicts, y_data)
        
        # 下面的反向传播、打印训练信息、更新参数、梯度清零都被封装到 Model.fit() 中
        # 反向传播 
        loss.backward()
        
        if (batch_id+1) % 900 == 0:
            print("epoch: {}, batch_id: {}, loss is: {}, acc is: {}".format(epoch, batch_id+1, loss.numpy(), acc.numpy()))
        # 更新参数 
        optim.step()
        # 梯度清零
        optim.clear_grad()
epoch: 0, batch_id: 900, loss is: [0.06991791], acc is: [0.96875]
epoch: 1, batch_id: 900, loss is: [0.02878829], acc is: [1.]
epoch: 2, batch_id: 900, loss is: [0.07192856], acc is: [0.96875]
epoch: 3, batch_id: 900, loss is: [0.20411499], acc is: [0.96875]
epoch: 4, batch_id: 900, loss is: [0.13589518], acc is: [0.96875]

3.2 模型评估(拆解 Model.evaluate)

飞桨框架通过基础 API 对训练好的模型进行评估,对应高层 API 的 Model.evaluate 。与模型训练相比,模型评估的流程有如下几点不同之处:

  1. 加载的数据从训练数据集改为测试数据集

  2. 模型实例从 train 模式改为 eval 模式

  3. 不需要反向传播、优化器参数更新和优化器梯度清零

# 加载测试数据集
test_loader = paddle.io.DataLoader(test_dataset, batch_size=64, drop_last=True)
# 设置损失函数
loss_fn = paddle.nn.CrossEntropyLoss()
# 将该模型及其所有子层设置为预测模式。这只会影响某些模块,如Dropout和BatchNorm
mnist.eval()
# 禁用动态图梯度计算
for batch_id, data in enumerate(test_loader()):
    
    x_data = data[0]            # 测试数据
    y_data = data[1]            # 测试数据标签
    predicts = mnist(x_data)    # 预测结果
    
    # 计算损失与精度
    loss = loss_fn(predicts, y_data)
    acc = paddle.metric.accuracy(predicts, y_data)
    
    # 打印信息
    if (batch_id+1) % 30 == 0:
        print("batch_id: {}, loss is: {}, acc is: {}".format(batch_id+1, loss.numpy(), acc.numpy()))
batch_id: 30, loss is: [0.23106411], acc is: [0.953125]
batch_id: 60, loss is: [0.4329119], acc is: [0.90625]
batch_id: 90, loss is: [0.07333981], acc is: [0.96875]
batch_id: 120, loss is: [0.00324837], acc is: [1.]
batch_id: 150, loss is: [0.0857158], acc is: [0.96875]

3.3 模型推理(拆解 Model.predict)

飞桨框架通过基础 API 对训练好的模型执行推理,对应高层 API 的 Model.predict 。模型的推理过程相对独立,是在模型训练与评估之后单独进行的步骤。只需要执行如下步骤:

  1. 加载待执行推理的测试数据,并将模型设置为 eval 模式

  2. 读取测试数据并获得预测结果

  3. 对预测结果进行后处理

# 加载测试数据集
test_loader = paddle.io.DataLoader(test_dataset, batch_size=64, drop_last=True)
# 将该模型及其所有子层设置为预测模式
mnist.eval()
for batch_id, data in enumerate(test_loader()):
    # 取出测试数据
    x_data = data[0] 
    # 获取预测结果
    predicts = mnist(x_data)
print("predict finished")

# 从测试集中取出一组数据
img, label = test_loader().next()

# 执行推理并打印结果
pred_label = mnist(img)[0].argmax()
print('true label: {}, pred label: {}'.format(label[0].item(), pred_label[0].item()))
# 可视化图片
from matplotlib import pyplot as plt
plt.imshow(img[0][0])
predict finished
true label: 7, pred label: 7





<matplotlib.image.AxesImage at 0x7f95f009be50>

png

四、总结

本节中介绍了在飞桨框架中使用高层 API 进行模型训练、评估和推理的方法,并拆解出对应的基础 API 实现方法。需要注意的是,这里的推理仅用于模型效果验证,实际生产应用中,则可使用飞桨提供的一系列推理部署工具,满足服务器端、移动端、网页/小程序等多种环境的模型部署上线需求,具体可参见 推理部署 章节。

同时,飞桨的高层 API 和基础 API 可以组合使用,并不是完全割裂开的,这样有助于开发者更便捷地完成算法迭代。示例代码如下:

from  paddle.vision.models import LeNet

class FaceNet(paddle.nn.Layer):
    def __init__(self):
        super().__init__()
        # 使用高层API组网
        self.backbone = LeNet()
        # 使用基础API组网
        self.outLayer1 = paddle.nn.Sequential(
            paddle.nn.Linear(10, 512),
            paddle.nn.ReLU(),
            paddle.nn.Dropout(0.2)
        )
        self.outLayer2 = paddle.nn.Linear(512, 10)
    
    def forward(self, inputs):
        out = self.backbone(inputs)
        out = self.outLayer1(out)
        out = self.outLayer2(out)
        return out
# 使用高层API封装网络
model = paddle.Model(FaceNet())
# 使用基础API定义优化器
optim = paddle.optimizer.Adam(learning_rate=1e-3, parameters=model.parameters())
# 使用高层API封装优化器和损失函数
model.prepare(optim, paddle.nn.CrossEntropyLoss(), metrics=paddle.metric.Accuracy())
# 使用高层API训练网络
model.fit(train_dataset, test_dataset, epochs=5, batch_size=64, verbose=1)
The loss value printed in the log is the current step, and the metric is the average value of previous steps.
Epoch 1/5
step 938/938 [==============================] - loss: 0.0863 - acc: 0.9369 - 22ms/step          
Eval begin...
step 157/157 [==============================] - loss: 0.1034 - acc: 0.9741 - 8ms/step          
Eval samples: 10000
Epoch 2/5
step 938/938 [==============================] - loss: 0.0366 - acc: 0.9769 - 21ms/step          
Eval begin...
step 157/157 [==============================] - loss: 0.0047 - acc: 0.9835 - 8ms/step          
Eval samples: 10000
Epoch 3/5
step 938/938 [==============================] - loss: 0.0073 - acc: 0.9813 - 21ms/step          
Eval begin...
step 157/157 [==============================] - loss: 0.0345 - acc: 0.9818 - 9ms/step          
Eval samples: 10000
Epoch 4/5
step 938/938 [==============================] - loss: 0.0025 - acc: 0.9842 - 21ms/step          
Eval begin...
step 157/157 [==============================] - loss: 0.0033 - acc: 0.9834 - 8ms/step          
Eval samples: 10000
Epoch 5/5
step 938/938 [==============================] - loss: 0.0294 - acc: 0.9859 - 20ms/step          
Eval begin...
step 157/157 [==============================] - loss: 0.0039 - acc: 0.9834 - 7ms/step          
Eval samples: 10000