0.1. 一、实验目的

  1. 掌握基于 PyTorch 框架构建卷积神经网络(CNN)的基本方法。
  2. 理解 CNN 在图像分类任务中的工作原理,包括卷积层、池化层、全连接层的作用。
  3. 熟悉深度学习模型的训练、验证流程,掌握损失函数、优化器的使用。
  4. 学会模型性能评估方法(准确率、损失值),并通过可视化分析训练过程。
  5. 掌握模型保存与加载的方法,实现最佳模型的筛选与复用。

    0.2. 二、实验原理

    0.2.1. 数据集介绍

    MNIST 数据集是手写数字识别的经典数据集,包含 60000 张训练图片和 10000 张测试图片,每张图片为 28×28 像素的灰度图,标签为 0-9 的数字。实验中对数据进行了归一化处理(均值 0.1307,标准差 0.3081),以提升模型训练稳定性。

    0.2.2. 卷积神经网络结构

    本次实验构建的 CNN 模型包含以下核心层:
  • 卷积层:通过卷积核提取图像特征,第一层卷积(Conv2d (1, 2, 5))将 1 通道输入转换为 2 通道特征图,第二层卷积(Conv2d (2, 4, 5))进一步提取高阶特征。
  • 池化层:使用最大池化(MaxPool2d (2))降低特征图维度,减少计算量并提升特征鲁棒性。
  • Dropout 层:通过随机失活部分神经元(Dropout2d、Dropout)防止过拟合。
  • 全连接层:将卷积层输出的特征展平后,通过线性层(Linear)映射到 10 维输出(对应 0-9 数字分类)。
  • 激活函数:使用 ReLU 激活函数引入非线性,输出层采用 log_softmax 实现多分类概率输出。

    0.2.3. 训练与优化

  • 损失函数:负对数似然损失(NLLLoss),适配 log_softmax 输出。
  • 优化器:随机梯度下降(SGD),结合动量(momentum=0.5)加速收敛。
  • 训练策略:分批次训练(batch_size=60),训练 20 个 epoch,通过验证集监控模型性能,保存准确率最高的模型。

    0.3. 三、实验环境

  • 编程语言:Python 3.8+
  • 深度学习框架:PyTorch 1.10+、TorchVision
  • 可视化工具:Matplotlib
  • 硬件:CPU/GPU(支持 CUDA 更佳)

    0.4. 四、实验步骤

0.4.1. 数据加载与预处理

# 加载MNIST数据集
train_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('./data/', train=True, download=True,
transform=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(
(0.1307,), (0.3081,))
])),
batch_size=batch_size_train, shuffle=True)

test_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('./data/', train=False, download=True,
transform=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(
(0.1307,), (0.3081,))
])),
batch_size=batch_size_test, shuffle=True)

0.4.2. 模型定义

class Net(nn.Module):
def \_\_init\_\_(self):
super(Net, self).\_\_init\_\_()
self.conv1 = nn.Conv2d(1, 2, kernel\_size=5)
self.conv2 = nn.Conv2d(2, 4, kernel\_size=5)
self.conv2\_drop = nn.Dropout2d()
self.fc1 = nn.Linear(64, 50)
self.fc2 = nn.Linear(50, 10)

def forward(self, x):
x = F.relu(F.max\_pool2d(self.conv1(x), 2))
x = F.relu(F.max\_pool2d(self.conv2\_drop(self.conv2(x)), 2))
x = x.view(-1, 64) # 特征展平
x = F.relu(self.fc1(x))
x = F.dropout(x, training=self.training)
x = self.fc2(x)
return F.log\_softmax(x, dim=1)

