目 录CONTENT

文章目录
AI

深度学习:Miniconda + PyTorch + MNIST

TalentQ
2026-05-20 / 1 评论 / 0 点赞 / 40 阅读 / 0 字

目标

  1. 在 Linux 上用 Miniconda 配置 PyTorch 环境

  2. 掌握 torch.nnDataLoadertorch.no_grad()、模型保存/加载

  3. 训练一个全连接网络(MLP)并在 MNIST 上达到约 97% 准确率

  4. 确保代码能在你的 CPU 和 GPU上运行

为什么使用 Miniconda?

  • 轻量:仅几百 MB,不会像 Anaconda 那样占用 3~5 GB 磁盘空间

  • 灵活:只安装你真正需要的包,环境干净

1. 环境配置步骤(Miniconda + PyTorch)

1.1 安装 Miniconda

参考链接

Linux 终端执行:

# 下载 Miniconda 安装脚本(Linux x86_64)
wget -c https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh

# 运行安装脚本
bash Miniconda3-latest-Linux-x86_64.sh

# 按提示操作:按 Enter 阅读协议,输入 yes 同意,默认安装路径即可,最后问是否初始化 conda 时选 yes

安装完成后,关闭并重新打开终端,或执行 source ~/.bashrc,然后测试:

conda --version   # 应显示版本号,如 26.3.2

1.2 创建专用环境(避免污染 base)

conda create -n mnist_cpu python=3.11 -y
conda activate mnist_cpu

1.3 安装 PyTorch(先 CPU 版本验证,后续可升级到 GPU)

CPU版本和GPU版本最好创建两个独立的python虚拟环境,分别验证。
GPU版本即CUDA版本。

在安装 PyTorch 前,首先执行 nvidia-smi 查看显卡和驱动信息(建议发给AI来确定安装什么版本的 PyTorch):

我的驱动已经升级到 582.53,支持 CUDA 13.0 及以下的所有版本,完全兼容目前所有主流 PyTorch 版本(无论使用 CUDA 11.8、12.x 还是未来更新的版本)。GTX 950M 虽然计算能力 5.0 稍老,但驱动足够新(我更新到了最新的驱动),因此可以放心使用 PyTorch 官方提供的 CUDA 12.x 预编译包,兼容性和性能都比 11.x 更好。

1.3.1 CPU 版本(仅使用 CPU 训练,不调用 GPU):

  • 适用场景:只想快速验证代码逻辑,或暂时不想用 GPU。

  • 特点:安装包小,无需 CUDA 驱动支持,但训练慢。

pip install torch==2.5.1 torchvision==0.20.1 torchaudio==2.5.1 --index-url https://download.pytorch.org/whl/cpu

1.3.2 GPU 版本(使用你的 GPU 加速,推荐)

pip install torch==2.5.1 torchvision==0.20.1 torchaudio==2.5.1 --index-url https://download.pytorch.org/whl/cu124

重要:CPU 和 GPU 版本不要混装在同一个环境中。如果你先装了 CPU 版,想换 GPU 版,请先用 pip uninstall torch torchvision torchaudio 卸载,再重新安装 GPU 版。

1.4 验证安装

import torch
print(torch.__version__)          # 例如 2.5.1+cpu 或 2.5.1+cu124
print(torch.cuda.is_available())  # 若安装 CPU 版本,应显示 False;若安装 GPU 版本且驱动正常,应显示 True

2. PyTorch 核心组件必学

在写代码前,先快速过一遍以下核心概念(每个都将在后面的完整代码中出现)。

组件

作用

示例

torch.nn.Module

所有神经网络的基类,自定义网络需继承它

class MyNet(nn.Module):

nn.Linear

全连接层(线性变换)

self.fc = nn.Linear(784, 128)

nn.ReLU / nn.LogSoftmax

激活函数 / 输出层变换

x = F.relu(x)

DataLoader

批量加载数据,支持自动打乱、多线程

DataLoader(dataset, batch_size=64, shuffle=True)

torch.no_grad()

禁用梯度计算,常用于评估/推理

with torch.no_grad():

模型保存/加载

torch.save(state, path) / model.load_state_dict(state_dict)

3. 完整实验:全连接网络在 MNIST 上分类 (建立概念即可)

3.0 快速了解(建立整体概念即可)

MNIST 是深度学习领域最经典、最基础的数据集之一,被称为“计算机视觉的 Hello World”。我要要做的事情称为“手写数字识别”,训练一个模型,实现输入一张手写数字的图片,输出图片上对应的数字。

