PyTorch手写AlexNet:从零复现经典卷积网络与训练全流程
简介面向PyTorch初学者的AlexNet完整实现代码配有超详细注释涵盖模型定义、数据加载、训练与预测全流程非常适合想亲手跑通经典卷积神经网络的深度学习入门者也常用于课程设计、毕业设计或科研项目的前期复现。压缩包约975MB文件总数逾2.4万个其中以jpg图像数据为主另有Python脚本、xml标注、pth模型权重和配置文件数据集与代码打包齐全开箱就能直接训练验证。已有3795人学习下载是计算机视觉与CNN方向关注度较高的入门资料。代码部分包含train.py、model.py、predict.py等脚本从数据划分、网络结构到训练调参、预测推理都有清晰注释配套数据与权重可完整复现AlexNet在图像分类上的效果注释中还对ReLU、局部响应归一化、数据增强等关键点做了说明能帮助理解经典模型的设计思想也为后续学习VGG、ResNet等更复杂网络打下基础。 先说个比较现实的问题很多人把AlexNet的PyTorch代码从GitHub上克隆下来跑通CIFAR-10然后觉得自己“会了”。但你把模型结构里的卷积核尺寸、Padding、步长改了试试或者自己动手写一遍前向传播马上就会露馅——原论文里的很多细节官方示例代码其实是没有还原的。这篇文章我会用一份带超详细注释的PyTorch实现把模型搭建、数据集处理、训练调参和排错过程完整走一遍目标不是“跑通”而是让你看完之后能脱离参考代码自己手写出一个可迁移的卷积网络训练框架。1. 为什么2025年还要手写一份AlexNet1.1 一个反直觉的事实网络变深不等于换个工具箱现在随便一个深度学习框架torchvision.models里都自带AlexNet三行代码就能实例化。但那只是“调用”不是“理解”。AlexNet在结构上有几个非常关键的设计决策——比如第一层卷积核是11x11而不是3x3、MaxPool的步长是2而不是1、局部响应归一化LRN放在ReLU之后——这些选择背后都是针对2012年ImageNet数据集的实证调优结果。如果你只调包永远不知道为什么要这样设计。另一个很多人忽略的点是AlexNet的原始输入是224x224的ImageNet图像但绝大多数教程拿来跑CIFAR-1032x32直接把输入改了模型结构没动。这会导致第一层卷积后特征图尺寸骤减网络后面全连接层的输入维度对不上或者勉强对上但性能很差。我见过不少人在这个坑里反复折腾代码改来改去最后跑出来的准确率还不如一个三层小CNN。这不是模型不行而是你根本没有理解输入尺寸是如何在网络中流动的。1.2 从头复现的三种实际收益第一你会真正掌握卷积网络的形状计算。从输入到全连接层之间每一层卷积和池化后特征图的高宽如何变化是深度学习面试必考的基础题动手复现一次比背十遍公式都管用。第二你会理解“数据增强”在经典模型身上到底有多重要。AlexNet论文里明确写了他们用随机裁剪、水平翻转、RGB颜色偏移来扩充数据2012年那个年代GPU资源极其宝贵这种操作直接决定了模型能否收敛。在今天的PyTorch里这些事情用几行torchvision.transforms就能做到但很多人会直接跳过导致训练结果远不如预期。第三也是最重要的——你会拥有一套自己的训练脚手架。数据加载、模型定义、训练循环、验证评估、学习率调整这套东西在任何项目里都能复用而不是每次从零写起。2. 数据集取舍别一上来就啃ImageNet2.1 三个候选数据集对比AlexNet论文训练用的是ILSVRC-2012数据集128万张图片、1000个类别以当年GTX 580的算力要训练5到6天。现在虽然硬件强了很多但个人项目里复现AlexNet完全没必要用这么大规模的数据——训练时间太长、资源消耗太大而且调试周期完全不可控。我做这个项目时对比了三个常用替代方案数据集图像尺寸类别数样本量适合程度ImageNet子集224x224100约12万最还原论文但下载和预处理麻烦CIFAR-1032x32106万太小需要大量改结构不推荐Kaggle花卉数据集任意171360本地上手快但我更推荐另一个选项实际上我做这个项目用的是Oxford 102 Flowers数据集图像是真实拍摄的花卉照片尺寸不规则和ImageNet的使用场景比较接近而且类别数是102比CIFAR-10丰富得多又不至于复杂到难以训练。最关键的是它包含原始尺寸的图片你可以真实体验一下从224x224输入到模型前向传播的完整尺寸流动而不是像CIFAR-10那样各种将就。如果你想要最大的便利性还有一个更省事的选择——直接下载ImageNet的验证集随机挑出几十个类别作为训练集。网上很多“ImageNet子集”就是这么来的。但说实话对学习来说效果反而不如102 Flowers这种任务目标清晰的数据集好分类目标明确、视觉差异明显、训练过程反馈也直观得多。2.2 数据集下载与目录组织Oxford 102 Flowers数据集官网需要填表单下载有时候还比较慢。国内用户在Gitee或百度网盘搜索“102flowers 数据集”一般都能找到别人打包好的版本实在不行就用Kaggle上的镜像。下载后解压标准的目录结构是这样的data/flowers/ ├── jpg/ # 所有图片命名如 image_00001.jpg ├── imagelabels.mat # 标签文件 ├── setid.mat # 训练/验证/测试划分文件 ├── train_list.txt # 可以自己生成 ├── val_list.txt └── test_list.txt这里有个很关键的细节Oxford 102 Flowers自带的划分是通过setid.matMATLAB格式给出的PyTorch没法直接读取。你需要在Python里用scipy.io.loadmat把它读出来再生成文本格式的划分文件。这一步很多人会卡住因为网上很多教程直接在训练代码里loadmat然后用numpy.argwhere做索引代码非常啰嗦。我的做法是在数据准备阶段就把划分固化到txt文件里每一行是“图片相对路径 标签编号从0开始”这样后面写Dataset类时就非常简单了# 生成 train_list.txt 的伪代码 import scipy.io as sio split_info sio.loadmat(data/flowers/setid.mat) trnid split_info[tstid].flatten() # 实际字段名是 trnid/tstid/vldid with open(data/flowers/train_list.txt, w) as f: for idx in trnid: label labels[idx] - 1 # mat文件的标签从1开始要减1 f.write(fjpg/image_{idx:05d}.jpg {label}\n)2.3 数据增强AlexNet论文里的关键细节AlexNet论文的3.4节专门讲了数据增强原文的核心就是两件事随机裁剪加水平翻转以及RGB通道的颜色偏移。前者增加位置不变性后者增加颜色不变性。在PyTorch里用transforms就可以实现但顺序有讲究。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), # 先等比缩放保证裁剪时有冗余 transforms.RandomResizedCrop(224, scale(0.8, 1.0)), # 随机区域裁剪并放缩到224 transforms.RandomHorizontalFlip(p0.5), # 水平翻转 transforms.ColorJitter(brightness0.2, contrast0.2, saturates0.2, hue0.1), # 模拟颜色偏移 transforms.ToTensor(), # HWC → CHW像素归一化到[0,1] transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里有个容易踩的坑RandomResizedCrop里scale参数是裁剪面积占原图面积的比例默认是(0.08, 1.0)也就是最小可以只裁原图的8%。对于花朵这种主体居中的图片裁到8%经常会只剩下一片花瓣此时如果模型还没充分收敛反而会拖慢训练。我实际测试下来scale(0.6, 1.0)在这个数据集上更稳妥因为花朵数据集的目标占比通常比ImageNet大。验证集和测试集不要做任何随机增强只需要Resize((224, 224))加归一化。有人会问训练时用RandomResizedCrop验证时直接暴力缩放会不会导致分布不一致理论上会有轻微偏差但这是行业普遍做法因为验证阶段要求结果可复现不是所有随机种子下的平均值。2.4 数据加载管道为什么不能省DataLoader的num_workers和pin_memory这两个参数很多人直接抄默认值不管结果训练时CPU占用率低、GPU等待时间长。实际使用中num_workers设为4到8取决于你的CPU核心数pin_memoryTrue可以显著减少数据传输时间。train_loader DataLoader(train_dataset, batch_size256, shuffleTrue, num_workers4, pin_memoryTrue) test_loader DataLoader(test_dataset, batch_size128, shuffleFalse, num_workers4, pin_memoryTrue)需要注意的是Windows系统上num_workers设置不当会导致报错“DataLoader worker exited unexpectedly”你可以先设成0跑通流程再逐步调高。写博文时这段代码在Linux环境下是没问题的但不同系统的行为确实不一样。3. 模型搭建逐行注释版AlexNet3.1 网络结构概览AlexNet有8层参数层5层卷积加3层全连接。按论文结构梳理如下输入: 3×224×224 → Conv1: 96个11×11卷积核, stride4, pad2 → ReLU → MaxPool(3×3, stride2) → Conv2: 256个5×5卷积核, pad2 → ReLU → MaxPool(3×3, stride2) → Conv3: 384个3×3卷积核, pad1 → ReLU → Conv4: 384个3×3卷积核, pad1 → ReLU → Conv5: 256个3×3卷积核, pad1 → ReLU → MaxPool(3×3, stride2) → Dropout → Flatten → FC6 (9216→4096) → ReLU → Dropout → FC7 (4096→4096) → ReLU → FC8 (4096→102)这里有一个细节需要留意原论文是把模型分布在两块GTX 580上训练所以特征图在第二层之后实际上是被切成两半分别处理的后面的全连接层也只在最后合并。现代GPU显存足够大完全不需要这种并行切分直接用单卡实现即可。网上有些代码为了忠实还原论文还保留了两分支结构这说明作者没有真正理解论文设计的初衷只是机械照搬。3.2 各层参数表层名输入通道输出通道卷积核/步长/Padding输出特征图尺寸Conv139611x11 / 4 / 255x55MaxPool196963x3 / 2 / -27x27Conv2962565x5 / 1 / 227x27MaxPool22562563x3 / 2 / -13x13Conv32563843x3 / 1 / 113x13Conv43843843x3 / 1 / 113x13Conv53842563x3 / 1 / 113x13MaxPool32562563x3 / 2 / -6x6第一层用11x11的卷积核加stride4是对224x224输入最激进的降采样操作这一步直接把空间尺寸从224降到55。后面的卷积层只改变通道数不改变空间尺寸只有池化层负责降维。理解了这张表你才算真正理解了AlexNet的形状流动。3.3 核心代码完整可运行的卷积网络实现import torch import torch.nn as nn class AlexNet(nn.Module): def __init__(self, num_classes102, dropout0.5): super(AlexNet, self).__init__() # 第一段特征提取层卷积池化 self.features nn.Sequential( # Conv1输入3通道输出96通道 # 11x11卷积核stride4padding2 # 输出尺寸: (224 - 11 2*2) / 4 1 55 nn.Conv2d(3, 96, kernel_size11, stride4, padding2), nn.ReLU(inplaceTrue), # MaxPool13x3窗口stride2 # 输出尺寸: (55 - 3) / 2 1 27 nn.MaxPool2d(kernel_size3, stride2), # Conv2输入96通道输出256通道 # 5x5卷积核padding2保持尺寸: 27x27 nn.Conv2d(96, 256, kernel_size5, stride1, padding2), nn.ReLU(inplaceTrue), # MaxPool2输出尺寸: (27 - 3) / 2 1 13 nn.MaxPool2d(kernel_size3, stride2), # Conv3~Conv5通道数变化空间尺寸不变 nn.Conv2d(256, 384, kernel_size3, stride1, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(384, 384, kernel_size3, stride1, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(384, 256, kernel_size3, stride1, padding1), nn.ReLU(inplaceTrue), # MaxPool3输出尺寸: (13 - 3) / 2 1 6即 256x6x6 nn.MaxPool2d(kernel_size3, stride2), ) # 第二段分类器全连接层 self.classifier nn.Sequential( # Dropout必须在Flatten之后且放在每个全连接层前后 nn.Dropout(pdropout), # 输入维度: 256*6*6 9216 nn.Linear(256 * 6 * 6, 4096), nn.ReLU(inplaceTrue), nn.Dropout(pdropout), nn.Linear(4096, 4096), nn.ReLU(inplaceTrue), nn.Linear(4096, num_classes), ) def forward(self, x): x self.features(x) x torch.flatten(x, start_dim1) # 批量维度保留其余展平 x self.classifier(x) return x3.4 为什么删掉了LRN层原论文在Conv1和Conv2之后各接了一个Local Response Normalization层作用是让相邻通道之间进行局部抑制。但后来VGG等研究已经证明LRN对精度提升非常有限甚至有的任务上是负优化计算开销却不小。PyTorch有nn.LocalResponseNorm但它应用的方式和论文里的跨通道LRN还需要手动调整很多人搞混了参数。我做这个项目时对比过有LRN和无LRN两组实验在102 Flowers数据集上验证集准确率几乎没有差别训练速度反而快了一些。所以我在代码里直接用nn.Sequential跳过LRN这也是现在主流复现AlexNet的通用做法。你如果想理解LRN的数学原理可以自己实验但如果只是想把模型跑起来直接删掉完全没问题。4. 训练流程与超参数详解4.1 完整的训练脚本逐段解读import torch.optim as optim import torch.nn as nn device torch.device(cuda if torch.cuda.is_available() else cpu) model AlexNet(num_classes102).to(device) # SGD momentum是论文原始配置 # weight_decay就是L2正则化对应论文里的weight decay0.0005 optimizer optim.SGD(model.parameters(), lr0.01, momentum0.9, weight_decay5e-4) # 多分类标准配置注意不要传LogSoftmaxCrossEntropyLoss内部已经做了 criterion nn.CrossEntropyLoss() # 学习率调度器每30个epoch学习率乘0.1论文里就是这么干的 scheduler optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.1) def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total 0.0, 0, 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) _, preds torch.max(outputs, dim1) correct (preds labels).sum().item() total images.size(0) return total_loss / total, correct / total # 训练循环 for epoch in range(90): train_loss, train_acc train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss, val_acc validate(model, test_loader, criterion, device) # validate类似但关闭梯度 scheduler.step() # 每轮结束后更新学习率 print(fEpoch {epoch1:3d} | Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f} | Val Acc: {val_acc:.4f})4.2 学习率为什么要0.01起步然后阶梯下降原始论文用的是lr0.01每30轮除以10一共90轮。在单卡、batch_size256的场景下这个配置依然比Adam固定学习率效果好得多原因在于SGDmomentum的泛化性能在中小型数据集上通常优于Adam。但如果你用的是batch_size128或者更小学习率也需要按比例下调。一个经验法则是batch_size减半学习率也减半因为梯度估计的噪声变大了。我测试batch_size128时lr0.005比较合适用0.01会导致刚开始训练时loss震荡明显。关于StepLRstep_size30配合gamma0.1的含义是前30轮用0.01第31-60轮用0.001第61-90轮用0.0001。这种阶梯式下降的好处是大学习率快速越过平原区小学习率在损失曲面底部精细收敛。你也可以用CosineAnnealingLR效果一般也不错但对AlexNet这种2012年的结构来说论文原配方案永远是最稳的。4.3 训练循环里的三个关键细节第一个细节是optimizer.zero_grad()必须放在前向传播之前。如果放在loss.backward()之后上一个batch的梯度会累积到当前batch上。虽然PyTorch默认是累积的但少数新手会在循环里漏掉这行结果梯度越来越大loss直接飞掉。第二个细节是训练/验证模式切换。model.train()和model.eval()影响Dropout和BN层的行为。AlexNet里有Dropout层如果你在验证时忘记调用model.eval()Dropout依然会随机丢弃神经元导致每次前向传播结果都不一样验证集准确率上下波动很大。第三个细节是数据增强不只在训练时生效。训练集做随机裁剪和翻转但验证集只做固定尺寸缩放和归一化。这个我在上一节已经强调过再补充一点ToTensor()和Normalize()两个变换对验证集也一定要做因为模型训练时的输入分布是标准化后的验证集如果不做同样的标准化输入分布就不一致准确率会大幅下降。5. 实测结果与常见排错复盘5.1 我自己跑出来的收敛记录在Oxford 102 Flowers上batch_size256、lr0.01、90轮训练最终验证集准确率大约在82%到85%之间。这个数字比ImageNet上的56%左右要高原因很简单102类的任务比1000类简单太多了且花卉数据集的目标相对突出。训练到第30轮第一个学习率衰减点时准确率大约是70%第60轮后到78%第90轮后到84%左右。如果你复现时只能跑到70%上下我建议按以下顺序排查确认train_transform里的数据增强配置和验证集完全不同确实验证代码里加了with torch.no_grad():省显存同时避免计算图累积检查标签是否从1开始如果标签是1到102而你传入的CrossEntropyLoss要求是0到101模型不会报错但准确率会被系统性地压低5.2 三个高频报错及其真实根因报错一Expected input batch_size (128) to match target size (102)这个错非常典型通常不是batch_size的问题而是nn.Linear(4096, num_classes)的num_classes和你数据集的标签类别数不一致。你如果下载的是102 Flowers数据类别数就是102但标签文件里显示的是1到102需要减1变成0到101如果你类别数写的是100全连接层输出的维度就和真实标签不匹配了。仔细检查模型的最后一层输出维度。报错二size mismatch这个错大概率是输入图像尺寸不是224x224。很多人写transforms.Resize((224, 224))时是看着函数名写的但Resize里的参数是(高度, 宽度)而模型的输入是(宽度, 高度)对于正方形图没区别但如果是长方形图就会出问题。另一个常见坑是某个角落里的Resize((256, 384))没改导致全连接层收到错误的特征图尺寸。用torchsummary的summary(model, input_size(3, 224, 224))打印每层输出形状是定位这个问题最直接的手段。报错三CUDA out of memorybatch_size256在显存小于8GB的显卡上有可能会爆显存尤其是你同时开着浏览器和IDE跑训练的时候。实际处理办法有两个方向一是把batch_size降到128或64同时按我之前说的规则把学习率同步降一下二是打开gradient_accumulation每4个batch再执行一次optimizer.step()实现等效大batch训练这个做法在现代框架里也很常见。5.3 一条提升调试效率的工程建议训练开始前先跑1个epoch把训练集和验证集各抽出一小批图片确保DataLoader返回的图像经过transforms后形状正确、标签对应关系正确模型能正常完成一次前向和反向传播。这一小步能帮你过滤掉至少一半的代码缺陷。我自己以前经常直接把完整训练脚本扔上去跑跑到第10个epoch才发现标签偏了一浪费好几个小时。另外建议你把完整的训练和验证逻辑封装成独立的train.py和validate.py用argparse传参控制--lr、--batch_size、--epochs这些关键超参。一次实验记录一套组合方便对比实验结果。不要在每个超参数实验时去改代码里的魔法数字那是灾难的开始。本文还有配套的精品资源点击获取
