深度学习入门教程:用全连接神经网络识别手写数字

1. 引言

1.1 什么是深度学习?

深度学习是机器学习的一个分支,它通过模拟人脑的神经网络结构,让计算机能够从数据中自动学习特征和规律。

就像我们人类通过不断学习来提高自己的能力一样,深度学习模型也可以通过大量的数据训练来提高自己的性能。

1.2 为什么要学习深度学习?

深度学习已经在很多领域取得了突破性的进展,比如:

  • 图像识别:自动驾驶、人脸识别、医疗影像分析
  • 自然语言处理:机器翻译、语音识别、聊天机器人
  • 推荐系统:电商推荐、视频推荐

学习深度学习可以让你掌握这些前沿技术,为未来的职业发展打下坚实的基础。

1.3 本教程的目标

在本教程中,我们将:

  • 在WSL Ubuntu上安装GPU版的PyTorch
  • 学习全连接深度神经网络的基本原理
  • 用PyTorch实现一个手写数字识别模型
  • 训练和测试模型,分析结果

2. 环境搭建

2.1 WSL Ubuntu安装

首先,我们需要在Windows上安装WSL(Windows Subsystem for Linux)。请按照微软官方文档的步骤进行安装:安装WSL

2.2 GPU驱动安装

要使用GPU加速深度学习,我们需要安装NVIDIA GPU驱动。请从NVIDIA官网下载并安装适合你GPU型号的驱动:NVIDIA驱动下载

2.3 安装Python环境

  1. 升级系统环境

    sudo apt update && sudo apt -y dist-upgrade
    
  2. 安装Python 3.12

    sudo apt -y install --upgrade python3 python3-pip python3.12-venv
    
  3. 设置国内镜像源(加速下载)

    pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
    

2.4 创建虚拟环境

虚拟环境可以隔离不同项目的依赖,避免版本冲突。

  1. 创建项目目录

    mkdir pytorch-code && cd pytorch-code
    
  2. 创建并激活虚拟环境

    python3 -m venv .venv && source .venv/bin/activate
    
  3. 升级基础依赖

    python -m pip install --upgrade pip setuptools wheel -i https://pypi.tuna.tsinghua.edu.cn/simple
    

2.5 安装PyTorch

PyTorch是一个流行的深度学习框架,它提供了丰富的工具和API,方便我们构建和训练深度学习模型。

pip install torch torchvision torchaudio -i https://pypi.tuna.tsinghua.edu.cn/simple
# cuda13预览版可使用以下命令 生产环境切勿使用以下命令
pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cu130

本案例使用到的其它依赖库

pip install matplotlib seaborn scikit-learn -i https://pypi.tuna.tsinghua.edu.cn/simple

2.6 验证安装

安装完成后,我们可以运行以下命令来验证PyTorch和CUDA是否正确安装:

import torch
print(f"PyTorch版本: {torch.__version__}")
print(f"CUDA是否可用: {torch.cuda.is_available()}")
if torch.cuda.is_available():
    print(f"GPU型号: {torch.cuda.get_device_name(0)}")
    print(f"CUDA版本: {torch.version.cuda}")

如果输出显示CUDA可用,并且显示了你的GPU型号,说明安装成功!

3. 神经网络原理大白话

3.1 什么是神经网络?

生活场景类比:神经网络就像一个工厂流水线,每个"工人"(神经元)负责一项特定的任务,将输入的"原材料"(数据)一步步加工成最终的"产品"(预测结果)。

3.2 全连接神经网络结构

生活场景类比:全连接神经网络就像一个多层流水线,每层有多个工人,每个工人都和下一层的所有工人有联系,信息可以在层与层之间自由流动。

输入层
784个神经元
隐藏层1
256个神经元
ReLU激活
隐藏层2
128个神经元
ReLU激活
隐藏层3
64个神经元
ReLU激活
输出层
10个神经元
Softmax激活
  • 输入层:接收原始数据,比如一张手写数字图片的像素值
  • 隐藏层:对输入数据进行处理和特征提取
  • 输出层:输出最终的预测结果,比如手写数字的类别

3.3 神经元的工作原理

每个神经元都可以看作是一个简单的函数,它接收一些输入,经过处理后输出一个结果。

让我们用一个生活场景来理解神经元:

假设你想决定今天是否去公园玩。你会考虑几个因素:天气、温度、是否有朋友一起。每个因素都会影响你的决定:

  • 如果天气好(晴天),你更可能去公园
  • 如果温度适宜(20-25℃),你更可能去公园
  • 如果有朋友一起,你更可能去公园

你会给每个因素一个权重,然后综合考虑这些因素,做出最终决定。

神经元的工作原理类似:

  1. 接收多个输入(比如天气、温度、是否有朋友)
  2. 给每个输入分配一个权重(比如天气的权重是0.4,温度的权重是0.3,是否有朋友的权重是0.3)
  3. 计算所有输入和对应权重的乘积之和
  4. 将结果输入到激活函数中,得到最终输出

3.4 激活函数

激活函数的作用是给神经网络引入非线性,让神经网络可以学习更复杂的特征。

常见的激活函数有:

  • ReLU:如果输入大于0,输出等于输入;否则输出0
  • Sigmoid:将输入映射到[0, 1]之间
  • Tanh:将输入映射到[-1, 1]之间

4. 全连接深度神经网络原理

4.1 手写数字识别问题

MNIST数据集是一个经典的手写数字识别数据集,它包含60,000个训练样本和10,000个测试样本,每个样本是一张28×28像素的灰度图像,对应的标签是0-9的数字。

我们的目标是构建一个神经网络,能够正确识别这些手写数字。

4.2 全连接神经网络架构

对于MNIST手写数字识别,我们将构建一个包含3个隐藏层的全连接神经网络:

输入层
784个神经元
隐藏层1
256个神经元
ReLU激活
隐藏层2
128个神经元
ReLU激活
隐藏层3
64个神经元
ReLU激活
输出层
10个神经元
Softmax激活
  • 输入层:28×28=784个神经元,对应图像的每个像素
  • 隐藏层1:256个神经元,使用ReLU激活函数
  • 隐藏层2:128个神经元,使用ReLU激活函数
  • 隐藏层3:64个神经元,使用ReLU激活函数
  • 输出层:10个神经元,对应0-9的数字,使用Softmax激活函数

4.3 前向传播

前向传播是指数据从输入层流向输出层的过程:

  1. 输入层接收图像的像素值
  2. 每个神经元计算输入和权重的乘积之和,再加上偏置项
  3. 将结果输入到激活函数中
  4. 输出传递给下一层,直到到达输出层
  5. 输出层使用Softmax激活函数,得到每个类别的概率
输入层 隐藏层1 隐藏层2 隐藏层3 输出层 外部 输入像素值 处理后的数据 处理后的数据 处理后的数据 Softmax激活 预测结果 输入层 隐藏层1 隐藏层2 隐藏层3 输出层 外部

4.4 损失函数

损失函数用于衡量模型的预测结果与真实标签之间的差距。对于分类问题,我们通常使用交叉熵损失函数:

Loss = − 1 N ∑ i = 1 N ∑ j = 1 C y i j log ⁡ ( p i j ) \text{Loss} = -\frac{1}{N} \sum_{i=1}^{N} \sum_{j=1}^{C} y_{ij} \log(p_{ij}) Loss=N1i=1Nj=1Cyijlog(pij)

其中:

  • N N N 是样本数量
  • C C C 是类别数量
  • y i j y_{ij} yij 是第 i i i个样本属于第 j j j类的真实标签(0或1)
  • p i j p_{ij} pij 是第 i i i个样本属于第 j j j类的预测概率

4.5 反向传播

反向传播是指梯度从输出层流向输入层的过程,它用于更新神经网络的权重和偏置,以减少损失函数的值。

  1. 计算输出层的梯度
  2. 将梯度反向传播到隐藏层
  3. 根据梯度更新权重和偏置
  4. 重复这个过程,直到损失函数的值足够小

4.6 优化器

优化器用于根据梯度更新神经网络的权重和偏置。常见的优化器有:

  • SGD(随机梯度下降):每次使用一个批次的数据来计算梯度
  • Adam:结合了动量和自适应学习率,收敛速度更快

5. 代码实现与解读

5.1 项目结构

我们的项目按照以下结构组织:

module4/
├── model.py          # 模型定义
├── data_loader.py    # 数据加载
├── utils.py          # 工具函数
├── train.py          # 模型训练
├── test.py           # 模型测试
├── models/           # 模型保存目录
├── data/             # 数据保存目录
└── results/          # 结果可视化目录
代码架构图
model.py
MLP模型定义
train.py
模型训练
test.py
模型测试
data_loader.py
数据加载
utils.py
工具函数
models/
模型保存
results/
结果可视化
代码流程图
flowchart TD
    A[开始] --> B[加载数据<br>data_loader.py]
    B --> C[创建模型<br>model.py]
    C --> D[训练模型<br>train.py]
    D --> E[保存最佳模型<br>models/]
    D --> F[绘制训练曲线<br>results/]
    E --> G[测试模型<br>test.py]
    F --> G
    G --> H[生成混淆矩阵<br>results/]
    G --> I[生成错误分类样本<br>results/]
    G --> J[随机样本测试<br>results/]
    H --> K[结束]
    I --> K
    J --> K
代码关系图
定义
实现
实现
提供
执行
执行
提供
提供
提供
提供
提供
提供
提供
提供
使用
使用
使用
使用
使用
使用
使用
使用
生成
生成
生成
生成
使用
使用
使用
使用
使用
使用
实现
实现
生成
包含
包含
包含
包含
包含
model.py
MLP模型类
MLP类
__init__方法
forward方法
data_loader.py
数据加载器
get_mnist_data_loaders函数
数据预处理
数据集划分
utils.py
工具函数
calculate_accuracy
train_epoch
evaluate_model
plot_training_curve
plot_confusion_matrix
plot_misclassified_samples
save_model
load_model
train.py
训练脚本
mlp_model.pth
training_curves.png
confusion_matrix.png
misclassified_samples.png
test.py
测试脚本
单张图像预测
随机样本测试
random_test_samples.png
models/目录
results/目录
代码时序图
User train.py model.py data_loader.py utils.py test.py 运行训练脚本 调用get_mnist_data_loaders 返回train_loader, val_loader, test_loader 创建MLP模型实例 调用train_epoch训练模型 返回训练损失和准确率 调用evaluate_model评估模型 返回验证损失和准确率 保存最佳模型 模型保存成功 绘制训练曲线 训练曲线生成成功 在测试集上评估模型 返回测试结果 绘制混淆矩阵 混淆矩阵生成成功 绘制错误分类样本 错误分类样本图生成成功 训练完成 运行测试脚本 调用get_mnist_data_loaders 返回测试加载器 创建MLP模型实例 加载训练好的模型 模型加载成功 在测试集上评估模型 返回测试结果 绘制混淆矩阵 混淆矩阵生成成功 绘制错误分类样本 错误分类样本图生成成功 随机样本测试 随机样本测试图生成成功 测试完成 User train.py model.py data_loader.py utils.py test.py

5.2 模型定义(model.py)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
模块4:全连接深度神经网络(手写数字识别)

# 开发思路
1. **问题分析**:
   - 任务:MNIST手写数字识别,属于多分类问题
   - 输入:28×28像素的灰度图像
   - 输出:0-9共10个数字类别
   - 挑战:需要从像素级数据中提取有效特征,实现高精度分类

2. **技术选型**:
   - 框架:选择PyTorch,因其动态计算图特性适合深度学习入门学习
   - 模型:采用多层感知器(MLP),结构简单清晰,便于理解神经网络基本原理
   - 激活函数:使用ReLU,解决传统Sigmoid函数的梯度消失问题
   - 正则化:引入Dropout,防止模型过拟合

3. **网络架构设计**:
   - 输入层:将28×28图像展平为784维向量
   - 隐藏层:设计3层全连接隐藏层,神经元数量依次为256、128、64
     - 逐层减少神经元数量,实现特征降维和抽象
     - 每层后添加ReLU激活和Dropout正则化
   - 输出层:10个神经元,对应10个数字类别

4. **损失与优化**:
   - 损失函数:交叉熵损失,适合多分类问题
   - 优化器:Adam优化器,具有自适应学习率,收敛速度快

# 开发过程
1. **环境配置**:
   - 安装PyTorch框架
   - 准备MNIST数据集(训练集60,000张,测试集10,000张)

2. **模型设计**:
   - 定义MLP类,继承自nn.Module
   - 实现__init__方法,初始化网络层和激活函数
   - 实现forward方法,定义数据前向传播路径

3. **代码实现**:
   - 编写模型类代码
   - 添加测试代码,验证模型输入输出形状
   - 编写详细注释,提高代码可读性

4. **功能验证**:
   - 运行测试代码,检查模型基本功能
   - 验证输入输出形状是否符合预期
   - 检查是否存在语法错误或运行时异常

5. **后续扩展**:
   - 添加模型训练代码
   - 实现模型评估功能
   - 优化超参数,提高模型准确率
   - 可视化训练过程和结果
