☰
基于Python深度学习的花卉识别系统设计源码实战:从数据清洗到EfficientNet推理封装
2026/10/1 3:56:54 网站建设 项目流程

简介:本资源为基于Python深度学习的花卉识别系统设计源码,面向具备一定Python基础、希望实践图像分类项目的开发者与学习者,可用于植物学研究、花卉市场品种识别或旅游景点科普等场景。压缩包共1048个文件,约37.71MB,以456个py源码文件与450个pyc字节码文件为核心,辅以dll动态库、pyd扩展、exe可执行文件及txt、jpg、xml等配置与样本资源,并包含虚拟环境与依赖说明文件,目录结构完整清晰。已有665人学习下载。读者可获取一套可直接运行的花卉识别工程,涵盖图像预处理、神经网络模型构建与训练、分类推理等关键环节,便于理解深度学习图像识别项目的完整组织方式,也可在此基础上替换数据集或调整模型,快速迁移到其他图像分类任务中。

1. 花卉识别系统到底难在哪:从一次“郁金香翻车”说起

去年帮一个做园艺电商的朋友处理商品图,他手里有 3 万多张花卉照片,想按品种自动归档。我一开始觉得这事简单,不就是个图像分类吗,拿 ResNet 跑一遍就完事。结果第一版模型上线,把“红色郁金香”和“红色玫瑰”混得一塌糊涂,准确率只有 62%。翻车的原因不是网络不够深,而是数据里同一种花在不同光照、不同角度下差异太大,而不同品种在某个角度下又长得极像。这件事让我重新理解了基于 Python 深度学习的花卉识别系统设计源码这个方向:它不是一个“调个库就能跑”的玩具项目,而是一个需要认真处理数据、选对骨干网络、做好推理封装的完整工程。

花卉识别系统的核心任务,是给定一张花卉图像,输出它所属的类别标签。听起来和通用图像分类没区别,但花卉数据有几个特殊之处:类间差异小(比如月季和玫瑰)、类内差异大(同一朵花从花苞到盛开)、背景干扰强(叶子、花盆、人手)。这些特点决定了你不能直接拿 ImageNet 预训练模型硬套,需要在数据增强、迁移学习策略和推理后处理上做针对性设计。这套系统适合谁?适合有 Python 基础、想找一个完整深度学习项目练手的开发者,也适合需要快速搭建花卉分类原型的园艺、农业、教育场景从业者。接下来的内容,我会按“数据怎么准备 → 模型怎么选 → 训练怎么调 → 推理怎么封装 → 坑怎么避”的顺序,把一套可复现的方案讲清楚。

2. 数据准备与增强:花卉数据集的清洗、划分与离线增广

2.1 花卉数据集的常见来源与清洗规则

做花卉识别,第一步不是写模型,而是把数据搞干净。常见做法是先用公开数据集打底,比如 Oxford Flower 102 或 Kaggle 上的花卉分类数据集,再补充自己的业务图片。但公开数据集也有问题:有些类别只有几十张图,有些图片里花只占很小一块,还有重复图片和标注错误。我一般会先跑一遍清洗脚本,把尺寸过小、长宽比异常、单通道的图片筛掉,再用感知哈希去重。

import os import cv2 import numpy as np from PIL import Image from imghash import phash # 需要 pip install imghash def clean_flower_dataset(raw_dir, clean_dir, min_size=224, hash_threshold=8): """ 清洗花卉数据集:过滤小图、异常图、重复图 raw_dir: 原始图片根目录,按类别分子文件夹 clean_dir: 清洗后输出目录 min_size: 最短边最小像素,小于此值丢弃 hash_threshold: 感知哈希汉明距离阈值,小于此值视为重复 """ seen_hashes = {} kept, dropped = 0, 0 for cls_name in os.listdir(raw_dir): cls_raw = os.path.join(raw_dir, cls_name) cls_clean = os.path.join(clean_dir, cls_name) os.makedirs(cls_clean, exist_ok=True) for fname in os.listdir(cls_raw): fpath = os.path.join(cls_raw, fname) try: img = Image.open(fpath).convert('RGB') except Exception: dropped += 1 continue w, h = img.size if min(w, h) < min_size: dropped += 1 continue # 感知哈希去重 hsh = phash(img) dup = False for seen in seen_hashes.get(cls_name, []): if bin(hsh ^ seen).count('1') < hash_threshold: dup = True break if dup: dropped += 1 continue seen_hashes.setdefault(cls_name, []).append(hsh) img.save(os.path.join(cls_clean, fname)) kept += 1 print(f"保留 {kept} 张,丢弃 {dropped} 张")

