☰
Python花朵识别卷积神经网络:从数据集整理到GUI部署的完整课程设计指南
2026/10/9 21:09:30 网站建设 项目流程

简介:这是一套面向高校计算机、智能科学与技术等专业学生的花朵识别卷积神经网络课程设计资源,以Python实现,涵盖数据预处理、特征提取与分类识别完整流程,适合课程实践、毕业设计参考及教学演示。压缩包共88个文件,约107.39MB,包含5个py源码文件、50张jpg花卉样本图、2个pb模型文件、2个md说明文档,以及css、js、html、yml、Dockerfile等前端与部署配置,另附npy数据文件与sh脚本,目录结构清晰,便于按模块查阅。已有43人学习下载。资源提供GUI演示界面与快速部署指南,模型经交叉验证测试,评估指标达到学术应用标准,使用者可基于现有算法模块进行功能扩展与参数优化,并借助文档理解系统架构完成二次开发。需具备基本编程与机器学习基础,仅限教育科研用途。

1. 花朵识别课设为什么总在答辩现场翻车

每年一到课程设计季,基于 Python 的花朵识别卷积神经网络就成了高频选题。原因很实在:数据集体量适中、类别语义清晰、GUI 演示直观,答辩时能讲的东西也多。但真正动手做过一轮的人会发现,翻车点从来不在"模型能不能跑",而在"跑出来的东西能不能讲清楚、能不能现场演示、能不能在别人电脑上复现"。

我见过太多这样的场景:训练脚本在本地跑出 95% 的准确率,答辩时换台机器打开 GUI,图片加载报路径错误;或者老师随手传一张自己拍的花,模型给出一个离谱的类别,追问一句"为什么"就答不上来。问题不在卷积神经网络本身,而在于整个方案从数据组织、训练配置到界面部署是一条断链,每一环都靠"我电脑上能跑"撑着。

这篇笔记要拆的就是这条链:用 Python 搭一个花朵识别卷积神经网络,从数据集整理、模型训练、GUI 演示到快速部署,把每一步的参数、坑和验证方法讲透。适合正在做课程设计的学生,也适合想拿一个完整小项目练手卷积神经网络落地流程的开发者。读完你应该能自己复现一套,并且知道答辩时哪些地方会被追问。

2. 花朵数据集怎么整理才能让训练不玄学

2.1 先搞清楚类别数和样本分布

花朵识别公开数据集里最常见的是按类别分文件夹存放的图片集,类别数通常在 5 到 102 之间。课程设计一般选 5 到 10 类,每类几百张,总量几千张。这个规模用卷积神经网络从头训练容易过拟合,所以选型上优先考虑迁移学习。

整理数据的第一步不是写代码,是数数。把每个类别的图片数量列出来,看有没有严重不均衡。如果某一类只有几十张,而其他类有几百张,训练时模型会偏向多数类,准确率看着高,实际对少数类几乎没识别能力。

import os from collections import Counter data_dir = "flowers_dataset" class_counts = {} for cls in sorted(os.listdir(data_dir)): cls_path = os.path.join(data_dir, cls) if os.path.isdir(cls_path): # 只统计常见图片格式,避免把隐藏文件算进去 imgs = [f for f in os.listdir(cls_path) if f.lower().endswith((".jpg", ".jpeg", ".png"))] class_counts[cls] = len(imgs) print("类别数:", len(class_counts)) for k, v in class_counts.items(): print(f"{k}: {v}") # 找出最多和最少的类别,判断是否需要重采样 counts = list(class_counts.values()) print("最多:", max(counts), "最少:", min(counts))

这段代码做的是数据体检。data_dir指向数据集根目录,每个子文件夹是一个类别。过滤图片格式这一步很关键,因为有些数据集里混着.DS_Store或缩略图缓存文件,不排除会导致后续读取报错。输出里如果最多和最少差三倍以上,就要考虑对少数类做数据增强或者用带权重的损失函数。

2.2 划分训练集、验证集、测试集的比例

常见做法是 7:1.5:1.5 或 8:1:1。课程设计里我一般用 8:1:1,因为总样本不多,验证集和测试集各留 10% 就够看趋势。划分时要注意按类别分层抽样,不能随机切,否则可能出现某个类在验证集里一张都没有的情况。