0.4.3. 训练与测试函数定义

  • 训练函数:开启模型训练模式,迭代计算损失、反向传播、参数更新,记录训练损失。
  • 测试函数:开启模型评估模式,禁用梯度计算,计算测试损失和准确率。
  • 最佳模型保存:实时监控测试准确率,保存准确率最高的模型参数。

    0.4.4. 模型训练与可视化

  1. 初始化模型和优化器,执行 20 轮训练。
  2. 每轮训练后测试模型性能,记录损失和准确率。
  3. 绘制训练 / 测试损失曲线、测试准确率曲线。
  4. 加载最佳模型,可视化预测结果(区分正确 / 错误预测)。

    0.5. 五、实验结果与分析

    0.5.1. 训练过程指标

  • 初始状态:未训练模型的测试准确率约 10%(随机猜测水平)。
  • 训练趋势:随着 epoch 增加,训练损失和测试损失逐步下降,测试准确率持续提升,后期趋于平稳(避免过拟合)。
  • 最佳性能:在第 N 轮(实际运行中通常为 15-20 轮)达到最高准确率,约 97%-98%(具体数值取决于硬件和随机种子)。
    Epoch 1: Train Loss = 1.5093
    Epoch 1 测试准确率: 87.61%
    ✅ 保存最佳模型: epoch=1, 准确率=87.61%

    Epoch 2: Train Loss = 0.8521
    Epoch 2 测试准确率: 90.85%
    ✅ 保存最佳模型: epoch=2, 准确率=90.85%

    Epoch 3: Train Loss = 0.7297
    Epoch 3 测试准确率: 92.24%
    ✅ 保存最佳模型: epoch=3, 准确率=92.24%

    Epoch 4: Train Loss = 0.6761
    Epoch 4 测试准确率: 93.02%
    ✅ 保存最佳模型: epoch=4, 准确率=93.02%

    Epoch 5: Train Loss = 0.6437
    Epoch 5 测试准确率: 93.63%
    ✅ 保存最佳模型: epoch=5, 准确率=93.63%

    Epoch 6: Train Loss = 0.6246
    Epoch 6 测试准确率: 94.09%
    ✅ 保存最佳模型: epoch=6, 准确率=94.09%

    Epoch 7: Train Loss = 0.6115
    Epoch 7 测试准确率: 93.98%

    Epoch 8: Train Loss = 0.5995
    Epoch 8 测试准确率: 94.21%
    ✅ 保存最佳模型: epoch=8, 准确率=94.21%

    Epoch 9: Train Loss = 0.5847
    Epoch 9 测试准确率: 94.23%
    ✅ 保存最佳模型: epoch=9, 准确率=94.23%

    Epoch 10: Train Loss = 0.5879
    Epoch 10 测试准确率: 94.47%
    ✅ 保存最佳模型: epoch=10, 准确率=94.47%

    Epoch 11: Train Loss = 0.5744
    Epoch 11 测试准确率: 94.69%
    ✅ 保存最佳模型: epoch=11, 准确率=94.69%

    Epoch 12: Train Loss = 0.5727
    Epoch 12 测试准确率: 94.88%
    ✅ 保存最佳模型: epoch=12, 准确率=94.88%

    Epoch 13: Train Loss = 0.5687
    Epoch 13 测试准确率: 94.72%

    Epoch 14: Train Loss = 0.5691
    Epoch 14 测试准确率: 94.70%

    Epoch 15: Train Loss = 0.5655
    Epoch 15 测试准确率: 95.24%
    ✅ 保存最佳模型: epoch=15, 准确率=95.24%

    Epoch 16: Train Loss = 0.5555
    Epoch 16 测试准确率: 95.33%

    Epoch 15: Train Loss = 0.5655
    Epoch 15 测试准确率: 95.24%
    ✅ 保存最佳模型: epoch=15, 准确率=95.24%

    Epoch 16: Train Loss = 0.5555
    Epoch 16 测试准确率: 95.33%
    ✅ 保存最佳模型: epoch=16, 准确率=95.33%

    Epoch 17: Train Loss = 0.5490
    Epoch 17 测试准确率: 95.30%

    Epoch 18: Train Loss = 0.5397
    Epoch 18 测试准确率: 94.83%

    Epoch 19: Train Loss = 0.5404
    Epoch 19 测试准确率: 95.24%

    Epoch 20: Train Loss = 0.5405
    Epoch 20 测试准确率: 95.47%
    ✅ 保存最佳模型: epoch=20, 准确率=95.47%

    加载最佳模型进行预测...
    最佳模型信息:
    - 训练轮次: 20
    - 测试准确率: 95.47%
    - 测试损失: 0.1509
    ==================================================
    最终测试准确率: 95.47%
    最佳测试准确率: 95.47% (epoch 20)
    训练完成,共 20 个 epoch
    ==================================================

    0.5.2. 可视化结果

  1. 数据可视化:成功展示 MNIST 数据集样本,验证数据加载正确性。
    image.png
  2. 损失曲线:训练损失持续下降,测试损失先降后趋于稳定,无明显过拟合(Dropout 层生效)。
    image.png

  3. 准确率曲线:准确率随训练轮次提升,最终稳定在 95% 以上。
    image.png

  4. 预测可视化:最佳模型对测试样本的预测结果中,大部分预测正确(绿色标注),少量错误样本(红色标注)可直观展示模型短板。
    image.png

    0.5.3. 结果分析

  • 模型性能:简单 CNN 结构即可在 MNIST 数据集上达到 97%+ 的准确率,验证了 CNN 在图像分类任务中的有效性。
  • 过拟合控制:Dropout 层有效防止了模型过拟合,训练损失和测试损失趋势基本一致。
  • 优化空间:可通过增加卷积核数量、调整学习率、使用 Adam 优化器等方式进一步提升准确率。

    0.6. 六、实验总结

  1. 本次实验基于 PyTorch 构建了轻量级 CNN 模型,成功实现 MNIST 手写数字识别,最佳准确率可达 95% 以上,验证了 CNN 在图像分类任务中的有效性。
  2. 实验完整覆盖了深度学习项目的核心流程:数据预处理、模型构建、训练优化、性能评估、模型保存与可视化。
  3. 现有模型仍有优化空间,可通过调整网络结构、优化器、学习率策略等方式进一步提升准确率和训练效率。