"""

# 导入PyTorch核心库,用于张量操作和自动微分
import torch

# 导入PyTorch神经网络模块,包含各种层、激活函数等组件
import torch.nn as nn

# 定义多层感知器(MLP)类,继承自nn.Module基类
class MLP(nn.Module):
    """
    多层感知器模型类,用于MNIST手写数字识别
    
    参数:
        input_size: int, 输入层神经元数量,默认784(28×28)
        hidden_sizes: list, 隐藏层神经元数量列表,默认[256, 128, 64]
        output_size: int, 输出层神经元数量,默认10(0-9数字)
    
    属性:
        fc1: nn.Linear, 第一层全连接层
        fc2: nn.Linear, 第二层全连接层
        fc3: nn.Linear, 第三层全连接层
        fc4: nn.Linear, 输出层
        relu: nn.ReLU, ReLU激活函数
        dropout: nn.Dropout, Dropout层,防止过拟合
    """
    
    # 类初始化方法,定义网络结构
    def __init__(self, input_size=784, hidden_sizes=[256, 128, 64], output_size=10):
        # 调用父类nn.Module的初始化方法
        super(MLP, self).__init__()
        
        # 定义第一层全连接层:输入层→第一个隐藏层
        # 输入维度:input_size,输出维度:hidden_sizes[0]
        self.fc1 = nn.Linear(input_size, hidden_sizes[0])
        
        # 定义第二层全连接层:第一个隐藏层→第二个隐藏层
        # 输入维度:hidden_sizes[0],输出维度:hidden_sizes[1]
        self.fc2 = nn.Linear(hidden_sizes[0], hidden_sizes[1])
        
        # 定义第三层全连接层:第二个隐藏层→第三个隐藏层
        # 输入维度:hidden_sizes[1],输出维度:hidden_sizes[2]
        self.fc3 = nn.Linear(hidden_sizes[1], hidden_sizes[2])
        
        # 定义输出层:第三个隐藏层→输出层
        # 输入维度:hidden_sizes[2],输出维度:output_size
        self.fc4 = nn.Linear(hidden_sizes[2], output_size)
        
        # 定义ReLU激活函数,用于隐藏层
        self.relu = nn.ReLU()
        
        # 定义Dropout层,随机失活50%神经元,防止过拟合
        self.dropout = nn.Dropout(0.5)
    
    # 前向传播方法,定义数据流动路径
    def forward(self, x):
        """
        前向传播方法,计算模型输出
        
        参数:
            x: torch.Tensor, 输入张量,形状为[batch_size, 1, 28, 28]
            - batch_size: 批次大小
            - 1: 通道数(灰度图像)
            - 28×28: 图像尺寸
        
        返回:
            x: torch.Tensor, 输出张量,形状为[batch_size, 10]
            - 每个元素表示对应类别的预测分数
        """
        # 将输入张量展平为一维向量
        # 形状转换:[batch_size, 1, 28, 28] → [batch_size, 784]
        # -1表示自动计算该维度大小,保持总元素数不变
        x = x.view(-1, 784)
        
        # 第一层全连接层计算 → ReLU激活
        # 先经过全连接层fc1,再通过ReLU激活函数
        x = self.relu(self.fc1(x))
        
        # 应用Dropout,随机失活50%神经元
        # 训练时启用,测试时自动关闭
        x = self.dropout(x)
        
        # 第二层全连接层计算 → ReLU激活
        x = self.relu(self.fc2(x))
        
        # 应用Dropout,随机失活50%神经元
        x = self.dropout(x)
        
        # 第三层全连接层计算 → ReLU激活
        x = self.relu(self.fc3(x))
        
        # 应用Dropout,随机失活50%神经元
        x = self.dropout(x)
        
        # 输出层计算,不使用激活函数
        # 后续会使用Softmax转换为概率
        x = self.fc4(x)
        
        # 返回模型输出
        return x

# 主程序入口,用于测试模型基本功能
if __name__ == "__main__":
    # 创建MLP模型实例,使用默认参数
    # input_size=784, hidden_sizes=[256, 128, 64], output_size=10
    model = MLP()
    
    # 创建随机测试输入,模拟MNIST图像
    # 形状:[1, 1, 28, 28]
    # - 1: 批次大小
    # - 1: 通道数
    # - 28×28: 图像尺寸
    test_input = torch.randn(1, 1, 28, 28)
    
    # 执行前向传播,获取模型输出
    # 调用model.forward(test_input)的简化写法
    output = model(test_input)
    
    # 打印输入形状,验证输入格式是否正确
    print(f"模型输入形状: {test_input.shape}")
    
    # 打印输出形状,验证模型输出是否符合预期
    # 预期形状:[1, 10],对应1个样本的10个类别预测
    print(f"模型输出形状: {output.shape}")
    
    # 打印输出值,查看模型初始预测结果
    # 初始权重随机,输出值无实际意义,仅用于验证模型功能
    print(f"模型输出: {output}")

5.3 数据加载器开发(data_loader.py)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
模块4:数据加载器(MNIST数据集)

# 开发思路
1. **问题分析**:
   - MNIST数据集是手写数字识别的经典数据集,包含60,000张训练图片和10,000张测试图片
   - 每张图片是28×28像素的灰度图像,标签为0-9的数字
   - 需要将原始数据转换为模型可处理的格式,并按批次加载

2. **技术选型**:
   - 使用torchvision.datasets.MNIST直接加载数据集
   - 使用transforms进行数据预处理
   - 使用DataLoader实现批量加载和多进程加速
   - 使用random_split将训练集划分为训练集和验证集

3. **数据预处理设计**:
   - ToTensor():将PIL图像转换为PyTorch张量,像素值范围从[0, 255]转换为[0, 1]
   - Normalize():使用MNIST数据集的均值(0.1307)和标准差(0.3081)进行归一化
   - 归一化后的数据分布更接近标准正态分布,有助于模型收敛

4. **数据集划分策略**:
   - 训练集:90%(54,000张),用于模型训练
   - 验证集:10%(6,000张),用于训练过程中的模型评估
   - 测试集:10,000张,用于最终模型性能评估

# 开发过程
1. **环境配置**:
   - 安装PyTorch和torchvision库
   - 确保网络连接正常,以便下载MNIST数据集

2. **代码设计**:
   - 定义get_mnist_data_loaders函数,封装数据加载逻辑
   - 设计灵活的参数,支持批量大小、验证集比例和进程数的调整
   - 添加测试代码,验证数据加载器功能

3. **实现步骤**:
   - 导入必要的库和模块
   - 定义数据预处理转换
   - 加载MNIST训练集和测试集
   - 划分训练集和验证集
   - 创建DataLoader实例
   - 返回三个数据加载器

4. **功能验证**:
   - 运行测试代码,检查数据集样本数是否正确
   - 验证数据批次的形状和类型是否符合预期
   - 确保数据加载过程没有错误
"""

# 导入PyTorch核心库,用于张量操作
import torch

# 从torchvision导入数据集和数据转换模块
from torchvision import datasets, transforms

# 从torch.utils.data导入数据加载器和随机划分工具
from torch.utils.data import DataLoader, random_split