import random import shutil def split_dataset(src_dir, dst_dir, ratios=(0.8, 0.1, 0.1)): random.seed(42) # 固定随机种子,保证每次划分一致 for cls in os.listdir(src_dir): cls_path = os.path.join(src_dir, cls) if not os.path.isdir(cls_path): continue imgs = [f for f in os.listdir(cls_path) if f.lower().endswith((".jpg", ".jpeg", ".png"))] random.shuffle(imgs) n = len(imgs) n_train = int(n * ratios[0]) n_val = int(n * ratios[1]) splits = { "train": imgs[:n_train], "val": imgs[n_train:n_train + n_val], "test": imgs[n_train + n_val:] } for split, files in splits.items(): out_dir = os.path.join(dst_dir, split, cls) os.makedirs(out_dir, exist_ok=True) for f in files: shutil.copy(os.path.join(cls_path, f), os.path.join(out_dir, f)) split_dataset("flowers_dataset", "flowers_split")

random.seed(42)是为了让划分结果可复现,答辩时如果老师让你重新跑一遍,结果不会变。ratios三个值分别对应训练、验证、测试。复制而不是移动,是为了保留原始数据,万一划分有问题还能重来。这一步做完,目录结构会变成flowers_split/train/类别名/图片,后面用ImageFolder或flow_from_directory都能直接读。

2.3 图像尺寸和归一化参数怎么定

卷积神经网络输入尺寸常见的有 224×224、256×256、299×299。选哪个取决于你用的骨干网络。如果做迁移学习,最好和预训练权重训练时用的尺寸一致。归一化用 ImageNet 的均值和标准差是通用做法,因为预训练权重就是在这个分布上学的。

from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize((224, 224)), # 统一尺寸 transforms.RandomHorizontalFlip(), # 随机水平翻转,轻量增强 transforms.RandomRotation(15), # 小角度旋转,花朵姿态多样 transforms.ToTensor(), transforms.Normalize( # ImageNet 统计值 mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ]) val_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ])

训练集用增强,验证集和测试集不用,这是基本纪律。RandomHorizontalFlip和RandomRotation对花朵识别是安全的,因为花不会因为左右翻转就变成另一个类别。但不要用颜色抖动,花朵识别里颜色是重要特征,抖过头反而掉点。归一化的均值和标准差如果换成自己数据集的统计值,理论上更贴合,但迁移学习场景下用 ImageNet 的值更稳,因为预训练权重认这个分布。

3. 卷积神经网络选型与训练脚本怎么写才不白跑

3.1 骨干网络选 ResNet18 还是自己搭

课程设计里两种路线都有人走。自己搭一个三四层卷积加全连接,参数量小,训练快,但准确率通常卡在 70% 上下,而且答辩时容易被问"为什么这么设计"。用 ResNet18 做迁移学习,冻结前面层只训练分类头,几轮就能到 90% 以上,讲起来也有依据。

我一般推荐 ResNet18 起步,理由是:参数量约 1100 万,普通笔记本 CPU 也能推理,GPU 上训练几分钟一轮;结构经典,残差连接、批归一化这些点都能讲;预训练权重容易获取,不需要自己从头训。

import torch import torch.nn as nn from torchvision import models def build_model(num_classes, freeze_backbone=True): # 加载预训练 ResNet18,weights 参数在新版本里替代了 pretrained model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) if freeze_backbone: # 冻结除 fc 外的所有参数,只训练分类头 for name, param in model.named_parameters(): if "fc" not in name: param.requires_grad = False # 替换最后的全连接层,输出类别数 in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) return model model = build_model(num_classes=5) print("可训练参数:", sum(p.numel() for p in model.parameters() if p.requires_grad))

freeze_backbone=True时只训练最后的全连接层,可训练参数大约几千个,训练非常快。如果数据量有几千张,可以解冻最后两个残差块一起微调,准确率还能再涨一两个点。weights=models.ResNet18_Weights.DEFAULT是新版 torchvision 的写法,老版本用pretrained=True,两者等价,但新写法更明确。

3.2 训练循环里必须记录的四个量

训练脚本最容易犯的错是只记 loss 不记准确率,或者只在训练集上算准确率。必须同时记录训练 loss、验证 loss、训练准确率、验证准确率。前两个看模型有没有在学,后两个看有没有过拟合。

def train_one_epoch(model, loader, criterion, optimizer, device): model.train() running_loss = 0.0 correct = 0 total = 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * imgs.size(0) _, preds = torch.max(outputs, 1) correct += (preds == labels).sum().item() total += labels.size(0) return running_loss / total, correct / total def evaluate(model, loader, criterion, device): model.eval() running_loss = 0.0 correct = 0 total = 0 with torch.no_grad(): # 验证阶段不计算梯度,省显存 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) loss = criterion(outputs, labels) running_loss += loss.item() * imgs.size(0) _, preds = torch.max(outputs, 1) correct += (preds == labels).sum().item() total += labels.size(0) return running_loss / total, correct / total

model.train()和model.eval()的切换不能省,因为 Dropout 和 BatchNorm 在两种模式下行为不同。验证阶段用torch.no_grad()包住,能明显降低显存占用。loss 累加时乘imgs.size(0)再除以总数,是为了让最后一个不满 batch 的数据也被正确加权,直接对 batch loss 求平均会在数据量不整除时产生偏差。

