第7章:训练循环实战

🎉摘要:本文详细讲解PyTorch中MNIST CNN模型的完整训练循环,包括数据加载、模型定义、损失函数、优化器设置,以及训练过程中Loss下降和准确率提升的观察。适合初学者理解深度学习训练流程。

这一章的目标

  • 把前面所有模块(数据、模型、损失函数、优化器)串起来

  • 写出一个完整的训练循环

  • 真正开始训练模型,看着 Loss 一点点下降

这是本教程最关键的一章。如果你能把这一章彻底搞懂,后面的验证、可视化、推理都只是锦上添花。

训练循环的整体结构

一个完整的训练循环是这样的:

for epoch in range(总轮数):                     # 外层循环:每个 epoch
    for images, labels in train_loader:         # 内层循环:每个 batch
        ① outputs = model(images)               # 1.向前传播
        ② loss = loss_fn(outputs, labels)       # 2.计算损失
        ③ optimizer.zero_grad()                 # 3.梯度归零
        ④ loss.backward()                       # 4.反向传播
        ⑤ optimizer.step()                      # 5.更新参数
        记录 loss 和准确率
    打印本轮训练摘要

就是这么简单。之前我们学过的所有东西,都是在为这个循环服务。

写一个完整的训练脚本

说的再多也不如演示一下记忆深刻,代码如下:

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# 复制第5章的网络定义(实际项目中你会从独立文件导入) 
class MNIST_CNN(nn.Module):
    def __init__(self):
        super().__init__()
        # 第一层卷积:输入单通道灰度图,输出32个特征图,3*3卷积、padding保持尺寸不变
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
        # 最大池化:窗口2×2,特征图宽高缩小一半
        self.pool1 = nn.MaxPool2d(kernel_size=2)

        # 第二层卷积:接收32路特征,输出64路特征
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        # 最大池化,再次缩小特征图尺寸
        self.pool2 = nn.MaxPool2d(kernel_size=2)

        # 全连接层1:展平后特征数量64*7*7,映射到128个神经元
        self.fc1 = nn.Linear(64 * 7 * 7, 128)

        # 输出层:映射为10个类别(数字0~9)
        self.fc2 = nn.Linear(128, 10)

    # 前向传播,模型(data)时会自动调用
    def forward(self, x):
        x = torch.relu(self.conv1(x))  # 卷积+激活函数,提取基础图像特征
        x = self.pool1(x)              # 池化,降低特征图尺寸,减少计算量
        x = torch.relu(self.conv2(x))  # 第二层卷积,提取深层特征
        x = self.pool2(x)              # 第二次池化
        x = x.view(x.size(0), -1)      # 展平:保留batch维度,其余维度合并成一维
        x = torch.relu(self.fc1(x))    # 全连接+激活
        x = self.fc2(x)                # 最终输出各类别得分
        return x


# 1. 数据准备
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])
train_dataset = datasets.MNIST("./data", train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)


# 2. 模型、损失函数、优化器
model = MNIST_CNN()
loss_fn = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)


# 3. 训练
num_epochs = 3  # 先跑3轮试试
for epoch in range(num_epochs):
    total_loss = 0.0         # 累计本轮所有 batch 的 loss
    correct = 0              # 累计本轮预测正确的数量
    total = 0                # 累计本轮处理的总样本数

    for batch_idx, (images, labels) in enumerate(train_loader):
        # 核心五步
        optimizer.zero_grad()               # 梯度清零
        outputs = model(images)             # 向前传播
        loss = loss_fn(outputs, labels)     # 计算损失
        loss.backward()                     # 反向传播
        optimizer.step()                    # 更新参数

        # 统计
        total_loss += loss.item()
        _, predicted = outputs.max(1)      # 取每行最大值的位置 = 预测的数字
        # predicted 预测出的数字:[2,4,7,1,4]
        #  labels 实际的正确数字:[2,4,6,1,5]
        # [2,4,7,1,4].eq([2,4,6,1,5]) → [True,True,False,True,Flase]
        # True 是 1,False 是 0,因此 [True,True,False,True,Flase].sum() → 3
        correct += predicted.eq(labels).sum().item()  # 预测正确的个数
        total += labels.size(0)

        # 每 100 个 batch 打印一次进度
        if (batch_idx + 1) % 100 == 0:
            print(f"  Epoch {epoch+1}, Batch {batch_idx+1}/{len(train_loader)}, "
                  f"Loss: {loss.item():.4f}")

    # 本轮摘要
    avg_loss = total_loss / len(train_loader)
    accuracy = 100.0 * correct / total
    print(f"Epoch {epoch+1} 完成 | 平均Loss: {avg_loss:.4f} | 训练准确率: {accuracy:.2f}%\n")

运行代码,输出如下:

  Epoch 1, Batch 100/938, Loss: 0.3488
  Epoch 1, Batch 200/938, Loss: 0.0519
  Epoch 1, Batch 300/938, Loss: 0.0906
  Epoch 1, Batch 400/938, Loss: 0.0577
  Epoch 1, Batch 500/938, Loss: 0.0885
  Epoch 1, Batch 600/938, Loss: 0.0186
  Epoch 1, Batch 700/938, Loss: 0.0222
  Epoch 1, Batch 800/938, Loss: 0.0455
  Epoch 1, Batch 900/938, Loss: 0.0526
