PaddlePaddle 详细学习笔记:第三章:完整训练与评估循环 (核心实战)
第三章:完整训练与评估循环 (核心实战)
万事俱备,只欠东风。这“东风”就是一个规范、健壮的训练循环(Training Loop)。本章的目标,是让你能够独立编写出一个工业级的训练脚本,能处理从模型训练、性能验证到断点续训的全过程。我们将以第二章构建的 LeNet 模型和飞桨内置的 MNIST 数据集为例,贯穿本章所有实战环节。
3.1 标准训练循环 (The Training Loop)
训练循环的本质,就是让模型不断地“看”数据,并通过“犯错-反思-修正”的过程,逐步优化自身的参数。这个过程被组织成 Epochs 和 Steps (或 Iterations):
- Epoch (时代):代表模型完整地看过一遍所有训练数据。例如,训练集有 10000 张图片,
batch_size为 100,那么一个Epoch就包含10000 / 100 = 100个Step。进行多个Epoch的训练是深度学习的常态。 - Step/Iteration (步/迭代):代表模型处理一个
batch的数据。每一次Step,模型的参数都会被更新一次。
3.1.1 实战:model.train() —— 开启“学习模式”
在开始训练循环之前,必须做的第一件事就是调用 model.train()。这行代码看似简单,却至关重要。它并非一个可有可无的标记,而是会切实地改变模型中某些层的行为模式。
| 模式 | model.train() (训练模式) |
model.eval() (评估模式) |
|---|---|---|
| Dropout 层 | 激活。在前向传播时,会以一定概率随机地将部分神经元的输出置为 0,用于防止过拟合。 | 关闭。所有神经元都会参与计算,以确保模型输出的确定性和稳定性。 |
| BatchNorm 层 | 使用当前批次数据的均值和方差来做归一化,同时会持续更新全局的均值和方差统计量。 | 使用在整个训练过程中学习到的全局均值和方差来进行归一化,且不会更新它们。 |
一句话总结:model.train() 确保了 Dropout 和 BatchNorm 等层处于它们在训练时应有的状态。忘记调用它,可能会导致模型不收敛或性能远低于预期。
3.1.2 实战:训练的“五步法” (The Five-Step Method)
每一个 Step 的核心,都可以被精炼为一套标准化的“五步法”。掌握了这个流程,你就掌握了所有监督学习任务的训练核心。
- 前向传播 (Forward Pass):将一个
batch的数据输入模型,计算得到预测结果。predictions = model(inputs) - 计算损失 (Compute Loss):使用损失函数,比较模型的预测结果和真实标签,计算出损失值
loss。loss = loss_fn(predictions, labels) - 反向传播 (Backward Pass):从
loss开始,自动计算出模型中每一个可学习参数相对于损失值的梯度(gradient)。loss.backward() - 更新参数 (Update Parameters):优化器根据上一步计算出的梯度,来更新模型的参数。
optimizer.step() - 梯度清零 (Clear Gradients):清除上一步计算的梯度,为下一次迭代做准备。
optimizer.clear_grad()
易错点对比:
clear_grad()vszero_grad()
- PaddlePaddle:
optimizer.clear_grad()- PyTorch:
optimizer.zero_grad()两者功能完全相同,但名称不同。这是从 PyTorch 迁移过来的开发者最容易犯的错误之一。忘记清零会导致梯度在多次迭代中累加,使得参数更新方向完全错误,模型无法收敛。
完整的 MNIST 训练循环示例:
现在,我们将所有概念整合到一个完整的、可运行的脚本中。
import paddle
import paddle.nn.functional as F
from paddle.vision.datasets import MNIST
from paddle.vision.transforms import ToTensor
from paddle.io import DataLoader
# --- 1. 准备工作 (复习第二章内容) ---
# 定义 LeNet 模型 (代码与第二章相同)
class LeNet(paddle.nn.Layer):
def __init__(self, num_classes=10):
super(LeNet, self).__init__()
self.conv1 = paddle.nn.Conv2D(in_channels=1, out_channels=6, kernel_size=5)
self.pool1 = paddle.nn.MaxPool2D(kernel_size=2, stride=2)
self.conv2 = paddle.nn.Conv2D(in_channels=6, out_channels=16, kernel_size=5)
self.pool2 = paddle.nn.MaxPool2D(kernel_size=2, stride=2)
self.fc1 = paddle.nn.Linear(in_features=16 * 4 * 4, out_features=120)
self.fc2 = paddle.nn.Linear(in_features=120, out_features=84)
self.fc3 = paddle.nn.Linear(in_features=84, out_features=num_classes)
def forward(self, x):
x = F.relu(self.conv1(x))
x = self.pool1(x)
x = F.relu(self.conv2(x))
x = self.pool2(x)
x = paddle.flatten(x, start_axis=1)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
# 设置设备
device = paddle.set_device('gpu' if paddle.is_compiled_with_cuda() else 'cpu')
# 准备数据
transform = ToTensor() # 将数据从 HWC 转为 CHW,并归一化到 [0, 1]
train_dataset = MNIST(mode='train', transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
# 实例化模型、损失函数、优化器
model = LeNet().to(device)
loss_fn = paddle.nn.CrossEntropyLoss()
optimizer = paddle.optimizer.Adam(learning_rate=0.001, parameters=model.parameters())
# --- 2. 核心:标准训练循环 ---
NUM_EPOCHS = 5
total_steps = len(train_loader)
for epoch in range(NUM_EPOCHS):
# **** 关键步骤 1: 切换到训练模式 ****
model.train()
for step, (images, labels) in enumerate(train_loader):
# 将数据移动到指定设备
images = images.to(device)
labels = labels.to(device)
# **** 核心五步法 ****
# 1. 前向传播
outputs = model(images)
# 2. 计算损失
loss = loss_fn(outputs, labels)
# 3. 反向传播
loss.backward()
# 4. 更新参数
optimizer.step()
# 5. 梯度清零
optimizer.clear_grad()
# 打印训练信息
if (step + 1) % 100 == 0:
print(f"Epoch [{epoch+1}/{NUM_EPOCHS}], Step [{step+1}/{total_steps}], Loss: {loss.item():.4f}")
print("训练完成!")
3.1.3 排错:显存 OOM (Out of Memory)
CUDA error: out of memory 是 GPU 编程中最令人头疼的错误之一。它意味着你的模型、数据或中间变量所需的显存,超过了你显卡的物理显存容量。
常见原因与解决方案:
-
batch_size过大 (最常见)- 原因:
batch_size直接决定了每个step中有多少数据被一次性加载到显存。更大的batch_size意味着更大的激活图(activations),消耗显存也随之线性增长。 - 排查: 在终端运行
watch -n 1 nvidia-smi实时监控显存占用。 - 解决: 减小
batch_size是最直接有效的办法。从一个较小的值(如 8 或 16)开始尝试,逐步增大,直到接近显存上限。如果batch_size减小后模型效果变差,可以考虑使用“梯度累积”技术来模拟大batch_size的效果。
- 原因:
-
Tensor未被及时释放- 原因: 在动态图模式下,Python 的垃圾回收机制通常能很好地处理不再需要的
Tensor。但如果你在循环中,无意间将Tensor(尤其是那些还连接着计算图的Tensor)存储在一个全局列表或字典中,它们占用的显存就无法被释放。 - 解决: 检查代码中是否有类似
history.append(loss)的操作。如果你只是想记录loss的数值,请使用history.append(loss.item())。.item()会将其转换为一个普通的 Python 数字,从而切断与计算图的连接,使其可以被正常回收。
- 原因: 在动态图模式下,Python 的垃圾回收机制通常能很好地处理不再需要的
-
模型本身过大
- 原因: 复杂的模型(如层数很深、通道数很宽)本身就包含大量的参数,这些参数本身就要占用大量显存。
- 解决:
- 换用显存更大的 GPU(成本高)。
- 使用更轻量级的模型架构。
- 使用混合精度训练(AMP,
paddle.amp),可以用一半的显存存储参数和激活值,同时通常能加速训练。
3.2 评估与验证循环
只训练不评估,就像闭门造车。我们需要一个独立的验证集(或测试集)来客观地评价模型在“未见过”的数据上的表现,以此来判断模型是否过拟合,并作为调参的依据。
评估循环与训练循环的核心区别在于:只进行前向传播,不进行反向传播和参数更新。
3.2.1 实战:model.eval() —— 切换到“考试模式”
与 model.train() 对应,在开始评估前,我们必须调用 model.eval()。如前文表格所述,这将关闭 Dropout 并固定 BatchNorm 的行为,保证评估结果的确定性和可复现性。
3.2.2 难点:在 paddle.no_grad() 环境下进行评估
这是评估循环中的一个关键优化。paddle.no_grad() 是一个上下文管理器,它会告诉飞桨:在这个代码块里,不要构建计算图。
这会带来两大好处:
- 节省显存: 因为不需要记录用于反向传播的中间变量(激活值),显存消耗会大幅降低。这常常允许我们在评估时使用比训练时大得多的
batch_size,从而加快评估速度。 - 提升速度: 省去了构建计算图的开销,前向传播的计算速度会更快。
# 准备验证数据
val_dataset = MNIST(mode='test', transform=transform)
val_loader = DataLoader(val_dataset, batch_size=128) # 评估时可用更大 batch_size
# --- 核心:标准评估循环 ---
# **** 关键步骤 1: 切换到评估模式 ****
model.eval()
correct = 0
total = 0
# **** 关键步骤 2: 使用 no_grad 上下文 ****
with paddle.no_grad():
for images, labels in val_loader:
images = images.to(device)
labels = labels.to(device)
# 1. 前向传播
outputs = model(images)
# 2. 计算评估指标 (例如: 准确率)
# outputs.shape is [batch_size, num_classes]
# 我们需要找到每个样本预测概率最高的那个类别的索引
_, predicted = paddle.max(outputs, axis=1)
total += labels.shape[0]
correct += (predicted == labels).sum().item()
accuracy = 100 * correct / total
print(f"在 {total} 张测试图片上的准确率: {accuracy:.2f} %")
# 评估结束后,如果还需要继续训练,记得切回 model.train()
# model.train()
3.3 模型的保存与加载 (重点)
辛苦训练得到的模型,如果不保存下来,程序一关就前功尽弃了。模型的保存与加载是深度学习项目中必不可少的一环,主要用于:
- 推理部署: 将训练好的模型用于实际预测。
- 断点续训: 在训练意外中断(如断电、程序崩溃)后,能从上次的进度继续训练,避免从头再来。
- 分享与复现: 将模型分享给他人。
飞桨提供了两种主要的保存方式。
3.3.1 实战:保存与加载“模型参数” (推荐方式)
这种方式只保存模型的可学习参数(权重和偏置),不保存模型结构。这是最常用、最灵活、最推荐的方式。
model.state_dict(): 返回一个 Python 字典,键是层的名字(如conv1.weight),值是对应的参数Tensor。paddle.save(): 将这个字典保存到磁盘,通常以.pdparams为后缀。paddle.load(): 从磁盘加载参数字典。model.set_state_dict(): 将加载的参数字典应用到模型实例上。
代码示例:
# --- 保存模型参数 ---
save_path = "lenet.pdparams"
paddle.save(model.state_dict(), save_path)
print(f"模型参数已保存到 {save_path}")
# --- 加载模型参数 ---
# 1. 必须先创建一个和保存时结构完全相同的模型实例
new_model = LeNet()
# 2. 加载参数字典
loaded_state_dict = paddle.load(save_path)
# 3. 将参数应用到新模型
new_model.set_state_dict(loaded_state_dict)
# 4. 同样,加载后要调用 .eval() 进行预测
new_model.eval()
# ... 接下来就可以用 new_model 进行预测了
优势: 这种方式解耦了模型代码和模型权重。只要你有模型的类定义,就可以加载权重。这使得代码的重构和模型的迁移变得非常容易。
3.3.2 实战:保存与加载“整个模型” (用于断点续训)
这种方式不仅保存参数,还使用 Python 的 pickle 技术将模型对象本身(包括其结构)序列化到磁盘。
代码示例:
# --- 保存整个模型 ---
# 注意:飞桨官方建议保存整个模型时,将模型切换到 eval 模式
model.eval()
paddle.save(model, "lenet_full.pdmodel")
print("整个模型已保存")
# --- 加载整个模型 ---
loaded_model = paddle.load("lenet_full.pdmodel")
# 加载后可以直接使用
loaded_model.eval()
# ... 进行预测
适用场景:
这种方式的最大优点是便利,尤其适合快速地进行断点续训。但它的缺点也很明显:它将代码结构和权重紧紧地绑定在一起,如果你的项目文件结构发生改变,或者 LeNet 类的定义被移动到其他 .py 文件,加载时就可能失败。
最佳实践:创建 Checkpoint 进行断点续训
在长时间的训练任务中,一个专业的做法是保存一个检查点 (Checkpoint)。这个检查点是一个字典,不仅包含模型参数,还应包含优化器的状态、当前的训练轮数 (epoch)、以及目前最好的验证集分数等。
# 在训练循环的每个 epoch 结束或验证集性能提升时保存
best_accuracy = 0.0
# ... 在验证循环后 ...
# if accuracy > best_accuracy:
# best_accuracy = accuracy
# checkpoint = {
# 'epoch': epoch + 1,
# 'model_state_dict': model.state_dict(),
# 'optimizer_state_dict': optimizer.state_dict(),
# 'best_accuracy': best_accuracy,
# 'loss': loss.item(),
# }
# paddle.save(checkpoint, 'best_model_checkpoint.pdparams')
# print(f"发现更优模型,已保存 Checkpoint at epoch {epoch+1}")
# --- 如何从 Checkpoint 恢复训练 ---
# resume_training = True
# if resume_training:
# checkpoint = paddle.load('best_model_checkpoint.pdparams')
# model.set_state_dict(checkpoint['model_state_dict'])
# optimizer.set_state_dict(checkpoint['optimizer_state_dict'])
# start_epoch = checkpoint['epoch']
# best_accuracy = checkpoint['best_accuracy']
# print(f"从 epoch {start_epoch} 恢复训练...")
# # ... 接下来修改 for 循环,从 start_epoch 开始
# # for epoch in range(start_epoch, NUM_EPOCHS):
# # ...
通过保存和加载 Checkpoint,你可以让训练任务变得无比健壮,即使面对数天乃至数周的长时间训练,也无需担心意外中断带来的损失。
至此,你已经完整掌握了使用 PaddlePaddle 进行模型训练、评估和管理的全套流程。你已经具备了独立完成一个端到-端深度学习项目的核心能力。下一章,我们将把目光投向更广阔的天地,探索飞桨强大的生态系统,学习如何利用预训练模型和开发套件,以十倍、百倍的效率解决真实世界的复杂问题。
PaddlePaddle 详细学习笔记 其它章节 链接
以下是整个系列的5章目录,点击章节标题即可跳转阅读(所有章节统一使用“DeepSpeed 详细学习笔记 第X章”格式;若链接暂未索引,可直接访问作者博客搜索相应标题获取最新链接)。
更多推荐

所有评论(0)