ARTICLE DETAIL

资讯详情

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

AlexNet卷积神经网络做MNIST手写数字识别:PyTorch源码与实验报告解析

AlexNet卷积神经网络做MNIST手写数字识别:PyTorch源码与实验报告解析 简介基于AlexNet卷积神经网络实现手写数字识别的Python项目面向毕业设计、期末大作业及课程设计等场景提供完整源码与配套实验报告适合具备一定深度学习基础、希望快速搭建识别模型的开发者参考使用。压缩包共18个文件、约11.07MB涵盖10个Python脚本及模型权重、配置文件、依赖清单、README说明等其中Python脚本覆盖数据加载、模型定义、训练、测试与可视化流程AlexNet、ResNet、BasicModel三种网络结构均有实现。代码附带详细注释结构清晰新手可读懂整体逻辑实验报告记录了网络设计思路与调参过程个人手打取得98分获导师认可部署门槛低下载后简单配置即可运行。目前已有390人学习适合作为深度学习入门实战或课程项目的高分参考。1. AlexNet卷积神经网络做手写数字识别这个组合到底值不值得复现很多人一看到“基于AlexNet卷积神经网络实现手写数字识别python源码实验报告”这个标题第一反应是杀鸡用牛刀——MNIST手写数字识别是入门级任务AlexNet是2012年ImageNet夺冠的 heavyweight 网络二者似乎不在一个量级上。但这个组合恰恰是理解卷积神经网络结构演进最好的跳板AlexNet把“深层卷积 ReLU激活 Dropout 最大池化”这套组合拳打出了名堂而MNIST又是唯一一份你在普通笔记本上几分钟就能跑完一个epoch的数据集。两件事叠在一起你能亲手验证卷积神经网络结构设计里最核心的问题——网络深了为什么有效、参数多了为什么过拟合、以及一个小数据集怎样把人带进真实项目的坑里。这个标题适合两类人一类是刚开始学卷积神经网络、想用一份能跑通的Python源码把原理钉死的入门者另一类是已经跑过几个demo、但想搞明白“怎么把实验报告写得像工程记录而不是课程作业”的在校学生。下面按我自己的落地路径拆解——先说网络结构怎么理解再给一份能直接跑的PyTorch源码最后把训练和写报告时必然翻车的点挨个排掉。全程只讲能复现的东西不掺水。2. AlexNet在网络结构上留下的东西比你想的多2.1 从8层结构看卷积神经网络的骨架AlexNet原始结构是5层卷积加3层全连接中间穿插ReLU、局部响应归一化LRN和重叠最大池化。LRN后来被证明收益有限基本被BatchNorm替代但ReLU和Dropout这两样是从AlexNet开始才真正成为标配的。ReLU解决的是梯度消失——深层网络用Sigmoid或者tanh反向传播到前几层时梯度已经小得没法更新权重ReLU在正区间梯度恒为1这条链路就断了。Dropout解决的是过拟合——全连接层参数量极大MNIST每张图只有28乘28像素模型很容易把训练集的噪声背下来Dropout在训练时随机掐掉一半神经元逼着网络学到冗余特征。MNIST上你不需要一个完整的AlexNet。原版第一个卷积层拿11乘11的卷积核去扫224乘224的输入而MNIST图像缩放到28乘28后11乘11的卷积核几乎覆盖全图等价于一个全连接变换完全失去了局部特征提取的意义。我一般会把卷积核改成3乘3或5乘5池化层保留全连接层的神经元数量从4096一路降到1024和512——这个尺寸在CPU上跑一个epoch也就半分钟保留的却是AlexNet完整的“卷积提特征–非线性激活–池化降维–全连接分类”链路。2.2 手写数字识别为什么适合验证卷积神经网络MNIST有6万张训练图、1万张测试图每张是28乘28的灰度图像素值在0到255之间。任务本身很简单但有两个值得观察的特性数字的笔画局部结构比如“7”的横和斜、 “1”的竖线天然适合卷积操作同一数字的写法在现实中变化极大倾斜、粗细、断笔、噪声都会出现。这两个特性让MNIST成为检验一个卷积神经网络“到底有没有学到特征”的极佳试纸——模型能力不够测试集准确率会卡在90%上下模型过拟合训练集能到99%但测试集只有95%左右。这种对照关系在真实数据集上往往要跑很久才看得清在MNIST上几个epoch就暴露了。2.3 Grad-CAM之外的另一个视角直接看卷积层的输出想验证网络到底学到了什么有一个比画Grad-CAM更简单的办法把第一层卷积的输出特征图直接打印出来。训练好的模型对着一张“7”前几个卷积核提取到的是边缘和笔画方向表现为特征图上高亮的区域恰好落在数字轮廓上训练不充分的模型特征图几乎全是均匀的噪声点。这一步不用装任何额外库把卷积输出的tensor用matplotlib画成16张小图排成4乘4的格子即可。这是我看模型第一层行为最常用的一招比盯着准确率数字更直观而且写进实验报告里说服力极强。3. 数据准备比搭网络更容易翻车MNIST也不例外3.1 Torchvision加载数据时最容易忽略的归一化PyTorch生态里加载MNIST几乎只有一种正确姿势——用Torchvision的dataset接口。但有一个细节很多人第一次会踩MNIST原始像素值是0到255的整数直接喂进网络损失函数会震荡得很厉害。因为卷积层输出的特征图数值范围跟着输入走输入大了梯度也大SGD更新一步就可能把权重推出合理范围。Torchvision官方给的归一化参数是均值0.1307、标准差0.3081这两个数是MNIST全体像素的统计量直接把像素值标准化到均值为0、方差为1的分布上。代码如下from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_set datasets.MNIST( root./data, trainTrue, transformtransform, downloadTrue ) test_set datasets.MNIST( root./data, trainFalse, transformtransform, downloadTrue )这里ToTensor会把PIL图像从H乘W乘C的uint8数组转成C乘H乘W的float32张量像素值顺便除以255缩放到0到1区间。之后Normalize按通道做标准化MNIST是单通道灰度图所以均值和小括号里的标准差都是一维的。千万别漏掉ToTensor直接对PIL图像做Normalize类型对不上会直接报错也别手滑把标准差写成0.5这类的自定义值——这不是不能跑而是你后续所有实验的基准都和官方结果对不上。3.2 DataLoader的四个参数按这个设不容易出问题from torch.utils.data import DataLoader train_loader DataLoader( train_set, batch_size128, shuffleTrue, num_workers2, pin_memoryTrue ) test_loader DataLoader( test_set, batch_size256, shuffleFalse, num_workers2 )batch_size选128而不是更大的512是因为MNIST单张图太小大batch反而会让梯度方向过于平滑收敛变慢。shuffle只在训练集打开测试集保持False——验证时不需要打乱数据顺序保持顺序也方便后面做错分样本的定位分析。num_workers2是按CPU核数设置的Windows上如果开了多进程数据加载报错直接改成0用主进程加载这是最常见的兼容问题。pin_memory只对GPU训练有意义纯CPU跑也不报错但别指望它带来增益。配置完DataLoader后我习惯先打印一个batch的shape确认无误再进入建模阶段for x, y in train_loader: print(x.shape, y.shape); break输出应该是torch.Size([128, 1, 28, 28]) torch.Size([128])。这一步能同时确认通道数、图像尺寸和标签数量是否匹配避免后面网络定义里维度对不上排查半天。4. 复现AlexNet用于MNIST的PyTorch源码网络定义与训练循环4.1 网络定义该保留的保留该缩的缩下面这份代码是我在MNIST上验证过多次的AlexNet裁剪版。完整保留AlexNet的5段卷积加3段全连接骨架但把卷积核尺寸从11乘11、5乘5、3乘3缩成统一的3乘3特征图数量从96、256、384逐段减少到32、64、128全连接从4096降到512和128。每一层都紧跟ReLU最后两层全连接之间插入Dropout。核心思想不变前段卷积不断把空间尺寸压小、把通道数加深后段全连接把特征图展平后映射到10个类别。import torch import torch.nn as nn class AlexNetMNIST(nn.Module): def __init__(self, num_classes10, dropout0.5): super().__init__() # 特征提取部分5段卷积对应原始AlexNet的前5层 self.features nn.Sequential( # 输入(1, 28, 28) - (32, 14, 14) nn.Conv2d(1, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # (32, 14, 14) - (64, 7, 7) nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # (64, 7, 7) - (128, 7, 7)这层不做池化 nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), # (128, 7, 7) - (128, 7, 7) nn.Conv2d(128, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), # (128, 7, 7) - (64, 4, 4)输出特征图尺寸到这里 nn.Conv2d(128, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), ) # 分类部分对应原始AlexNet的3层全连接 self.classifier nn.Sequential( nn.Dropout(pdropout), nn.Linear(64 * 4 * 4, 512), nn.ReLU(inplaceTrue), nn.Dropout(pdropout), nn.Linear(512, 128), nn.ReLU(inplaceTrue), nn.Linear(128, num_classes) ) def forward(self, x): x self.features(x) x torch.flatten(x, start_dim1) x self.classifier(x) return x这份代码里第一个值得注意的地方是输入通道数写的是1而不是3——彩色图像是3通道MNIST是单通道灰度图很多从ImageNet代码抄过来的人会漏改这个参数导致前向传播直接报维度错误。第二个关键是特征图尺寸变化28乘28经过两次池化变成7乘7第三、四层卷积不改变空间尺寸第五层卷积后再池化变成4乘4所以全连接层输入维度是64乘4乘4等于1024。这个数字必须和池化层设置严格对齐改任一层卷积的padding或stride后面全连接的in_features都得跟着改。卷积核统一用3乘3、padding为1时输出尺寸不变这是ResNet风格的做法和原始AlexNet的11乘11大核不同。MNIST图像只有28乘28大核感受野会迅速覆盖全图损失局部特征提取能力。实际效果上3乘3小核在这个数据集上准确率更高训练也更快。Dropout取0.5是AlexNet原文的配置全连接层放在Dropout后面意味着训练时这一层的输入会随机失活一半神经元测试时则全部保留。这个不对称设计正是防止过拟合的关键。4.2 训练循环学习率、损失函数和优化器的选择import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model AlexNetMNIST().to(device) criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.01, momentum0.9) def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0.0 correct 0 total 0 for images, labels in loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) _, predicted torch.max(outputs, dim1) correct (predicted labels).sum().item() total labels.size(0) return total_loss / total, correct / total损失函数用CrossEntropyLoss它内部把softmax和负对数似然合在一起所以网络最后一层不需要额外加softmax激活。优化器用带动量的SGDlr取0.01momentum取0.9。MNIST上没有太大必要上AdamSGD加动量在这个数据集上收敛稳定测试集准确率往往比Adam高一两个百分点——这也是老任务里大家仍然抱着SGD不放的重要原因。model.train()必须显式调用它会让Dropout生效下面验证循环里对应的model.eval()则关闭Dropout让所有神经元都参与推理。少了这两行训练和测试阶段的模型行为就是错乱的。4.3 验证循环与模型保存三个容易被忽视的细节def evaluate(model, loader, criterion, device): model.eval() total_loss 0.0 correct 0 total 0 with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) total_loss loss.item() * images.size(0) _, predicted torch.max(outputs, dim1) correct (predicted labels).sum().item() total labels.size(0) return total_loss / total, correct / total # 训练主循环10个epoch每轮打印训练和验证指标 model AlexNetMNIST().to(device) for epoch in range(1, 11): train_loss, train_acc train_one_epoch(model, train_loader, optimizer, criterion, device) test_loss, test_acc evaluate(model, test_loader, criterion, device) print(fEpoch {epoch:02d} | fTrain Loss {train_loss:.4f} | Train Acc {train_acc:.4f} | fTest Loss {test_loss:.4f} | Test Acc {test_acc:.4f}) # 保存模型结构和参数方便后续单独加载验证 torch.save(model.state_dict(), alexnet_mnist.pth)验证循环里的with torch.no_grad()不止是省显存的问题——它告诉PyTorch这块计算不需要构建反向传播图省掉大量中间缓存跑验证集的速度能快一倍以上。我见过不少新手验证时忘写这一行准确率计算没错但每次验证都要等很久还容易把显存撑爆。模型保存用的是state_dict()而不是整个model对象前者只存参数字典跨机器迁移时不需要版本完全一致的PyTorch环境是工程上的常规做法。加载时用AlexNetMNIST().load_state_dict(torch.load(alexnet_mnist.pth))即可。5. 训练手写数字识别必踩的五个坑现象、原因、解决5.1 loss一直不降稳定在2.3左右现象训练了5个epochloss纹丝不动准确率一直卡在10%附近——和随机猜测一样。原因学习率太大SGD更新一步直接越过最优区域loss在代价函数表面震荡但始终落不进谷底。MNIST数据分布本身相对干净从0.01这个AlexNet原文常用的学习率起步大概率是正常的但如果输入没做归一化、像素值以0到255的整数进入网络同样的学习率就会偏大梯度大权重一步跳出太远。解决先把输入归一化到0到1区间并标准化再确认学习率从0.01往下搜——0.01不降就试0.005、0.001。我的经验是如果loss曲线的波动幅度超过0.5基本可以判定学习率过大这个现象在MNIST上尤其明显因为任务简单正常loss应该平滑下降才对。5.2 训练集准确率能到97%以上测试集却卡在90%上不去现象训练集一轮比一轮准测试集涨到90%左右就停住了再怎么跑都上不去。原因网络结构表达能力不足或者说这个结构在这个深度下欠拟合——注意欠拟合不一定只表现为训练集准确率低也可能表现为训练集很好但测试集上不去的“结构瓶颈”。MNIST看起来简单但手写数字的边界噪声和小形变不少只有两层卷积的结构很难充分建模这些变化。解决换用上面第4节给出的5层卷积结构或者增加卷积核数量比如把第一层32个卷积核加到64个。我把这类问题称为结构欠拟合——这时候不用调学习率换网络本身才是正确的路。5.3 epoch 8之后测试集准确率不升反降现象前8个epoch测试集准确率稳步上升从第9个开始小幅回落而训练集准确率仍然在涨。原因过拟合开始占据主导。模型参数数量远多于训练信息量开始把训练数据里的个体噪声当作通用规律记住。这是全连接层大量参数带来的经典问题。解决检查Dropout是否启用训练阶段是否调用了model.train()把Dropout的失活比例从0.5调高到0.6或0.7。如果数据增强目前是关的可以加一个随机的平移或旋转——MNIST上轻度平移就够幅度控制在正负两个像素旋转控制在正负10度以内太大会把“6”和“9”这类数字搞混反而伤害准确率。5.4 第一次跑报错预期输入是4D张量实际拿到的是2D现象数据进入卷积层时报错大意是Expected 4D input, got 2D。原因卷积层的输入要求是[N, C, H, W]四维N是batch sizeC是通道数H和W是图像高宽。直接把单张图或一维数组喂进去维度必然对不上。这个问题在从全连接网络代码改成卷积网络的场景里特别常见——全连接网络接受的是展平后的二维输入[N, features]。解决确认DataLoader输出的每个batch是四维张量形状为[128, 1, 28, 28]如果是单张图用tensor.unsqueeze(0)扩出一个batch维度。这是新手最常见的前向传播报错看到关键字4D先查shape不要先去改网络结构。5.5 用GPU训练反而比CPU更慢现象明明是CUDA环境跑一个epoch比CPU还慢GPU利用率也上不去。原因MNIST单张图太小数据搬运和kernel启动的开销盖过了计算收益batch小时尤其明显。这不是代码错误是任务和数据规模的错配。解决如果坚持用GPU把batch_size提高到512或1024同时把pin_memoryTrue打开如果是在本地体验学习过程纯CPU跑这个小项目完全足够每个epoch半分钟级别根本犯不着上GPU。这也正是MNIST作为入门数据集最大的好处——它对算力零要求任何机器都能在十分钟内完成全流程验证。6. 实验报告的正确写法不堆过程让数据自己说话6.1 两张图决定报告的说服力loss曲线和准确率曲线很多实验报告写得像流水账记录每一天跑了什么、改了什么但没有一条能说明问题的曲线这在评审眼里等同于没做实验。正确做法是让代码自动记录每个epoch的训练loss和测试准确率训练结束后画两张图并排展示左图是loss随epoch下降的曲线右图是测试准确率随epoch上升的曲线。两张图放在一起就能直观看出模型是否收敛、有没有过拟合拐点、学习率是否合适。配合上文特征可视化产出的卷积核输出图这份实验报告就有了一层大多数人没有的深度——别人只报准确率数字你还能解释这个数字为什么是合理的。6.2 复杂度和复现性描述是区分课程作业和工程记录的分水岭报告里至少要有三个数字模型参数量、单epoch训练时间、最终测试集准确率。参数量可以用sum(p.numel() for p in model.parameters())一行代码算出来我的裁剪版结构大约是几十万到一百万的量级远小于原始AlexNet的6000万。记录训练时间用time.time()包住训练循环即可。这三个数字加上用torch.manual_seed(0)固定随机种子可以让任何一个拿到源码的人复现出接近的结果。实验报告的写法上我习惯把上面避坑章节里的关键问题作为“调参记录”写进附录——某个现象出现了、怎么定位原因、怎么解决这种叙述比任何华丽的实验结果都可信。6.3 动手改一个参数把训练脚本变成调参实验台全文的最后一条经验也是我最想让读者带走的一条把这里的SGD换掉。比如换成Adam保持其他条件不变跑完10个epoch对比两组准确率和loss曲线的差异。再比如把Dropout从0.5改成0.2或0.8观察过拟合程度的变化。这些单变量实验每一组只花几分钟却是理解卷积神经网络训练动力学最便宜的方式。我当年就是靠反复改这一处参数才把Dropout、学习率、batch size这几件事从“背结论”变成“有手感”的。这个项目真正的价值也正在于此——价值不在跑出99%的准确率而在于你亲手改过的每个参数都会在后续真实项目里变成直觉。希望这篇笔记帮到你少踩我刚才说的那些坑。本文还有配套的精品资源点击获取
返回列表