当前位置: 首页 > news >正文

卷积神经网络搭建实战(一)——torch云端的MNIST手写数字识别(全解一)

目录

引言

一、MNIST数据集:手写数字识别的"黄金基准"

1.1 数据集背景与特点

1.2 为什么选择MNIST?

1.3 应用场景

二、MNIST+CNN搭建全流程:从数据到模型

2.1 数据准备与预处理

2.2 模型构建(CNN设计)

2.3 损失函数与优化器选择

2.4 训练循环与模型评估

三、代码实现:从0到1搭建MNIST识别系统

3.1 完整代码(带逐行注释)

3.2 代码分段详细解析

代码块1:导入必要库

代码块2:下载并加载MNIST数据集

代码块3:创建数据加载器(DataLoader)

代码块4:选择计算设备(GPU优先)

代码块5:定义卷积神经网络(CNN)模型

代码块6:定义损失函数与优化器

代码块7:定义训练函数

代码块8:定义测试函数

代码块9:启动训练与测试主循环

四、训练结果与优化方向

4.1 典型训练结果

4.2 常见优化方向

五、总结


引言

在深度学习的入门学习中,"Hello World"级别的任务往往不是简单的打印语句,而是一个能直观验证模型能力的经典数据集训练任务。对于计算机视觉领域而言,这个"Hello World"就是​​MNIST手写数字识别​​。它不仅是学术界验证新模型的"试金石",也是工业界快速搭建视觉模型的"基准线"。本文将以PyTorch框架为核心,手把手带你从数据加载到模型训练,完成一个基于卷积神经网络(CNN)的MNIST手写数字识别系统,并深入解析每一步的技术细节。


一、MNIST数据集:手写数字识别的"黄金基准"

1.1 数据集背景与特点

MNIST数据集由加拿大高级研究所(CIFAR)于1998年发布,包含​​70,000张28×28像素的灰度手写数字图像​​(0-9共10个类别)。其中60,000张用于训练模型(train=True),10,000张用于测试模型泛化能力(train=False)。所有图像均经过严格居中处理,有效减少了预处理的工作量,使得研究者能更专注于模型本身的设计。

1.2 为什么选择MNIST?

  • ​标准化​​:全球研究者和开发者使用同一数据集,实验结果可直接对比;
  • ​小而精​​:70,000张图像的规模适中,适合快速验证模型思路;
  • ​低门槛​​:单张图像仅784个像素(28×28),计算资源需求低,即使是入门级GPU也能轻松处理;
  • ​典型性​​:手写数字识别是典型的多分类任务,覆盖了卷积神经网络的核心应用场景。

1.3 应用场景

MNIST不仅是教学工具,更是工业界的"基础能力验证器"。例如:

  • 验证新提出的卷积层结构(如深度可分离卷积)的有效性;
  • 测试不同优化器(如Adam、SGD)在基础任务上的表现;
  • 快速搭建模型原型,为复杂任务(如OCR文字识别)提供技术储备。

二、MNIST+CNN搭建全流程:从数据到模型

在PyTorch中搭建MNIST识别系统,通常遵循以下核心步骤:

2.1 数据准备与预处理

  • ​下载数据集​​:通过torchvision.datasets.MNIST接口自动下载并存储到本地;
  • ​数据转换​​:使用ToTensor将PIL图像转换为PyTorch张量(Tensor),并自动归一化到[0,1]区间;
  • ​数据加载​​:通过DataLoader封装数据集,设置batch_size实现批量加载,提升训练效率。

2.2 模型构建(CNN设计)

卷积神经网络(CNN)是处理图像任务的核心模型,其核心思想是通过​​局部感知​​和​​权值共享​​高效提取图像特征。典型CNN结构包含:

  • ​卷积层(Conv2d)​​:通过滑动窗口(卷积核)提取局部特征(如边缘、纹理);
  • ​激活函数(ReLU)​​:引入非线性变换,增强模型对复杂模式的表达能力;
  • ​池化层(MaxPool2d)​​:通过下采样(如2×2区域取最大值)降低特征图维度,减少计算量并增强平移不变性;
  • ​全连接层(Linear)​​:将高维特征映射到类别空间,输出分类概率。

2.3 损失函数与优化器选择

  • ​损失函数​​:多分类任务首选​​交叉熵损失(CrossEntropyLoss)​​,它直接衡量预测概率分布与真实标签的差异;
  • ​优化器​​:Adam优化器因自适应学习率的特性,常作为默认选择,平衡了收敛速度与稳定性。