我们先从宏观上理解一个深度学习实验的完整生命周期。整个实验可以划分为 6 个逻辑阶段,每个阶段解决一个特定问题:

阶段

核心任务

为什么必须有这个阶段?

1. 数据准备

下载数据集、预处理(归一化)、批量加载

原始图像数据格式不统一(0~255 整数,形状 H×W×C),需要转为张量并缩放到 [0,1];标准化(减均值除标准差)能让梯度下降更稳定;批量加载避免显存爆炸且实现随机梯度下降。

2. 模型定义

搭建神经网络结构(层数、神经元个数、激活函数)

模型是学习能力的载体。全连接网络假设每个像素与下一层所有神经元相连,通过非线性变换拟合复杂的输入输出关系。

3. 损失与优化器

选择衡量预测与真实值差距的指标(损失函数);选择参数更新策略(优化器)

损失函数是训练的“指南针”:交叉熵适合多分类,它惩罚错误的高置信度预测。优化器是“舵手”:Adam 自适应调整学习率,初学者友好。

4. 训练循环

重复:前向传播 → 计算损失 → 反向传播 → 更新参数

这是模型学习的核心。每次迭代模型看到一批数据,计算预测误差,然后通过反向传播求出每个参数对误差的贡献(梯度),最后按梯度方向微调参数,使误差下降。

5. 测试评估

在未见过的数据上计算损失和准确率

评估模型泛化能力。训练集上的低损失不代表能预测新数据,测试集模拟真实应用环境。测试时需关闭 Dropout 和梯度计算。

6. 保存与预测

保存最佳模型参数;对单张图片进行预测并可视化结果

训练后模型保存在磁盘,下次可直接加载使用,无需重新训练。预测演示了模型的实际应用:输入一张手写数字图片,输出识别结果。

训练循环的内部细节(一个 batch 经历了什么)

下图(概念图)展示了一次迭代中数据的流动过程(读者可脑补):

输入 batch (64张手写数字) 
  ↓ [前向传播]
模型输出 logits (64×10) 
  ↓ [损失函数]
损失值 (标量) 
  ↓ [反向传播]
梯度 (每个参数的偏导数) 
  ↓ [优化器更新]
更新后的模型参数

关键机制说明

  • 前向传播:输入数据依次流过各层(线性 → ReLU → Dropout → …),最终输出 10 个类别的原始分数(logits),此时尚未转为概率。

  • 损失计算CrossEntropyLoss 内部自动对 logits 做 LogSoftmax,然后取出正确类别的负对数概率。若模型对正确类别预测分数高,则损失小。

  • 反向传播:基于链式法则,从输出端向输入端逐层计算损失对每个参数的偏导数(梯度)。PyTorch 的自动求导机制(.backward())自动完成这一过程。

  • 参数更新:优化器根据当前梯度及历史动量,计算出参数的变化量,加到原参数上。例如 SGD 为 W_new = W_old - lr * grad

为什么全连接网络在 MNIST 上有效?

  • MNIST 手写数字尺寸小(28×28),数字居中且较规范,全连接层可以捕捉像素间的全局相关性。

  • 三层网络(512→256→10)有约 40 万个参数,足够拟合训练集(6 万样本),Dropout 缓解了过拟合。

  • 但全连接网络忽略了图像的局部结构(相邻像素应共享权重),因此准确率上限约 98.5%,而 LeNet-5 这种 CNN 可达 99.3% 以上。

3.1 导入所有需要的库

import torch                     # PyTorch 核心库,提供张量操作、自动求导等
import torch.nn as nn            # 神经网络模块,包含所有层(Linear, Dropout等)
import torch.nn.functional as F  # 常用函数(如 relu, softmax),不含可训练参数
import torch.optim as optim      # 优化器模块,包含 Adam, SGD 等
from torchvision import datasets, transforms  # torchvision 提供常用数据集和图像预处理工具
from torch.utils.data import DataLoader       # 数据加载器,批量读取数据
import time                      # 计时,统计训练时长
import matplotlib.pyplot as plt  # 绘图库,用于保存预测图片

3.2 检查硬件设备(GPU/CPU)

# torch.device 用于指定计算设备
# torch.cuda.is_available() 返回 True 表示 PyTorch 检测到了 NVIDIA GPU 且驱动正常
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