3.3 学习率和 batch size 的搭配经验

迁移学习场景下,只训练分类头时学习率可以设 1e-3,解冻部分骨干层后要降到 1e-4 甚至 1e-5。batch size 受显存限制,8 到 32 都常见。如果 batch size 调大,学习率可以适当调大,但不是线性关系。

import torch.optim as optim device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = build_model(num_classes=5, freeze_backbone=True).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam( filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3, weight_decay=1e-4 ) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1) for epoch in range(15): train_loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc = evaluate(model, val_loader, criterion, device) scheduler.step() print(f"Epoch {epoch+1}: train_loss={train_loss:.4f} " f"train_acc={train_acc:.4f} val_loss={val_loss:.4f} val_acc={val_acc:.4f}")

filter(lambda p: p.requires_grad, ...)只把需要更新的参数交给优化器,冻结的层不参与更新。weight_decay=1e-4是轻量正则,防止分类头过拟合。StepLR每 7 轮把学习率乘 0.1,后期收敛更稳。如果验证 loss 连续几轮不降反升,就是过拟合信号,该早停或者加数据增强。

4. GUI 演示怎么做得像样又不拖后腿

4.1 用 Tkinter 搭最小可用界面

课程设计的 GUI 不需要花哨,能选图片、能显示、能出结果就够。Tkinter 是 Python 自带库,不用额外装,打包也方便。常见做法是一个按钮选图,一个标签显示图片,一个文本区显示预测类别和置信度。

import tkinter as tk from tkinter import filedialog from PIL import Image, ImageTk import torch from torchvision import transforms class FlowerApp: def __init__(self, model, class_names, device): self.model = model self.class_names = class_names self.device = device self.transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) self.root = tk.Tk() self.root.title("花朵识别演示") self.root.geometry("500x450") self.btn = tk.Button(self.root, text="选择图片", command=self.load_image) self.btn.pack(pady=10) self.img_label = tk.Label(self.root) self.img_label.pack() self.result_label = tk.Label(self.root, text="等待输入", font=("Arial", 14)) self.result_label.pack(pady=10) def load_image(self): path = filedialog.askopenfilename( filetypes=[("图片", "*.jpg *.jpeg *.png")] ) if not path: return img = Image.open(path).convert("RGB") display = img.copy() display.thumbnail((300, 300)) self.tk_img = ImageTk.PhotoImage(display) self.img_label.config(image=self.tk_img) tensor = self.transform(img).unsqueeze(0).to(self.device) self.model.eval() with torch.no_grad(): outputs = self.model(tensor) probs = torch.softmax(outputs, dim=1) conf, pred = torch.max(probs, 1) cls_name = self.class_names[pred.item()] self.result_label.config( text=f"预测: {cls_name} 置信度: {conf.item():.2%}" ) def run(self): self.root.mainloop()

Image.open(path).convert("RGB")这一步不能省,因为有些 PNG 带透明通道,直接转 tensor 会报错。unsqueeze(0)是给图片加一个 batch 维度,模型要求输入是四维。torch.softmax把输出转成概率,置信度显示成百分比更直观。self.tk_img必须存成实例属性,否则会被垃圾回收,图片显示不出来,这是 Tkinter 的经典坑。

4.2 模型加载和界面线程的注意事项

GUI 里加载模型要在界面启动前完成,不要放在按钮回调里,否则每次点按钮都重新加载,卡到没法用。如果模型较大,加载时界面会短暂无响应,可以在界面上先显示"加载中"。

def main(): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") class_names = ["daisy", "dandelion", "rose", "sunflower", "tulip"] model = build_model(num_classes=len(class_names), freeze_backbone=False) model.load_state_dict(torch.load("flower_model.pth", map_location=device)) model = model.to(device) app = FlowerApp(model, class_names, device) app.run() if __name__ == "__main__": main()

map_location=device保证在 CPU 机器上也能加载 GPU 训出来的权重,答辩换机器时这个参数能救命。freeze_backbone=False是因为加载权重时结构要完全匹配,冻结与否不影响结构,但保持一致更清晰。class_names的顺序必须和训练时ImageFolder的类别顺序一致,否则预测结果会张冠李戴。这个顺序可以用ImageFolder.classes打印出来核对。

5. 部署与复现路上最容易踩的五个坑

5.1 路径写死导致换机器就崩

现象:在自己电脑上 GUI 正常,拷到答辩电脑上打开就报FileNotFoundError。原因:代码里用了绝对路径,比如C:/Users/xxx/flowers_dataset。解决:所有路径用os.path.join基于脚本所在目录拼接,或者用相对路径,并且把模型文件、类别文件放在脚本同级目录。