2.4 训练循环与模型评估

  • ​训练阶段​​:通过前向传播计算预测值,反向传播计算梯度,优化器更新模型参数;
  • ​测试阶段​​:关闭梯度计算(torch.no_grad()),评估模型在测试集上的准确率和损失,验证泛化能力。

三、代码实现:从0到1搭建MNIST识别系统

3.1 完整代码(带逐行注释)

# ---------------------- 代码块1:导入必要库 ----------------------
# 导入PyTorch核心库,提供张量运算和自动微分功能
import torch
# 导入神经网络模块,包含卷积层、全连接层、激活函数等组件
from torch import nn
# 导入数据加载器,用于批量加载和打乱数据
from torch.utils.data import DataLoader
# 导入视觉数据集模块,包含MNIST等经典数据集的加载接口
from torchvision import datasets
# 导入张量转换工具,将PIL图像或numpy数组转为PyTorch张量
from torchvision.transforms import ToTensor# ---------------------- 代码块2:下载并加载MNIST数据集 ----------------------
# 训练数据集(60,000张图像+标签)
# root:数据集存储根目录(自动创建,若不存在)
# train=True:标记为训练集(对应文件training.pt)
# download=True:若本地无数据则自动从官网下载
# transform=ToTensor():数据转换函数(PIL图像→Tensor,像素值[0,255]→[0,1])
training_data = datasets.MNIST(root="data",train=True,download=True,transform=ToTensor()
)# 测试数据集(10,000张图像+标签)
# train=False:标记为测试集(对应文件test.pt)
test_data = datasets.MNIST(root="data",train=False,download=True,transform=ToTensor()  # 与训练集使用相同的转换规则
)# ---------------------- 代码块3:创建数据加载器(DataLoader) ----------------------
# 训练数据加载器:按批次加载训练数据
# dataset:指定要加载的数据集(训练集)
# batch_size=64:每批加载64张图像(平衡内存占用与训练效率)
# shuffle=True:训练时随机打乱数据顺序(避免模型学习到数据排列规律)
train_dataloader = DataLoader(dataset=training_data,batch_size=64,shuffle=True
)# 测试数据加载器:按批次加载测试数据
# shuffle=False:测试时保持数据顺序(便于后续分析错误样本)
test_dataloader = DataLoader(dataset=test_data,batch_size=64,shuffle=False
)# ---------------------- 代码块4:选择计算设备(GPU优先) ----------------------
# 自动检测可用设备:优先CUDA(NVIDIA GPU),其次MPS(Apple Silicon GPU),最后CPU
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"当前使用的计算设备:{device}")  # 输出设备信息(如"cuda"或"mps")# ---------------------- 代码块5:定义卷积神经网络(CNN)模型 ----------------------
class CNN(nn.Module):  # 继承PyTorch的神经网络基类nn.Module(所有模型需继承此类)def __init__(self):super(CNN, self).__init__()  # 调用父类初始化方法(必须保留,否则无法正确初始化模型)# ---------------------- 子代码块5.1:卷积块1(特征提取) ----------------------# nn.Sequential:顺序容器,按添加顺序执行各层(无需手动在forward中调用)self.conv1 = nn.Sequential(# 卷积层:输入1通道(灰度图),输出16通道(16个卷积核),5×5卷积核,步长1,填充2# in_channels:输入通道数(MNIST是灰度图,仅1个颜色通道)# out_channels:输出通道数(16个卷积核,生成16张特征图)# kernel_size:卷积核尺寸(5×5的滑动窗口)# stride:滑动步长(每次移动1像素,避免特征图尺寸缩小过快)# padding:边缘填充像素数(使输出尺寸与输入相同:(28+2×2-5)/1 +1=28)nn.Conv2d(in_channels=1, out_channels=16, kernel_size=5, stride=1, padding=2),# ReLU激活函数:引入非线性变换(公式:max(0, x)),增强模型表达能力nn.ReLU(),# 最大池化层:2×2窗口取最大值,步长2(输出尺寸减半:28×28→14×14)# kernel_size:池化窗口尺寸(2×2)nn.MaxPool2d(kernel_size=2))  # 输出形状:[batch_size, 16, 14, 14](batch_size为动态批次大小)# ---------------------- 子代码块5.2:卷积块2(特征深化) ----------------------self.conv2 = nn.Sequential(# 卷积层:输入16通道→输出32通道,5×5核,步长1,填充2(输出尺寸14×14)nn.Conv2d(in_channels=16, out_channels=32, kernel_size=5, stride=1, padding=2),nn.ReLU(),  # 激活函数# 卷积层:输入32通道→输出32通道,5×5核,步长1,填充2(输出尺寸14×14)nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2),nn.ReLU(),  # 激活函数# 最大池化层:2×2窗口取最大值,步长2(输出尺寸14×14→7×7)nn.MaxPool2d(kernel_size=2))  # 输出形状:[batch_size, 32, 7, 7]# ---------------------- 子代码块5.3:卷积块3(特征精炼) ----------------------self.conv3 = nn.Sequential(# 卷积层:输入32通道→输出64通道,5×5核,步长1,填充2(输出尺寸7×7)nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),nn.ReLU()  # 激活函数(无池化层,保持特征图尺寸))  # 输出形状:[batch_size, 64, 7, 7]# ---------------------- 子代码块5.4:全连接层(分类决策) ----------------------# 全连接层:输入维度64×7×7(展平后的特征向量),输出维度10(对应0-9分类)# in_features:输入特征数(64通道×7×7特征图=3136)# out_features:输出特征数(10个类别)self.out = nn.Linear(in_features=64 * 7 * 7, out_features=10)def forward(self, x):  # 前向传播函数(定义数据在模型中的流动路径)# 输入x形状:[batch_size, 1, 28, 28](批量图像,1通道,28×28像素)x = self.conv1(x)  # 经过卷积块1→形状:[batch_size, 16, 14, 14]x = self.conv2(x)  # 经过卷积块2→形状:[batch_size, 32, 7, 7]x = self.conv3(x)  # 经过卷积块3→形状:[batch_size, 64, 7, 7]# 展平操作:将4维张量[batch_size, 64, 7, 7]转为2维[batch_size, 64×7×7]# x.size(0)获取批量大小(batch_size),-1表示自动计算剩余维度(64×7×7)x = x.view(x.size(0), -1)# 全连接层输出:[batch_size, 10](未归一化的分类得分,即logits)output = self.out(x)return output  # 返回预测结果# 初始化模型实例,并将模型参数移动到目标设备(GPU/MPS/CPU)
model = CNN().to(device)
# 打印模型结构(可选,但有助于调试)
print("
模型结构:
", model)# ---------------------- 代码块6:定义损失函数与优化器 ----------------------
# 交叉熵损失函数(适用于多分类任务,直接比较预测概率与真实标签)
# CrossEntropyLoss结合了Softmax激活和负对数似然损失,无需额外添加Softmax层
loss_fn = nn.CrossEntropyLoss()# Adam优化器(自适应矩估计优化算法,平衡收敛速度与稳定性)
# params=model.parameters():指定需要优化的参数(模型的所有可学习参数,如卷积核权重、全连接层权重)
# lr=0.001:学习率(控制参数更新步长,0.001是Adam的常用初始值)
optimizer = torch.optim.Adam(params=model.parameters(), lr=0.001)# ---------------------- 代码块7:定义训练函数 ----------------------
def train(dataloader, model, loss_fn, optimizer):"""训练模型的核心函数(单轮训练逻辑)"""model.train()  # 开启训练模式(影响Dropout、BatchNorm等层的随机行为)batch_count = 0  # 记录当前处理的批次序号(用于打印进度)# 遍历数据加载器(逐批次获取训练数据)for X, y in dataloader:# 将图像数据X和标签y移动到目标设备(与模型所在设备一致)# .to(device)确保数据和模型在同一设备上计算(GPU/MPS/CPU)X, y = X.to(device), y.to(device)# ---------------------- 子代码块7.1:前向传播 ----------------------# 模型预测:输入X(批量图像)→输出预测值pred(未归一化的logits)# pred形状:[batch_size, 10](10个类别的得分)pred = model(X)# 计算损失:预测值pred与真实标签y的交叉熵损失# loss形状:[1](标量,表示当前批次的平均损失)loss = loss_fn(pred, y)# ---------------------- 子代码块7.2:反向传播与参数更新 ----------------------optimizer.zero_grad()  # 清空上一轮训练的梯度(避免梯度累积导致错误更新)loss.backward()        # 反向传播:计算各参数的梯度(存储在参数的.grad属性中)optimizer.step()       # 优化器更新:根据梯度调整参数(沿梯度下降方向)# ---------------------- 子代码块7.3:打印训练进度 ----------------------loss_value = loss.item()  # 将损失张量转换为Python数值(脱离计算图,避免内存泄漏)batch_count += 1# 打印当前批次损失(格式:批次序号: 损失值)print(f"批次 {batch_count}: 损失值 = {loss_value:.6f}")# ---------------------- 代码块8:定义测试函数 ----------------------
def test(dataloader, model, loss_fn):"""测试模型的核心函数(评估泛化能力)"""model.eval()  # 开启测试模式(关闭Dropout、BatchNorm的随机行为)total_loss = 0.0  # 累计测试集总损失(用于计算平均损失)correct = 0       # 累计正确预测的样本数(用于计算准确率)total_samples = len(dataloader.dataset)  # 测试集总样本数(10,000)num_batches = len(dataloader)            # 测试集总批次数(10,000/64≈157)# 关闭梯度计算(测试阶段无需更新参数,节省内存和时间)with torch.no_grad():# 遍历测试数据加载器(逐批次获取测试数据)for X, y in dataloader:# 将图像数据X和标签y移动到目标设备X, y = X.to(device), y.to(device)# 前向传播(仅计算预测值,不记录梯度)pred = model(X)# 累计测试损失(.item()避免张量运算,仅保留数值)total_loss += loss_fn(pred, y).item()# 计算正确预测数:# pred.argmax(1)获取每行预测值的最大值索引(即模型预测的类别)# (pred.argmax(1) == y)生成布尔张量(预测正确为True,错误为False)# .type(torch.float)转换为浮点型张量(True=1.0,False=0.0)# .sum()求和得到当前批次的正确数correct += (pred.argmax(1) == y).type(torch.float).sum().item()# 计算平均损失(总损失/总批次数)avg_loss = total_loss / num_batches# 计算准确率(正确数/总样本数,转换为百分比)accuracy = (correct / total_samples) * 100# 打印测试结果(格式:准确率和平均损失)print(f"
测试结果:准确率 = {accuracy:.2f}% | 平均损失 = {avg_loss:.6f}
")# ---------------------- 代码块9:启动训练与测试主循环 ----------------------
if __name__ == "__main__":epochs = 10  # 训练轮次(模型将遍历整个训练集10次)# 循环执行训练与测试for epoch in range(epochs):# 打印当前轮次信息print(f"
===== 第 {epoch+1}/{epochs} 轮训练开始 =====")# 执行一轮训练(传入训练数据加载器)train(train_dataloader, model, loss_fn, optimizer)# 每轮训练后执行一次测试(传入测试数据加载器)test(test_dataloader, model, loss_fn)# 所有轮次训练完成后,执行最终测试print("
===== 训练完成!最终测试 =====")test(test_dataloader, model, loss_fn)

3.2 代码分段详细解析

代码块1:导入必要库
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor
  • ​作用​​:导入PyTorch生态中需要的核心模块,为后续数据加载、模型构建、训练测试提供工具支持。
  • ​解析​​:
    • torch:PyTorch核心库,包含张量(Tensor)运算和自动微分功能,是深度学习计算的基础;
    • nn:神经网络模块,提供卷积层(Conv2d)、全连接层(Linear)、激活函数(ReLU)等组件,是构建模型的核心工具;
    • DataLoader:数据加载工具,支持批量加载数据、随机打乱(shuffle)、多线程加载(num_workers),提升训练效率;
    • datasets:包含MNIST、CIFAR-10等经典数据集的加载接口,MNIST类专门用于加载手写数字数据集;
    • ToTensor:数据转换工具,将PIL图像或numpy数组转换为PyTorch张量(Tensor),并自动将像素值从[0,255]归一化到[0,1]。
代码块2:下载并加载MNIST数据集
training_data = datasets.MNIST(root="data", train=True, download=True, transform=ToTensor())
test_data = datasets.MNIST(root="data", train=False, download=True, transform=ToTensor())
  • ​作用​​:下载MNIST数据集并加载为PyTorch可识别的数据集对象,分别用于训练和测试。
  • ​解析​​:
    • root="data":指定数据集存储路径。若本地data目录不存在,PyTorch会自动创建;若已存在数据集(如之前下载过),则直接读取;
    • train=True:加载训练集(60,000张图像+标签),train=False加载测试集(10,000张图像+标签);
    • download=True:若本地无数据集(即data目录下没有training.pttest.pt文件),则自动从PyTorch官网下载;
    • transform=ToTensor():对原始图像进行转换。原始MNIST图像是PIL格式的灰度图(形状(28, 28),像素值[0, 255]),转换后变为张量(形状(1, 28, 28),通道在前,像素值[0, 1])。
代码块3:创建数据加载器(DataLoader)
train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=False)
  • ​作用​​:将数据集包装为可迭代的加载器,实现批量加载数据、随机打乱(训练集),提升训练效率。
  • ​解析​​:
    • dataset:指定要加载的数据集对象(training_datatest_data);
    • batch_size=64:每批加载64张图像。batch_size是超参数,太小会导致计算开销增加(梯度波动大),太大可能导致内存溢出(GPU内存不足);
    • shuffle=True(训练集):训练时随机打乱数据顺序。若不打乱,模型可能学习到数据的顺序规律(如前100张都是0),导致泛化能力下降;
    • shuffle=False(测试集):测试时保持数据顺序。测试集的目的是评估模型对未知数据的泛化能力,保持顺序便于定位错误样本(如第500张图像总是预测错误)。