print(f"使用设备: {device}")
if device.type == "cuda":
    # torch.cuda.get_device_name(0) 返回第0块GPU的名称
    print(f"GPU 名称: {torch.cuda.get_device_name(0)}")

详细解释:

  • torch.device("cuda") 会创建一个代表 GPU 的对象;若没有 GPU 则用 "cpu"。

  • 后续所有张量和模型都需要 .to(device) 移动到该设备。

3.3 数据预处理与加载(Transform & DataLoader)

3.3.1 定义数据预处理流程 transform

transform = transforms.Compose([
    transforms.ToTensor(),                        # 步骤1:将 PIL 图像或 numpy 数组转为 PyTorch 张量
    transforms.Normalize((0.1307,), (0.3081,))    # 步骤2:标准化(减均值,除标准差)
])

详细解释

  • transforms.Compose:将多个图像变换操作组合成一个流水线,按顺序执行。

  • transforms.ToTensor()

    • 输入:PIL Image 或 numpy 数组,取值范围 0~255(uint8)。

    • 输出:torch.FloatTensor,形状从 (H, W, C) 转为 (C, H, W),并将数值缩放至 [0.0, 1.0]

    • MNIST 是灰度图,所以形状变为 (1, 28, 28),通道数=1。

  • transforms.Normalize(mean, std)

    • 公式:output = (input - mean) / std,其中 meanstd 可以是单个数值(对所有通道用相同值)或元组(每个通道独立)。

    • MNIST 数据集的全局均值约为 0.1307,标准差约为 0.3081。这些数值是预先计算好的(通过统计所有训练图像获得)。

    • 标准化后,数据分布将接近均值为 0、标准差为 1 的标准正态分布,有助于梯度下降更快收敛,避免梯度消失或爆炸。

3.3.2 下载并加载数据集

# 训练集:train=True,第一次运行会从网络下载到 './data' 目录
train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
# 测试集:train=False,同样下载/加载
test_dataset  = datasets.MNIST('./data', train=False, download=True, transform=transform)

参数解释

  • './data':数据存储的目录(相对路径,在当前工作目录下创建 data 文件夹)。

  • train=True:加载 60000 张训练图片;train=False 加载 10000 张测试图片。

  • download=True:如果本地没有数据,则自动从官网 yann.lecun.com/exdb/mnist/ 下载。

  • transform:将上面定义的预处理流水线应用到每张图片上。

3.3.3 创建数据加载器 DataLoader

batch_size = 64   # 每个 mini-batch 包含 64 张图片

train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader  = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

参数详解

  • batch_size:一次迭代(一个 step)送入网络的样本数。太大容易超出显存,太小则梯度更新噪声大。64 是入门常见值。

  • shuffle=True:每个 epoch 开始前打乱数据顺序,防止模型记住样本顺序(提高泛化能力)。测试集不需要打乱。

  • DataLoader 还支持 num_workers(多进程加载数据),但入门保持默认 0 即可。

数据加载器的作用

  • 自动迭代产生批次数据,无需手动切片。

  • 例如:for images, labels in train_loader:,其中 images.shape = (64, 1, 28, 28)labels.shape = (64,)

3.4 定义全连接网络(MLP 类)

class MLP(nn.Module):
    """三层全连接网络,使用 ReLU 激活函数和 Dropout 正则化"""
    def __init__(self):
        super(MLP, self).__init__()   # 必须调用父类构造函数
        # 第一层全连接:输入维度 784(28*28),输出维度 512
        self.fc1 = nn.Linear(28*28, 512)
        # 第二层:512 → 256
        self.fc2 = nn.Linear(512, 256)
        # 第三层(输出层):256 → 10(对应 0-9 十个类别)
        self.fc3 = nn.Linear(256, 10)
        # Dropout 层:训练时以 0.2 的概率随机将部分神经元输出置为 0,防止过拟合
        self.dropout = nn.Dropout(0.2)

    def forward(self, x):
        """
        前向传播函数,x 的形状: (batch_size, 1, 28, 28)
        """
        # 步骤1:将图像展平为一维向量
        # x.size(0) 获取 batch_size; -1 表示自动计算剩余维度
        # 展平后形状: (batch_size, 28*28) = (batch_size, 784)
        x = x.view(x.size(0), -1)
        
        # 步骤2:第一层线性变换 + ReLU 激活 + Dropout
        x = F.relu(self.fc1(x))
        x = self.dropout(x)
        
        # 步骤3:第二层线性变换 + ReLU 激活 + Dropout
        x = F.relu(self.fc2(x))
        x = self.dropout(x)
        
        # 步骤4:输出层,不添加激活函数(原始 logits)
        # 原因见下节损失函数的解释
        x = self.fc3(x)
        return x