import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) MODEL_PATH = os.path.join(BASE_DIR, "flower_model.pth")

__file__在交互式环境里可能不存在,但脚本运行时一定有。这样写之后,整个文件夹拷到任何地方都能跑。

5.2 类别顺序不一致导致预测全错

现象:模型准确率明明 90% 多,GUI 里预测结果却乱七八糟。原因:训练时ImageFolder按文件夹名排序生成类别索引,GUI 里手写的class_names顺序和它不一致。解决:训练完把train_dataset.classes保存成 json,GUI 加载这个 json,不要手写。

import json with open("class_names.json", "w") as f: json.dump(train_dataset.classes, f)

这个坑很隐蔽,因为模型本身没问题,错在映射关系。答辩前一定用几张测试集图片在 GUI 里过一遍,确认结果和训练时验证输出一致。

5.3 图片预处理和训练时不一致

现象:GUI 里预测置信度普遍偏低,或者同一张图在测试脚本里对、在 GUI 里错。原因:GUI 里的transforms和训练时的验证集transforms不一致,比如漏了归一化,或者 Resize 尺寸不同。解决:把验证集的 transform 抽成一个函数,训练和 GUI 共用。

def get_val_transform(): return transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])

预处理不一致是推理阶段最常见的精度损失来源,而且不容易发现,因为不会报错,只是结果变差。

5.4 显存不够导致训练中断

现象:训练到一半报CUDA out of memory。原因:batch size 太大,或者验证阶段没加torch.no_grad()。解决:先把 batch size 减半,确认验证阶段有no_grad,还可以用torch.cuda.empty_cache()清理缓存。如果还不行,把图片尺寸从 224 降到 128,但准确率会掉一些。

5.5 打包成 exe 后模型加载失败

现象:用 PyInstaller 打包后运行,报找不到模型文件。原因:打包后__file__指向临时解压目录,模型文件没被打进去。解决:用--add-data把模型和类别文件一起打包,代码里用sys._MEIPASS判断运行环境。

import sys def resource_path(relative): if hasattr(sys, "_MEIPASS"): return os.path.join(sys._MEIPASS, relative) return os.path.join(os.path.abspath("."), relative)

这个坑在课程设计里出现频率很高,因为很多人最后一步才想着打包,结果卡在这里。建议提前用--add-data "flower_model.pth;."试一次。

6. 把准确率再往上推两个点的三个技巧

第一个技巧是测试时增强。对同一张测试图片做几次不同变换,比如原图、水平翻转、小角度旋转,分别推理后把 softmax 概率平均,取最大值的类别。这个做法不增加训练成本,推理时多算几次,通常能涨 0.5 到 1.5 个点。代码上就是把验证 transform 复制几份,循环推理后torch.stack求均值。

第二个技巧是解冻最后两个残差块做微调。前面冻结骨干只训分类头,准确率到 90% 左右会平。这时候把layer3和layer4解冻,学习率降到 1e-4,再训 5 到 8 轮,通常能再涨 1 到 2 个点。注意解冻后要重新建优化器,把新解冻的参数加进去,否则它们不会被更新。

# 解冻 layer3 和 layer4 for name, param in model.named_parameters(): if "layer3" in name or "layer4" in name or "fc" in name: param.requires_grad = True else: param.requires_grad = False # 重新建优化器,学习率调小 optimizer = optim.Adam( filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4, weight_decay=1e-4 )

第三个技巧是看混淆矩阵找问题类别。准确率是个总数,掩盖了类别间的差异。用sklearn.metrics.confusion_matrix画一下,经常能发现某两类互相误判特别多,比如雏菊和蒲公英。针对这两类补数据或者单独调增强策略,比盲目加轮数有效。

from sklearn.metrics import confusion_matrix, classification_report import numpy as np all_preds, all_labels = [], [] model.eval() with torch.no_grad(): for imgs, labels in test_loader: imgs = imgs.to(device) outputs = model(imgs) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_names=class_names)) cm = confusion_matrix(all_labels, all_preds) print(cm)

classification_report会给出每个类别的精确率、召回率和 F1,比总准确率信息量大得多。如果某个类召回率明显低,说明模型漏检多,可能是该类样本太少或者特征和其他类太像。答辩时能拿出这张表,比只说一个准确率数字有说服力。

我自己的习惯是每做完一轮训练,先把混淆矩阵打出来看一遍,再决定下一步是加数据、调增强还是解冻层。这个顺序比反复调学习率有效得多。模型训练这件事,玄学的地方在于超参,不玄学的地方在于数据分布和评估方式,把后者做扎实,前者就不用瞎试。希望帮到你。

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

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

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

立即咨询