代码块4:选择计算设备(GPU优先)
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"当前使用的计算设备:{device}")
  • ​作用​​:自动检测可用的计算设备(优先GPU,其次是Apple Silicon的MPS,最后是CPU),并将模型和张量移动到该设备,利用硬件加速提升训练速度。
  • ​解析​​:
    • torch.cuda.is_available():检测是否有可用的NVIDIA GPU(需安装CUDA驱动和cuDNN库)。若有,返回True,使用cuda设备;
    • torch.backends.mps.is_available():检测是否有Apple Silicon芯片(如M1、M2)的MPS(Metal Performance Shaders)加速支持。若有,返回True,使用mps设备;
    • 若以上两种GPU均不可用,使用cpu设备(CPU计算速度较慢,但适合小规模实验);
    • .to(device):后续模型(model.to(device))和张量(X.to(device)y.to(device))都会移动到该设备,确保计算在同一设备上进行(避免CPU与GPU之间的数据传输开销)。
代码块5:定义卷积神经网络(CNN)模型
class CNN(nn.Module):def __init__(self):super(CNN, self).__init__()self.conv1 = nn.Sequential(...)self.conv2 = nn.Sequential(...)self.conv3 = nn.Sequential(...)self.out = nn.Linear(64 * 7 * 7, 10)def forward(self, x):x = self.conv1(x)x = self.conv2(x)x = self.conv3(x)x = x.view(x.size(0), -1)output = self.out(x)return output
  • ​作用​​:定义CNN模型的结构(层连接方式)和前向传播逻辑(数据流动路径)。
  • ​解析​​:
    • nn.Module:PyTorch的神经网络基类,所有自定义模型必须继承此类。它提供了参数管理(如parameters()方法)、设备移动(to()方法)等功能;
    • super(CNN, self).__init__():调用父类(nn.Module)的初始化方法。若省略此句,模型无法正确初始化参数,导致训练时报错;
    • ​卷积块1(conv1)​​:
      • nn.Conv2d(in_channels=1, out_channels=16, kernel_size=5, stride=1, padding=2):输入1通道(灰度图),使用16个5×5的卷积核,步长1,边缘填充2像素。输出特征图尺寸为(28 + 2×2 - 5) / 1 + 1 = 28(与输入尺寸相同);
      • nn.ReLU():对卷积输出应用ReLU激活函数,公式为max(0, x),引入非线性变换(否则多层卷积等价于单层线性变换);
      • nn.MaxPool2d(kernel_size=2):对ReLU输出进行2×2最大池化,步长2(默认与kernel_size相同)。输出特征图尺寸为28 / 2 = 14(尺寸减半);
      • 最终输出形状:[batch_size, 16, 14, 14]batch_size为批量大小,动态变化)。
    • ​卷积块2(conv2)​​:
      • nn.Conv2d(16, 32, 5, 1, 2):输入16通道(conv1的输出通道数),使用32个5×5的卷积核,步长1,边缘填充2像素。输出特征图尺寸保持14×14;
      • nn.ReLU():激活函数;
      • nn.Conv2d(32, 32, 5, 1, 2):输入32通道,使用32个5×5的卷积核,步长1,边缘填充2像素。输出特征图尺寸仍为14×14;
      • nn.ReLU():激活函数;
      • nn.MaxPool2d(2):2×2最大池化,输出特征图尺寸减半为7×7;
      • 最终输出形状:[batch_size, 32, 7, 7]
    • ​卷积块3(conv3)​​:
      • nn.Conv2d(32, 64, 5, 1, 2):输入32通道(conv2的输出通道数),使用64个5×5的卷积核,步长1,边缘填充2像素。输出特征图尺寸保持7×7;
      • nn.ReLU():激活函数;
      • 无池化层,输出形状:[batch_size, 64, 7, 7]
    • ​全连接层(out)​​:
      • nn.Linear(64 * 7 * 7, 10):将展平后的高维特征(64×7×7=3136维)映射到10维输出(对应0-9的分类)。Linear层内部包含权重矩阵(64×7×7 × 10)和偏置向量(10维);
    • forward方法​​:
      • 定义数据在模型中的流动路径:输入图像→卷积块1→卷积块2→卷积块3→展平→全连接层→输出预测值;
      • x.view(x.size(0), -1):将4维张量[batch_size, 64, 7, 7]展平为2维张量[batch_size, 64×7×7],以便输入全连接层(全连接层仅接受1维或2维输入)。