# 定义获取MNIST数据加载器的函数
def get_mnist_data_loaders(batch_size=64, val_split=0.1, num_workers=4):
    """
    获取MNIST数据集的训练集、验证集和测试集数据加载器
    
    参数说明:
        batch_size: int, 每次加载的样本数量,默认64
        val_split: float, 验证集占训练集的比例,默认0.1(10%)
        num_workers: int, 数据加载使用的进程数,默认4
    
    返回值:
        tuple: 包含三个DataLoader对象
            - train_loader: 训练集数据加载器
            - val_loader: 验证集数据加载器
            - test_loader: 测试集数据加载器
    """
    # 定义数据预处理转换流水线
    # transforms.Compose将多个转换操作组合成一个
    transform = transforms.Compose([
        # 将PIL图像转换为PyTorch张量,像素值范围[0,255]→[0,1]
        transforms.ToTensor(),
        # 对张量进行归一化,使用MNIST数据集的均值和标准差
        # 公式:output = (input - mean) / std
        transforms.Normalize((0.1307,), (0.3081,))
    ])
    
    # 加载MNIST训练集
    # root: 数据集保存路径
    # train: True表示加载训练集
    # download: True表示如果本地没有数据集则自动下载
    # transform: 应用数据预处理转换
    train_dataset = datasets.MNIST(
        root='data',
        train=True,
        download=True,
        transform=transform
    )
    
    # 加载MNIST测试集
    # train: False表示加载测试集
    test_dataset = datasets.MNIST(
        root='data',
        train=False,
        download=True,
        transform=transform
    )
    
    # 计算验证集大小
    # val_split为验证集比例,乘以训练集总长度得到验证集样本数
    val_size = int(len(train_dataset) * val_split)
    
    # 计算训练集大小
    # 原始训练集减去验证集大小
    train_size = len(train_dataset) - val_size
    
    # 随机划分训练集和验证集
    # random_split将数据集随机划分为指定大小的子集
    train_subset, val_subset = random_split(train_dataset, [train_size, val_size])
    
    # 创建训练集数据加载器
    # shuffle: True表示每个epoch打乱数据顺序,提高模型泛化能力
    # num_workers: 使用4个进程加载数据,提高效率
    train_loader = DataLoader(
        train_subset,
        batch_size=batch_size,
        shuffle=True,
        num_workers=num_workers
    )
    
    # 创建验证集数据加载器
    # shuffle: False表示不打乱数据顺序,验证集不需要
    val_loader = DataLoader(
        val_subset,
        batch_size=batch_size,
        shuffle=False,
        num_workers=num_workers
    )
    
    # 创建测试集数据加载器
    # shuffle: False表示不打乱数据顺序,测试集不需要
    test_loader = DataLoader(
        test_dataset,
        batch_size=batch_size,
        shuffle=False,
        num_workers=num_workers
    )
    
    # 返回三个数据加载器
    return train_loader, val_loader, test_loader

# 主程序入口,用于测试数据加载器功能
if __name__ == "__main__":
    # 调用函数获取数据加载器,设置批量大小为32
    train_loader, val_loader, test_loader = get_mnist_data_loaders(batch_size=32)
    
    # 打印训练集样本数
    # len(train_loader.dataset)获取数据集总样本数
    print(f"训练集样本数: {len(train_loader.dataset)}")
    
    # 打印验证集样本数
    print(f"验证集样本数: {len(val_loader.dataset)}")
    
    # 打印测试集样本数
    print(f"测试集样本数: {len(test_loader.dataset)}")
    
    # 遍历训练集数据加载器,获取第一个批次的数据
    # 每次迭代返回一个批次的图像和对应的标签
    for images, labels in train_loader:
        # 打印图像批次形状:[batch_size, channels, height, width]
        # 对于MNIST,channels=1(灰度图像),height=28,width=28
        print(f"图像批次形状: {images.shape}")
        
        # 打印标签批次形状:[batch_size]
        # 每个标签是0-9的整数
        print(f"标签批次形状: {labels.shape}")
        
        # 打印图像数据类型,应为torch.float32
        print(f"图像数据类型: {images.dtype}")
        
        # 打印标签数据类型,应为torch.int64(即long类型)
        print(f"标签数据类型: {labels.dtype}")
        
        # 只打印一个批次的数据,然后退出循环
        break

5.4 工具函数(utils.py)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
模块4:工具函数集合(MLP训练和评估)

# 开发思路
1. **问题分析**:
   - 需要将模型训练、评估和可视化功能模块化,提高代码复用性
   - 实现通用的训练和评估函数,支持不同模型和数据集
   - 提供结果可视化功能,便于分析模型性能
   - 实现模型保存和加载功能,便于模型复用

2. **技术选型**:
   - 使用PyTorch框架实现模型训练和评估
   - 使用matplotlib和seaborn进行结果可视化
   - 使用scikit-learn计算混淆矩阵
   - 采用模块化设计,每个函数负责单一功能

3. **功能设计**:
   - 准确率计算:实现分类准确率的计算
   - 训练函数:实现一个epoch的训练逻辑
   - 评估函数:实现模型评估逻辑,返回损失和准确率
   - 可视化功能:
     - 绘制训练曲线,显示损失和准确率变化
     - 绘制混淆矩阵,显示各类别预测情况
     - 绘制错误分类样本,分析模型错误
   - 模型管理:实现模型保存和加载功能

4. **设计原则**:
   - 高内聚低耦合:每个函数负责单一功能,函数之间依赖关系清晰
   - 通用性:函数设计支持不同模型和数据集
   - 可扩展性:便于添加新的功能和修改现有功能
   - 可读性:代码结构清晰,注释详细

# 开发过程
1. **环境配置**:
   - 确保PyTorch、matplotlib、seaborn和scikit-learn已正确安装
   - 确保已创建results目录,用于保存可视化结果

2. **代码设计**:
   - 导入必要的库和模块
   - 实现准确率计算函数
   - 实现训练一个epoch的函数
   - 实现模型评估函数
   - 实现训练曲线绘制函数
   - 实现混淆矩阵绘制函数
   - 实现错误分类样本绘制函数
   - 实现模型保存和加载函数

3. **实现步骤**:
   - 导入依赖库
   - 实现calculate_accuracy函数
   - 实现train_epoch函数
   - 实现evaluate_model函数
   - 实现plot_training_curve函数
   - 实现plot_confusion_matrix函数
   - 实现plot_misclassified_samples函数
   - 实现save_model和load_model函数

4. **功能验证**:
   - 与train.py和test.py集成,测试各函数功能
   - 验证训练过程中各函数是否正常工作
   - 检查生成的可视化结果是否正确
   - 验证模型保存和加载功能是否正常