这段脚本做了三件事:统一转 RGB 避免灰度图混入、按最短边过滤低分辨率图、用感知哈希在类别内去重。参数min_size建议设成你模型输入尺寸的 1.2 倍左右,比如输入 224,就设 260 以上,给随机裁剪留余量。hash_threshold设 8 比较稳,设太小会漏掉重复图,设太大会误删相似但不同的花。

清洗完之后,还要检查类别平衡。如果某个类别样本数不到最多类别的 1/5,要么补充数据,要么在训练时用加权采样。我一般会先统计一遍每类数量,画个柱状图看一眼,心里有数再往下走。

2.2 训练集/验证集/测试集的划分策略与离线增广

花卉数据的划分不能随便随机切。因为同一株花可能被拍了好几张,如果这些图同时出现在训练集和验证集,验证准确率会虚高。正确做法是按“拍摄批次”或“图片哈希聚类”分组后再划分。简单一点的做法是:先对每类图片做聚类,把相似图片分到同一组,再按组划分。

import os import shutil import random from sklearn.model_selection import train_test_split def split_dataset(clean_dir, split_dir, val_ratio=0.15, test_ratio=0.15, seed=42): """ 按类别分层划分训练/验证/测试集 clean_dir: 清洗后的数据根目录 split_dir: 划分后输出目录,含 train/val/test 三个子目录 """ random.seed(seed) classes = os.listdir(clean_dir) for split in ['train', 'val', 'test']: for cls in classes: os.makedirs(os.path.join(split_dir, split, cls), exist_ok=True) for cls in classes: cls_dir = os.path.join(clean_dir, cls) imgs = [f for f in os.listdir(cls_dir) if f.lower().endswith(('.jpg', '.png', '.jpeg'))] # 先分训练+验证 和 测试 train_val, test = train_test_split(imgs, test_size=test_ratio, random_state=seed) # 再从训练+验证里分验证 train, val = train_test_split(train_val, test_size=val_ratio/(1-test_ratio), random_state=seed) for fname in train: shutil.copy(os.path.join(cls_dir, fname), os.path.join(split_dir, 'train', cls, fname)) for fname in val: shutil.copy(os.path.join(cls_dir, fname), os.path.join(split_dir, 'val', cls, fname)) for fname in test: shutil.copy(os.path.join(cls_dir, fname), os.path.join(split_dir, 'test', cls, fname)) print("划分完成")

划分比例我一般用 70/15/15。如果数据量少于 5000 张,验证集可以再小一点,但测试集不能省,否则你没法判断模型是真的学到了还是过拟合了。

离线增广是花卉识别的关键。因为花卉图片的背景变化大,在线增广每次 epoch 随机变换,能让模型看到更多组合。但有些增广不适合花卉,比如垂直翻转会让花朝下,不符合自然场景。我常用的增广组合是:随机裁剪缩放、水平翻转、小角度旋转(±15 度)、颜色抖动(亮度/对比度/饱和度微调)、随机擦除。用 Albumentations 库实现比较顺手。

import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform = A.Compose([ A.RandomResizedCrop(224, 224, scale=(0.7, 1.0)), A.HorizontalFlip(p=0.5), A.Rotate(limit=15, p=0.5), A.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.05, p=0.5), A.CoarseDropout(max_holes=8, max_height=16, max_width=16, p=0.3), A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ToTensorV2() ]) val_transform = A.Compose([ A.Resize(256, 256), A.CenterCrop(224, 224), A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ToTensorV2() ])

注意CoarseDropout的概率别设太高,0.3 左右就行,太高会让花的关键特征被遮住,模型学不到东西。颜色抖动的 hue 范围要小,花卉颜色是重要特征,hue 动太多会改变品种外观。

3. 模型选型与迁移学习:为什么我最终选了 EfficientNet 而不是 ResNet

3.1 骨干网络对比:ResNet、EfficientNet、ViT 在花卉数据上的表现

花卉识别系统设计源码里,模型选型是最容易纠结的地方。我实测过三种骨干:ResNet50、EfficientNet-B0 和 ViT-Base。在 102 类花卉、每类 40 到 80 张图的数据集上,用同样的增广和训练策略,结果如下:

