ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python复现AlexNet花卉分类:经典卷积网络在小数据集的强大表现

Python复现AlexNet花卉分类:经典卷积网络在小数据集的强大表现 简介这套基于Python的AlexNet花卉分类识别系统面向深度学习和图像分类初学者也适合作为毕业设计、课程作业或工程实训项目参考。采用预训练AlexNet模型在Kaggle数据集上训练可区分10类花卉准确率约96%。资源共49个文件包含5个Python脚本训练、预测、模型定义等、40张花卉及预测结果JPEG图片以及JSON类别配置和Markdown说明文档压缩包仅1.67MB结构清晰易上手。已有345人学习下载。通过学习代码结构和运行结果读者既能掌握迁移学习与卷积神经网络分类任务的完整流程也能获得数据集组织、模型评估与预测可视化的实践思路是一份轻量实用的入门级AI项目资料。1. 用 Python 复现 AlexNet 做花卉分类为什么 2012 年的网络反而更好用你拿着 102 类花卉数据集兴致勃勃地上了个 Vision Transformer跑了二十个 epoch验证集准确率卡在七成上下不去显存倒是吃了不少。换回 AlexNet同一份数据、同一个训练流程反而快速收敛到 85% 以上。这不是玄学是小数据集和模型容量匹配的必然结果。标题里说的「基于 Python 的 AlexNet 的花卉分类识别系统」就是用 Python 生态把 AlexNet 这个经典卷积网络搭起来在花卉数据集上完成训练、验证和单张图片预测的完整闭环。它适合两类人一类是刚入门深度学习的 Python 开发者想找个能跑通、能看懂、能改参数的网络练手另一类是做课程设计或小型工程验证的人需要一个不太吃显卡、收敛稳定、结果好解释的基线方案。下面这套流程我反复用过照着走能少踩一半的坑。2. AlexNet 网络结构选型8 层卷积为什么至今是小数据集花卉分类的默认基线2.1 2012 年的竞赛结构凭什么现在还用AlexNet 在 2012 年 ImageNet 竞赛上的成绩是 top-5 错误率 15.3%比第二名低了近 10 个百分点这件事几乎所有教程都会提。但真正决定「现在还用不用它」的不是历史光环而是它在小规模图像分类任务里的实际表现。花卉分类数据集和 ImageNet 完全是两个物种常见公开花卉集一般是几百到几千张图、17 类到 102 类不等。这个量级下ResNet-50 这类稍深一点的网络都容易过拟合更别提 Transformer 结构对数据量的渴求。AlexNet 的参数量约 6100 万主体是 5 个卷积层加 3 个全连接层。这个规模放在今天不算大但对几千张花卉图片来说容量刚好够用。我用它做过对比实验同一个 17 类花卉数据集从零训练 AlexNet 能在 30 个 epoch 内稳定收敛到 88% 左右而 ResNet-50 从零训练经常在 70% 附近震荡。原因不复杂——模型越大小数据集越难提供足够的梯度信号去拟合那么多参数。还有一个实际理由AlexNet 对硬件要求低。卷积集中在前面几层全连接层参数多但计算量相对小一张入门级显卡甚至纯 CPU 都能在几分钟内跑完一个 epoch。对要做课程设计或者快速验证的人来说这是极大的试错优势。2.2 逐层拆解卷积核大小、池化步长与特征图尺寸AlexNet 的完整结构是 5 个卷积层加 3 个全连接层。理解这个网络最好的方式是把每一层的输入输出尺寸算一遍。以 224×224 的 RGB 输入为例我整理了下面这张表这也是我在代码里手写网络时的依据。层名操作核大小 / 步长输出尺寸conv1卷积96 个 11×11步长 4padding 255×55×96pool1最大池化3×3步长 227×27×96conv2卷积256 个 5×5步长 1padding 227×27×256pool2最大池化3×3步长 213×13×256conv3卷积384 个 3×3步长 1padding 113×13×384conv4卷积384 个 3×3步长 1padding 113×13×384conv5卷积256 个 3×3步长 1padding 113×13×256pool5最大池化3×3步长 26×6×256fc6全连接9216 → 40964096fc7全连接4096 → 40964096fc8全连接4096 → 类别数N注意 conv1 用大核 11×11 并且步长 4这是 AlexNet 的一个显著特征。大卷积核在浅层能快速扩大感受野对花卉这种主体集中在画面中央的数据来说第一层就能捕捉到花瓣边缘和整体轮廓信息。后面的 conv3 到 conv5 全部换成 3×3 小核用来提取更精细的纹理特征。池化层全部用最大池化窗口 3×3、步长 2这比常见的 2×2 池化有轻微重叠。重叠池化在当时是个创新点能减少信息丢失现在实现里基本都保留了这个设定。fc6 之前要把卷积输出展平6×6×256 算出来是 9216这个数字要在代码里写对错了网络直接跑不起来。2.3 LRN、Dropout、ReLU哪些组件被留下来了原版 AlexNet 里有局部响应归一化LRN作用是让相邻卷积核的输出互相抑制。PyTorch 的 torchvision 实现里默认去掉了这一层实测对花卉分类的结果影响很小性能还能提升一些因为 LRN 计算开销不小且在当前任务上收益有限。我的建议是手写时直接不写 LRN省得给自己找麻烦。ReLU 是必须保留的。卷积层和全连接层后面都跟 ReLU 激活它解决了深层网络梯度消失的问题。另一个必须保留的是 Dropout原版网络在 fc6 和 fc7 后面各接了一个概率为 0.5 的 Dropout。这点在小数据集上极其关键——全连接层的参数量占整个网络的 90% 以上是过拟合的重灾区Dropout 相当于让全连接层每次训练只更新一部分神经元变相做了模型集成。花卉分类任务里fc8 的输出维度要从 ImageNet 的 1000 改成你的类别数比如 17 类就改 17。整个网络的其余部分完全不用动。这也就是 AlexNet 作为基线最舒服的地方——改一行数字就能切换到不同规模的数据集。3. 手写 AlexNet 的 PyTorch 实现卷积核、池化步长与全连接层的参数细节3.1 手写 AlexNet 网络类每行代码对应的结构PyTorch 里搭 AlexNet 非常直接我用 nn.Module 把每一层写清楚。下面是完整可运行的网络定义forward 里每一步都标注了特征图的形状变化方便你对着 2.2 节的表格检查。import torch import torch.nn as nn class AlexNet(nn.Module): def __init__(self, num_classes17): super().__init__() # 特征提取部分5 个卷积层 3 个池化层 self.features nn.Sequential( # 输入: 3 x 224 x 224 nn.Conv2d(3, 96, kernel_size11, stride4, padding2), # - 96 x 55 x 55 nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2), # - 96 x 27 x 27 nn.Conv2d(96, 256, kernel_size5, stride1, padding2), # - 256 x 27 x 27 nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2), # - 256 x 13 x 13 nn.Conv2d(256, 384, kernel_size3, stride1, padding1), # - 384 x 13 x 13 nn.ReLU(inplaceTrue), nn.Conv2d(384, 384, kernel_size3, stride1, padding1), # - 384 x 13 x 13 nn.ReLU(inplaceTrue), nn.Conv2d(384, 256, kernel_size3, stride1, padding1), # - 256 x 13 x 13 nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2), # - 256 x 6 x 6 ) # 分类部分3 个全连接层中间带 Dropout self.classifier nn.Sequential( nn.Dropout(p0.5), nn.Linear(256 * 6 * 6, 4096), # 展平后 9216 维 nn.ReLU(inplaceTrue), nn.Dropout(p0.5), 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) # 保留 batch 维度展平其余维度 x self.classifier(x) return x这段代码的关键点有几个。第一卷积层的 padding 和 stride 必须和表里一致conv1 的 padding 是 2 而不是 0因为 torchvision 实现统一按这个配置调过照着写输出尺寸才精确匹配 55×55。第二展平用的是 torch.flatten(x, start_dim1)这样会把每个样本的 256×6×6 特征图拉成 9216 维向量保持 batch 维度不动batch size 是多少都不影响这一行。第三classifier 里第一个 Linear 的输入维度必须写 256 * 6 * 6 9216如果前面任何一个池化层步长写错这里就会报维度不匹配。训练时注意最后的 Linear 层输出的是原始 logits不要在模型内部加 Softmax交叉熵损失函数内部会处理。加了反而会在计算损失时出问题。3.2 数据集目录组织与 DataLoader 参数花卉分类数据集的目录组织直接决定你能否用几行代码完成数据加载。常见的做法是按类别建文件夹训练集和验证集分开。我一般是这样的目录布局flower_data/ ├── train/ │ ├── rose/ │ │ ├── 001.jpg │ │ └── 002.jpg │ ├── tulip/ │ └── sunflower/ └── val/ ├── rose/ ├── tulip/ └── sunflower/PyTorch 的 torchvision.datasets.ImageFolder 可以自动读取这种结构并按文件夹名生成类别索引。配合 transforms 做数据预处理加载代码如下from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集预处理随机裁剪、水平翻转、归一化 train_transform transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 验证集预处理只做中心裁剪不做数据增强 val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.ImageFolder(rootflower_data/train, transformtrain_transform) val_dataset datasets.ImageFolder(rootflower_data/val, transformval_transform)训练集先 Resize 到 256再随机裁剪到 224这是 AlexNet 官方输入尺寸的标准做法。Resize 到 256 不是为了好看而是给随机裁剪留出空间——如果直接 Resize 成 224 再裁裁剪的随机性就没了。RandomHorizontalFlip 对花卉这类对称性较强的物体是免费的数据增强几乎不会引入错误标注。验证集只用 CenterCrop不做随机增强才能保证评估指标稳定可复现。DataLoader 有两个参数需要设置合理。batch_size 在显存允许的情况下取 32 或 64num_workers 在 Windows 上建议设为 0在 Linux 上可以设成 CPU 核心数的一半否则会碰到多进程加载数据导致程序卡死的怪问题。3.3 torchvision 里现成的 AlexNet什么时候手写什么时候偷懒torchvision 提供了官方实现加载方式很简单。我一般两种场景下直接用官方版本一是要做迁移学习二是懒得自己调试网络定义。但如果你正在学习或者需要魔改网络结构手写是更好的选择。在实际代码里两者可以配合使用。import torchvision.models as models # 加载官方实现的 AlexNet不带预训练权重 model models.alexnet(weightsNone) # 修改最后一层适配花卉类别数 model.classifier[6] nn.Linear(4096, 17)官方实现和手写版本有一个细微差别官方将 Dropout、ReLU 等层直接嵌入 classifier 这个 Sequential 中索引为 6 的位置就是最后的全连接层。修改时用 model.classifier[6] 替换即可这个写法在各类开源代码里几乎是标准操作。手写版的好处是每个参数都暴露在眼前出了问题能快速定位官方版的好处是代码更少更不容易出错。4. 训练闭环与调参损失曲线判读、三个必调参数与验证习惯4.1 最小训练脚本从 DataLoader 到 loss.backward网络定义好、数据加载好后训练循环本身并不复杂。这里给一个可以直接改用的最小脚本每轮记录训练损失和验证集准确率并保存效果最好的模型。import torch import torch.nn as nn from torch.optim import SGD from torch.optim.lr_scheduler import StepLR device torch.device(cuda if torch.cuda.is_available() else cpu) model AlexNet(num_classes17).to(device) criterion nn.CrossEntropyLoss() optimizer SGD(model.parameters(), lr0.001, momentum0.9, weight_decay5e-4) scheduler StepLR(optimizer, step_size15, gamma0.1) epochs 40 best_acc 0.0 for epoch in range(epochs): # 训练阶段 model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) epoch_loss running_loss / len(train_loader.dataset) # 验证阶段 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_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() val_acc correct / total print(fEpoch {epoch1}/{epochs} | Loss: {epoch_loss:.4f} | Val Acc: {val_acc:.4f}) # 保存验证集准确率最高的模型 if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_alexnet_flower.pth) scheduler.step()训练脚本选择 SGD 而不是 Adam是因为 AlexNet 在小数据集上配合 SGD 的收敛更平滑不容易像 Adam 那样在后期震荡。momentum0.9 是几乎所有 CNN 训练的默认值它能加速收敛并减小梯度方向的波动。weight_decay5e-4 是一个经验值相当于对权重做 L2 正则专门对付全连接层的过拟合风险。每 15 个 epoch 学习率乘 0.1 这种阶梯式下降比全程固定学习率效果好很多——前期大步快跑后期小步精修。4.2 三个必调参数learning rate、batch size、weight decay 的取值与组合这三个参数是训练效果的分水岭。我把在花卉分类任务上的经验区间和对应的现象整理成下表方便你对照着调。参数推荐区间调小 / 调大时的表现learning rate0.0001 ~ 0.001偏大loss 震荡不降偏小loss 下降极慢batch size16 ~ 64偏大GPU 显存吃紧偏小训练不稳定weight decay1e-4 ~ 5e-4偏大模型欠拟合偏小过拟合加重学习率是对结果影响最大的单个参数。从零训练时我通常从 0.001 开始如果 loss 在前几个 epoch 没有明显下降就先降到 0.0003 试一轮而不是反复加大。batch size 在花卉任务里不建议超过 64因为数据集本身不大过大的 batch 会让梯度方向太接近全局梯度反而减弱了随机性带来的正则化效果。weight decay 建议从 5e-4 起步如果你的验证集准确率和训练集差距超过 15 个百分点就把 weight decay 加到 1e-3同时加大 Dropout 概率。这三个参数不是独立的。学习率大时weight decay 的作用也会被放大因为权重的更新步长更大正则项的梯度相对更明显。训练初期可以先固定 batch size用学习率和 weight decay 的组合做几次对比实验找到让验证集准确率平滑上升而非剧烈波动的组合。4.3 看曲线而不是看 Loss 数值训练日志与评估逻辑训练过程中我只看两条曲线训练损失曲线和验证集准确率曲线。很多人习惯盯着 loss 的绝对值其实绝对值没有太大意义重要的是它的变化趋势。如果训练损失在前 10 个 epoch 内从 4.x 降到 0.8 左右说明收敛方向正确如果 loss 在第 20 个 epoch 后还在缓慢下降但验证准确率不再上升说明模型开始进入过拟合区间应该停止训练或者调低学习率继续微调。验证集准确率的判读标准是训练完成时训练集准确率如果能到 95% 以上验证集至少应该有 80% 以上。如果验证集卡在 70% 以下别急着堆数据增强先检查是不是预处理环节出了纰漏——比如没有对验证集做归一化或者训练和验证的输入尺寸不一致。这类低级错误造成的准确率瓶颈比任何调参问题都常见。保存 best 模型也是值得养成的好习惯。每一轮验证结束后如果准确率比历史最高值高就把 state_dict 存下来。这样即使后续训练过拟合也能立刻回滚到验证集表现最好的权重。这份保存下来的模型文件就是你这套系统最终要用的那个黑匣子。5. 避坑与常见问题排查让花卉分类模型翻车的 5 个真实场景5.1 输入尺寸不一致训练和验证用了不同的裁剪方式现象训练过程一切正常loss 稳步下降但在验证集上准确率始终只有 50% 左右没比随机猜好多少。检查了网络结构、数据集路径都没有问题。原因训练时用了 RandomResizedCrop把图片随机裁剪缩放成 224验证时如果忘了 CenterCrop直接把尺寸不一的原始图片送进网络模型在训练时见过的输入分布和验证时的输入分布完全对不上。AlexNet 的卷积层本身对输入尺寸有一定容忍度但全连接层要求输入维度固定如果验证图片经过 Resize 后不是 224×224前向传播会直接报错如果数据加载阶段隐式地把图片 Resize 成了其他尺寸就会造成上述这种「网络能跑但结果离谱」的情况。解决严格保证训练和验证共用同一套尺寸逻辑即先 Resize 到 256再分别执行 RandomResizedCrop(224) 和 CenterCrop(224)。把两个 transform 定义写在一起改动时同时改不要分开维护。5.2 训练时 loss 卡住不降学习率和归一化的问题现象训练损失从初始值开始就没怎么动一直徘徊在 4.0 左右或者下降极其缓慢20 个 epoch 才从 4.2 降到 3.9。原因最常见的原因是学习率设置不当。0.001 是一个合适的起点但如果你的 batch size 比较小或者数据集比较难这个学习率可能偏大导致 loss 在某个区间震荡更大的可能是输入没有被正确归一化。如果 transform 里忘了加 Normalize 步骤或者忘了 ToTensor图片数据会以 0 到 255 的整数形式进入网络卷积层输出的数值分布会异常梯度更新方向混乱。解决先确认 transform 的正常顺序是 Resize → Crop → ToTensor → Normalize。然后打印一批数据的均值images.mean()如果结果不是接近 0 的值说明归一化没生效。最后把学习率设为 0.0003 跑 5 个 epoch看 loss 是否有下降趋势逐层排查。5.3 训练集准确率 95%验证集只有 60%经典的过拟合现象训练集准确率快速冲高10 个 epoch 就到 95% 以上但验证集准确率停在 60% 附近并且随着训练继续也没有提升。原因AlexNet 的全连接层参数量有近 6000 万在几千张图片上训练模型很容易把训练集样本的噪声细节也记下来而不是学到泛化的花瓣纹理特征。Dropout 概率 0.5 已经被设置但仅靠它不足以对抗这种规模的参数冗余同时训练集样本太少也放大了过拟合问题。解决按优先级从高到低做三件事。第一给训练集加上更强的数据增强随机旋转 ±15 度、颜色抖动ColorJitter相当于用更少的原始样本生成更多样的训练数据。第二把 weight_decay 从 5e-4 提高到 1e-3压缩权重的数值空间。第三降低全连接层容量比如把 fc6 和 fc7 的输出维度从 4096 改为 2048这对手写网络来说是一行代码的事对 torchvision 官方模型则需要重建 classifier。5.4 环境问题VS Code 和 Python 解释器的经典报错现象在 VS Code 里写好训练脚本按 F5 运行弹出类似cannot be resolved against python helper roots的错误代码完全无法执行。这在刚配置好 Python 环境的新机器上特别常见。原因VS Code 的 Python 插件需要指定一个明确的解释器路径来解析代码当你的训练脚本位于 conda 虚拟环境或 venv 虚拟环境中而 VS Code 默认使用了全局 Python 解释器时就会报这个错。它本质上不是你的代码问题是编辑器和解释器之间的映射没配对。解决在 VS Code 中按CtrlShiftP输入并选择Python: Select Interpreter从列表里选中你创建虚拟环境对应的 python 路径然后重新加载窗口。如果你用的虚拟环境没有出现在列表里先用命令行的which python找到路径再在设置里手动填入。这里顺便建议新建项目时就用python -m venv venv建好独立环境避免全局环境里各类库版本互相污染。5.5 预测单张图片时准确率暴跌预处理没有保持一致现象训练和验证集准确率都有 90% 以上但拿一张网上的花卉图片跑预测结果全是错的。换成验证集里的图片又一切正常。原因预测单张图片时的预处理环节和训练时不一致。最典型的是用 OpenCV 的 cv2.imread 读取图片它是按 BGR 通道顺序读的和训练时 PIL 读取的 RGB 顺序相反网络看到的颜色信息完全不同另一个典型问题是直接把原始尺寸的图片塞进网络没有做 Resize、Crop 和归一化。解决我的建议是预测时统一用 PIL 读取图片并且把训练时的 transform 全部复用上只把 RandomResizedCrop 换成一个固定的 Resize 加 CenterCrop。写一个单独的预处理函数确保输入网络的张量和训练时的分布一致不要在图省事上翻车。6. 一个进阶技巧迁移学习配合预训练权重把准确率再往上提一档自己从零训练 AlexNet 能到 85% 以上但想冲 95%最优路径是复用 ImageNet 上的预训练权重做迁移学习。这个思路不是新东西但在花卉分类这种中规模数据集上效果立竿见影因为 ImageNet 上预训练的卷积层已经学会了丰富的边缘、纹理和颜色特征这些底层特征对花卉同样适用。import torchvision.models as models # 加载 ImageNet 预训练权重 model models.alexnet(weightsmodels.AlexNet_Weights.IMAGENET1K_V1) # 替换最后一层输出维度改成花卉类别数 model.classifier[6] nn.Linear(4096, 17) # 冻结全部的卷积特征层只训练分类器 for param in model.features.parameters(): param.requires_grad False # 微调只更新最后的全连接层 optimizer SGD(model.classifier.parameters(), lr0.001, momentum0.9, weight_decay5e-4)这样做的好处是训练量大幅减少脚本可以在 CPU 上也能较快跑完。实践上还能更进一步先在冻结卷积层的情况下训练 10 个 epoch让分类器收敛然后解冻最后两个卷积层把学习率降到 0.0001 再微调 10 个 epoch往往能再提升 1 到 2 个百分点。判断这套做法是否有效我固定看一个指标微调后的验证集准确率和从零训练版本之间的差值。如果提升不足 3 个百分点说明你的数据集规模和预训练特征的相关性不匹配这时就要考虑减少冻结层数、让更多层参与微调。如果提升非常明显除了享受这个结果也要在最终记录里写清楚实验组是预训练微调对照组是从零训练两组在相同随机种子下运行不要拿一次随机结果下结论。这是我踩过数据波动带来的教训每次做对比实验都乖乖加 seed 固定随机性之后的结果才拿得出手。希望这在你的实验里也能少走点弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表