model = MLP().to(device)   # 将模型的所有参数移动到指定设备(GPU 或 CPU)
print(model)               # 打印模型结构,方便检查

详细解释每个组件

nn.Module

  • 所有神经网络的基类。你的网络必须继承它,并实现 __init__forward

  • super(MLP, self).__init__() 会初始化内部结构(如参数列表、训练/评估状态)。

nn.Linear(in_features, out_features)

  • 全连接层(线性层),数学表达式:y = x @ W.T + b,其中 W 是权重矩阵(shape (out, in)),b 是偏置向量(shape (out,))。

  • 参数 in_features:输入特征数(对于展平后的图像就是 784)。

  • out_features:输出特征数(隐藏层神经元个数)。

  • 可训练参数:self.fc1.weight 形状 (512, 784)self.fc1.bias 形状 (512,)

nn.Dropout(p)

  • 在训练时,以概率 p 随机将输入张量的某些元素置为 0,同时将剩余元素缩放 1/(1-p) 以保持期望不变。

  • 作用:防止神经元之间产生复杂的共适应关系,提高泛化能力。

  • 注意:Dropout 只在训练模式(model.train())下生效;评估模式(model.eval())下自动关闭。

F.relu(x)

  • 激活函数:ReLU(x) = max(0, x)。非线性变换使网络能够拟合复杂函数。

  • 优点:计算简单、缓解梯度消失、带来稀疏性。

x.view(x.size(0), -1)

  • view 是 PyTorch 中改变张量形状的方法,类似 NumPy 的 reshape

  • x.size(0) 是 batch 大小;-1 表示根据总元素数量自动计算。例如输入 (64,1,28,28) 总元素 64*1*28*28=50176view(64, -1) 会变成 (64, 50176/64=784)

  • 等价于 torch.flatten(x, start_dim=1)

为什么输出层不加 softmax?

  • 因为后面使用的 nn.CrossEntropyLoss 内部已经组合了 LogSoftmax + NLLLoss

  • 具体流程:CrossEntropyLoss 会对网络输出的 logits 自动应用 softmax,再计算交叉熵。

  • 如果手动在 forward 里加 F.softmax(x, dim=1),会导致两次 softmax,破坏概率分布,造成错误结果。

3.5 定义损失函数和优化器

criterion = nn.CrossEntropyLoss()          # 交叉熵损失(用于多分类)
optimizer = optim.Adam(model.parameters(), lr=0.001)  # Adam 优化器

nn.CrossEntropyLoss 详细解剖

  • 公式:对于单个样本,损失 = -log(p_target),其中 p_target 是模型对正确类别的预测概率。

  • 输入要求:模型输出应为 未经 softmax 的原始分数(logits),形状 (batch_size, num_classes)

  • 内部步骤

    1. 对 logits 应用 LogSoftmaxlog_softmax_i = log( exp(x_i) / sum_j exp(x_j) )

    2. 然后计算负对数似然(NLL):取正确类别对应的 log_softmax 值,取负号,再求平均。

  • 为什么这样设计?数值稳定性。直接计算 softmax 后再计算 log 容易溢出或出现 log(0)。组合起来用 log_softmax 更稳定。

  • 等价写法loss = F.nll_loss(F.log_softmax(model(x), dim=1), target)

optim.Adam

  • Adam(Adaptive Moment Estimation)是目前最常用的优化器之一。

  • 工作原理:维护每个参数的一阶矩(梯度均值)和二阶矩(梯度未中心化的方差),自适应调整学习率。

  • 参数

    • model.parameters():传入模型的所有可训练参数(weightbias)。

    • lr=0.001:初始学习率。Adam 对学习率不太敏感,0.001 是良好默认值。

  • 优点:收敛快,对超参数鲁棒,适合初学者。

3.6 训练函数(每个 epoch 调用)