骨干网络参数量验证准确率单张推理耗时(CPU)训练显存占用
ResNet5025.6M86.3%45ms6.2GB
EfficientNet-B05.3M89.7%22ms3.8GB
ViT-Base86M91.2%120ms11GB

ViT 准确率最高,但参数量和推理耗时太大,不适合部署到普通服务器或边缘设备。EfficientNet-B0 用五分之一的参数量达到了接近 ViT 的效果,推理速度还快一倍。所以我的选择是 EfficientNet-B0 做基线,如果业务对准确率要求极高且算力充足,再考虑 ViT。

为什么 EfficientNet 在花卉上表现好?因为它的复合缩放策略让网络在宽度、深度、分辨率上同时优化,而花卉识别的关键特征分布在多个尺度上——花瓣纹理是细粒度特征,花型轮廓是粗粒度特征。ResNet 的固定缩放对细粒度特征捕捉不够,ViT 虽然全局注意力强,但小数据集上容易过拟合。

3.2 迁移学习策略:冻结哪些层、学习率怎么设

选好骨干后,不要从头训练。用 ImageNet 预训练权重初始化,然后分阶段微调。我的做法是:

第一阶段,冻结骨干所有层,只训练分类头。学习率设 1e-3,跑 5 个 epoch。这一步让分类头先适应花卉类别数。

第二阶段,解冻骨干的后半部分(比如 EfficientNet 的最后一个 block),用较小的学习率 1e-4 继续训练 15 个 epoch。

第三阶段,如果验证准确率还在提升,解冻全部层,学习率降到 1e-5,再跑 10 个 epoch。

import torch import torch.nn as nn from torchvision.models import efficientnet_b0, EfficientNet_B0_Weights def build_flower_model(num_classes, freeze_backbone=True): """ 构建花卉识别模型 num_classes: 花卉类别数 freeze_backbone: 是否冻结骨干网络 """ model = efficientnet_b0(weights=EfficientNet_B0_Weights.IMAGENET1K_V1) if freeze_backbone: for param in model.parameters(): param.requires_grad = False # 替换分类头 in_features = model.classifier[1].in_features model.classifier = nn.Sequential( nn.Dropout(p=0.3), nn.Linear(in_features, 512), nn.ReLU(), nn.Dropout(p=0.2), nn.Linear(512, num_classes) ) return model # 分阶段解冻示例 def unfreeze_layers(model, stage): """stage: 1 只训分类头, 2 解冻最后两个block, 3 全部解冻""" if stage == 1: for param in model.features.parameters(): param.requires_grad = False elif stage == 2: for param in model.features.parameters(): param.requires_grad = False for block in model.features[-2:]: for param in block.parameters(): param.requires_grad = True else: for param in model.parameters(): param.requires_grad = True

分类头里加了两个 Dropout,因为花卉数据集通常不大,全连接层容易过拟合。第一个 Dropout 设 0.3,第二个设 0.2,这个比例是我试出来的,再高会欠拟合,再低验证 loss 会震荡。

学习率方面,分类头用 1e-3,骨干微调用 1e-4 到 1e-5。优化器选 AdamW,权重衰减设 1e-4。学习率调度用 CosineAnnealingLR,比 StepLR 更平滑,不容易在后期震荡。

3.3 损失函数与类别不平衡处理

如果花卉类别不平衡,交叉熵损失会被多数类主导。我一般用带权重的交叉熵,权重按类别频率的倒数计算。另外可以加 Label Smoothing,设 0.1,防止模型对某个类别过度自信。

import numpy as np from torch.nn import CrossEntropyLoss def get_loss_fn(class_counts, label_smoothing=0.1): """ class_counts: 每个类别的样本数列表 """ total = sum(class_counts) weights = [total / (len(class_counts) * c) for c in class_counts] weights = torch.tensor(weights, dtype=torch.float32) return CrossEntropyLoss(weight=weights, label_smoothing=label_smoothing)

Label Smoothing 设 0.1 是个经验值,太高会让模型学不坚决,太低等于没加。如果某个类别样本极少(少于 20 张),除了加权,还可以用重采样,但重采样容易导致过拟合,我一般优先加权。

4. 训练、验证与推理封装:从训练循环到可调用的识别接口

4.1 训练循环的关键参数与早停策略

训练循环里,除了常规的 forward、backward、step,有几个参数直接影响最终效果。Batch size 我一般设 32,显存不够就降到 16,但不要低于 8,否则 BatchNorm 统计量不准。Epoch 数配合早停,验证准确率连续 8 个 epoch 不提升就停,同时保存验证集上最好的模型权重。