代码块6:定义损失函数与优化器
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
  • ​作用​​:定义损失函数(衡量预测误差)和优化器(更新模型参数)。
  • ​解析​​:
    • nn.CrossEntropyLoss():多分类任务的交叉熵损失函数。它结合了Softmax激活函数和负对数似然损失(NLLLoss),因此模型输出无需额外添加Softmax层。输入为模型预测的logits(未归一化的概率)和真实标签(类别索引),输出为当前批次的平均损失;
    • torch.optim.Adam():Adam优化器,是一种自适应学习率的优化算法,结合了动量(Momentum)和RMSProp的优点,通常比传统SGD(随机梯度下降)收敛更快、更稳定;
    • params=model.parameters():指定需要优化的参数,即模型的所有可学习参数(如卷积核的权重、全连接层的权重和偏置);
    • lr=0.001:学习率(Learning Rate),控制参数更新的步长。学习率过大可能导致模型在最优解附近震荡,过小则收敛缓慢。
代码块7:定义训练函数
def train(dataloader, model, loss_fn, optimizer):model.train()batch_count = 0for X, y in dataloader:X, y = X.to(device), y.to(device)pred = model(X)loss = loss_fn(pred, y)optimizer.zero_grad()loss.backward()optimizer.step()loss_value = loss.item()batch_count += 1print(f"批次 {batch_count}: 损失值 = {loss_value:.6f}")
  • ​作用​​:实现单轮训练的核心逻辑(前向传播→损失计算→反向传播→参数更新)。
  • ​解析​​:
    • model.train():显式声明模型进入训练模式。某些层(如DropoutBatchNorm)的行为依赖于训练状态:Dropout层在训练时会随机失活部分神经元(防止过拟合),在测试时关闭失活;BatchNorm层在训练时会计算当前批次的均值和方差,在测试时使用训练阶段统计的全局均值和方差;
    • X, y = X.to(device), y.to(device):将图像数据X和标签y移动到模型所在的设备(GPU/MPS/CPU),确保计算在同一设备上进行(避免CPU与GPU之间的数据传输开销);
    • pred = model(X):前向传播,输入批量图像X,输出模型预测的logitspred(形状[batch_size, 10]);
    • loss = loss_fn(pred, y):计算预测值pred与真实标签y的损失。y是类别索引(如3表示数字3),CrossEntropyLoss会自动将y转换为One-Hot编码,与pred的Softmax结果计算交叉熵;
    • optimizer.zero_grad():清空优化器的梯度缓存。PyTorch会累积梯度(多次反向传播后梯度相加),因此每轮训练前需清空上一轮的梯度,避免错误更新;
    • loss.backward():反向传播,计算各参数的梯度(存储在参数的.grad属性中)。梯度表示损失函数对参数的敏感程度,梯度越大,参数对损失的影响越大;
    • optimizer.step():优化器根据梯度更新参数。Adam优化器会根据历史梯度的均值和方差自适应调整学习率,使参数更新更稳定;
    • loss_value = loss.item():将损失张量转换为Python数值(脱离计算图)。loss是一个张量(包含梯度信息),item()方法提取其数值,用于打印或记录日志;
    • 打印批次损失:实时监控训练进度。正常情况下,损失值应随训练轮次增加逐渐下降(从初始的2.3左右降至0.03以下)。