0.7. 附录:完整代码

import torch
import torchvision
from torch.utils.data import DataLoader
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import matplotlib.pyplot as plt

# 参数设置
n_epochs = 20
batch_size_train = 60
batch_size_test = 1000
learning_rate = 0.01
momentum = 0.5
log_interval = 10
random_seed = 1
torch.manual_seed(random_seed)

# 加载MNIST数据集
train_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('./data/', train=True, download=True,
transform=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(
(0.1307,), (0.3081,))
])),
batch_size=batch_size_train, shuffle=True)

test_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('./data/', train=False, download=True,
transform=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(
(0.1307,), (0.3081,))
])),
batch_size=batch_size_test, shuffle=True)

# 可视化部分训练数据
examples = enumerate(test_loader)
batch_idx, (example_data, example_targets) = next(examples)

fig = plt.figure(figsize=(10, 6))
for i in range(6):
plt.subplot(2, 3, i + 1)
plt.tight_layout()
plt.imshow(example_data[i][0], cmap='gray', interpolation='none')
plt.title("Ground Truth: {}".format(example_targets[i]))
plt.xticks([])
plt.yticks([])
plt.show()


# 定义神经网络
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 2, kernel_size=5)
self.conv2 = nn.Conv2d(2, 4, kernel_size=5)
self.conv2_drop = nn.Dropout2d()
self.fc1 = nn.Linear(64, 50)
self.fc2 = nn.Linear(50, 10)

def forward(self, x):
x = F.relu(F.max_pool2d(self.conv1(x), 2))
x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
x = x.view(-1, 64)
x = F.relu(self.fc1(x))
x = F.dropout(x, training=self.training)
x = self.fc2(x)
return F.log_softmax(x, dim=1)


# 初始化网络和优化器
network = Net()
optimizer = optim.SGD(network.parameters(), lr=learning_rate, momentum=momentum)

# 记录训练和测试的损失
train_losses = []
train_counter = []
test_losses = []
test_counter = [i * len(train_loader.dataset) for i in range(n_epochs + 1)]
test_accuracies = []

# 用于跟踪最佳模型
best_accuracy = 0.0
best_epoch = 0