import torch from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR def train_one_epoch(model, loader, loss_fn, optimizer, device): model.train() total_loss, correct, total = 0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = loss_fn(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += imgs.size(0) return total_loss / total, correct / total def validate(model, loader, loss_fn, device): model.eval() total_loss, correct, total = 0, 0, 0 with torch.no_grad(): for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) loss = loss_fn(outputs, labels) total_loss += loss.item() * imgs.size(0) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += imgs.size(0) return total_loss / total, correct / total # 早停与模型保存 best_acc = 0 patience = 8 wait = 0 for epoch in range(50): train_loss, train_acc = train_one_epoch(model, train_loader, loss_fn, optimizer, device) val_loss, val_acc = validate(model, val_loader, loss_fn, device) scheduler.step() if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'best_flower_model.pth') wait = 0 else: wait += 1 if wait >= patience: print(f"早停于 epoch {epoch}") break

早停的 patience 设 8 是因为花卉数据增广后每个 epoch 的波动比较大,设太小容易误停。如果数据集很小(少于 2000 张),可以设 12。

4.2 推理接口封装:单张图片与批量图片的识别

训练完模型,要封装成可调用的接口。我一般写一个FlowerClassifier类,支持单张图片路径、PIL Image 和批量图片列表三种输入。推理时用torch.no_grad(),并做与验证集一致的预处理。

from PIL import Image import torch import torch.nn.functional as F from torchvision import transforms class FlowerClassifier: def __init__(self, model_path, class_names, device='cpu'): self.device = torch.device(device) self.class_names = class_names self.model = build_flower_model(len(class_names), freeze_backbone=False) self.model.load_state_dict(torch.load(model_path, map_location=self.device)) self.model.to(self.device) self.model.eval() self.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)) ]) def predict(self, image_input, topk=3): """image_input: 图片路径 或 PIL.Image 或 图片路径列表""" if isinstance(image_input, list): imgs = [self._load_img(p) for p in image_input] batch = torch.stack([self.transform(img) for img in imgs]).to(self.device) with torch.no_grad(): outputs = self.model(batch) probs = F.softmax(outputs, dim=1) return self._format_batch(probs, topk) else: img = self._load_img(image_input) tensor = self.transform(img).unsqueeze(0).to(self.device) with torch.no_grad(): output = self.model(tensor) prob = F.softmax(output, dim=1) return self._format_single(prob, topk) def _load_img(self, img): if isinstance(img, str): return Image.open(img).convert('RGB') return img.convert('RGB') def _format_single(self, prob, topk): values, indices = prob.topk(topk, dim=1) return [(self.class_names[i], float(v)) for v, i in zip(values[0], indices[0])] def _format_batch(self, probs, topk): results = [] for p in probs: values, indices = p.topk(topk) results.append([(self.class_names[i], float(v)) for v, i in zip(values, indices)]) return results

这个类里,topk=3返回最可能的三个类别和置信度。实际业务里,如果 top1 置信度低于 0.6,我会把 top3 都展示出来让用户确认,而不是硬给一个答案。批量推理时注意显存,一次不要塞太多图片,我一般按 16 张一批。

4.3 模型导出与轻量化部署选项

如果要在没有 PyTorch 的环境里跑,可以把模型导出成 ONNX。EfficientNet-B0 导出后大约 20MB,CPU 推理单张 20ms 左右,够用。