Epoch 1 完成 | 平均Loss: 0.1380 | 训练准确率: 95.83%

  Epoch 2, Batch 100/938, Loss: 0.1136
  Epoch 2, Batch 200/938, Loss: 0.0166
  Epoch 2, Batch 300/938, Loss: 0.0113
  Epoch 2, Batch 400/938, Loss: 0.0275
  Epoch 2, Batch 500/938, Loss: 0.0373
  Epoch 2, Batch 600/938, Loss: 0.0098
  Epoch 2, Batch 700/938, Loss: 0.1189
  Epoch 2, Batch 800/938, Loss: 0.0375
  Epoch 2, Batch 900/938, Loss: 0.0253
Epoch 2 完成 | 平均Loss: 0.0415 | 训练准确率: 98.69%

  Epoch 3, Batch 100/938, Loss: 0.0062
  Epoch 3, Batch 200/938, Loss: 0.0304
  Epoch 3, Batch 300/938, Loss: 0.0054
  Epoch 3, Batch 400/938, Loss: 0.0272
  Epoch 3, Batch 500/938, Loss: 0.0148
  Epoch 3, Batch 600/938, Loss: 0.0317
  Epoch 3, Batch 700/938, Loss: 0.0547
  Epoch 3, Batch 800/938, Loss: 0.0059
  Epoch 3, Batch 900/938, Loss: 0.0396
Epoch 3 完成 | 平均Loss: 0.0288 | 训练准确率: 99.15%

看见了吗?第三轮后训练准确度已经 99% 了,太疯狂了。但是,训练准确率不能完全代表模型的水平。

📌 术语:准确率(Accuracy)

预测正确的样本数 ÷ 总样本数。比如模型看了 100 张图,猜对了 85 张,准确率就是 85%。

注意:这里的准确率是在训练集上算的(模型已经看过这些图),所以不能完全代表模型的真实水平。第 8 章会在测试集上评估。

关于 outputs.max(1) 的解释

这行代码可能让初学者困惑:

outputs = model(images)       # 形状: (64, 10) — 64张图各10个得分
_, predicted = outputs.max(1) # 沿第1维(10个得分)取最大值

# 等价于:
# predicted = outputs.argmax(dim=1)  # 每行最大值的索引 = 预测的数字

outputs.max(1) 返回两个东西:(最大值, 最大值的位置)。我们用 _ 扔掉最大值本身,只拿位置(也就是预测的数字)。

为什么需要多个 Epoch?

如果你只跑 1 个 epoch,模型只能把训练集看一遍。对于 MNIST 这种简单任务,1 个 epoch 准确率可能只有 80% 左右。

跑多个 epoch 意味着:

  • 模型反复看同样的数据,每次都根据上一次的反馈微调参数

  • 一般来说 epoch 越多效果越好,但有个天花板

  • 对于 MNIST,3~5 个 epoch 就能达到 97%+ 的准确率,10 个 epoch 能达到 99%

⚠️ 不是 epoch 越多越好!

太多 epoch 会导致过拟合 —— 模型把训练集背下来了,但遇到没见过的图反而不会了。第 8 章会详细讲。

把训练过程封装成一个函数

为了代码整洁,我们把训练逻辑封装起来:

def train_one_epoch(model, loader, loss_fn, optimizer):
    """训练一个 epoch,返回平均 loss 和准确率"""
    model.train()  # 切换为训练模式(启用 Dropout、BatchNorm 等)
    total_loss, correct, total = 0.0, 0, 0

    for images, labels in loader:
        optimizer.zero_grad()
        outputs = model(images)
        loss = loss_fn(outputs, labels)
        loss.backward()
        optimizer.step()

        total_loss += loss.item()
        correct += outputs.argmax(dim=1).eq(labels).sum().item()
        total += labels.size(0)

    avg_loss = total_loss / len(loader)
    accuracy = 100.0 * correct / total
    return avg_loss, accuracy

📌 model.train()vs model.eval()

model.train():告诉模型"你现在在训练",某些层(如 Dropout、BatchNorm)会按训练模式运行

model.eval():告诉模型"你现在在推理",这些层会按推理模式运行

在 MNIST_CNN 这个简单网络中两者没区别(因为没有 Dropout/BatchNorm),但养成训练前调 train()、评估前调 eval() 的习惯很重要。

本章小结

你已经完成了本教程最核心的一步:写出完整的训练循环

回顾一下整个流程:

数据(DataLoader)→ 模型(CNN)→ 预测 → 损失函数(看差多少)
                                              ↓
                                     优化器(往好的方向改)
                                              ↓
                                   再来一轮(下一个 epoch)

接下来要做的:

  • 在测试集上评估(第 8 章)

  • 画图看训练过程(第 9 章)

  • 保存模型做推理(第 10 章)

点击查看完整的 完整训练 MNIST CNN 示例代码。

  

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号