LeNet-5手写数字识别实战:用PyTorch从零搭建并训练你的第一个CNN模型

张开发
2026/4/5 2:28:00 15 分钟阅读

分享文章

LeNet-5手写数字识别实战:用PyTorch从零搭建并训练你的第一个CNN模型
LeNet-5手写数字识别实战用PyTorch从零搭建并训练你的第一个CNN模型在深度学习的世界里LeNet-5就像是一把打开计算机视觉大门的钥匙。这个由Yann LeCun团队在1998年提出的经典网络架构虽然结构简单却蕴含着现代卷积神经网络(CNN)的核心思想。对于刚踏入深度学习领域的开发者来说没有什么比亲手实现一个LeNet-5模型更能帮助你理解CNN的工作原理了。本文将带你用PyTorch框架从零开始构建LeNet-5网络并在MNIST手写数字数据集上完成训练和测试。不同于单纯的理论讲解我们会重点关注实际编码中的各种细节和技巧包括网络各层的PyTorch实现代码数据预处理的最佳实践训练过程中的常见问题及解决方案模型性能评估与可视化1. 环境准备与数据加载在开始编码之前我们需要确保开发环境配置正确。建议使用Python 3.8和PyTorch 1.8版本。可以通过以下命令安装必要的库pip install torch torchvision matplotlib numpyMNIST数据集是深度学习领域的Hello World包含60,000张训练图像和10,000张测试图像每张都是28×28像素的手写数字灰度图。PyTorch的torchvision库提供了便捷的加载方式import torch import torchvision import torchvision.transforms as transforms # 定义数据预处理 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) # 加载训练集和测试集 train_set torchvision.datasets.MNIST( root./data, trainTrue, downloadTrue, transformtransform ) test_set torchvision.datasets.MNIST( root./data, trainFalse, downloadTrue, transformtransform ) # 创建数据加载器 train_loader torch.utils.data.DataLoader( train_set, batch_size64, shuffleTrue ) test_loader torch.utils.data.DataLoader( test_set, batch_size1000, shuffleFalse )这里有几个关键点需要注意数据标准化MNIST图像的像素值范围是0-255我们通过ToTensor()将其转换为0-1的浮点数然后用Normalize进行标准化。这里的均值0.1307和标准差0.3081是MNIST数据集的统计值。批处理大小batch_size的选择会影响训练速度和内存使用。对于MNIST这样的简单数据集64是一个合理的起点。数据增强虽然原始LeNet-5论文中没有使用数据增强但在实际项目中可以考虑添加旋转、平移等变换来提高模型鲁棒性。2. LeNet-5网络架构实现让我们仔细分析LeNet-5的经典结构然后用PyTorch实现它。原始网络包含7层不含输入层C1: 卷积层 (6个5×5卷积核)S2: 平均池化层 (2×2窗口)C3: 卷积层 (16个5×5卷积核)S4: 平均池化层 (2×2窗口)C5: 卷积层 (120个5×5卷积核)F6: 全连接层 (84个神经元)OUTPUT: 输出层 (10个神经元)现代实现通常会做一些调整使用ReLU替代原始的sigmoid激活函数使用最大池化代替平均池化输出层使用softmax代替原始的RBF下面是PyTorch实现代码import torch.nn as nn import torch.nn.functional as F class LeNet5(nn.Module): def __init__(self): super(LeNet5, self).__init__() self.conv1 nn.Conv2d(1, 6, 5, padding2) # C1 self.pool1 nn.MaxPool2d(2, 2) # S2 self.conv2 nn.Conv2d(6, 16, 5) # C3 self.pool2 nn.MaxPool2d(2, 2) # S4 self.conv3 nn.Conv2d(16, 120, 5) # C5 self.fc1 nn.Linear(120, 84) # F6 self.fc2 nn.Linear(84, 10) # OUTPUT def forward(self, x): x F.relu(self.conv1(x)) # 28x28 - 28x28 x self.pool1(x) # 28x28 - 14x14 x F.relu(self.conv2(x)) # 14x14 - 10x10 x self.pool2(x) # 10x10 - 5x5 x F.relu(self.conv3(x)) # 5x5 - 1x1 x x.view(-1, 120) # 展平 x F.relu(self.fc1(x)) x self.fc2(x) return x关键实现细节说明padding设置原始LeNet-5输入是32×32图像而MNIST是28×28。我们在第一个卷积层添加padding2来保持尺寸一致。参数初始化PyTorch默认使用Kaiming初始化针对ReLU优化这比原始论文中的初始化方式更有效。展平操作在卷积层到全连接层的过渡处需要使用view或flatten将特征图展平为一维向量。激活函数虽然原始论文使用sigmoid但我们改用ReLU以获得更好的训练效果。3. 模型训练与优化有了网络架构后我们需要定义损失函数和优化器然后编写训练循环。这是深度学习中非常关键的一步很多初学者在这里会遇到各种问题。import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model LeNet5().to(device) criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.01, momentum0.9) def train(model, device, train_loader, optimizer, epoch): model.train() 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() 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})训练过程中的关键技巧学习率选择对于MNIST这样的简单任务0.01是一个合理的初始学习率。如果训练不稳定可以尝试减小。动量参数momentum0.9可以帮助加速收敛并减少震荡。批处理显示每100个batch打印一次进度既不会太频繁也能及时了解训练情况。设备选择使用GPU可以显著加速训练代码中自动检测可用的设备。在实际训练中你可能会遇到以下常见问题及解决方案问题现象可能原因解决方案损失不下降学习率太小逐步增加学习率损失震荡学习率太大减小学习率或增加batch size准确率卡住模型容量不足增加网络深度或宽度过拟合训练数据不足添加数据增强或正则化4. 模型评估与可视化训练完成后我们需要评估模型在测试集上的表现并可视化一些结果以更好地理解模型行为。首先定义一个测试函数def test(model, device, test_loader): model.eval() 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() pred output.argmax(dim1, keepdimTrue) correct pred.eq(target.view_as(pred)).sum().item() test_loss / len(test_loader.dataset) print(f\nTest set: Average loss: {test_loss:.4f}, fAccuracy: {correct}/{len(test_loader.dataset)} f({100. * correct / len(test_loader.dataset):.2f}%)\n)运行训练和测试循环for epoch in range(1, 11): train(model, device, train_loader, optimizer, epoch) test(model, device, test_loader)一个训练良好的LeNet-5在MNIST上通常能达到98.5%-99%的准确率。为了更直观地理解模型我们可以可视化卷积层的滤波器import matplotlib.pyplot as plt # 获取第一层卷积的权重 weights model.conv1.weight.detach().cpu() # 可视化滤波器 fig, axes plt.subplots(1, 6, figsize(12, 2)) for i, ax in enumerate(axes): ax.imshow(weights[i, 0], cmapgray) ax.axis(off) ax.set_title(fFilter {i1}) plt.show()这些滤波器通常能学到边缘检测、角点检测等基础特征提取器。你还可以可视化中间层的特征图来观察网络是如何逐步抽象特征的# 选择一个测试样本 data, target next(iter(test_loader)) sample data[0:1].to(device) # 获取中间层输出 activations {} def get_activation(name): def hook(model, input, output): activations[name] output.detach() return hook model.conv1.register_forward_hook(get_activation(conv1)) model.pool1.register_forward_hook(get_activation(pool1)) output model(sample) # 可视化第一层卷积后的特征图 fig, axes plt.subplots(2, 3, figsize(12, 8)) for i, ax in enumerate(axes.flat): ax.imshow(activations[conv1][0, i].cpu(), cmapgray) ax.axis(off) ax.set_title(fFeature map {i1}) plt.show()通过这些可视化你可以直观地看到网络是如何从原始像素逐步提取出越来越抽象的特征最终实现数字分类的。

更多文章