"""

# 导入PyTorch核心库
import torch

# 导入matplotlib,用于结果可视化
import matplotlib.pyplot as plt

# 导入numpy,用于数值计算
import numpy as np

# 从sklearn.metrics导入混淆矩阵计算函数
from sklearn.metrics import confusion_matrix

# 导入seaborn,用于绘制热力图形式的混淆矩阵
import seaborn as sns

# 定义计算准确率的函数
def calculate_accuracy(outputs, labels):
    """
    计算分类准确率
    
    参数说明:
        outputs: torch.Tensor, 模型输出,形状为[batch_size, num_classes]
        labels: torch.Tensor, 真实标签,形状为[batch_size]
    
    返回值:
        accuracy: float, 准确率,范围为[0, 1]
    """
    # 使用torch.max获取预测标签,返回最大值和索引,索引即为预测类别
    _, predicted = torch.max(outputs.data, 1)
    
    # 获取总样本数
    total = labels.size(0)
    
    # 计算正确预测的样本数
    # (predicted == labels)返回布尔张量,sum()计算True的数量,item()转换为Python数值
    correct = (predicted == labels).sum().item()
    
    # 计算准确率:正确样本数 / 总样本数
    accuracy = correct / total
    
    return accuracy

# 定义训练一个epoch的函数
def train_epoch(model, train_loader, criterion, optimizer, device):
    """
    训练模型一个epoch
    
    参数说明:
        model: torch.nn.Module, 要训练的模型
        train_loader: torch.utils.data.DataLoader, 训练集数据加载器
        criterion: torch.nn.Module, 损失函数
        optimizer: torch.optim.Optimizer, 优化器
        device: torch.device, 计算设备(CPU或GPU)
    
    返回值:
        avg_loss: float, 平均训练损失
        avg_accuracy: float, 平均训练准确率
    """
    # 设置模型为训练模式,启用dropout和batch normalization
    model.train()
    
    # 初始化累计损失和准确率
    running_loss = 0.0
    running_accuracy = 0.0
    total_samples = 0
    
    # 遍历训练集数据加载器
    for images, labels in train_loader:
        # 将图像和标签移到指定设备
        images, labels = images.to(device), labels.to(device)
        
        # 清零优化器的梯度
        optimizer.zero_grad()
        
        # 前向传播,获取模型输出
        outputs = model(images)
        
        # 计算损失
        loss = criterion(outputs, labels)
        
        # 反向传播,计算梯度
        loss.backward()
        
        # 更新模型参数
        optimizer.step()
        
        # 计算当前批次的准确率
        accuracy = calculate_accuracy(outputs, labels)
        
        # 获取当前批次的样本数
        batch_size = images.size(0)
        
        # 累计损失,乘以批次大小以保持总损失的正确性
        running_loss += loss.item() * batch_size
        
        # 累计准确率,乘以批次大小以保持总准确率的正确性
        running_accuracy += accuracy * batch_size
        
        # 累计总样本数
        total_samples += batch_size
    
    # 计算平均损失
    avg_loss = running_loss / total_samples
    
    # 计算平均准确率
    avg_accuracy = running_accuracy / total_samples
    
    return avg_loss, avg_accuracy

# 定义评估模型的函数
def evaluate_model(model, val_loader, criterion, device):
    """
    评估模型在验证集或测试集上的性能
    
    参数说明:
        model: torch.nn.Module, 要评估的模型
        val_loader: torch.utils.data.DataLoader, 验证集或测试集数据加载器
        criterion: torch.nn.Module, 损失函数
        device: torch.device, 计算设备(CPU或GPU)
    
    返回值:
        avg_loss: float, 平均损失
        avg_accuracy: float, 平均准确率
        all_preds: numpy.ndarray, 所有样本的预测标签,形状为[num_samples]
        all_labels: numpy.ndarray, 所有样本的真实标签,形状为[num_samples]
    """
    # 设置模型为评估模式,禁用dropout和batch normalization
    model.eval()
    
    # 初始化累计损失和准确率
    running_loss = 0.0
    running_accuracy = 0.0
    total_samples = 0
    
    # 初始化存储所有预测结果和真实标签的列表
    all_preds = []
    all_labels = []
    
    # 使用torch.no_grad()上下文管理器,禁用梯度计算,节省内存和计算资源
    with torch.no_grad():
        # 遍历验证集或测试集数据加载器
        for images, labels in val_loader:
            # 将图像和标签移到指定设备
            images, labels = images.to(device), labels.to(device)
            
            # 前向传播,获取模型输出
            outputs = model(images)
            
            # 计算损失
            loss = criterion(outputs, labels)
            
            # 计算当前批次的准确率
            accuracy = calculate_accuracy(outputs, labels)
            
            # 获取当前批次的预测标签
            _, preds = torch.max(outputs.data, 1)
            
            # 将预测结果和真实标签添加到列表中
            # 注意:需要将张量转换为numpy数组并移到CPU
            all_preds.extend(preds.cpu().numpy())
            all_labels.extend(labels.cpu().numpy())
            
            # 获取当前批次的样本数
            batch_size = images.size(0)
            
            # 累计损失
            running_loss += loss.item() * batch_size
            
            # 累计准确率
            running_accuracy += accuracy * batch_size
            
            # 累计总样本数
            total_samples += batch_size
    
    # 计算平均损失
    avg_loss = running_loss / total_samples
    
    # 计算平均准确率
    avg_accuracy = running_accuracy / total_samples
    
    # 将预测结果和真实标签转换为numpy数组
    all_preds = np.array(all_preds)
    all_labels = np.array(all_labels)
    
    return avg_loss, avg_accuracy, all_preds, all_labels

# 定义绘制训练曲线的函数
def plot_training_curve(train_losses, val_losses, train_accuracies, val_accuracies):
    """
    绘制训练曲线,包括损失曲线和准确率曲线
    
    参数说明:
        train_losses: list, 训练损失列表,长度为训练轮数
        val_losses: list, 验证损失列表,长度为训练轮数
        train_accuracies: list, 训练准确率列表,长度为训练轮数
        val_accuracies: list, 验证准确率列表,长度为训练轮数
    """
    # 生成轮数列表,从1开始
    epochs = range(1, len(train_losses) + 1)
    
    # 创建画布,包含2个子图(损失曲线和准确率曲线)
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
    
    # 绘制损失曲线
    # 训练损失用蓝色线,验证损失用红色线
    ax1.plot(epochs, train_losses, 'b', label='Training Loss')
    ax1.plot(epochs, val_losses, 'r', label='Validation Loss')
    # 设置子图标题
    ax1.set_title('Training and Validation Loss')
    # 设置x轴标签
    ax1.set_xlabel('Epochs')
    # 设置y轴标签
    ax1.set_ylabel('Loss')
    # 显示图例
    ax1.legend()
    
    # 绘制准确率曲线
    # 训练准确率用蓝色线,验证准确率用红色线
    ax2.plot(epochs, train_accuracies, 'b', label='Training Accuracy')
    ax2.plot(epochs, val_accuracies, 'r', label='Validation Accuracy')
    # 设置子图标题
    ax2.set_title('Training and Validation Accuracy')
    # 设置x轴标签
    ax2.set_xlabel('Epochs')
    # 设置y轴标签
    ax2.set_ylabel('Accuracy')
    # 显示图例
    ax2.legend()
    
    # 调整子图间距
    plt.tight_layout()
    
    # 保存图像到results目录
    plt.savefig('results/training_curves.png')
    
    # 关闭图像,释放内存
    plt.close()

# 定义绘制混淆矩阵的函数
def plot_confusion_matrix(all_labels, all_preds, class_names):
    """
    绘制混淆矩阵
    
    参数说明:
        all_labels: numpy.ndarray, 所有样本的真实标签,形状为[num_samples]
        all_preds: numpy.ndarray, 所有样本的预测标签,形状为[num_samples]
        class_names: list, 类别名称列表,长度为类别数
    """
    # 使用scikit-learn计算混淆矩阵
    cm = confusion_matrix(all_labels, all_preds)
    
    # 创建画布
    plt.figure(figsize=(10, 8))
    
    # 使用seaborn绘制热力图形式的混淆矩阵
    # annot=True:显示数值
    # fmt='d':使用整数格式
    # cmap='Blues':使用蓝色配色方案
    # xticklabels和yticklabels:设置坐标轴标签为类别名称
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=class_names, yticklabels=class_names)
    
    # 设置图像标题
    plt.title('Confusion Matrix')
    
    # 设置x轴标签
    plt.xlabel('Predicted Label')
    
    # 设置y轴标签
    plt.ylabel('True Label')
    
    # 调整布局
    plt.tight_layout()
    
    # 保存图像到results目录
    plt.savefig('results/confusion_matrix.png')
    
    # 关闭图像,释放内存
    plt.close()

# 定义绘制错误分类样本的函数
def plot_misclassified_samples(model, test_loader, device, num_samples=10):
    """
    绘制模型错误分类的样本
    
    参数说明:
        model: torch.nn.Module, 已训练好的模型
        test_loader: torch.utils.data.DataLoader, 测试集数据加载器
        device: torch.device, 计算设备(CPU或GPU)
        num_samples: int, 要显示的错误分类样本数量,默认10
    """
    # 设置模型为评估模式
    model.eval()
    
    # 初始化存储错误分类样本的列表
    misclassified = []
    
    # 使用torch.no_grad()上下文管理器,禁用梯度计算
    with torch.no_grad():
        # 遍历测试集数据加载器
        for images, labels in test_loader:
            # 将图像和标签移到指定设备
            images, labels = images.to(device), labels.to(device)
            
            # 前向传播,获取模型输出
            outputs = model(images)
            
            # 获取预测标签
            _, preds = torch.max(outputs, 1)
            
            # 找出错误分类的样本
            for i in range(len(preds)):
                if preds[i] != labels[i]:
                    # 将错误分类的样本添加到列表中
                    misclassified.append({
                        'image': images[i].cpu().numpy(),  # 图像转换为numpy数组并移到CPU
                        'true_label': labels[i].item(),  # 真实标签转换为Python整数
                        'predicted_label': preds[i].item()  # 预测标签转换为Python整数
                    })
                    
                    # 如果已收集到足够的错误分类样本,退出循环
                    if len(misclassified) >= num_samples:
                        break
            
            # 如果已收集到足够的错误分类样本,退出外层循环
            if len(misclassified) >= num_samples:
                break
    
    # 创建画布,包含2行5列共10个子图
    fig, axes = plt.subplots(2, 5, figsize=(15, 6))
    
    # 将axes转换为一维数组,方便遍历
    axes = axes.ravel()
    
    # 遍历错误分类样本并绘制
    for i, sample in enumerate(misclassified):
        # 绘制图像,使用灰度色彩映射
        # squeeze()去除通道维度,从[1,28,28]变为[28,28]
        axes[i].imshow(sample['image'].squeeze(), cmap='gray')
        
        # 设置子图标题,显示真实标签和预测标签
        axes[i].set_title(f"True: {sample['true_label']}\nPred: {sample['predicted_label']}")
        
        # 隐藏坐标轴
        axes[i].axis('off')
    
    # 调整子图间距
    plt.tight_layout()
    
    # 保存图像到results目录
    plt.savefig('results/misclassified_samples.png')
    
    # 关闭图像,释放内存
    plt.close()

# 定义保存模型的函数
def save_model(model, path):
    """
    保存模型的状态字典
    
    参数说明:
        model: torch.nn.Module, 要保存的模型
        path: str, 模型保存路径
    """
    # 保存模型的状态字典
    torch.save(model.state_dict(), path)
    
    # 打印保存成功的信息
    print(f"模型已保存到 {path}")

# 定义加载模型的函数
def load_model(model, path):
    """
    加载模型的状态字典
    
    参数说明:
        model: torch.nn.Module, 模型实例
        path: str, 模型文件路径
    
    返回值:
        model: torch.nn.Module, 加载了权重的模型实例
    """
    # 加载模型的状态字典
    model.load_state_dict(torch.load(path))
    
    # 打印加载成功的信息
    print(f"模型已从 {path} 加载")
    
    return model

5.5 训练脚本(train.py)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
模块4:模型训练脚本(MLP训练)

# 开发思路
1. **问题分析**:
   - 需要训练一个多层感知器(MLP)模型,用于MNIST手写数字识别
   - 训练过程包括数据加载、模型创建、损失计算、梯度更新和模型评估
   - 需要保存最佳模型,并生成训练曲线和评估结果

2. **技术选型**:
   - 使用PyTorch框架进行模型训练
   - 采用交叉熵损失函数,适合多分类问题
   - 使用Adam优化器,具有自适应学习率特性
   - 采用早停策略,保存验证集上表现最好的模型

3. **训练流程设计**:
   - 环境准备:创建结果和模型保存目录
   - 设备选择:自动检测GPU或使用CPU
   - 数据加载:调用data_loader模块加载数据集
   - 模型创建:实例化MLP模型并移至指定设备
   - 训练循环:
     - 训练一个epoch,计算训练损失和准确率
     - 在验证集上评估模型
     - 记录训练历史
     - 保存最佳模型
   - 结果可视化:生成训练曲线、混淆矩阵和错误分类样本图

4. **超参数设计**:
   - 批量大小:64,平衡内存占用和训练效率
   - 学习率:0.001,适合Adam优化器的默认学习率
   - 训练轮数:15,足够模型收敛,避免过拟合
   - 验证集比例:10%,用于训练过程中的模型评估

# 开发过程
1. **环境配置**:
   - 确保PyTorch和相关库已正确安装
   - 创建results和models目录,用于保存结果和模型

2. **代码设计**:
   - 导入必要的库和模块
   - 实现目录创建和设备检测
   - 设计超参数配置
   - 实现模型创建和训练循环
   - 添加结果可视化和模型保存功能

3. **实现步骤**:
   - 导入依赖库
   - 创建目录结构
   - 检测计算设备
   - 设置超参数
   - 加载数据
   - 创建模型
   - 定义损失函数和优化器
   - 训练模型
   - 可视化结果
   - 评估模型

4. **功能验证**:
   - 运行训练脚本,检查是否能正常执行
   - 验证模型是否能保存到指定目录
   - 检查是否能生成预期的可视化结果
   - 确认模型在测试集上的准确率是否符合预期
"""