def train(epoch):
    """在训练集上训练一个完整的 epoch"""
    model.train()          # 设置为训练模式(启用 Dropout)
    total_loss = 0         # 累加每个 batch 的损失
    
    # enumerate 同时返回索引和值;batch_idx 从 0 开始
    for batch_idx, (data, target) in enumerate(train_loader):
        # 将数据和标签移动到指定设备(GPU/CPU)
        data, target = data.to(device), target.to(device)
        
        # 清空上一次的梯度(因为梯度会累积)
        optimizer.zero_grad()
        
        # 前向传播:输入数据,得到预测 logits
        output = model(data)      # output.shape = (batch_size, 10)
        
        # 计算当前 batch 的损失值(标量)
        loss = criterion(output, target)
        
        # 反向传播:计算损失相对于模型参数的梯度
        loss.backward()
        
        # 优化器更新参数:参数 = 参数 - 学习率 * 梯度
        optimizer.step()
        
        # 累加 loss.item()(将标量张量转为 Python float)
        total_loss += loss.item()
        
        # 每 200 个 batch 打印一次进度
        if batch_idx % 200 == 0:
            # 已处理的样本数 = batch_idx * batch_size
            processed = batch_idx * len(data)
            total = len(train_loader.dataset)
            percent = 100. * batch_idx / len(train_loader)
            print(f'Train Epoch: {epoch} [{processed}/{total} ({percent:.0f}%)]\tLoss: {loss.item():.6f}')
    
    # 计算当前 epoch 的平均损失
    avg_loss = total_loss / len(train_loader)
    print(f'====> Epoch {epoch} Average loss: {avg_loss:.4f}')

每一步的详细解释

  • model.train():使 Dropout 层生效,BatchNorm 层使用当前 batch 的统计量。必须调用,否则 Dropout 在训练时不会随机丢弃神经元。

  • optimizer.zero_grad():PyTorch 默认梯度累积,每次反向传播前需要将旧梯度清零。不清零会导致梯度叠加,参数更新异常。

  • loss.backward():利用自动求导机制,计算损失函数对所有 requires_grad=True 的张量(即模型参数)的梯度。梯度会存储在 .grad 属性中。

  • optimizer.step():根据梯度更新参数。Adam 内部使用 lr 和梯度的一阶、二阶矩估计计算出新的参数值。

  • loss.item():从包含单个数值的张量中提取 Python 浮点数。注意:如果后面还需要计算图(如继续反向传播),不要用 .item() 切断图,但这里仅用于打印,安全。

3.7 测试函数(评估模型性能)

def test():
    """在测试集上计算损失和准确率"""
    model.eval()               # 设置为评估模式(关闭 Dropout)
    test_loss = 0
    correct = 0
    
    # 禁用梯度计算,节省显存和计算时间
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            
            # 累加测试损失(同样是 CrossEntropyLoss)
            test_loss += criterion(output, target).item()
            
            # 获取预测类别:沿 dim=1(类别维)取最大值索引
            pred = output.argmax(dim=1, keepdim=True)
            # 比较预测与真实标签,将正确预测的数量累加
            # target.view_as(pred) 将 target 形状变为与 pred 相同
            correct += pred.eq(target.view_as(pred)).sum().item()
    
    # 平均损失
    test_loss /= len(test_loader)
    # 准确率 = 正确预测数 / 总样本数
    accuracy = 100. * correct / len(test_loader.dataset)
    
    print(f'Test set: Average loss: {test_loss:.4f}, Accuracy: {correct}/{len(test_loader.dataset)} ({accuracy:.2f}%)\n')
    return accuracy

关键点详解

  • model.eval():关闭 Dropout,使所有神经元都参与计算(不再随机丢弃)。同时如果网络中有 BatchNorm,它会使用训练阶段统计的全局均值和方差。

  • with torch.no_grad()::上下文管理器,其内部的代码不会被记录到计算图中,从而不保存中间变量的梯度。这能大幅降低显存占用,并加快前向传播速度。

  • output.argmax(dim=1, keepdim=True)

    • argmax 返回最大值索引。dim=1 表示在类别维度(10个输出)上找最大值。

    • keepdim=True 保持维度数量,例如从 (64,10)(64,1),方便后续比较。

  • pred.eq(target.view_as(pred))

    • target 原始形状 (64,)view_as(pred) 将其变为 (64,1),形状匹配后逐元素比较。

    • 返回布尔张量,例如 [[True], [False], ...]

    • .sum().item() 统计 True 的个数(True 计为 1,False 为 0)。

3.8 训练循环并保存最佳模型

epochs = 10            # 迭代训练集 10 次
best_acc = 0           # 记录到目前为止的最高准确率
start_time = time.time()

