
简介基于AlexNet的花卉分类识别系统是一套完整的预训练模型应用项目面向Python与深度学习入门者、进阶学习者以及正在筹备毕业设计或课程设计的同学可快速用于图像分类实践或项目立项。系统能区分10种不同类别的花卉训练数据来自Kaggle当前数据集下准确率约为96%代码结构清晰便于二次拓展。压缩包共49个文件以5个Python脚本、40张JPEG样本与结果图像、1个JSON类别配置及1份Markdown说明为主整体大小仅1.67MB结构紧凑。其中包含训练脚本、预测脚本、模型定义、工具函数、数据集与类别映射、结果可视化图片和README说明文档能够帮助读者理清训练、评估与推断的完整流程并根据自带样例快速验证效果。已有346人学习适合用作入门深度学习图像分类的基础参考也可作为课程设计或工程实训的起点。1. 花卉分类识别系统为什么值得用AlexNet复现成本与效果边界花卉分类识别是图像分类任务里最适合上手的场景之一而「基于Python的AlexNet花卉分类识别系统」这个组合几乎是这类小体量任务的标准答案。AlexNet虽然发布年头不短但它的8层结构、6000万参数放在几十类花卉数据上依旧够用训练门槛低、单张推理在CPU上也能到毫秒级还能直接复用ImageNet预训练权重做迁移学习。做这件事的人我通常见到两类一类是想把课程作业做成完整Demo的学生另一类是想快速验证花卉识别效果、不打算从零造轮子的开发者。下面按数据准备、网络实现、训练调参、踩坑记录、推理封装五个环节展开全程给可复现的Python代码。2. 数据集与预处理把花卉图片整理成227×227输入的Python脚本2.1 数据集选型公开花卉集与自建数据集的取舍公共数据集方面Oxford 17 Flowers17个类别每类约80张和Oxford 102 Flowers102类是最经典的两个选择很多分类论文都以它们为基准。第一次做建议先用17类版本类别少、样本量适中跑完一轮30个epoch在普通消费级显卡上十几分钟就够适合先把整条链路跑通。102类的版本留到后面验证模型泛化能力时再用。如果决定自建数据集有个问题要想在前面类别怎么定义。同类花在不同光照、不同角度下差异很大而不同品种的菊花在照片里可能长得几乎一样。我一般要求每类不少于100张刻意收进同株不同角度、晴天和阴天、近景和远景这些情况让模型学的是花的形态而不是拍摄环境。图源选择上用自己拍的照片或明确开放许可的资源更稳妥避免后续演示或部署时惹版权麻烦。2.2 目录结构与批量整理脚本8:2切分训练集和验证集数据目录统一成「类别名作为文件夹名」的结构这是PyTorch的ImageFolder默认支持的组织方式。整理脚本我一般写成这样from pathlib import Path import shutil import random src_root Path(raw_flower_photos) # 原始图片第一层目录是类别名 train_root Path(flower_data/train) val_root Path(flower_data/val) for label_dir in src_root.iterdir(): if not label_dir.is_dir(): continue # 一次收集该类别下所有常见格式的图片 images [p for ext in (*.jpg, *.jpeg, *.png) for p in label_dir.rglob(ext)] if len(images) 20: print(f警告{label_dir.name} 只有{len(images)}张图建议补充) continue random.shuffle(images) split int(len(images) * 0.8) for img in images[:split]: dst train_root / label_dir.name / img.name dst.parent.mkdir(parentsTrue, exist_okTrue) shutil.copy2(img, dst) for img in images[split:]: dst val_root / label_dir.name / img.name dst.parent.mkdir(parentsTrue, exist_okTrue) shutil.copy2(img, dst)逻辑说明遍历原始目录下每个类别文件夹把该类别所有jpg、jpeg、png图片的路径收集起来随机打乱后按8:2比例分别复制到train和val下对应的类别文件夹。用copy2而不是move是为了保留一份原始素材之后补充了新图想重新切分时不需要重新采集。参数说明split 0.8是经验值。每类样本不足50张时我会改成0.85尽量多留训练样本样本充足时0.7也可以验证集更有统计意义。脚本里漏掉格式后缀是常见的翻车点如果原始图里有bmp或webp记得加进扩展名元组里否则训练集比预想少一大截。2.3 预处理管道Resize、Crop与ImageNet归一化的参数细节AlexNet默认输入是227×227×3的RGB图注意不是224×224这个数字经常被人抄错。原因在原始论文第一层卷积核是11×11、步长4输入227时输出才是55×55floor((227-11)/4)155。拿224硬套第一层输出变成54×54后面每层尺寸都会错位报错时一堆人盯着维度问题排查半天。既然要用ImageNet预训练权重预处理也得和ImageNet保持一致from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(227), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.4, contrast0.4, saturation0.4), 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, 256)), transforms.CenterCrop(227), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])逻辑说明训练时先Resize到256再RandomResizedCrop到227等于每轮从原图随机位置、随机大小裁一块区域是一种不需要额外数据就能生效的数据增强验证时不希望结果带随机性所以固定用CenterCrop裁正中间。Normalize的均值和标准差取自ImageNet全量统计只要用预训练权重这组值就不能动改了等于把输入分布整体偏移训练会明显变慢。参数说明RandomResizedCrop默认scale范围是(0.08, 1.0)。对花朵这类主体通常占画面较大面积的场景我会把scale收紧到(0.5, 1.0)避免裁出来的小块里几乎没有花。ColorJitter里saturation建议克制一点花卉识别对颜色敏感饱和度抖动过猛会让模型学到失真的颜色分布。3. AlexNet模型搭建用PyTorch逐层实现卷积与全连接3.1 AlexNet结构拆解从卷积到全连接的层与尺寸AlexNet的主体可以拆成特征提取器加分类器两段。特征提取器是5层卷积前两层后面接MaxPool第3、4层卷积不接池化第5层卷积后面再接一次MaxPool分类器是3层全连接前两层各接一个ReLU和一个Dropout。整个网络的核心思想很简单用卷积逐步把空间尺寸压小、把通道数加多最后展平成全连接向量做分类。网上不少实现把第一层卷积核数量写成96这是照抄原始论文的结果。原始论文因为当年单卡显存不够把96个卷积核拆到两张GPU上并行训练torchvision的现代单卡实现第一层只有64个卷积核。这个差异直接决定你能不能加载官方预训练权重后面避坑章节会详细展开。各层参数可以对照下面这张表层名类型卷积核/池化窗口输出尺寸Conv1卷积64个11×11, stride 455×55×64MaxPool1池化3×3, stride 227×27×64Conv2卷积192个5×5, padding 227×27×192MaxPool2池化3×3, stride 213×13×192Conv3卷积384个3×3, padding 113×13×384Conv4卷积256个3×3, padding 113×13×256Conv5卷积256个3×3, padding 113×13×256MaxPool3池化3×3, stride 26×6×256FC1全连接9216→40964096FC2全连接4096→40964096FC3全连接4096→类别数类别数3.2 PyTorch逐层实现可直接复制的AlexNet代码实际项目里我不会自己从零写features部分直接用torchvision.models.alexnet接预训练权重最省事但为了把结构讲清楚先给你一份自建版本import torch.nn as nn class AlexNet(nn.Module): def __init__(self, num_classes10, dropout0.5): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 64, kernel_size11, stride4), # 227 - 55 nn.ReLU(inplaceTrue), nn.MaxPool2d(3, stride2), # 55 - 27 nn.Conv2d(64, 192, kernel_size5, padding2), # 27 - 27 nn.ReLU(inplaceTrue), nn.MaxPool2d(3, stride2), # 27 - 13 nn.Conv2d(192, 384, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(384, 256, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(256, 256, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(3, stride2), # 13 - 6 ) self.classifier nn.Sequential( nn.Dropout(dropout), nn.Linear(256 * 6 * 6, 4096), nn.ReLU(inplaceTrue), nn.Dropout(dropout), nn.Linear(4096, 4096), nn.ReLU(inplaceTrue), nn.Linear(4096, num_classes), ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) # 展平保留batch维度 x self.classifier(x) return x逻辑说明features部分把227×227×3的输入逐步变成6×6×256的特征图view(x.size(0), -1)把每个样本展平成9216维向量再进分类器。Dropout放在全连接层前面而不是卷积层前面这是AlexNet的原始设计——卷积层本身有空间冗余对Dropout不敏感全连接层参数占比最大、最容易过拟合所以只在分类器里加。参数说明dropout0.5是原始论文的值在迁移学习场景下如果数据量不大我会把前面那个Dropout提到0.7后面那个保持0.5。原始论文里还有一个Local Response Normalization层现代框架里它对最终精度的贡献很小还会拖慢训练和推理速度我一般直接省略实测对花卉分类这类小类别任务几乎没有影响。3.3 训练前必须定的参数Dropout、学习率与预训练权重模型结构定了之后有三个参数要在写训练脚本前想清楚。第一个是学习率。用预训练权重做微调时我习惯从0.001起步比从头训练常用的0.01低一个数量级因为预训练权重已经在ImageNet特征空间里收敛得很好了步子迈太大会把学好的特征冲掉。第二个是优化器。花卉分类这种图像任务动量SGD仍然比Adam稳Adam收敛快但容易落在尖锐的极小值上泛化会差一些。第三个是预训练权重的取舍。如果你手里数据每类只有几十张固定住features部分只训练分类器是更稳的做法数据超过每类200张再放开全模型微调。4. 训练与评估让花卉分类准确率逼近可用的调参流程4.1 训练主循环损失函数、优化器与学习率调度加载数据和训练主循环可以合并成一个脚本关键是把验证逻辑独立成函数每个epoch结束后跑一次方便观察曲线。下面这个流程我在多个花卉数据集上用的都是同一套骨架import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, models train_ds datasets.ImageFolder(flower_data/train, transformtrain_transform) val_ds datasets.ImageFolder(flower_data/val, transformval_transform) train_loader DataLoader(train_ds, batch_size64, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size64, shuffleFalse, num_workers4, pin_memoryTrue) device torch.device(cuda if torch.cuda.is_available() else cpu) model models.alexnet(weightsmodels.AlexNet_Weights.IMAGENET1K_V1) in_features model.classifier[6].in_features model.classifier[6] nn.Linear(in_features, len(train_ds.classes)) model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lr0.001, momentum0.9, weight_decay5e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.5) epochs 30 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() val_loss, val_acc evaluate(model, val_loader, criterion, device) scheduler.step() print(fEpoch {epoch1:02d} | train_loss {running_loss/len(train_loader):.4f} f| val_acc {val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_alexnet_flower.pth)逻辑说明models.alexnet(weights...)会自动下载ImageNet预训练权重最后一行分类器被替换成你的花卉类别数量这是迁移学习的标准改法。训练循环里每个batch做一次清零、前向、算损失、反向、更新五步。验证和保存都放在epoch层面做以验证集准确率为基准保存最佳权重而不是用最后一个epoch的权重。参数说明batch_size64在8GB显存上跑227×227输入没有压力显存不够就降到32。lr0.001配合StepLR每5个epoch降一半是微调场景的稳妥组合。weight_decay5e-4是AlexNet原始配置全模型微调时保留它能明显抑制过拟合。num_workers4在Windows上如果报DataLoader相关错误改成0跑单进程最省心。4.2 训练曲线诊断过拟合、欠拟合与Early Stopping训练曲线比任何指标都先暴露问题。正常情况应该是训练Loss一路下降验证Loss先降后平缓验证准确率同步爬升。如果训练Loss还在降、验证Loss却掉头向上说明模型开始背训练集了这时候继续训练只会让验证集越来越差。Early Stopping的实现不复杂也不需要额外装库记录当前最佳验证准确率连续N个epoch没有刷新就提前终止。N我一般取7到10。如果你用的是上面那个脚本torch.save已经天然只保留最佳权重所以Early Stopping只是帮你省训练时间不省也可以但数据量小的时候多跑那几个epoch纯属浪费电。4.3 评估不止看准确率混淆矩阵与单类召回率准确率是平均数最容易骗人。如果数据集里玫瑰占了一半模型把所有图片都判成玫瑰准确率也有50%但你真正关心的可能是郁金香一张都认不出来。花卉分类的评估我至少要打印一份分类报告和混淆矩阵from sklearn.metrics import classification_report, confusion_matrix import numpy as np def evaluate(model, loader, criterion, device): model.eval() total, correct, loss_sum 0, 0, 0.0 all_preds, all_labels [], [] with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) loss_sum criterion(outputs, labels).item() _, preds torch.max(outputs, 1) total labels.size(0) correct (preds labels).sum().item() all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) report classification_report( all_labels, all_preds, target_namesloader.dataset.classes) print(report) cm confusion_matrix(all_labels, all_preds) print(混淆矩阵\n, cm) return loss_sum / len(loader), correct / total逻辑说明torch.no_grad()里面只做前向不做反向省显存也提速。preds.cpu().numpy()把预测结果搬到CPU并转成numpy这是传给sklearn做指标计算的标准姿势。分类报告会输出每个类别的精确率、召回率和F1混淆矩阵能直接看出哪些类别互相混淆——比如雏菊和蒲公英经常被搞混那就该去检查这两个类别的样本是不是拍摄角度太接近。5. AlexNet花卉分类避坑记录4个常见的训练与推理问题5.1 验证集准确率卡住不动学习率与归一化的锅现象训练Loss在下降但验证集准确率从第1个epoch到第10个epoch都纹丝不动稳定在某个低值附近。原因常见的有两个。一是预处理里漏了Normalize原始RGB像素值范围是0到255预训练权重期望的是标准化后的分布输入差异太大会让模型一直找不到有效梯度方向二是学习率对当前数据量来说偏大损失在loss曲面里震荡验证集指标跟着来回抖。解决先检查transform里有没有带上ImageNet的均值和标准差没有就补上。学习率从0.001降到0.0005或0.0003并确认scheduler确实在step。还有一个容易忽略的点验证时也要走val_transform不能拿训练用的RandomResizedCrop去验证随机裁剪会让验证指标每次都变。5.2 训练集99%验证集65%数据量撑不起全参数微调现象训练集准确率一路涨到99%以上验证集却停留在60%多两个曲线之间的gap越来越大。这是最典型的过拟合长相。原因AlexNet全模型有近6000万参数如果每类只有50张图模型完全有能力把训练图背下来但学不到能泛化的花形特征。数据量越小过拟合来得越快。解决按优先级排序做三件事。先把训练集数据增强加满RandomResizedCrop的scale收紧到(0.5, 1.0)加上翻转和颜色抖动。然后把features部分冻结只训练最后的分类器用for param in model.features.parameters(): param.requires_grad False这样实际参与训练的参数只有百万级。最后把第一个Dropout从0.5提高到0.7。这三步做完验证集通常能拉回85%以上。5.3 加载预训练权重报尺寸不匹配96核与64核的历史坑现象自己按论文结构写了nn.Conv2d(3, 96, kernel_size11, stride4)然后执行model.load_state_dict(torch.load(alexnet_weights.pth))报错提示features.0.weight尺寸不匹配期望[96,3,11,11]实际是[64,3,11,11]。原因原创论文因为当年GPU显存限制把第一层96个卷积核拆到两张显卡上torchvision实现的是单卡版本第一层只有64个核。网上流传的不少代码结构抄的是论文原版而权重文件来自torchvision两者天然对不上。解决直接用torchvision.models.alexnet加载官方结构不要自己写features部分。如果坚持用论文96核结构就只能从随机初始化开始训放弃预训练带来的迁移收益。在花卉这种小数据集上放弃预训练权重基本等于放弃最好的提升路径。5.4 推理结果全乱预处理不一致与漏掉model.eval()现象训练时验证集准确率90%以上把图片单独拎出来走一遍推理代码结果不是错得离谱就是每次都预测同一个类别。原因两个很隐蔽的错。一是推理代码用的预处理和训练时不一致比如训练用了Resize(256)CenterCrop(227)推理时直接Resize((227,227))图片比例被压扁变形二是model.eval()没调用Dropout在推理时还在随机丢弃神经元输出自然不稳定。解决把推理用的transform参数和验证集对齐严格保持Resize((256,256)) → CenterCrop(227) → Normalize的顺序。调用模型前先model.eval()并且推理全程包在torch.no_grad()里。还有一个细节用PIL打开图片后记得.convert(RGB)否则遇到带透明通道的PNG或灰度图通道数不一致会直接报错。6. 收尾技巧把训练好的权重封装成单图分类接口6.1 保存类别名权重和类别顺序必须一起落盘训练脚本里很多人只存了model.state_dict()没存类别名。权重文件里只有数字索引0对应哪个花名完全取决于训练时的ImageFolder目录顺序。换个环境重跑数据加载类别顺序一变化索引全错位模型表现像完全没训练过一样。我的习惯是训练结束顺手把类别名落盘import json with open(class_names.json, w, encodingutf-8) as f: json.dump(train_ds.classes, f, ensure_asciiFalse, indent2)train_ds.classes是ImageFolder按文件夹名排序后的类别列表和模型输出的索引一一对应。把这个文件放到和权重同一个目录推理时只认文件里的顺序不依赖环境里重新加载数据集。6.2 推理封装从单张图片路径到Top-K结果封装一个独立的分类器类把权重加载、预处理、推理合并在一起后续不管是做Web接口还是批量跑目录图片都复用同一个类import json import torch import torch.nn as nn from PIL import Image from torchvision import models, transforms class FlowerClassifier: def __init__(self, weights_path, class_names_path, deviceNone): self.device device or torch.device( cuda if torch.cuda.is_available() else cpu) with open(class_names_path, r, encodingutf-8) as f: self.class_names json.load(f) self.model models.alexnet(weightsNone) in_features self.model.classifier[6].in_features self.model.classifier[6] nn.Linear(in_features, len(self.class_names)) self.model.load_state_dict( torch.load(weights_path, map_locationself.device)) self.model.to(self.device) self.model.eval() self.transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(227), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) def predict_topk(self, image_path, top_k3): image Image.open(image_path).convert(RGB) x self.transform(image).unsqueeze(0).to(self.device) with torch.no_grad(): logits self.model(x) probs torch.softmax(logits, dim1) top_probs, top_indices torch.topk(probs, top_k, dim1) return [(self.class_names[idx], float(prob)) for prob, idx in zip(top_probs[0], top_indices[0])]调用方式很直接classifier FlowerClassifier(best_alexnet_flower.pth, class_names.json) for name, prob in classifier.predict_topk(test_rose.jpg, top_k3): print(f{name}: {prob:.2%})参数说明map_locationself.device让权重文件在只有CPU的机器上也能加载。unsqueeze(0)给单张图片补上batch维度因为模型要求输入是四维张量。top_k3返回概率最高的三个类别在演示场景里比只给一个答案更有说服力——如果排名第一的置信度不足60%排名第二的往往也有参考价值。这套方案做到今天给我感触最深的不是网络结构本身而是数据管道和评估闭环一定要做扎实。我最早跑通时偷懒没存类别名换机器推理时输出全乱排查了一下午才发现是索引错位从那以后权重和类别名永远一起落盘。希望帮到你。本文还有配套的精品资源点击获取