# 导入PyTorch核心库,用于张量操作和自动微分
import torch

# 导入PyTorch神经网络模块,包含各种层和激活函数
import torch.nn as nn

# 导入PyTorch优化器模块,包含各种优化算法
import torch.optim as optim

# 导入os模块,用于文件和目录操作
import os

# 从model模块导入MLP模型类
from model import MLP

# 从data_loader模块导入获取MNIST数据加载器的函数
from data_loader import get_mnist_data_loaders

# 从utils模块导入工具函数
from utils import (
    train_epoch,  # 训练一个epoch的函数
    evaluate_model,  # 模型评估函数
    plot_training_curve,  # 绘制训练曲线的函数
    plot_confusion_matrix,  # 绘制混淆矩阵的函数
    plot_misclassified_samples,  # 绘制错误分类样本的函数
    save_model  # 保存模型的函数
)

# 创建结果保存目录,如果不存在则创建
if not os.path.exists('results'):
    os.makedirs('results')

# 创建模型保存目录,如果不存在则创建
if not os.path.exists('models'):
    os.makedirs('models')

# 检测是否有GPU可用,优先使用GPU,否则使用CPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 打印使用的设备
print(f"使用设备: {device}")

# 设置超参数
batch_size = 64  # 批量大小,每次训练的样本数
val_split = 0.1  # 验证集比例,10%
num_workers = 4  # 数据加载的进程数
learning_rate = 0.001  # 学习率,控制模型参数更新的步长
epochs = 15  # 训练轮数,模型将在训练集上迭代15次