for epoch in range(1, epochs + 1):
    train(epoch)       # 完成一个 epoch 的训练
    acc = test()       # 在测试集上评估
    
    # 如果当前模型准确率更高,则保存
    if acc > best_acc:
        best_acc = acc
        # torch.save 可以保存任何 Python 对象,这里保存模型的 state_dict(参数字典)
        torch.save(model.state_dict(), 'best_mnist_mlp.pth')
        print(f"保存新最佳模型,准确率: {acc:.2f}%")

end_time = time.time()
print(f"训练完成,总耗时: {end_time - start_time:.2f} 秒,最佳准确率: {best_acc:.2f}%")

详解

  • model.state_dict():返回一个字典,将每个层参数名(如 'fc1.weight')映射到其张量值。这是推荐的保存方式,只包含参数,不包含代码结构,跨平台兼容性好。

  • 保存的文件 'best_mnist_mlp.pth' 可以随后用 model.load_state_dict(torch.load(...)) 恢复。

  • epochs=10:对于 MNIST,10 轮通常已经足够收敛。更多轮数可能过拟合,但 MLP 不易过拟合严重,10~20 皆可。

3.9 加载模型并对单张图片预测 + 保存结果图

# 加载之前保存的最佳模型参数
# weights_only=False 是为了避免安全性警告(因为是你自己训练的模型)
model.load_state_dict(torch.load('best_mnist_mlp.pth', map_location=device, weights_only=False))
model.eval()          # 切换到评估模式

# 取测试集中的第一个 batch
test_iter = iter(test_loader)   # 将 DataLoader 转换为迭代器
images, labels = next(test_iter) # 获取第一个 batch:images 形状 (64,1,28,28)

# 取出 batch 中的第一张图片,并增加 batch 维度
img = images[0].unsqueeze(0).to(device)   # unsqueeze(0) 在第0维增加一维,形状变 (1,1,28,28)
true_label = labels[0].item()             # 真实标签

# 预测
with torch.no_grad():
    output = model(img)          # 输出 logits,形状 (1,10)
    pred = output.argmax(dim=1).item()  # 取最大值索引,得到预测数字

print(f"单张图片预测结果: 预测值 = {pred}, 真实标签 = {true_label}")

# 使用 matplotlib 保存图片(不弹出窗口)
fig, ax = plt.subplots()                  # 创建图形和坐标轴
ax.imshow(images[0].squeeze(), cmap='gray')  # .squeeze() 移除尺寸为1的维度 (1,28,28) -> (28,28)
ax.set_title(f'Predicted: {pred}, True: {true_label}')
fig.savefig('mnist_prediction.png', dpi=150, bbox_inches='tight')  # 保存为 PNG
print(f"预测图片已保存至当前目录下的 'mnist_prediction.png'")
plt.close(fig)   # 关闭图形,释放内存

详细介绍

  • torch.load 加载时指定 map_location=device,确保将张量加载到正确的设备(即使模型是在 GPU 上保存的,加载到 CPU 也需映射)。

  • weights_only=False:PyTorch 2.x 开始默认安全模式为 True,但加载自己训练的文件不会有风险,显式设为 False 可避免警告。

  • unsqueeze(0):因为模型期望输入是 4 维 (batch, channels, height, width),单张图片只有 3 维,增加 batch 维度。

  • plt.subplots():返回 (fig, ax),其中 fig 是整个画布,ax 是坐标系对象。

  • imshow:显示灰度图,参数 cmap='gray' 使用灰度颜色映射。

  • savefigdpi=150 控制分辨率,bbox_inches='tight' 裁剪掉多余的空白区域。

  • plt.close(fig):防止在脚本或交互式环境中累积大量图片对象导致内存泄漏。

3.10 完整代码

以下是将上述所有部分整合后的完整代码,可直接保存为 .py 文件运行。

"""
PyTorch 入门实验:MNIST 手写数字分类(全连接网络)
环境:Ubuntu + PyTorch 2.5.1 + Python 3.11 + CUDA 12.x
作者:TalentQ
日期:2026-05-21
说明:本代码包含环境检查、数据加载、模型定义、训练、测试、模型保存、单张预测及结果保存。
      每行代码都有详细注释,适合零基础学习者。
"""