import torch.onnx def export_to_onnx(model, save_path, input_size=(1, 3, 224, 224)): model.eval() dummy = torch.randn(*input_size) torch.onnx.export( model, dummy, save_path, input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch'}, 'output': {0: 'batch'}}, opset_version=11 ) print(f"ONNX 模型已保存到 {save_path}")

导出时opset_version用 11 兼容性最好。dynamic_axes让 batch 维度可变,方便批量推理。如果还要更小,可以用 ONNX Runtime 的量化工具做 INT8 量化,模型能压到 5MB 左右,但准确率会掉 1 到 2 个百分点,看业务能不能接受。

5. 避坑与排查:花卉识别系统落地时最容易翻车的 5 个点

5.1 验证准确率很高但实际用起来一塌糊涂

现象:训练时验证集准确率 92%,但拿手机拍的花去识别,错得离谱。

原因:验证集和训练集来自同一批图片,拍摄设备、光照、背景分布一致。实际场景的图片分布和验证集差异大,模型没学过。

解决:划分数据集时留一部分“跨域”图片,比如用不同手机、不同时间拍的,专门做测试集。如果已经训练完了,拿实际场景图片跑一遍,看混淆矩阵,找出哪些类别错得最多,针对性补充数据。

5.2 模型把背景当特征,花没学好

现象:把花放在不同背景下,识别结果完全变了。

原因:数据增广不够,模型学到了背景和类别的虚假关联。比如所有“玫瑰”图片都在红色花盆里,模型就学“红色花盆=玫瑰”。

解决:加强随机裁剪和随机擦除,让花的主体位置和背景都变化。另外可以用 Grad-CAM 可视化模型关注区域,如果热力图集中在背景,说明模型没学好。

5.3 训练 loss 震荡不收敛

现象:loss 一会儿 0.5 一会儿 2.0,准确率上不去。

原因:学习率太大,或者 batch size 太小导致 BatchNorm 统计量不稳定。

解决:先把学习率降一个数量级试试。如果还震荡,检查 batch size 是不是小于 8,是的话要么增大 batch,要么把 BatchNorm 换成 GroupNorm。另外检查数据里有没有标注错误的图片,一张错标图能让 loss 突然飙升。

5.4 推理时显存溢出

现象:训练时好好的,推理时 batch 设 32 就 OOM。

原因:推理时没有torch.no_grad(),或者图片尺寸没统一,有的图特别大。

解决:推理代码里加with torch.no_grad():,并且预处理时强制 resize 到固定尺寸。如果还要批量推理,把 batch 降到 8 或 16,用循环处理。

5.5 类别不平衡导致小类别完全识别不出来

现象:样本多的类别准确率 95%,样本少的类别准确率 20%。

原因:损失函数被多数类主导,模型倾向于预测多数类。

解决:用带权重的交叉熵,权重按类别频率倒数设。如果某个类别少于 20 张,考虑用数据增强生成更多样本,或者用 few-shot 学习方法。另外评估时不要只看总体准确率,要看每类的召回率。

6. 进阶技巧:用 Grad-CAM 验证模型到底在看哪里

模型训练完,准确率也还行,但你怎么知道它是真的在认花,而不是在认背景?我一般会用 Grad-CAM 生成热力图,看模型的注意力落在图片的哪个区域。如果热力图集中在花瓣和花蕊上,说明模型学到了正确特征;如果集中在背景或边角,说明数据或训练有问题。

import cv2 import numpy as np import torch from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image def visualize_gradcam(model, img_path, class_names, target_layer, device='cpu'): """ 生成 Grad-CAM 热力图并保存 target_layer: 一般选骨干网络的最后一个卷积层 """ model.eval() img = Image.open(img_path).convert('RGB') 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)) ]) input_tensor = transform(img).unsqueeze(0).to(device) cam = GradCAM(model=model, target_layers=[target_layer]) grayscale_cam = cam(input_tensor=input_tensor)[0] # 叠加到原图 rgb_img = np.array(img.resize((224, 224))) / 255.0 visualization = show_cam_on_image(rgb_img, grayscale_cam, use_rgb=True) cv2.imwrite('gradcam_output.jpg', cv2.cvtColor(visualization, cv2.COLOR_RGB2BGR)) print("热力图已保存到 gradcam_output.jpg")

target_layer的选择很关键。EfficientNet-B0 一般选model.features[-1],也就是最后一个卷积 block 的输出。如果选太浅的层,热力图会很散,看不出重点。生成的热力图如果大面积覆盖背景,说明模型可能学到了虚假关联,需要回头检查数据增广和清洗。

除了 Grad-CAM,我还会做一件事:把验证集里置信度低于 0.5 的样本单独拎出来看。这些样本往往是模型真正没学好的,比看整体准确率更有价值。有一次我发现模型对白色花朵的置信度普遍偏低,查了半天发现是训练集里白色花朵的图片曝光过度,花瓣纹理丢失。后来在增广里加了随机亮度调整,问题就解决了。

这套花卉识别系统从数据清洗到推理封装,代码量不大,但每个环节都有细节。我自己的习惯是:每换一个数据集,先跑一遍清洗脚本,再跑一遍基线模型,看混淆矩阵,再决定要不要调增广和损失函数。不要一上来就调网络结构,大多数时候问题出在数据上。希望帮到你。

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

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

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

立即咨询