# 获取数据加载器
train_loader, val_loader, test_loader = get_mnist_data_loaders(
    batch_size=batch_size,  # 设置批量大小
    val_split=val_split,  # 设置验证集比例
    num_workers=num_workers  # 设置数据加载进程数
)

# 创建MLP模型实例,并将模型移至指定设备
model = MLP().to(device)
# 打印模型结构,查看网络架构
print(model)

# 定义损失函数,使用交叉熵损失函数,适合多分类问题
criterion = nn.CrossEntropyLoss()

# 定义优化器,使用Adam优化器,学习率为0.001
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

# 初始化训练历史记录列表
train_losses = []  # 存储每轮训练的损失值
train_accuracies = []  # 存储每轮训练的准确率
val_losses = []  # 存储每轮验证的损失值
val_accuracies = []  # 存储每轮验证的准确率

# 初始化最佳验证准确率
best_val_accuracy = 0.0

# 打印开始训练的提示信息
print("开始训练...")

# 训练循环,共训练epochs轮
for epoch in range(epochs):
    # 打印当前训练轮次信息
    print(f"\n第 {epoch+1}/{epochs} 轮训练:")
    
    # 训练一个epoch,返回训练损失和准确率
    train_loss, train_accuracy = train_epoch(
        model, train_loader, criterion, optimizer, device
    )
    
    # 在验证集上评估模型,返回验证损失和准确率
    val_loss, val_accuracy, _, _ = evaluate_model(
        model, val_loader, criterion, device
    )
    
    # 将当前轮次的训练损失和准确率添加到历史记录
    train_losses.append(train_loss)
    train_accuracies.append(train_accuracy)
    # 将当前轮次的验证损失和准确率添加到历史记录
    val_losses.append(val_loss)
    val_accuracies.append(val_accuracy)
    
    # 打印当前轮次的训练和验证结果
    print(f"训练损失: {train_loss:.4f}, 训练准确率: {train_accuracy:.4f}")
    print(f"验证损失: {val_loss:.4f}, 验证准确率: {val_accuracy:.4f}")
    
    # 如果当前验证准确率高于最佳验证准确率,则保存模型
    if val_accuracy > best_val_accuracy:
        # 更新最佳验证准确率
        best_val_accuracy = val_accuracy
        # 保存模型到指定路径
        save_model(model, './models/mlp_model.pth')
        # 打印保存模型的信息
        print(f"保存最佳模型,验证准确率: {best_val_accuracy:.4f}")

# 打印训练完成的提示信息
print("\n训练完成!")

# 绘制训练曲线,包括损失曲线和准确率曲线
plot_training_curve(train_losses, val_losses, train_accuracies, val_accuracies)
# 打印训练曲线生成完成的信息
print("已生成训练曲线")

# 打印在测试集上评估模型的提示信息
print("\n在测试集上评估模型:")
# 在测试集上评估模型,返回测试损失、准确率和预测结果
test_loss, test_accuracy, all_preds, all_labels = evaluate_model(
    model, test_loader, criterion, device
)
# 打印测试结果
print(f"测试损失: {test_loss:.4f}, 测试准确率: {test_accuracy:.4f}")

# 生成类别名称列表,0-9的数字
class_names = [str(i) for i in range(10)]
# 绘制混淆矩阵
plot_confusion_matrix(all_labels, all_preds, class_names)
# 打印混淆矩阵生成完成的信息
print("已生成混淆矩阵")

# 绘制错误分类的样本,显示10个错误分类的样本
plot_misclassified_samples(model, test_loader, device, num_samples=10)
# 打印错误分类样本图生成完成的信息
print("已生成错误分类样本图")

# 打印所有评估完成的提示信息
print("\n所有评估完成!")

5.6 测试脚本(test.py)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
模块4:模型测试脚本(MLP测试)

# 开发思路
1. **问题分析**:
   - 需要测试已训练好的MLP模型在MNIST测试集上的性能
   - 测试过程包括数据加载、模型加载、模型评估和结果可视化
   - 需要支持单张图像预测和随机样本测试功能

2. **技术选型**:
   - 使用PyTorch框架进行模型测试
   - 采用交叉熵损失函数评估模型性能
   - 使用matplotlib进行结果可视化
   - 使用PIL处理单张图像输入

3. **测试流程设计**:
   - 环境准备:检测GPU或使用CPU
   - 数据加载:加载MNIST测试集
   - 模型加载:加载训练好的模型权重
   - 模型评估:在测试集上评估模型,计算损失和准确率
   - 结果可视化:
     - 生成混淆矩阵,显示各类别预测情况
     - 生成错误分类样本图,展示模型预测错误的样本
   - 功能扩展:
     - 实现单张图像预测功能
     - 实现随机样本测试功能

4. **功能设计**:
   - 批量测试:在整个测试集上评估模型性能
   - 单张图像预测:支持预测指定路径的图像
   - 随机样本测试:从测试集中随机抽取样本进行测试
   - 结果可视化:生成混淆矩阵和错误分类样本图

# 开发过程
1. **环境配置**:
   - 确保PyTorch和相关库已正确安装
   - 确保已训练好的模型文件存在于指定路径

2. **代码设计**:
   - 导入必要的库和模块
   - 实现设备检测和数据加载
   - 实现模型加载和评估
   - 实现结果可视化功能
   - 实现单张图像预测和随机样本测试功能

3. **实现步骤**:
   - 导入依赖库
   - 检测计算设备
   - 设置超参数
   - 加载测试数据
   - 创建模型并加载权重
   - 评估模型
   - 可视化结果
   - 实现单张图像预测函数
   - 实现随机样本测试函数
   - 调用测试函数

4. **功能验证**:
   - 运行测试脚本,检查是否能正常加载模型
   - 验证模型评估结果是否合理
   - 检查生成的可视化结果是否正确
   - 测试单张图像预测功能是否正常工作