# ========================== 1. 导入必要的库 ==========================
import torch                     # PyTorch 主库:张量、自动求导
import torch.nn as nn            # 神经网络层(Linear, Dropout等)
import torch.nn.functional as F  # 激活函数、损失函数等(不含参数)
import torch.optim as optim      # 优化器(Adam, SGD等)
from torchvision import datasets, transforms  # 数据集与图像预处理
from torch.utils.data import DataLoader       # 数据批量加载器
import time                      # 计算训练耗时
import matplotlib.pyplot as plt  # 保存预测图片

# ========================== 2. 检查 GPU 可用性 ==========================
# torch.device 对象,表示计算设备(CPU 或 CUDA GPU)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")
if device.type == "cuda":
    print(f"GPU 名称: {torch.cuda.get_device_name(0)}")

# ========================== 3. 数据预处理与加载 ==========================
# 定义图像变换流水线:转为张量 → 标准化
transform = transforms.Compose([
    transforms.ToTensor(),  # 将 PIL Image 或 numpy 数组转为 Tensor,并缩放到 [0, 1]
    transforms.Normalize((0.1307,), (0.3081,))  # 标准化:减均值除标准差,加速收敛
])

# 下载 MNIST 训练集(60000张)和测试集(10000张),并应用上述变换
train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
test_dataset  = datasets.MNIST('./data', train=False, download=True, transform=transform)

# 批次大小:每次迭代送入网络 64 张图片
batch_size = 64
# 训练加载器:打乱顺序,每个 epoch 重新洗牌
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
# 测试加载器:不需要打乱
test_loader  = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

# ========================== 4. 定义全连接神经网络模型 ==========================
class MLP(nn.Module):
    """三层全连接网络,使用 ReLU 激活和 Dropout 正则化"""
    def __init__(self):
        super(MLP, self).__init__()
        # fc1: 输入 784 (28*28) -> 输出 512
        self.fc1 = nn.Linear(28*28, 512)
        # fc2: 512 -> 256
        self.fc2 = nn.Linear(512, 256)
        # fc3: 256 -> 10 (0-9 十个类别)
        self.fc3 = nn.Linear(256, 10)
        # Dropout 层: 训练时随机丢弃 20% 的神经元,防止过拟合
        self.dropout = nn.Dropout(0.2)

    def forward(self, x):
        """
        前向传播
        x 形状: (batch_size, 1, 28, 28)
        """
        # 步骤1:展平。将每个图像从 (1,28,28) 变成 (784,)
        x = x.view(x.size(0), -1)   # x.size(0) = batch_size, -1 自动计算剩余维度
        
        # 步骤2:第一层 -> ReLU -> Dropout
        x = F.relu(self.fc1(x))
        x = self.dropout(x)
        
        # 步骤3:第二层 -> ReLU -> Dropout
        x = F.relu(self.fc2(x))
        x = self.dropout(x)
        
        # 步骤4:输出层,返回 logits(未经过 softmax)
        x = self.fc3(x)
        return x

# 实例化模型,并移动到指定设备(GPU 或 CPU)
model = MLP().to(device)
print(model)   # 打印模型结构,便于检查

# ========================== 5. 定义损失函数和优化器 ==========================
# 交叉熵损失:内部组合了 LogSoftmax + NLLLoss,接受原始 logits 作为输入
criterion = nn.CrossEntropyLoss()
# Adam 优化器:自适应学习率,传入模型参数和初始学习率
optimizer = optim.Adam(model.parameters(), lr=0.001)

# ========================== 6. 训练函数 ==========================
def train(epoch):
    """在一个完整 epoch 上训练模型"""
    model.train()          # 设置为训练模式(启用 Dropout)
    total_loss = 0         # 累计该 epoch 所有 batch 的损失
    
    # 遍历训练数据加载器
    for batch_idx, (data, target) in enumerate(train_loader):
        # 将数据移动到设备(GPU/CPU)
        data, target = data.to(device), target.to(device)
        
        # 清零梯度(因为梯度会累积)
        optimizer.zero_grad()
        
        # 前向传播:计算预测 logits
        output = model(data)          # output shape: (batch_size, 10)
        # 计算损失
        loss = criterion(output, target)   # 标量张量
        
        # 反向传播:计算梯度
        loss.backward()
        
        # 更新参数
        optimizer.step()
        
        # 累加损失值(.item() 将张量转为 Python 浮点数)
        total_loss += loss.item()
        
        # 每 200 个 batch 打印一次进度
        if batch_idx % 200 == 0:
            processed = batch_idx * len(data)   # 已处理样本数
            total = len(train_loader.dataset)   # 总样本数
            percent = 100. * batch_idx / len(train_loader)
            print(f'Train Epoch: {epoch} [{processed}/{total} ({percent:.0f}%)]\tLoss: {loss.item():.6f}')
    
    avg_loss = total_loss / len(train_loader)
    print(f'====> Epoch {epoch} Average loss: {avg_loss:.4f}')