代码块8:定义测试函数
def test(dataloader, model, loss_fn):model.eval()total_loss = 0.0correct = 0total_samples = len(dataloader.dataset)num_batches = len(dataloader)with torch.no_grad():for X, y in dataloader:X, y = X.to(device), y.to(device)pred = model(X)total_loss += loss_fn(pred, y).item()correct += (pred.argmax(1) == y).type(torch.float).sum().item()avg_loss = total_loss / num_batchesaccuracy = (correct / total_samples) * 100print(f"
测试结果:准确率 = {accuracy:.2f}% | 平均损失 = {avg_loss:.6f}
")
  • ​作用​​:评估模型在测试集上的泛化能力(准确率和平均损失)。
  • ​解析​​:
    • model.eval():显式声明模型进入测试模式。此时,Dropout层会关闭失活(所有神经元参与计算),BatchNorm层会使用训练阶段统计的全局均值和方差,确保测试结果稳定;
    • torch.no_grad():上下文管理器,关闭梯度计算。测试阶段无需更新参数,因此关闭梯度计算可以节省内存(避免存储中间变量的梯度)和时间;
    • total_loss:累计测试集的总损失(用于计算平均损失);
    • correct:累计正确预测的样本数(用于计算准确率);
    • total_samples = len(dataloader.dataset):测试集总样本数(10,000);
    • num_batches = len(dataloader):测试集总批次数(10,000/64≈157);
    • with torch.no_grad()::在上下文管理器内执行测试逻辑,关闭梯度计算;
    • pred.argmax(1):获取每行预测值(pred)的最大值索引(即模型预测的类别)。例如,pred形状为[64, 10]argmax(1)返回长度为64的张量,每个元素是0-9的整数;
    • (pred.argmax(1) == y):生成布尔张量(预测正确为True,错误为False);
    • .type(torch.float):将布尔值转换为浮点型张量(True=1.0False=0.0);
    • .sum().item():求和得到当前批次的正确预测数,并转换为Python数值;
    • avg_loss = total_loss / num_batches:计算测试集的平均损失(总损失除以批次数);
    • accuracy = (correct / total_samples) * 100:计算准确率(正确数除以总样本数,转换为百分比)。
代码块9:启动训练与测试主循环
if __name__ == "__main__":epochs = 10for epoch in range(epochs):print(f"
===== 第 {epoch+1}/{epochs} 轮训练开始 =====")train(train_dataloader, model, loss_fn, optimizer)test(test_dataloader, model, loss_fn)print("
===== 训练完成!最终测试 =====")test(test_dataloader, model, loss_fn)
  • ​作用​​:控制训练轮次(epochs),并依次执行训练和测试。
  • ​解析​​:
    • epochs=10:训练轮次,即模型将遍历整个训练集10次。每轮训练会更新模型参数,测试阶段评估当前参数下的模型性能;
    • 主循环:for epoch in range(epochs)遍历每一轮训练;
    • print(f" ===== 第 {epoch+1}/{epochs} 轮训练开始 ====="):打印当前轮次信息,提示用户训练进度;
    • train(...):执行一轮训练,调用之前定义的train函数;
    • test(...):每轮训练后执行一次测试,调用之前定义的test函数,评估模型在测试集上的性能;
    • 最终测试:所有轮次训练完成后,执行一次最终测试,验证模型在未见过数据上的最终表现。

四、训练结果与优化方向

4.1 典型训练结果

在云端环境(如Google Colab,使用Tesla T4 GPU)运行上述代码,10轮训练后通常能达到​​99%以上的测试准确率​​。训练过程中,损失值会随着轮次增加逐渐下降(从初始的2.3左右降至0.03以下),准确率稳步上升(从随机猜测的10%升至99%+)。

4.2 常见优化方向

  • ​调整网络深度​​:增加卷积层或全连接层(需注意过拟合,可通过Dropout层缓解);
  • ​调整超参数​​:尝试不同的batch_size(如32、128)、lr(如0.0001、0.01);
  • ​数据增强​​:对训练图像进行旋转、平移、缩放等变换(torchvision.transforms中的RandomRotation等),提升模型泛化能力;
  • ​正则化​​:添加nn.Dropout层(如在全连接层前加nn.Dropout(0.5)),随机失活部分神经元,减少过拟合;
  • ​学习率调度​​:使用torch.optim.lr_scheduler动态调整学习率(如StepLR每10轮降低学习率)。

五、总结

通过本文的实战,我们完整实现了基于PyTorch的MNIST手写数字识别系统,涵盖了从数据加载到模型训练的全流程。关键收获包括:

  1. ​数据预处理的重要性​​:ToTensor转换和DataLoader批量加载是高效训练的基础;
  2. ​CNN的核心结构​​:卷积层提取特征,池化层降维,全连接层分类;
  3. ​训练循环的关键步骤​​:前向传播、损失计算、反向传播、参数更新的协同工作;
  4. ​设备选择的影响​​:GPU/MPS加速能显著缩短训练时间(相比CPU可能快10-100倍)。

MNIST是深度学习入门的"起点",但绝不是终点。掌握本文的方法后,你可以尝试挑战更复杂的任务(如Fashion-MNIST多分类、CIFAR-10彩色图像识别),或探索更先进的模型(如ResNet残差网络、Transformer视觉模型)。记住,深度学习的核心是"实践-观察-改进"的循环,动手编写代码并分析结果是提升能力的最快途径!

​附:完整代码可直接复制到Google Colab(选择GPU运行时)或本地PyTorch环境运行,建议尝试修改网络结构(如增加卷积层)并观察准确率变化,加深对CNN设计的理解。​


文章转载自:

http://IXI2S5D7.tLqsL.cn
http://t92PJ2M1.tLqsL.cn
http://QxAPORlq.tLqsL.cn
http://8GXmKLfQ.tLqsL.cn
http://vNPjn75c.tLqsL.cn
http://eDl7wRfF.tLqsL.cn
http://CmdEeRYB.tLqsL.cn
http://q6bx16ee.tLqsL.cn
http://VRJWdgmM.tLqsL.cn
http://c0QSNC2G.tLqsL.cn
http://qfm1J5uK.tLqsL.cn
http://OSZBr5fS.tLqsL.cn
http://clKh4Eyh.tLqsL.cn
http://hMGrFHev.tLqsL.cn
http://C8yic3PJ.tLqsL.cn
http://Gs67LfDU.tLqsL.cn
http://F656xYDo.tLqsL.cn
http://5oA9TD0h.tLqsL.cn
http://CDf9kbAr.tLqsL.cn
http://jEp7r0hC.tLqsL.cn
http://YbKIX02e.tLqsL.cn
http://WOeS8jEL.tLqsL.cn
http://Aq00p9zs.tLqsL.cn
http://I87sfAVz.tLqsL.cn
http://hoAXuZYW.tLqsL.cn
http://fXDauDJA.tLqsL.cn
http://GX12fkvt.tLqsL.cn
http://UlYkmoMe.tLqsL.cn
http://Alq2ywf5.tLqsL.cn
http://cvVwhK2b.tLqsL.cn
http://www.dtcms.com/a/386839.html

相关文章:

  • 实验四 Cache 3种不同的地址映射机制(仿真)
  • 北航计算机保研机试题+解答
  • Python Flask快速入门
  • AirPodsDesktop,一个AirPods 桌面助手
  • Java 调用 C++ 动态库(DLL)完整实践:有图像有实体处理场景
  • 教育行业智慧文档平台:构建安全合规、高效协同的教学研究与资源共享解决方案
  • 网编day7(网络词典)(部分)
  • CodeBuddy AI 深度体验:模型怎么选不踩坑?
  • MQ高级.
  • 46.Mysql基础及案例
  • 贪心算法应用:文件合并问题详解
  • 什么是“孤块”?
  • 神卓N600 公网盒子公网访问群晖NAS绿联飞牛
  • 浅谈背包DP(C++实现,配合lc经典习题讲解)
  • 虚拟化嵌套支持在云服务器容器化Hyper-V环境的配置标准
  • 修改el-checkbox默认颜色
  • ROS接口信息整理
  • 【C++11】lambda匿名函数、包装器、新的类功能
  • 【Linux系统】深入理解线程,互斥及其原理
  • 1. C++ 中的 C
  • 探讨基于国产化架构的非结构化数据管理平台建设路径与实践
  • C++11移动语义
  • 代码随想录第14天| 翻转、对称与深度
  • 算法改进篇 | 改进 YOLOv12 的水面垃圾检测方法
  • 一个我自己研发的支持k-th路径查询的数据结构-owl tree
  • 首款“MODA”游戏《秘境战盟》将在Steam 新品节中开放公开试玩
  • ε-δ语言(Epsilon–Delta 语言)
  • QCA9882 Module with IPQ4019 Mainboard High-Performance Mesh Solution
  • xv6实验:Ubuntu2004 WSL2实验环境配置(包括git clone网络问题解决方法)
  • ICE-Interactive Connectivity Establishment-交互式连接建立