☰
基于Python的AlexNet花卉分类识别系统:原理、实现与调优
2026/10/12 11:26:59 网站建设 项目流程

简介:基于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 Flowers(17个类别,每类约80张)和Oxford 102 Flowers(102类)是最经典的两个选择,很多分类论文都以它们为基准。第一次做建议先用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(parents=True, exist_ok=True) shutil.copy2(img, dst) for img in images[split:]: dst = val_root / label_dir.name / img.name dst.parent.mkdir(parents=True, exist_ok=True) 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×55:floor((227-11)/4)+1=55。拿224硬套,第一层输出变成54×54,后面每层尺寸都会错位,报错时一堆人盯着维度问题排查半天。

既然要用ImageNet预训练权重,预处理也得和ImageNet保持一致:

from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(227), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.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×64
MaxPool1池化3×3, stride 227×27×64
Conv2卷积192个5×5, padding 227×27×192
MaxPool2池化3×3, stride 213×13×192
Conv3卷积384个3×3, padding 113×13×384
Conv4卷积256个3×3, padding 113×13×256
Conv5卷积256个3×3, padding 113×13×256
MaxPool3池化3×3, stride 26×6×256
FC1全连接9216→40964096
FC2全连接4096→40964096
FC3全连接4096→类别数类别数

3.2 PyTorch逐层实现:可直接复制的AlexNet代码

实际项目里我不会自己从零写features部分,直接用torchvision.models.alexnet接预训练权重最省事,但为了把结构讲清楚,先给你一份自建版本:

import torch.nn as nn class AlexNet(nn.Module): def __init__(self, num_classes=10, dropout=0.5): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 64, kernel_size=11, stride=4), # 227 -> 55 nn.ReLU(inplace=True), nn.MaxPool2d(3, stride=2), # 55 -> 27 nn.Conv2d(64, 192, kernel_size=5, padding=2), # 27 -> 27 nn.ReLU(inplace=True), nn.MaxPool2d(3, stride=2), # 27 -> 13 nn.Conv2d(192, 384, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(256, 256, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(3, stride=2), # 13 -> 6 ) self.classifier = nn.Sequential( nn.Dropout(dropout), nn.Linear(256 * 6 * 6, 4096), nn.ReLU(inplace=True), nn.Dropout(dropout), nn.Linear(4096, 4096), nn.ReLU(inplace=True), 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不敏感,全连接层参数占比最大、最容易过拟合,所以只在分类器里加。

参数说明:dropout=0.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", transform=train_transform) val_ds = datasets.ImageFolder("flower_data/val", transform=val_transform) train_loader = DataLoader(train_ds, batch_size=64, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_ds, batch_size=64, shuffle=False, num_workers=4, pin_memory=True) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = models.alexnet(weights=models.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(), lr=0.001, momentum=0.9, weight_decay=5e-4) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.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(f"Epoch {epoch+1: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_size=64在8GB显存上跑227×227输入没有压力;显存不够就降到32。lr=0.001配合StepLR每5个epoch降一半,是微调场景的稳妥组合。weight_decay=5e-4是AlexNet原始配置,全模型微调时保留它能明显抑制过拟合。num_workers=4在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_names=loader.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_size=11, stride=4),然后执行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", encoding="utf-8") as f: json.dump(train_ds.classes, f, ensure_ascii=False, indent=2)

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, device=None): self.device = device or torch.device( "cuda" if torch.cuda.is_available() else "cpu") with open(class_names_path, "r", encoding="utf-8") as f: self.class_names = json.load(f) self.model = models.alexnet(weights=None) 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_location=self.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_k=3): 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, dim=1) top_probs, top_indices = torch.topk(probs, top_k, dim=1) 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_k=3): print(f"{name}: {prob:.2%}")

参数说明:map_location=self.device让权重文件在只有CPU的机器上也能加载。unsqueeze(0)给单张图片补上batch维度,因为模型要求输入是四维张量。top_k=3返回概率最高的三个类别,在演示场景里比只给一个答案更有说服力——如果排名第一的置信度不足60%,排名第二的往往也有参考价值。

这套方案做到今天,给我感触最深的不是网络结构本身,而是数据管道和评估闭环一定要做扎实。我最早跑通时偷懒没存类别名,换机器推理时输出全乱,排查了一下午才发现是索引错位,从那以后权重和类别名永远一起落盘。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询