CNN卷积神经网络实战:MNIST手写识别从零到99%准确率

发布时间:2026/10/11 16:50:01
CNN卷积神经网络实战:MNIST手写识别从零到99%准确率 简介面向深度学习初学者、TensorFlow入门者以及需要完成图像识别课程设计的读者这份资源以经典MNIST手写数字识别为切入点用一个紧凑的CNN实现展示从数据输入、卷积层、池化层、全连接层到softmax分类的完整流程。zip压缩包内共2个Python脚本整体仅5KBMnist_cnn.py负责模型搭建、训练与评估在测试集上达到99.21%准确率Mnist_cnn_tensorboard.py在原有基础上加入TensorBoard可视化方便查看网络结构、loss曲线及参数变化便于理解和调试训练过程。资源文件精简适合直接阅读源码、对照复现也可作为后续做参数调优、网络结构修改或扩展其他图像数据集的起点。目前已有9768人学习对于希望从零动手完成一个图像识别项目或准备在基础CNN样例上继续深入研究TensorFlow的开发者是一份轻量且实用的参考脚本。1. CNN卷积神经网络实现MNIST手写数字识别一张28×28灰度图背后的技术含量很多人第一次接触深度学习就是从 MNIST 手写数字识别开始的。这个任务看起来简单到不像话——把一张 28×28 的灰度图分到 0 到 9 十个类别里人眼一瞄就能认出来但用 CNN 卷积神经网络把它跑通背后涉及数据加载、卷积核设计、损失函数选择、训练节奏把控一整套链路。你以为是在学一个玩具项目其实是在搭一套可以迁移到任何图像分类任务上的最小可运行框架。这篇笔记会把我做这个项目时踩过的坑、调过的参数、验证过的方法完整拆开目标是让你看完就能动手复现并且知道自己每一步在干什么。2. 搭 CNN 之前必须搞清的三个问题数据格式、网络选型、环境准备2.1 MNIST 数据集到底长什么样不只是「一堆图片」MNIST 的全称是 Modified National Institute of Standards and Technology 数据库由 60000 张训练图片和 10000 张测试图片组成。每张图是 28×28 像素的灰度图像素值范围 0 到 255标签是 0 到 9 的整数。这里有个关键点标签不是 one-hot 编码的向量就是一个整数。PyTorch 的交叉熵损失函数会内部帮你做 one-hot 转换所以你不需要手动把标签变成 10 维向量。数据本身虽然是图片但实际使用时有两种载体。一种是原始 IDX 文件格式需要自己写解析代码另一种是通过 PyTorch 的torchvision.datasets.MNIST直接下载并封装好的对象。我强烈建议用第二种不是因为偷懒而是torchvision已经帮你处理好了下载、解压、训练集测试集划分这些脏活。第一次跑的时候想弄明白数据长什么样可以下下来之后用matplotlib画几张看看确认标签和图片对得上这个习惯能帮你避免后面很多「模型准确率很高但实际是数据错位」的诡异问题。数据预处理有一个容易被忽视的步骤归一化Normalization。原始像素值 0 到 255直接喂给网络会让初始梯度计算变得不稳定收敛速度明显变慢。常见的做法是把像素值缩放到 [0, 1]除以 255更推荐的做法是标准化即减均值再除以标准差。MNIST 的全局均值和标准差是 0.1307 和 0.3081这两个值是别人算好的直接用就行。这一行代码对最终准确率的影响可能比你换一个网络结构还大。2.2 为什么是 CNN 而不是全连接网络参数爆炸与平移不变性很多初学者会问MNIST 图片这么小直接用全连接网络把 784 个像素拍平了输入不是也行吗确实行但效果和效率都差很多。假设第一层全连接有 256 个神经元那这一层就有 784×256 约 20 万个参数。而 CNN 用一个 3×3 的卷积核在整张图上滑动同一层只有 9 个权重不算 bias哪怕用 32 个卷积核也才 288 个参数。这就是「参数共享」带来的参数爆炸缓解。更重要的是「平移不变性」。手写数字的笔迹千奇百怪同一个数字在图片里的位置可能偏左可能偏右笔画可能粗可能细。全连接网络对每个像素位置都单独学习权重数字挪了个位置它可能就不认了。CNN 的卷积核是在整张图上共享的不管数字出现在哪个位置只要局部纹理特征比如横线、竖线、圆圈在卷积核就能把它提出来。这就是 CNN 对图像任务天然友好的根本原因。MNIST 这种小图任务网络深度不需要太深。经典的 LeNet-5 结构两层卷积加三层全连接就能跑到 99% 以上的准确率。我一般会在这个基础上稍作调整用两个卷积层加两个全连接层参数量控制在几十万级别CPU 上跑一个 epoch 也就几十秒非常适合用来理解每个组件的作用。如果你一上来就上 ResNet 这种几十层的结构反而会因为训练时间太长、调参复杂度太高而失去对核心机制的感知。2.3 环境怎么备一个 CPU 也能跑得动的项目这个项目对硬件的要求低到令人发指。MNIST 单张图 28×28一个 batch 128 张图也才 1M 左右的数据量CPU 训练一个 epoch 大约 30 到 60 秒整个训练流程 10 到 15 个 epoch 也就十几分钟。所以我一般建议入门阶段不用折腾 CUDA先把 CPU 流程跑通理解每个模块的作用之后再切到 GPU 只是改一行代码的事。环境配置方面核心依赖就三个PyTorch、TorchVision、Matplotlib。安装直接用 pip版本不用追求最新稳定版就行。Python 版本 3.8 以上基本都没问题。有一个小坑是 Windows 上 PyTorch 的安装命令和 Linux 不太一样CPU 版本要明确指定cpu的源不然默认会去下载 CUDA 版本体积大且装完跑不了。我见过好几个初学者卡在这一步报错信息是各种 DLL 找不到其实就是 CPU/GPU 版本装错了。3. 用 PyTorch 写出第一个 CNN从网络定义到训练循环3.1 网络结构怎么搭两个卷积块加两个全连接层我用的是 LeNet-5 的改良版结构非常清晰第一个卷积块提取低级特征边缘、线条第二个卷积块提取高级特征形状、局部组合最后接全连接层做分类。下面是完整定义import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes10): super(SimpleCNN, self).__init__() # 第一个卷积块1通道输入 - 32通道输出 self.conv1 nn.Sequential( nn.Conv2d(in_channels1, out_channels32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2) ) # 第二个卷积块32通道输入 - 64通道输出 self.conv2 nn.Sequential( nn.Conv2d(in_channels32, out_channels64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2) ) # 全连接层64*7*7 - 128 - 10 self.fc nn.Sequential( nn.Linear(in_features64 * 7 * 7, out_features128), nn.ReLU(inplaceTrue), nn.Linear(in_features128, out_featuresnum_classes) ) def forward(self, x): x self.conv1(x) # 输入 1x28x28 - 输出 32x14x14 x self.conv2(x) # 输出 32x14x14 - 64x7x7 x x.view(x.size(0), -1) # 展平64*7*73136 x self.fc(x) # 3136 - 128 - 10 return x这段代码的逻辑是输入一张 1×28×28 的灰度图第一个卷积块用 32 个 3×3 卷积核提取局部纹理经过 ReLU 激活后做 2×2 最大池化尺寸减半变成 14×14第二个卷积块把通道数从 32 扩到 64再次池化后变成 7×7。view操作把 64×7×7 的特征图拉平成 3136 维向量最后接两层全连接输出 10 个类别的 logits。几个参数需要说明kernel_size3是卷积核尺寸3×3 是图像任务里最常用的感受野够用而且参数量小padding1是为了让卷积操作不改变特征图尺寸否则 28×28 经过 3×3 卷积会变成 26×26后续计算特征图维度容易算错MaxPool2d的下采样不仅减小计算量还带来一定的平移不变性。最后一个全连接层不需要额外接 Softmax因为后面用的交叉熵损失函数自带 Softmax 操作这一点很多初学者会搞混在输出层手动加 Softmax 反而可能出问题。3.2 数据加载DataLoader 的 shuffle 和 num_workers 怎么设数据加载是整个流程里最容易被低估的环节。DataLoader的几个参数直接影响训练效果和硬件利用率。import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms # 数据预处理转 Tensor 标准化 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean(0.1307,), std(0.3081,)) ]) # 训练集下载 应用预处理 train_dataset datasets.MNIST( root./data, trainTrue, downloadTrue, transformtransform ) # 测试集同样预处理但不需要 shuffle test_dataset datasets.MNIST( root./data, trainFalse, downloadTrue, transformtransform ) train_loader DataLoader( datasettrain_dataset, batch_size128, shuffleTrue, num_workers2 ) test_loader DataLoader( datasettest_dataset, batch_size256, shuffleFalse, num_workers2 )shuffleTrue只在训练集设置这是为了让每个 epoch 看到的样本顺序不同避免模型学到数据排列的「捷径」。测试集不需要 shuffle因为验证时只看整体准确率顺序没有意义。num_workers控制数据加载的子进程数量Windows 上如果设得过高会报错一般 2 到 4 就够了Linux 上可以设成 CPU 核心数。ToTensor做了两件事把 PIL Image 或者 numpy 数组转成 PyTorch Tensor同时把像素值从 0-255 缩放到 0-1。Normalize在ToTensor之后做用公式 (x - mean) / std 对每个像素做标准化。注意Normalize的 mean 和 std 是元组因为 MNIST 是单通道所以只传一个值。如果你用的是 RGB 三通道图这里要传三个值很多人在这一步报维度错误。3.3 训练循环optimizer.zero_grad 为什么不能省训练循环是整套流程的核心每个步骤都有明确的职责。新手最容易犯的错误是漏掉optimizer.zero_grad()导致梯度不断累加。import torch.optim as optim model SimpleCNN() device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) criterion nn.CrossEntropyLoss() # 交叉熵损失多分类标配 optimizer optim.Adam(model.parameters(), lr0.001) # Adam 优化器 num_epochs 10 for epoch in range(num_epochs): model.train() # 切换到训练模式 running_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) # 前向传播计算预测输出 outputs model(images) loss criterion(outputs, labels) # 反向传播清空旧梯度 - 计算新梯度 - 更新参数 optimizer.zero_grad() loss.backward() optimizer.step() running_loss loss.item() _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() train_acc 100.0 * correct / total print(fEpoch [{epoch1}/{num_epochs}], Loss: {running_loss/len(train_loader):.4f}, Accuracy: {train_acc:.2f}%) # 每个 epoch 结束后在测试集上验证 evaluate(model, test_loader, device)前向传播用model(images)隐式调用forward输出是一个 128×10 的张量表示每个样本属于每个类别的得分。CrossEntropyLoss的输入是未经过 Softmax 的 logits配合整数标签使用内部会先做 Softmax 再计算负对数似然。optimizer.zero_grad()必须放在loss.backward()之前因为 PyTorch 的梯度是累积的不清空的话上一轮的梯度会加到这一轮上参数更新方向就会错乱。model.train()和model.eval()这两个状态切换也值得注意。虽然当前网络结构里没有 Dropout 和 BatchNorm这两个函数看起来没有实际作用但养成习惯很重要。后面你加 BatchNorm 或者 Dropout 时忘了切状态会导致训练和验证行为不一致出问题非常难排查。torch.max(outputs, 1)返回每个样本得分最高的类别索引predicted labels得到一个布尔张量sum()算出的就是本 batch 预测正确的样本数。3.4 验证函数torch.no_grad 到底在干什么验证阶段和训练阶段有一个本质区别验证不需要计算梯度因为你不更新参数。通过torch.no_grad()关闭梯度计算能省掉大量内存和计算时间。def evaluate(model, test_loader, device): model.eval() # 切到评估模式 correct 0 total 0 with torch.no_grad(): # 关闭梯度计算 for images, labels in test_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() acc 100.0 * correct / total print(fTest Accuracy: {acc:.2f}%) return acc注意这里没有model.train()的切换。训练循环里每个 epoch 结束调用这个函数时先把 model 切到 eval 模式验证完再回到训练循环。如果验证完忘记切回train()下一个 epoch 就会以 eval 模式训练如果网络里有 Dropout 或 BatchNorm表现会非常奇怪。我之前就在这个点上吃过亏——加了 BatchNorm 之后训练准确率突然掉到 70% 左右查了半天发现是模式没切回来。4. 训练启动后最值得调的四组参数学习率、Batch Size、Epoch、优化器4.1 学习率0.001 是默认但你真的知道它在干嘛吗学习率决定每一步参数更新迈多大步子。太大损失函数会在最小值附近震荡甚至发散太小收敛慢到你怀疑人生。我用 Adam 优化器时习惯从 0.001 起步这是 PyTorch 官方文档里的推荐值也是大量实验验证过的「甜点区」。但学习率不是设一次就完事的。训练后期模型已经接近最优解这时候还保持最初的步长很容易在最优解附近来回横跳准确率卡在某个值上不去。常见的做法是用学习率衰减PyTorch 里用torch.optim.lr_scheduler.StepLR每隔多少个 epoch 把学习率乘以一个系数比如每 5 个 epoch 乘以 0.1。也可以等 loss 进了平台期再手动调低这个后面进阶部分细说。如果你发现 loss 曲线在前几个 epoch 不降反升或者出现了 NaN大概率是学习率太大了。这时候不要犹豫直接除以 10 再试。反过来如果 loss 降得极其缓慢一个 epoch 才从 0.3 降到 0.29说明步长太小把学习率乘以 10 试试。这种「试错法」听起来很玄学但其实有章可循每次调整一个数量级观察 2 到 3 个 epoch 的变化趋势。4.2 Batch Size64、128、256 怎么选Batch Size 是每次喂给网络多少张图一起计算梯度。MNIST 这种小数据集我通常用 128这个值在收敛速度和显存占用之间比较均衡。选 Batch Size 时有三个考量点。第一个是梯度稳定性。Batch 越大梯度越接近整个训练集的真实梯度方向更新越稳定Batch 太小梯度噪声大训练曲线会抖但也正因为有噪声有时候反而更容易跳出局部最优。第二个是训练速度Batch 越大每次参数更新利用的数据越多理论上同样 epoch 数下收敛更快但单次迭代的显存占用和计算时间也更高。第三个是硬件限制显存不够时不得不调小 Batch Size但 Batch Size 太小比如 8 或 16会导致训练不稳定这时候需要相应地调低学习率。具体到 MNIST我用过 32、64、128、256 四种配置结论是 64 和 128 差异不大256 收敛略快但准确率峰值没有明显提升。如果用的是 AdamBatch Size 设 128 配学习率 0.001基本不用操心太多。一个实用的检查方法是观察训练 loss 曲线如果非常平滑没有波动说明 Batch 偏大或学习率偏小如果抖得像心电图说明 Batch 偏小或学习率偏大。4.3 Epoch 数量10 个还是 50 个用 early stopping 判断Epoch 是指整个训练集被完整遍历的次数。MNIST 用上面的网络结构一般 10 到 15 个 epoch 就能收敛到 99% 左右。继续训练可能会出现两种情况一是训练准确率还在微涨但测试准确率开始波动甚至下降这是过拟合的信号二是两条曲线都持平说明模型已经饱和。我一般不提前固定死 epoch 数而是盯着验证集准确率的走势。如果连续 3 个 epoch 测试准确率都没有提升就停止训练。这就是 early stopping 的朴素实现。在 PyTorch 里可以存一个变量best_acc每轮验证后如果比best_acc高就保存当前模型权重最后用保存的最佳模型做测试。这样既不会欠拟合也不会过拟合是最省心的策略。best_acc 0.0 for epoch in range(num_epochs): # ... 训练代码 ... acc evaluate(model, test_loader, device) if acc best_acc: best_acc acc torch.save(model.state_dict(), best_model.pt) print(fNew best model saved: {best_acc:.2f}%)model.state_dict()保存的是模型的权重参数不包含网络结构。加载时需要先实例化一个相同结构的模型再load_state_dict。这比保存整个模型要灵活得多因为你可以改完代码后再加载旧权重继续训练。torch.save和torch.load是配套的记得权重文件放到和代码同一个目录下不然会报路径错误。4.4 优化器对比Adam 还是 SGDMNIST 这个规模说实话差异不大初学者一上来用 Adam 基本不会错它对学习率的敏感程度比 SGD 低不少收敛也快。但如果你想让模型的泛化性能做到极致SGD 配合动量Momentum在 MNIST 上能达到比 Adam 略高的峰值准确率代价是需要手动调学习率训练周期也更长。这个现象在很多图像任务里都存在Adam 收敛快但可能在泛化边界上略逊SGD 收敛慢但最终落点往往更好。PyTorch 里 SGD 加动量就一行代码optimizer optim.SGD(model.parameters(), lr0.01, momentum0.9, weight_decay5e-4)momentum0.9是最常用的动量系数让参数更新方向兼顾历史梯度方向能有效抑制震荡。weight_decay是 L2 正则化约束权重不要过大对防止过拟合有帮助但这会让训练 loss 不会降到 0。用 SGD 时学习率要比 Adam 的 0.001 大一些我一般从 0.01 起步然后逐步衰减。没有绝对的对错可以两个都跑一遍观察测试准确率的差异。我自己试下来在 MNIST 上两者差距通常在 0.1 到 0.3 个百分点之内普通人肉眼很难分出高下。5. CNN 训练 MNIST 常见问题与排查从 loss 不降到准确率卡壳5.1 报错「mat1 and mat2 shapes cannot be multiplied」全连接层维度算错了现象训练脚本前向传播时报错提示矩阵乘法维度不匹配比如mat1 shape [128, 3136]和mat2 shape [128, 3136]对不上。原因这是新手最常踩的坑之一。全连接层的输入维度写成64*7*73136但实际经过卷积池化后特征图的尺寸和你预想的不一样。问题通常出在padding或者kernel_size的设置上——你以为是 7×7实际是 6×6维度就对不上了。另一种情况是改了卷积层参数比如 kernel 从 3 改成 5忘记同步修改全连接层的输入维度。解决不要靠心算直接在view之前打印一下特征图的形状。在forward函数里加一行调试代码def forward(self, x): x self.conv1(x) x self.conv2(x) print(Shape after conv2:, x.shape) # 调试用确认维度 x x.view(x.size(0), -1) x self.fc(x) return x跑一次看输出[128, 64, 7, 7]的话全连接层的输入就是64*7*7然后把print删掉。我碰到这类问题从来不算直接打印几秒钟的事。另外x.view(x.size(0), -1)这种写法是自适应展平的-1表示自动推断剩下的维度所以只要你把nn.Linear的第一个参数写对view这边永远不用改。5.2 Loss 下降但准确率不变用了 BCE 而不是 CrossEntropy现象训练 loss 从 0.5 降到 0.2但准确率始终卡在 10% 左右和随机猜测一个水平。原因多分类任务误用了BCELoss二元交叉熵。BCELoss期望输入是经过 Sigmoid 的 0 到 1 之间的概率且每个类别是独立的二分类问题。而你的输出层是 10 个类别的 logits数值范围可能是负数也可能大于 1BCELoss 算出来的 loss 虽然也在降但模型根本不是在学正确的分类边界。另一种可能你用了CrossEntropyLoss但在输出层手动加了 Softmax导致梯度方向和数值都出现偏差。解决确认损失函数和输出层之间的搭配。用nn.CrossEntropyLoss时模型最后一层保持 10 维 logits 输出不要加 Softmax。用BCELoss时最后一层需要接nn.Sigmoid()且标签要做成 one-hot 编码。MNIST 这种多分类任务标准做法就是 CrossEntropy不要在这上面发挥创造力。# 正确 ✅ criterion nn.CrossEntropyLoss() # 模型输出层nn.Linear(128, 10)无 Softmax # 错误 ❌ # criterion nn.BCELoss()5.3 训练集准确率 100%测试集只有 90%过拟合了现象前几个 epoch 测试准确率随训练准确率一起涨后几个 epoch 训练准确率继续涨到 99.5%测试准确率反而从 98% 回落到 96%。原因典型的过拟合。模型把训练样本的细节包括噪声和无关特征都记下来了但对没见过的样本泛化能力弱。MNIST 虽然简单12 个 epoch 之后照样会进入过拟合区间尤其是数据没有做增强的情况下。解决三个手段从易到难。第一减少 epoch 数量用 early stopping 在测试准确率不再提升时及时停。第二加 Dropout 层在两层全连接之间加一个nn.Dropout(0.5)训练时随机丢弃一半神经元迫使模型学到更鲁棒的特征。第三做数据增强对训练图片做随机旋转、随机平移相当于免费扩充数据集。注意 Dropout 只在训练时生效这就是为什么model.train()和model.eval()的切换这么重要。5.4 准确率在 97% 左右上不去归一化参数填错了现象训练 loss 正常下降但准确率始终卡在 97% 上下怎么调学习率和网络结构都突破不了。原因大概率归一化参数不对。MNIST 数据用transforms.Normalize(mean(0.1307,), std(0.3081,))是经过统计验证的标准值。如果你把 mean 和 std 填成 0.5 和 0.5或者漏了归一化直接用 0-255 的像素值训练网络虽然在收敛但特征分布不理想准确率天花板明显偏低。还有一种情况测试集和训练集用的是不同预处理训练用归一化测试用原图或者反过来几乎必然导致准确率掉 2 到 3 个百分点。解决检查训练和测试的 transform 是否完全一致。把transforms.Compose定义成一份训练和测试共用一个从根源上杜绝不一致。然后确认Normalize的 mean 和 std 是对应 MNIST 的那两个值。如果不是 MNIST 而是其他数据集可以用一个简单的脚本算一下数据的全局均值和标准差再填进去。这个坑我自己掉过换了个数据集忘了更新 Normalize 参数准确率直接掉了 5 个点。5.5 GtPU 相关报错但不影响运行放心忽略还是需要处理现象显存不足报错或者 CUDA 不可用的警告。比如CUDA out of memory或UserWarning: CUDA initialization: The NVIDIA driver on your system is too old。原因MNIST 的数据量非常小显存不足几乎不会出现除非你把 Batch Size 调到了几千。CUDA 不可用则多半是 PyTorch 装成了 CPU 版本或者驱动版本太旧。如果你的机器实际上没有独立显卡直接忽略这个报错CPU 训练一样能完成。解决如果代码里用了device torch.device(cuda if torch.cuda.is_available() else cpu)PyTorch 会自动回退到 CPU不需要手动改任何代码。如果报显存不足先查是不是其他程序占用了显存把 Batch Size 调小比如 128 改 64。如果确认有显卡但torch.cuda.is_available()返回 False需要重新安装对应 CUDA 版本的 PyTorch。这个问题的排查思路是先看硬件有没有设备管理器或nvidia-smi再看驱动版本最后看 PyTorch 版本按顺序来。6. 从 98% 到 99.5%数据增强、学习率衰减和结构微调三板斧模型跑通准确率卡在 98% 左右再往上推需要的是精细化调优。这一节说三个我实际验证过的方法按投入产出比排序先是数据增强再是学习率衰减最后是网络结构调整。合起来可以稳定把 MNIST 推到 99.4% 以上。第一个是数据增强。MNIST 训练集只有 6 万张图但对数字识别来说人写的数字有平移、旋转、缩放、笔画粗细等各种变化原始数据集不够覆盖。PyTorch 里用transforms组合即可transform_train transforms.Compose([ transforms.RandomAffine(degrees10, translate(0.1, 0.1), scale(0.9, 1.1)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) transform_test transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])RandomAffine的degrees10表示随机旋转正负 10 度translate是平移比例scale是缩放范围。关键点测试集不要做随机增强只做 Tensor 化和归一化否则验证准确率会上下波动指标失真。增强之后每个 epoch 看到的都是不同的图等价于数据量扩大了几十倍过拟合问题也一并缓解。我自己跑了对比增强前测试准确率最高 98.5%增强后直接跳到 99.2%。第二个是学习率衰减。Adam 在训练前期收敛很快后期容易在最优解附近震荡。用 StepLR 每个 epoch 检查一次训练到后期自动缩小步长scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.1) # 每个 epoch 结束调用 scheduler.step() 更新学习率step_size5是每隔 5 个 epoch 衰减一次gamma0.1是乘以 0.1。效果是前 5 个 epoch 用 0.001 快速逼近后面用 0.0001 精细微调。这一步通常能再提升 0.1 到 0.2 个百分点。ReduceLROnPlateau是更智能的版本可以设置当测试准确率连续几个 epoch 不提升时自动衰减。第三个是结构调整。目前的网络在池化之后直接接全连接可以在第二个MaxPool2d之后加一个 BatchNorm 层。nn.BatchNorm2d(64)会标准化每个 batch 的特征分布让训练更稳定。另一个微调是第一个卷积层从 32 个卷积核加到 64 个增加模型容量但训练时间会翻倍收益约 0.1 个百分点。权衡下来更划算的做法是加一层nn.Dropout(0.3)在最后一个全连接层之前增强泛化能力。我的经验是数据增强贡献 0.7 个百分点学习率衰减贡献 0.1 到 0.2结构微调贡献 0.1三者叠加之后模型在测试集上能稳定达到 99.4% 以上。调参的过程本质上是在找收益的边际递减点——数据增强收益最大原因在于它从源头解决了数据量不足结构微调收益最小因为现有容量已经足够拟合 MNIST 的复杂度。如果你想让数字准确率再往上走可以考虑集成学习或者用更多数据但投入产出比会急剧下降。最后说一个我的习惯每次改完一组参数我会把配置记到实验日志里包括当时的数据增强方式、学习率、batch size、最终准确率跑完 8 到 10 组之后回头看你会发现哪些调整真的有效、哪些只是心理安慰。MNIST 这个项目虽然小但整套「假设 → 实验 → 验证 → 记录」的流程和你日后做任何图像识别项目完全一致。我第一次跑通它的时候光维度报错就折腾了两个小时但正是这些坑让我对每一层输出的形状都了然于胸。希望这篇笔记能帮你在同样的路上少绕几个弯。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询