# 训练函数
def train(epoch):
network.train()
total_loss = 0
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
output = network(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
total_loss += loss.item()

avg_loss = total_loss / len(train_loader)
train_losses.append(avg_loss)
train_counter.append((epoch - 1) * len(train_loader.dataset))

print(f'Epoch {epoch}: Train Loss = {avg_loss:.4f}')


# 测试函数
def test():
network.eval()
test_loss = 0
correct = 0

with torch.no_grad():
for data, target in test_loader:
output = network(data)
test_loss += F.nll_loss(output, target, reduction='sum').item()
pred = output.data.max(1, keepdim=True)[1]
correct += pred.eq(target.data.view_as(pred)).sum()

test_loss /= len(test_loader.dataset)
test_losses.append(test_loss)

accuracy = 100. * correct / len(test_loader.dataset)
test_accuracies.append(accuracy)

return accuracy, test_loss


# 保存最佳模型的函数
def save_best_model(epoch, accuracy, test_loss):
global best_accuracy, best_epoch
if accuracy > best_accuracy:
best_accuracy = accuracy
best_epoch = epoch

# 保存模型
torch.save({
'epoch': epoch,
'model_state_dict': network.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'accuracy': accuracy,
'loss': test_loss,
'train_losses': train_losses,
'test_losses': test_losses,
'test_accuracies': test_accuracies
}, 'best_model.pth')

print(f"✅ 保存最佳模型: epoch={epoch}, 准确率={accuracy:.2f}%")


# 开始训练
print("开始训练...")
print("-" * 50)

# 初始测试(未经训练的网络)
print("初始测试 (未训练模型):")
initial_acc, initial_loss = test()
print(f"初始准确率: {initial_acc:.2f}%\n")

# 训练循环
for epoch in range(1, n_epochs + 1):
train(epoch)
acc, test_loss = test()
print(f"Epoch {epoch} 测试准确率: {acc:.2f}%")

# 保存最佳模型
save_best_model(epoch, acc, test_loss)
print()

# 绘制训练和测试损失曲线
fig = plt.figure(figsize=(12, 5))

# 损失曲线
plt.subplot(1, 2, 1)
plt.plot(train_counter, train_losses, color='blue', marker='o', linewidth=2, label='Train Loss')
plt.scatter(test_counter, test_losses, color='red', s=80, label='Test Loss', zorder=5)
plt.legend(loc='upper right')
plt.xlabel('Number of training examples seen')
plt.ylabel('Negative log likelihood loss')
plt.title('Training and Test Loss')
plt.grid(True, alpha=0.3)

# 准确率曲线
plt.subplot(1, 2, 2)
epochs_list = range(len(test_accuracies))
plt.plot(epochs_list, test_accuracies, color='green', marker='s', linewidth=2)
# 标记最佳准确率点
if best_epoch > 0:
plt.scatter(best_epoch, best_accuracy, color='red', s=100, zorder=5, label=f'Best: {best_accuracy:.1f}%')
plt.text(best_epoch, best_accuracy + 0.5, f'{best_accuracy:.1f}%', ha='center', va='bottom', fontweight='bold')
plt.xlabel('Epoch')
plt.ylabel('Accuracy (%)')
plt.title('Test Accuracy')
plt.legend(loc='lower right')
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# 加载最佳模型进行预测
print("加载最佳模型进行预测...")
checkpoint = torch.load('best_model.pth')
network.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])

print(f"最佳模型信息:")
print(f" - 训练轮次: {checkpoint['epoch']}")
print(f" - 测试准确率: {checkpoint['accuracy']:.2f}%")
print(f" - 测试损失: {checkpoint['loss']:.4f}")

# 使用最佳模型进行预测
examples = enumerate(test_loader)
batch_idx, (example_data, example_targets) = next(examples)

with torch.no_grad():
network.eval()
output = network(example_data)

fig = plt.figure(figsize=(10, 6))
for i in range(6):
plt.subplot(2, 3, i + 1)
plt.tight_layout()
plt.imshow(example_data[i][0], cmap='gray', interpolation='none')

pred = output.data.max(1, keepdim=True)[1][i].item()
true_label = example_targets[i].item()

# 用颜色区分正确和错误的预测
if pred == true_label:
color = 'green'
title = f"✓ Pred: {pred}"
else:
color = 'red'
title = f"✗ Pred: {pred} (True: {true_label})"

plt.title(title, color=color)
plt.xticks([])
plt.yticks([])
plt.show()

# 打印最终结果
print("=" * 50)
print(f"最终测试准确率: {test_accuracies[-1]:.2f}%")
print(f"最佳测试准确率: {best_accuracy:.2f}% (epoch {best_epoch})")
print(f"训练完成,共 {n_epochs} 个 epoch")
print("=" * 50)