Pytorch实现mnist手写数字识别 - ** 本文为[365天深度学习训练营](https://mp.weixin.qq.com/s/o-DaK6aQQLkJ8uE4YX1p3Q) 中的学习记录博客**- ** 原作者[K同学啊](https://mtyjkh.blog.csdn.net/)**前言这次是CNN实操流程为数据导入、数据可视化、构建CNN网络、设置超参数、编写训练函数、编写测试函数、正式训练、结果可视化。实验MNIST手写数字数据集来源于是美国国家标准与技术研究所是著名的公开数据集之一。数据集中的数字图片是由250个不同职业的人纯手写绘制数据集获取的网址为Index of /exdb/mnist下载后需解压。我们一般会采用(train_images, train_labels), (test_images, test_labels) datasets.mnist.load_data()这行代码直接调用这样就比较简单。代码实现1.设置GPUimport torch import torch.nn as nn import matplotlib.pyplot as plt import torchvision device torch.device(cuda if torch.cuda.is_available() else cpu) device2.导入数据train_ds torchvision.datasets.MNIST(data, # 数据存放目录当前目录下的 data 文件夹 trainTrue, # 加载训练集6 万张图 transformtorchvision.transforms.ToTensor(), # 把 PIL 图片转成 Tensor数值归一化到 [0,1] downloadTrue) # 本地没有就自动从网上下载 test_ds torchvision.datasets.MNIST(data, trainFalse, transformtorchvision.transforms.ToTensor(), downloadTrue)两条语句的区别只有一处不同——train参数train_ds训练集6 万张用于模型学习参数梯度下降更新权重test_ds测试集1 万张从未参与训练用于评估模型泛化能力batch_size 32 # 每批取 32 个样本 train_dl torch.utils.data.DataLoader(train_ds, # 数据源上一段的训练集 Dataset batch_sizebatch_size, # 每次取 32 张图打包成一个 batch shuffleTrue) # 每个 epoch 前打乱顺序训练必须打乱 test_dl torch.utils.data.DataLoader(test_ds, batch_sizebatch_size) # 测试集不打乱保证评估结果可复现 # 取一个批次查看数据格式 # 数据的shape为[batch_size, channel, height, weight] # 其中batch_size为自己设定channelheight和weight分别是图片的通道数高度和宽度。 imgs, labels next(iter(train_dl)) imgs.shape这段代码在上一段的基础上把 Dataset 打包成可分批、可打乱的 DataLoader数据加载器​是 PyTorch 训练流程的第二步。iter(train_dl) — 把 DataLoader 变成迭代器想象成一摞排好队的 batchnext(...) — 从迭代器里取出第一个 batch解包成 imgs图像张量和 labels对应的数字标签imgs.shape 查看这个 batch 的形状torch.Size([32, 1, 28, 28])四个维度分别是第 1 维32-batch_size —— 本批 32 张图第 2 维1-通道数灰度图只有 1 个通道彩色 RGB 是 3第 3 维28——高 28 像素第 4 维28——宽 28 像素。对应的 labels 形状是 [32]即 32 个 0~9 的整数。batch批​神经网络不是一张一张学而是一批一批算——既利用 GPU 并行又让梯度估计更稳定shuffleTrue每轮打乱防止模型记住样本顺序比如前 3 万张恰好都是同一类测试不打乱评估时顺序无所谓不打乱便于结果复现和对比一句话总结DataLoader 负责把 6 万张散装图片整理成“每批 32 张的传送带”next(iter(...)) 是从传送带上取第一箱货验一下货的规格 [32, 1, 28, 28] 是否符合预期。import numpy as np # 指定图片大小图像大小为20宽、5高的绘图(单位为英寸inch) plt.figure(figsize(20, 5)) for i, imgs in enumerate(imgs[:20]): # img 是第 i 张图单张shape [1,28,28] npimg np.squeeze(imgs.numpy()) # 去掉通道维 → [28,28] # 将整个figure分成2行10列绘制第i1个子图。 plt.subplot(2, 10, i1) plt.imshow(npimg, cmapplt.cm.binary) plt.axis(off)这段代码是把刚取出的那个 batch 里的前 20 张手写数字图画成 2 行 × 10 列的网格用来“肉眼验收”数据是否加载正确。plt.figure(figsize(20, 5))新建一张“画布”宽 20 英寸、高 5 英寸。20:5 4:1 的横长比例正好容纳 2 行 × 10 列的小图。单位是英寸实际显示像素还取决于 dpi默认 100即约 2000×500 像素。np.squeeze(img.numpy())img.numpy() — 把 PyTorch 张量转成 numpy 数组matplotlib 不认识 torch 张量np.squeeze() — 去掉所有长度为 1 的维度[1, 28, 28] → [28, 28]不降维的话imshow 会报错或画错因为它只接受二维灰度或三维 H×W×C彩色数组plt.subplot(2, 10, i1)把画布切成 2 行 × 10 列 20 个格子并把画笔定位到第 i1 个格子从 1 开始计数从左到右、从上到下编号。下次画图就落在那个格子里不会互相覆盖。plt.imshow(npimg, cmapplt.cm.binary)imshow — 在当前子图上绘制图像cmapplt.cm.binary — 使用二值灰度映射0黑→ 255白。MNIST 里数字笔画是白、背景是黑这样显示出来就是“黑底白字”和你看到的数据集预览图一致想要“白底黑字”更接近手写体可改用 cmapgray_rplt.axis(off)关闭坐标轴刻度和边框。默认 imshow 会显示 0~27 的像素坐标轴关掉后小图更干净纯看数字。#类的定义框架 import torch.nn.functional as F num_classes 10 # 图片的类别数 class Model(nn.Module): def __init__(self): super().__init__() #所有 PyTorch 网络都要继承 nn.Module #super().__init__() 初始化父类注册参数、显存管理等机制才能生效 # 特征提取网络 self.conv1 nn.Conv2d(1, 32, kernel_size3) # 第一层卷积输入 1 个通道灰度图→ 输出 32 个通道32 个 3×3 卷积核每个核学会提取一种特征如边缘、拐角。输出通道数可以理解为“这一层学出了 32 种特征图”。 self.pool1 nn.MaxPool2d(2) # 设置池化层2×2 最大池化每 2×2 区域只保留最大值 → 图像宽高减半。作用是压缩尺寸、保留显著特征、增强平移鲁棒性。 self.conv2 nn.Conv2d(32, 64, kernel_size3) # 第二层卷积,卷积核大小为3*3 self.pool2 nn.MaxPool2d(2) # 分类网络 self.fc1 nn.Linear(1600, 64) # 全连接层1600 → 64 self.fc2 nn.Linear(64, num_classes) # 64 → 10输出 10 个类别的得分 # 前向传播 def forward(self, x): x self.pool1(F.relu(self.conv1(x))) # [32,1,28,28] # → conv1 → [32,32,26,26] 3×3 卷积28−3126 # → relu → 同上负值归零 # → pool1 → [32,32,13,13] 宽高减半 x self.pool2(F.relu(self.conv2(x))) # → conv2 → [32,64,11,11] # → pool2 → [32,64,5,5] x torch.flatten(x, start_dim1) # → [32, 1600] 64×5×51600摊平成一维 x F.relu(self.fc1(x)) # → [32, 64] x self.fc2(x) # → [32, 10] 每张图 10 个类别得分 return x这段代码定义了一个经典的 CNN卷积神经网络​用来做手写数字分类。结构是两层“卷积池化”做特征提取再接两层全连接做分类。__init__ 是“备料”——声明卷积核、池化窗、全连接矩阵这些可学习部件forward 是“加工流程”——图像经“卷积→激活→池化”两轮提炼从 784 个像素浓缩成 1600 个特征再映射到 10 个数字类别的得分。​ 这是 MNIST 上能轻松跑到 98% 准确率的经典 LeNet 变体结构。pip install torchinfo #首次使用需要安装from torchinfo import summary # 将模型转移到GPU中我们模型运行均在GPU中进行 model Model().to(device) summary(model)Layer (type:depth-idx) Param #Model --├─Conv2d: 1-1 320 # 3×3×1×32 32 偏置├─MaxPool2d: 1-2 -- # 池化无参数├─Conv2d: 1-3 18,496 # 3×3×32×64 64├─MaxPool2d: 1-4 --├─Linear: 1-5 64,064 # 1600×64 64├─Linear: 1-6 650 # 64×10 10Total params: 83,530Trainable params: 83,530Non-trainable params: 0summary(model) 不带输入信息时只能显示各层参数看不到中间张量形状。model.to(device) 是“把工厂搬进 GPU 车间”summary() 是“贴一张设备清单”——列清每一层的结构和参数量让你在训练前确认网络搭得对不对、模型有多大。loss_fn nn.CrossEntropyLoss() # 创建损失函数 learn_rate 1e-2 # 学习率 opt torch.optim.SGD(model.parameters(),lrlearn_rate) # 训练循环 def train(dataloader, model, loss_fn, optimizer): size len(dataloader.dataset) # 训练集的大小一共60000张图片 num_batches len(dataloader) # 批次数目187560000/32 train_loss, train_acc 0, 0 # 初始化训练损失和正确率 for X, y in dataloader: # 获取图片及其标签 X, y X.to(device), y.to(device) # 计算预测误差 pred model(X) # 网络输出 loss loss_fn(pred, y) # 计算网络输出和真实值之间的差距targets为真实值计算二者差值即为损失 # 反向传播 optimizer.zero_grad() # grad属性归零 loss.backward() # 反向传播 optimizer.step() # 每一步自动更新 # 记录acc与loss train_acc (pred.argmax(1) y).type(torch.float).sum().item() train_loss loss.item() train_acc / size train_loss / num_batches return train_acc, train_loss def test (dataloader, model, loss_fn): size len(dataloader.dataset) # 测试集的大小一共10000张图片 num_batches len(dataloader) # 批次数目31310000/32312.5向上取整 test_loss, test_acc 0, 0 # 当不进行训练时停止梯度更新节省计算内存消耗 with torch.no_grad(): for imgs, target in dataloader: imgs, target imgs.to(device), target.to(device) # 计算loss target_pred model(imgs) loss loss_fn(target_pred, target) test_loss loss.item() test_acc (target_pred.argmax(1) target).type(torch.float).sum().item() test_acc / size test_loss / num_batches return test_acc, test_loss epochs 5 train_loss [] train_acc [] test_loss [] test_acc [] for epoch in range(epochs): model.train() epoch_train_acc, epoch_train_loss train(train_dl, model, loss_fn, opt) model.eval() epoch_test_acc, epoch_test_loss test(test_dl, model, loss_fn) train_acc.append(epoch_train_acc) train_loss.append(epoch_train_loss) test_acc.append(epoch_test_acc) test_loss.append(epoch_test_loss) template (Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%Test_loss:{:.3f}) print(template.format(epoch1, epoch_train_acc*100, epoch_train_loss, epoch_test_acc*100, epoch_test_loss)) print(Done)