# ========================== 7. 测试函数 ==========================
def test():
    """在测试集上评估模型,返回准确率"""
    model.eval()           # 设置为评估模式(关闭 Dropout)
    test_loss = 0
    correct = 0
    
    # 禁用梯度计算(节省内存和计算)
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            # 累加测试损失
            test_loss += criterion(output, target).item()
            # 获取预测类别:沿维度 1 取最大值索引
            pred = output.argmax(dim=1, keepdim=True)
            # 统计正确预测数
            correct += pred.eq(target.view_as(pred)).sum().item()
    
    # 平均损失
    test_loss /= len(test_loader)
    # 准确率 = 正确数 / 总样本数 * 100%
    accuracy = 100. * correct / len(test_loader.dataset)
    print(f'Test set: Average loss: {test_loss:.4f}, Accuracy: {correct}/{len(test_loader.dataset)} ({accuracy:.2f}%)\n')
    return accuracy

# ========================== 8. 训练循环并保存最佳模型 ==========================
epochs = 10                # 训练轮数
best_acc = 0               # 最佳准确率初始为 0
start_time = time.time()   # 记录开始时间

for epoch in range(1, epochs + 1):
    train(epoch)           # 训练一个 epoch
    acc = test()           # 测试当前模型
    
    # 如果当前准确率优于历史最佳,则保存模型参数
    if acc > best_acc:
        best_acc = acc
        # 保存 state_dict(参数字典)到文件
        torch.save(model.state_dict(), 'best_mnist_mlp.pth')
        print(f"保存新最佳模型,准确率: {acc:.2f}%")

end_time = time.time()
print(f"训练完成,总耗时: {end_time - start_time:.2f} 秒,最佳准确率: {best_acc:.2f}%")

# ========================== 9. 加载最佳模型并对单张图片预测 ==========================
# 从文件加载模型参数(映射到当前设备,并关闭安全检查)
model.load_state_dict(torch.load('best_mnist_mlp.pth', map_location=device, weights_only=False))
model.eval()   # 评估模式

# 取测试集中的第一个 batch
test_iter = iter(test_loader)
images, labels = next(test_iter)   # images: (64,1,28,28), labels: (64,)

# 取该 batch 中的第一张图片,并增加 batch 维度
img = images[0].unsqueeze(0).to(device)   # 形状变为 (1,1,28,28)
true_label = labels[0].item()             # 真实标签(0-9)

# 预测
with torch.no_grad():
    output = model(img)           # (1,10)
    pred = output.argmax(dim=1).item()  # 预测数字

print(f"单张图片预测结果: 预测值 = {pred}, 真实标签 = {true_label}")

# ========================== 10. 保存预测图片(不弹窗) ==========================
fig, ax = plt.subplots()                     # 创建画布和坐标轴
ax.imshow(images[0].squeeze(), cmap='gray')  # squeeze 去掉单通道维度 (28,28)
ax.set_title(f'Predicted: {pred}, True: {true_label}')
fig.savefig('mnist_prediction.png', dpi=150, bbox_inches='tight')  # 保存为 PNG
print("预测图片已保存至当前目录下的 'mnist_prediction.png'")
plt.close(fig)   # 关闭图形,释放资源

3.11 实验结果与讨论

  • 预期准确率:95% ~ 98%(MLP 在 MNIST 上的上限约为 98.5%)。

  • 训练时间:训练 10 个 epoch,GTX 950M 上约 30 秒,纯 CPU 约 2~3 分钟。

  • 为什么准确率不是 100%:全连接网络缺乏平移不变性,对图像局部结构不敏感。CNN 可轻松超过 99%。

下一步建议

  • 尝试调整网络结构(增加/减少层数、改变 Dropout 比例)。

  • 使用 TensorBoard 可视化损失曲线。

  • 进入 CNN 实验,在 CIFAR-10 数据集上实现卷积神经网络。

4. 参考资料

  1. PyTorch 官方教程:60分钟入门

  2. MNIST 数据集介绍

  3. Miniconda 官方文档


0

评论区