ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于PyTorch与CNN的手写数字识别实战:从MNIST数据到模型部署

基于PyTorch与CNN的手写数字识别实战:从MNIST数据到模型部署 简介卷积神经网络CNN是计算机视觉领域的核心模型它通过卷积核自动提取图像的局部空间特征并利用池化层实现平移不变性这一原理使其在图像识别任务中表现出色。在工程实践中PyTorch框架以其动态计算图和Pythonic的设计成为实现CNN模型的流行选择便于开发者理解底层机制并进行快速原型开发。手写数字识别作为经典的入门项目不仅能够帮助学习者掌握数据预处理、模型构建、训练评估的全流程更是理解图像分类、特征提取等基础概念的绝佳场景。本文将以MNIST数据集为例详细阐述如何使用PyTorch构建一个轻量级CNN模型并完成从环境搭建、模型训练到自定义图片预测的完整实践其中涉及的关键技术点包括数据标准化、Dropout正则化以及优化器选择。1. 项目概述从零构建一个手写数字识别系统如果你对机器学习感兴趣想找一个既能入门又能看到实际效果的练手项目手写数字识别绝对是不二之选。它就像是机器学习领域的“Hello World”但比打印一行字要有趣和实用得多。这个项目用Python来实现核心目标就是教会计算机看懂我们手写的0到9这十个数字。听起来很酷对吧它背后涉及图像处理、特征提取和分类算法等一系列经典技术是理解现代人工智能特别是计算机视觉和深度学习的一个绝佳切入点。我之所以推荐这个项目是因为它麻雀虽小五脏俱全。你不需要准备海量的数据经典的MNIST数据集就足够你也不需要昂贵的GPU普通的笔记本电脑就能跑起来。更重要的是从数据加载、预处理到模型构建、训练、评估再到最后的预测你能完整地走一遍一个机器学习项目的标准流程。无论你是刚学完Python语法想找点有成就感的事做还是已经有一定基础想深入理解算法这个项目都能给你带来实实在在的收获。接下来我会带你一步步拆解把每个环节的原理、代码和踩过的坑都讲清楚。2. 核心思路与方案选型为什么是卷积神经网络手写数字识别本质上是一个多分类问题。给定一张28x28像素的灰度图片我们需要判断它属于0-9中的哪一个类别。历史上人们尝试过很多方法比如直接用逻辑回归、支持向量机SVM或者传统的多层感知机MLP。这些方法在早期都取得过不错的效果但它们有一个共同的局限把图片当成一个长长的、784维的向量来处理完全忽略了像素之间的空间结构信息。想象一下数字“8”和数字“0”它们的像素分布都有中间的空洞但位置和形状不同。传统的全连接网络很难高效地捕捉这种局部特征。而卷积神经网络CNN的提出正是为了解决这个问题。CNN通过卷积核在图像上滑动能够自动学习到像边缘、角点这样的局部特征并且通过池化层对特征进行降维使得模型对图像的位置变化具有一定的不变性比如数字写歪了一点模型依然能认出来。因此对于手写数字识别这个任务使用CNN是目前公认的最佳实践也是从入门到精通必须掌握的核心技术。它不仅在MNIST上能达到接近99.8%的准确率其设计思想更是贯穿了现代计算机视觉的几乎所有领域。我们的项目也将围绕构建一个轻量级的CNN模型来展开。2.1 工具栈选择PyTorch vs TensorFlow/Keras确定了用CNN下一个问题就是用什么框架来实现。主流选择有两个PyTorch和TensorFlow及其高级API Keras。PyTorch以其动态计算图和“Pythonic”的设计哲学著称。它的代码写起来非常直观调试起来就像调试普通Python程序一样方便特别适合研究和快速原型开发。对于初学者来说理解其张量操作和自动求导机制相对平滑。TensorFlow/KerasKeras作为TensorFlow的高级API以其极简的接口和快速的模型搭建能力闻名。一句model Sequential([Dense(128, activationrelu)])就能搞定一层网络对新手非常友好。TensorFlow 2.x之后也采用了Eager Execution默认模式体验上向PyTorch靠拢。我个人的选择是PyTorch。原因有三第一其设计更贴近Python编程思维有助于你深入理解模型运作的每一个步骤而不是被高度封装的API挡住视线。第二PyTorch在学术研究和工业界前沿领域的活跃度非常高掌握它意味着你能更快地接触和复现最新的论文。第三从学习曲线来看虽然初期可能比Keras多写几行代码但长远来看对理解深度学习底层机制更有帮助。当然这个选择没有对错。如果你追求最快的上手速度Keras是极好的选择。但为了内容的深度和可迁移性本文将基于PyTorch进行讲解。你学到的概念和思路完全可以平移到其他框架。3. 环境搭建与数据准备工欲善其事必先利其器。在开始写代码之前我们需要把环境和数据准备好。3.1 Python环境与依赖库安装首先确保你安装了Python建议3.8及以上版本。然后我们通过pip安装必要的库。打开你的终端或命令提示符执行以下命令pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu pip install numpy matplotlib pandas jupyter这里解释一下torch: PyTorch深度学习框架本体。torchvision: 包含计算机视觉相关的数据集、模型架构和图像转换工具我们加载MNIST数据集就靠它。numpy: Python科学计算的基础包处理多维数组。matplotlib: 绘图库用于可视化图片、损失曲线等。pandas: 数据处理库虽然本项目直接使用较少但它是数据科学工作流的标准组件。jupyter: 交互式笔记本环境非常适合进行教学、探索和演示你可以边写代码边看结果。注意第一条安装命令指定了CPU版本。如果你的电脑有NVIDIA显卡并配置好了CUDA可以安装对应的GPU版本以加速训练。但对于MNIST这样的小数据集CPU训练也完全在可接受的时间内几分钟到十几分钟。3.2 MNIST数据集详解与加载MNIST数据集包含60000张训练图片和10000张测试图片每张都是28x28像素的灰度手写数字。PyTorch的torchvision.datasets模块让它变得唾手可得。import torch from torchvision import datasets, transforms # 定义数据预处理转换 transform transforms.Compose([ transforms.ToTensor(), # 将PIL图像或numpy数组转换为PyTorch张量并自动缩放到[0,1]区间 transforms.Normalize((0.1307,), (0.3081,)) # 标准化减去均值0.1307除以标准差0.3081 ]) # 下载并加载训练集和测试集 train_dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) # 创建数据加载器 train_loader torch.utils.data.DataLoader(train_dataset, batch_size64, shuffleTrue) test_loader torch.utils.data.DataLoader(test_dataset, batch_size1000, shuffleFalse)关键点解析ToTensor(): 这是必须的一步。它不仅将图像数据从PIL格式或NumPy数组转换成PyTorch张量torch.Tensor还做了一个重要的操作将像素值从原始的[0, 255]整数范围归一化到[0.0, 1.0]的浮点数范围。这对于神经网络的稳定训练至关重要因为大的输入值会导致梯度爆炸等问题。Normalize(): 标准化或叫归一化是另一个提升模型性能的常用技巧。这里的均值0.1307和标准差0.3081是MNIST数据集的全局统计值。标准化后的数据分布接近均值为0、标准差为1的标准正态分布这能使梯度下降等优化算法收敛得更快、更平稳。DataLoader: 它负责管理数据的批量加载、打乱顺序等。batch_size64意味着每次训练模型会看64张图片计算一个平均损失然后更新一次参数。打乱顺序shuffleTrue可以防止模型学习到数据顺序带来的虚假模式。3.3 数据可视化看看我们正在处理什么在训练前看一眼数据总是一个好习惯这能帮你建立直观感受也能检查数据加载是否正确。import matplotlib.pyplot as plt # 从训练加载器中获取一个批次的数据 images, labels next(iter(train_loader)) # 创建一个子图网格来显示图片 fig, axes plt.subplots(4, 8, figsize(12, 6)) for i, ax in enumerate(axes.flat): # 图像张量的形状是 [通道, 高, 宽]需要转换为 [高, 宽] 才能用imshow显示 ax.imshow(images[i].squeeze(), cmapgray) ax.set_title(fLabel: {labels[i].item()}) ax.axis(off) plt.tight_layout() plt.show()运行这段代码你会看到一个4行8列的网格每格显示一张手写数字图片及其对应的标签。这能让你确信数据已正确加载并对数据的“长相”有个底。4. 构建卷积神经网络模型现在进入核心环节定义我们的CNN模型。我们将构建一个经典的LeNet-5的简化变体它结构清晰效果出色非常适合入门。4.1 模型架构设计我们将模型定义为一个继承自torch.nn.Module的类。import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self): super(SimpleCNN, self).__init__() # 第一个卷积层输入通道1灰度图输出通道32卷积核3x3 self.conv1 nn.Conv2d(in_channels1, out_channels32, kernel_size3, padding1) # 第二个卷积层输入通道32输出通道64卷积核3x3 self.conv2 nn.Conv2d(in_channels32, out_channels64, kernel_size3, padding1) # 最大池化层窗口2x2步长2 self.pool nn.MaxPool2d(kernel_size2, stride2) # Dropout层随机丢弃50%的神经元连接防止过拟合 self.dropout1 nn.Dropout2d(0.25) # 第一个全连接层输入维度是 64 * 7 * 7输出维度128 self.fc1 nn.Linear(64 * 7 * 7, 128) self.dropout2 nn.Dropout(0.5) # 第二个全连接层输出层输入128输出10对应10个数字类别 self.fc2 nn.Linear(128, 10) def forward(self, x): # 输入x形状: [batch_size, 1, 28, 28] # 卷积 - 激活(ReLU) - 池化 x self.pool(F.relu(self.conv1(x))) # 输出形状: [batch_size, 32, 14, 14] x self.pool(F.relu(self.conv2(x))) # 输出形状: [batch_size, 64, 7, 7] x self.dropout1(x) # 将特征图展平成一维向量为全连接层做准备 x x.view(-1, 64 * 7 * 7) # 形状: [batch_size, 64*7*7] x F.relu(self.fc1(x)) x self.dropout2(x) x self.fc2(x) # 形状: [batch_size, 10] # 我们不在这里做Softmax因为损失函数CrossEntropyLoss内部会做 return x # 实例化模型 model SimpleCNN() print(model)逐层拆解与计算Conv2d(1, 32, 3, padding1): 输入是1通道的28x28图像。padding1意味着在图像四周各补一圈0这样经过3x3卷积后输出特征图的高度和宽度保持不变仍是28。输出通道数为32即产生了32张不同的特征图。MaxPool2d(2,2): 2x2最大池化步长为2。它将2x2区域内的最大值作为输出这样特征图尺寸减半从28x28变为14x14。池化层的作用是降低空间维度增加特征的平移不变性同时减少参数量。第二组卷积池化Conv2d(32, 64, 3, padding1)输出64通道的14x14特征图再经过池化变为64通道的7x7特征图。展平x.view(-1, 64*7*7): 将[batch_size, 64, 7, 7]的四维张量重塑为[batch_size, 3136]的二维张量其中3136 64 * 7 * 7。-1表示让PyTorch自动计算这个维度即batch_size。全连接层与输出经过一个128维的隐藏层后最终输出10维的向量每个维度对应一个数字类别的“得分”logits。4.2 关键组件原理解析ReLU激活函数F.relu()将负值置零正值保留。它的引入极大地缓解了深层网络中的梯度消失问题是目前最常用的激活函数。Dropout一种正则化技术。在训练时随机将一部分神经元的输出置零可以防止神经元之间产生复杂的共适应关系迫使网络学习更鲁棒的特征是防止过拟合的利器。Dropout2d用于卷积层后的特征图Dropout用于全连接层后。为什么不在forward里做Softmax因为我们使用的损失函数nn.CrossEntropyLoss已经将Softmax运算和负对数似然损失NLLLoss合并了。它要求输入的是未经过Softmax的原始得分logits这样在数值计算上更稳定。所以模型最后一层直接输出logits即可。5. 模型训练与评估模型定义好了接下来就是喂数据让它学习。5.1 定义损失函数与优化器import torch.optim as optim # 将模型移动到GPU如果可用 device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) # 定义损失函数交叉熵损失适用于多分类问题 criterion nn.CrossEntropyLoss() # 定义优化器Adam优化器学习率设为0.001 optimizer optim.Adam(model.parameters(), lr0.001)CrossEntropyLoss: 分类任务的标准损失函数。它衡量模型预测的概率分布与真实标签的“距离”。Adam优化器: 自适应矩估计优化器它结合了动量Momentum和自适应学习率的优点通常比传统的SGD随机梯度下降收敛更快且对超参数如学习率不那么敏感是默认的推荐选择。5.2 训练循环的实现训练是一个迭代过程前向传播计算预测和损失反向传播计算梯度优化器根据梯度更新模型参数。def train(model, device, train_loader, optimizer, criterion, epoch): model.train() # 将模型设置为训练模式启用Dropout等 train_loss 0 correct 0 total 0 for batch_idx, (data, target) in enumerate(train_loader): data, target data.to(device), target.to(device) optimizer.zero_grad() # 清除上一轮迭代的梯度 output model(data) # 前向传播 loss criterion(output, target) # 计算损失 loss.backward() # 反向传播计算梯度 optimizer.step() # 根据梯度更新参数 train_loss loss.item() # 累加损失 _, predicted output.max(1) # 获取预测结果最大值的索引 total target.size(0) correct predicted.eq(target).sum().item() # 统计正确数 # 每处理100个batch打印一次进度 if batch_idx % 100 0: print(fTrain Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} f({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}) # 计算本轮epoch的平均损失和准确率 avg_loss train_loss / len(train_loader) accuracy 100. * correct / total print(f\nTraining set: Average loss: {avg_loss:.4f}, Accuracy: {correct}/{total} ({accuracy:.2f}%)\n) return avg_loss, accuracy关键步骤解释model.train(): 告诉模型现在是训练阶段这会启用Dropout和BatchNorm等层的训练行为。optimizer.zero_grad():至关重要在每次计算新梯度之前必须将上一轮累积的梯度清零。否则梯度会不断累加导致训练失控。loss.backward(): PyTorch的自动求导引擎会计算损失相对于每个模型参数的梯度并存储在参数的.grad属性中。optimizer.step(): 优化器根据.grad中的梯度和预设的学习率等规则更新所有参数的值。5.3 测试与评估训练完成后我们需要在从未见过的测试集上评估模型的泛化能力。def test(model, device, test_loader, criterion): model.eval() # 将模型设置为评估模式关闭Dropout等 test_loss 0 correct 0 total 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() # 累加损失 _, predicted output.max(1) total target.size(0) correct predicted.eq(target).sum().item() avg_loss test_loss / len(test_loader) accuracy 100. * correct / total print(fTest set: Average loss: {avg_loss:.4f}, Accuracy: {correct}/{total} ({accuracy:.2f}%)\n) return avg_loss, accuracymodel.eval(): 切换到评估模式这会禁用Dropout和BatchNorm的随机性使用训练好的固定参数进行前向传播。with torch.no_grad(): 在这个上下文管理器内所有计算都不会构建计算图不保存中间变量极大提升推理速度并减少内存占用。5.4 执行多轮训练现在我们将训练和测试循环组合起来运行多个轮次epoch。epochs 10 train_losses, train_accs [], [] test_losses, test_accs [], [] for epoch in range(1, epochs 1): print(f\n--- Epoch {epoch} ---) train_loss, train_acc train(model, device, train_loader, optimizer, criterion, epoch) test_loss, test_acc test(model, device, test_loader, criterion) train_losses.append(train_loss) train_accs.append(train_acc) test_losses.append(test_loss) test_accs.append(test_acc) print(训练完成)运行这段代码你会看到控制台输出每个epoch的训练和测试损失、准确率。通常经过10个epoch测试准确率能达到99%以上。5.5 可视化训练过程绘制损失和准确率曲线能直观地观察模型的学习情况。plt.figure(figsize(12, 4)) plt.subplot(1, 2, 1) plt.plot(range(1, epochs1), train_losses, b-, labelTraining Loss) plt.plot(range(1, epochs1), test_losses, r-, labelTest Loss) plt.xlabel(Epoch) plt.ylabel(Loss) plt.title(Training and Test Loss) plt.legend() plt.grid(True) plt.subplot(1, 2, 2) plt.plot(range(1, epochs1), train_accs, b-, labelTraining Accuracy) plt.plot(range(1, epochs1), test_accs, r-, labelTest Accuracy) plt.xlabel(Epoch) plt.ylabel(Accuracy (%)) plt.title(Training and Test Accuracy) plt.legend() plt.grid(True) plt.tight_layout() plt.show()一个健康的训练过程应该是训练损失和测试损失都稳步下降训练准确率和测试准确率都稳步上升并且两者最终差距不大。如果训练准确率远高于测试准确率可能是过拟合了。6. 模型应用预测自己的手写数字模型训练好了最大的成就感莫过于用它来识别我们自己写的数字。这里我们需要处理一个关键问题如何将一张任意来源的图片处理成模型能接受的格式。6.1 自定义图片预处理流程假设我们用画图工具写了一个数字“7”保存为my_digit_7.png。预处理步骤如下from PIL import Image import torch def preprocess_image(image_path): 预处理单张手写数字图片使其符合模型输入要求。 参数: image_path: 图片文件路径 返回: 处理后的张量形状为 [1, 1, 28, 28] # 1. 用PIL打开图片并转换为灰度图 img Image.open(image_path).convert(L) # 2. 调整大小为28x28像素与MNIST一致 img img.resize((28, 28), Image.Resampling.LANCZOS) # 3. 转换为numpy数组并反转颜色可选取决于你的背景 # MNIST是白底黑字如果你的图片是黑底白字需要反转。 import numpy as np img_array np.array(img) # 假设我们的是白底黑字但为了保险可以做一个自适应判断或提供参数 # 这里我们假设输入是黑底白字所以需要反转成白底黑字。 # 一个简单的判断如果图片平均像素值大于127偏白则可能是白底黑字需要反转。 if img_array.mean() 127: img_array 255 - img_array # 4. 归一化到[0, 1]范围并转换为张量 img_tensor torch.from_numpy(img_array).float() / 255.0 # 5. 应用与训练数据相同的标准化重要 # 使用MNIST的均值和标准差 img_tensor (img_tensor - 0.1307) / 0.3081 # 6. 调整维度从 [H, W] 变成 [C, H, W]再增加一个批次维度 [B, C, H, W] img_tensor img_tensor.unsqueeze(0).unsqueeze(0) # 形状变为 [1, 1, 28, 28] return img_tensor # 使用示例 my_image_tensor preprocess_image(my_digit_7.png).to(device)预处理要点与避坑指南尺寸与通道必须调整为28x28单通道灰度图。颜色反转这是最容易出错的地方。MNIST数据集是白底像素值255黑字像素值0。如果你用黑色笔在白纸上写字然后拍照或者用画图工具默认黑笔白底那么你的图片是黑底白字直接输入模型效果会极差。关键技巧在预处理中加入一个简单的判断如果图片整体偏白平均像素值127就认为它是白底黑字需要反转成黑底白字即255 - img_array。反之则保持原样。你可以通过可视化中间结果来调试。标准化必须使用与训练数据完全相同的均值和标准差0.1307和0.3081。这是很多人在部署模型时忽略的一点导致模型性能骤降。维度PyTorch模型期望的输入维度是[Batch_size, Channels, Height, Width]。对于单张图片我们需要用unsqueeze(0)两次来增加批次维度和通道维度。6.2 进行预测并解读结果def predict_digit(model, image_tensor): model.eval() with torch.no_grad(): output model(image_tensor) # 获取概率应用Softmax probabilities torch.nn.functional.softmax(output[0], dim0) # 获取预测的类别和置信度 predicted_class torch.argmax(probabilities).item() confidence probabilities[predicted_class].item() return predicted_class, confidence, probabilities predicted, confidence, probs predict_digit(model, my_image_tensor) print(f预测数字为: {predicted}) print(f置信度: {confidence:.2%}) print(所有类别概率:) for i, prob in enumerate(probs): print(f {i}: {prob:.2%})运行后你会看到模型预测的数字以及它对这个判断的“信心”概率。还可以打印出所有10个数字的概率看看模型有没有把“7”误判成“1”或“9”。6.3 可视化预测结果我们可以把原始图片、预处理后的图片和预测概率条形图放在一起看更直观。fig, axes plt.subplots(1, 3, figsize(12, 4)) # 显示原始图片预处理前 orig_img Image.open(my_digit_7.png).convert(L) axes[0].imshow(orig_img, cmapgray) axes[0].set_title(原始图片) axes[0].axis(off) # 显示预处理后的张量需要从设备移回CPU并转换 processed_img my_image_tensor.cpu().squeeze().numpy() # 形状 [28, 28] axes[1].imshow(processed_img, cmapgray) axes[1].set_title(预处理后 (输入模型)) axes[1].axis(off) # 显示预测概率条形图 axes[2].bar(range(10), probs.cpu().numpy()) axes[2].set_xlabel(数字类别) axes[2].set_ylabel(预测概率) axes[2].set_title(f预测结果: {predicted} (置信度: {confidence:.2%})) axes[2].set_xticks(range(10)) plt.tight_layout() plt.show()这个可视化能帮你快速诊断问题。如果预测错了看看预处理后的图片是否还像原来的数字或者概率分布是否非常平均说明模型不确定。7. 模型保存、加载与优化思路一个完整的项目还需要考虑模型的持久化和后续优化。7.1 保存与加载模型训练好的模型可以保存下来下次直接使用无需重新训练。# 保存整个模型包括结构和参数 torch.save(model, mnist_cnn_model.pth) # 更推荐的方式只保存模型参数state_dict更轻量且与模型结构解耦 torch.save(model.state_dict(), mnist_cnn_model_weights.pth) # 加载模型对应第一种保存方式 loaded_model torch.load(mnist_cnn_model.pth) loaded_model.eval() # 加载模型参数对应第二种保存方式需要先有模型结构实例 # new_model SimpleCNN() # 先实例化一个结构相同的空模型 # new_model.load_state_dict(torch.load(mnist_cnn_model_weights.pth)) # new_model.eval()注意保存和加载模型时要确保PyTorch版本和模型定义代码一致否则可能会出错。只保存state_dict是更灵活和推荐的做法。7.2 性能优化与改进方向如果你的模型准确率已经达到99%以上恭喜你但学习不止于此。这里有一些可以继续探索的方向数据增强在训练时对图片进行随机变换如小幅旋转、平移、缩放、添加噪声等。这能显著提升模型的鲁棒性防止过拟合。torchvision.transforms提供了丰富的工具。train_transform transforms.Compose([ transforms.RandomRotation(10), # 随机旋转±10度 transforms.RandomAffine(degrees0, translate(0.1, 0.1)), # 随机平移 transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])调整网络结构尝试更深的网络如增加卷积层、不同的卷积核大小、更多的滤波器数量、添加批归一化层nn.BatchNorm2d等。超参数调优系统性地调整学习率、批大小、Dropout比率、优化器类型如SGD with momentum等。可以使用网格搜索或随机搜索或者更高级的工具如Optuna。学习率调度使用torch.optim.lr_scheduler在训练过程中动态降低学习率有助于模型在后期更精细地收敛。scheduler optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.1) # 在每个epoch的optimizer.step()之后调用 scheduler.step()尝试其他模型在MNIST上玩转后可以挑战更复杂的模型如ResNet、VGG的简化版甚至自己设计网络。8. 常见问题与排查技巧实录在实际操作中你几乎一定会遇到下面这些问题。我把它们和解决方法整理出来希望能帮你节省大量调试时间。8.1 准确率始终上不去低于95%检查数据预处理这是头号嫌疑犯。务必确认你的图片颜色模式黑底白字还是白底黑字与预处理逻辑匹配。用matplotlib把preprocess_image函数处理后的张量画出来看看它应该是一个清晰的白底黑字数字。检查数据加载确保DataLoader的shuffleTrue训练集并且batch_size设置合理如64, 128。检查模型模式训练时用model.train()评估/预测时用model.eval()。弄反了会导致Dropout在预测时还起作用准确率随机波动。学习率问题学习率太大可能导致损失震荡不下降太小则下降缓慢。尝试0.01, 0.001, 0.0001等不同值。Adam优化器对学习率不敏感但0.001是个不错的起点。模型容量不足如果网络太简单比如只用了一层全连接可能无法拟合数据。尝试增加卷积层或全连接层的神经元数量。8.2 训练损失下降但测试损失上升过拟合引入或加强正则化增加Dropout层的丢弃概率如从0.25/0.5增加到0.5/0.7。在全连接层后加入L2权重衰减在优化器中设置weight_decay参数如weight_decay1e-4。使用数据增强如上文所述这是对抗过拟合最有效的手段之一。简化模型如果模型过于复杂尝试减少层数或每层的通道数。获取更多数据对于MNIST可以尝试使用数据增强来“创造”更多数据。8.3 预测自己的图片总是出错99%是预处理问题再次强调请可视化预处理每一步的结果从原始图片到resize后到颜色反转后到归一化后每一步都打印一下形状和像素值范围确保最终输入模型的张量是[1, 1, 28, 28]数值范围大致在-1到1之间经过标准化后。背景干扰确保你的手写数字图片背景干净没有杂乱的线条或噪点。可以用图像处理软件先进行二值化处理。数字位置和大小尽量让数字位于图片中央并且大小适中不要太小或太大撑满整个图片。8.4 运行时错误与调试维度不匹配错误仔细检查每一层输入和输出的张量形状。可以在forward函数中插入print(x.shape)来跟踪形状变化。常见的错误来源是卷积/池化后的特征图尺寸计算错误导致展平后的维度与全连接层输入维度对不上。CUDA内存不足如果使用GPU减小batch_size。MNIST数据小一般不会遇到但复杂模型和大图片时会常见。RuntimeError: expected scalar type Float but found Byte: 这通常是因为图像数据在转换为张量后还是uint8类型。确保在ToTensor()之后或自定义预处理中将数据转换为float类型img_tensor img_tensor.float()。这个手写数字识别项目虽然基础但它像一把钥匙为你打开了深度学习的大门。从数据准备到模型部署的每一个环节你在其中遇到的问题和学到的解决方法在更复杂的视觉任务如图像分类、目标检测中都会再次遇到。我建议你在达到99%准确率后不要就此止步而是选择上面提到的一两个优化方向去实践比如实现数据增强或调参亲身体会这些技术如何影响模型性能这才是从“会跑代码”到“理解代码”的关键一步。本文还有配套的精品资源点击获取
返回列表