"""

# 导入PyTorch核心库,用于张量操作
import torch

# 导入PyTorch神经网络模块,包含损失函数
import torch.nn as nn

# 导入PIL库,用于图像加载和处理
from PIL import Image

# 导入numpy,用于数值计算
import numpy as np

# 导入matplotlib,用于结果可视化
import matplotlib.pyplot as plt

# 从model模块导入MLP模型类
from model import MLP

# 从data_loader模块导入获取MNIST数据加载器的函数
from data_loader import get_mnist_data_loaders

# 从utils模块导入工具函数
from utils import (
    evaluate_model,  # 模型评估函数
    load_model,  # 加载模型的函数
    plot_confusion_matrix,  # 绘制混淆矩阵的函数
    plot_misclassified_samples  # 绘制错误分类样本的函数
)

# 检测是否有GPU可用,优先使用GPU,否则使用CPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 打印使用的设备
print(f"使用设备: {device}")

# 设置超参数
batch_size = 64  # 批量大小,每次测试的样本数
val_split = 0.1  # 验证集比例,10%
num_workers = 4  # 数据加载的进程数

# 获取数据加载器,只使用测试集
# 前两个返回值为训练集和验证集数据加载器,这里用下划线占位
_, _, test_loader = get_mnist_data_loaders(
    batch_size=batch_size,  # 设置批量大小
    val_split=val_split,  # 设置验证集比例
    num_workers=num_workers  # 设置数据加载进程数
)

# 创建MLP模型实例,并将模型移至指定设备
model = MLP().to(device)

# 训练好的模型文件路径
model_path = './models/mlp_model.pth'
# 加载训练好的模型权重
model = load_model(model, model_path)

# 定义损失函数,使用交叉熵损失函数
criterion = nn.CrossEntropyLoss()

# 打印开始测试模型的提示信息
print("\n开始测试模型...")

# 在测试集上评估模型,返回测试损失、测试准确率、所有预测结果和真实标签
test_loss, test_accuracy, all_preds, all_labels = evaluate_model(
    model, test_loader, criterion, device
)

# 打印测试结果
print(f"测试损失: {test_loss:.4f}")
print(f"测试准确率: {test_accuracy:.4f}")

# 生成类别名称列表,0-9的数字
class_names = [str(i) for i in range(10)]
# 绘制混淆矩阵
plot_confusion_matrix(all_labels, all_preds, class_names)
# 打印混淆矩阵生成完成的信息
print("已生成混淆矩阵")

# 绘制错误分类的样本,显示10个错误分类的样本
plot_misclassified_samples(model, test_loader, device, num_samples=10)
# 打印错误分类样本图生成完成的信息
print("已生成错误分类样本图")

# 定义预测单张图像的函数
def predict_single_image(image_path):
    """
    预测单张图像的数字类别
    
    参数说明:
        image_path: str, 图像文件路径
    
    返回值:
        predicted_label: int, 预测的数字类别(0-9)
        probabilities: numpy.ndarray, 各类别的概率分布,形状为[10]
    """
    # 导入torchvision.transforms,用于图像预处理
    from torchvision import transforms
    
    # 定义图像预处理转换流水线
    # 与训练时的预处理保持一致
    transform = transforms.Compose([
        transforms.Grayscale(),  # 将图像转换为灰度图像
        transforms.Resize((28, 28)),  # 将图像调整为28×28像素
        transforms.ToTensor(),  # 转换为PyTorch张量
        transforms.Normalize((0.1307,), (0.3081,))  # 归一化处理
    ])
    
    # 使用PIL加载图像
    image = Image.open(image_path)
    # 应用预处理转换,并添加批次维度(从[1,28,28]变为[1,1,28,28])
    image_tensor = transform(image).unsqueeze(0).to(device)
    
    # 设置模型为评估模式
    model.eval()
    
    # 使用torch.no_grad()上下文管理器,禁用梯度计算
    with torch.no_grad():
        # 前向传播,获取模型输出
        output = model(image_tensor)
        # 计算各类别的概率,使用softmax函数
        probabilities = torch.softmax(output, dim=1)
        # 获取预测标签,即概率最大的类别
        _, predicted_label = torch.max(output, 1)
    
    # 返回预测标签(转换为Python原生整数)和概率分布(转换为numpy数组)
    return predicted_label.item(), probabilities.squeeze().cpu().numpy()

# 定义使用随机样本测试模型的函数
def test_with_random_samples(num_samples=5):
    """
    使用测试集中的随机样本测试模型
    
    参数说明:
        num_samples: int, 要测试的样本数量,默认为5
    """
    # 打印测试信息
    print(f"\n使用测试集中的 {num_samples} 个随机样本进行测试:")
    
    # 导入random模块,用于随机抽样
    import random
    
    # 将测试集转换为列表,方便随机抽样
    test_data = list(test_loader.dataset)
    # 从测试集中随机抽取num_samples个样本
    random_samples = random.sample(test_data, num_samples)
    
    # 创建画布,包含num_samples个子图
    fig, axes = plt.subplots(1, num_samples, figsize=(15, 3))
    
    # 遍历随机抽取的样本
    for i, sample in enumerate(random_samples):
        # 获取图像和真实标签
        image, true_label = sample
        
        # 设置模型为评估模式
        model.eval()
        
        # 禁用梯度计算
        with torch.no_grad():
            # 添加批次维度,并将图像移到指定设备
            image_tensor = image.unsqueeze(0).to(device)
            # 前向传播,获取模型输出
            output = model(image_tensor)
            # 计算各类别的概率
            probabilities = torch.softmax(output, dim=1)
            # 获取预测标签
            _, predicted_label = torch.max(output, 1)
        
        # 绘制图像,使用灰度色彩映射
        axes[i].imshow(image.squeeze(), cmap='gray')
        # 设置子图标题,显示真实标签和预测标签
        axes[i].set_title(f"True: {true_label}\nPred: {predicted_label.item()}")
        # 隐藏坐标轴
        axes[i].axis('off')
        
        # 打印样本测试结果
        print(f"样本 {i+1}: 真实标签={true_label}, 预测标签={predicted_label.item()}, "
              f"置信度={probabilities[0][predicted_label.item()]:.4f}")
    
    # 调整子图间距
    plt.tight_layout()
    # 保存图像到results目录
    plt.savefig('results/random_test_samples.png')
    # 关闭图像,释放内存
    plt.close()
    
    # 打印图像生成完成的信息
    print("已生成随机测试样本结果图")

# 调用随机样本测试函数,测试5个样本
test_with_random_samples(num_samples=5)

# 打印模型测试完成的提示信息
print("\n模型测试完成!")
# 打印最终测试准确率
print(f"最终测试准确率: {test_accuracy:.4f}")

# 提供预测单张图像的功能说明
print("\n如果需要预测单张图像,可以使用以下代码:")
print("from test import predict_single_image")
print("predicted_label, probabilities = predict_single_image('path/to/image.png')")
print("print(f'预测结果: {predicted_label}, 概率分布: {probabilities}')")

7. 脚本执行顺序与作用

7.1 脚本执行顺序

  1. 数据准备:运行 data_loader.py 获取数据集(自动下载并预处理MNIST数据)
  2. 模型定义model.py 中定义了MLP模型结构
  3. 模型训练:运行 train.py 训练模型,生成训练曲线和保存最佳模型
  4. 模型测试:运行 test.py 评估模型性能,生成混淆矩阵和错误分类样本图

7.2 各脚本作用

脚本文件 主要作用 执行命令
model.py 定义MLP模型结构 python model.py
data_loader.py 加载和预处理MNIST数据 python data_loader.py
utils.py 提供训练、评估和可视化工具函数 被train.py和test.py调用
train.py 训练模型并保存最佳模型 python train.py
test.py 评估模型性能并生成可视化结果 python test.py

7.3 执行流程图

开始
定义模型
model.py
准备数据
data_loader.py
训练模型
train.py
保存模型
models/mlp_model.pth
生成训练曲线
results/training_curves.png
测试模型
test.py
生成混淆矩阵
results/confusion_matrix.png
生成错误分类样本
results/misclassified_samples.png
生成随机测试样本
results/random_test_samples.png
结束

8. 运行示例

8.1 训练模型

python train.py

8.2 测试模型

python test.py

8.3 单张图像预测

from test import predict_single_image
predicted_label, probabilities = predict_single_image('path/to/image.png')
print(f'预测结果: {predicted_label}')
print(f'概率分布: {probabilities}')

更多推荐