把前面所有模块(数据、模型、损失函数、优化器)串起来
写出一个完整的训练循环
真正开始训练模型,看着 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 = model(images) # 形状: (64, 10) — 64张图各10个得分
_, predicted = outputs.max(1) # 沿第1维(10个得分)取最大值
# 等价于:
# predicted = outputs.argmax(dim=1) # 每行最大值的索引 = 预测的数字outputs.max(1) 返回两个东西:(最大值, 最大值的位置)。我们用 _ 扔掉最大值本身,只拿位置(也就是预测的数字)。
如果你只跑 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 示例代码。