简介:基于Python深度学习的花卉识别课程设计源码,专注于利用卷积神经网络实现花卉图像的自动分类与识别,可作为计算机视觉方向课程设计、毕业设计参考,也适合初学者入门图像识别实战。压缩包内共四十九个文件,整体容量约三点三六兆字节,内部以花卉图像数据、程序源码、训练测试日志及说明文档为主,其中包含二十八张不同品种的花卉图片,涵盖菊花等多个类别。代码部分提供两套深度学习模型实现,一套采用预训练权重进行迁移学习,一套为自定义卷积神经网络,并配有数据读取、数据集划分、模型测试等模块,帮助学习者快速掌握从图像预处理到模型训练与评估的完整流程。项目还保留模型关注区域热图以及训练测试日志,可直观观察模型关注特征并追踪损失与准确率变化;同时附带忽略规则文件、使用说明文本等,目录结构清晰,便于复现与二次开发。已有三百六十三人学习下载,适合想要系统掌握卷积神经网络与迁移学习图像分类、快速搭建完整花卉识别项目的开发者参考。
1. 课程设计选花卉识别:为什么它是深度学习的“最佳练手题”
如果你正在为Python深度学习课程设计发愁,花卉识别几乎是所有选题里性价比最高的一个:数据集好找、任务直观、模型效果容易可视化,而且它同时覆盖了图像分类、数据增强、迁移学习和模型部署这几块硬骨头。很多人的第一个深度学习项目就是从“把一张花照片分成菊花、玫瑰、蒲公英”开始的。做这样一个基于Python深度学习的花卉识别课程设计源码项目,你真正要交付的不只是一段能跑通的代码,而是一套“数据怎么处理、模型怎么选、参数怎么调、结果怎么解释”的完整思路——这也是答辩时老师真正关心的东西。
这篇笔记我按自己做课程设计带组员时的习惯,从torchvision自带的花卉数据集讲起,把从数据预处理到模型训练的完整链路拆开,最后把最容易翻车的几个点和验证技巧也一并交代清楚。
2. 数据集与预处理:先解决“喂什么”的问题,再谈模型
2.1 用torchvision自带数据集跑通最小流程
花卉识别的公开数据集很多,但做课程设计我强烈建议先用torchvision.datasets里自带的花卉数据集跑通全流程。原因很现实:它不需要你额外去下载、解压、手工划分目录,API直接返回已经分好类的图像和标签,能帮你把“数据集的问题”和“模型的问题”分开,排查起来省一半的力气。
import torchvision from torchvision import transforms from torch.utils.data import DataLoader 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]) ]) train_data = torchvision.datasets.Flowers102( root='./data', split='train', download=True, transform=transform ) train_loader = DataLoader(train_data, batch_size=32, shuffle=True, num_workers=4)这段代码会从torchvision自动下载Flowers102数据集——102个类别的花卉图片,每个类别在训练集里有10张、验证集里有10张,测试集里数量不等。这里的预处理管线Resize((224, 224))是配合ImageNet预训练模型的固定输入尺寸,Normalize用ImageNet的均值和标准差,这两项是迁移学习场景下的标准做法,不是随便选的。
参数说明:batch_size在课程设计里设32或64都合适,显存不够就降到16;num_workers取决于你的CPU核心数,Windows下设成2~4就足够了,设太高反而容易在数据加载时报错(这个坑后面细说)。split='train'和split='test'会分别拿到训练集和验证集,不需要自己再划分。
2.2 自制数据集目录结构与标签编码
如果你不想用现成的Flowers102,想自己拍照片或者从网上收集花卉图片,那目录结构建议直接按ImageFolder的规范来组织,这样torchvision.datasets.ImageFolder能直接读取,少写一堆自定义Dataset的代码:
# 目录结构:data/train/rose/*.jpg, data/train/sunflower/*.jpg # 目录结构:data/val/rose/*.jpg, data/val/sunflower/*.jpg from torchvision.datasets import ImageFolder train_dataset = ImageFolder(root='./data/train', transform=transform) val_dataset = ImageFolder(root='./data/val', transform=transform) print(train_dataset.class_to_idx) # 打印类别名到索引的映射这里有个细节值得关注:每个类别的训练图片数量最好控制在几十张到两三百张之间,太少模型学不到类内差异,太多对课程设计来说训练时间不划算。另外,采集图片时尽量让同一类花有不同背景、不同角度、不同光照的样本,否则模型很容易学会“识别背景”而不是“识别花”——这是个玄学问题,但根源在数据,不在模型。
预处理这一步的关键结论是:不管用什么数据源,预处理都不该只做一个Resize加ToTensor,至少要加上RandomHorizontalFlip或RandomRotation做数据增强。训练集和验证集的transform要分开写,验证集不能做随机增强,否则评估指标会失真。
3. 模型选型与训练:ResNet是底线,微调参数决定上限
3.1 为什么课程设计首选ResNet而不是自己搭CNN
很多人一开始会想自己搭一个几层的卷积神经网络,觉得这样“更像自己做的”。我的建议是:如果你不想在答辩时被问到“为什么你的模型过拟合这么严重”而答不上来,那就直接用torchvision里预训练的ResNet18做迁移学习。理由有三个:
- ResNet18只有约1100万参数,训练速度和显存占用都友好,CPU也能跑推理——这很关键,因为很多课程设计最终是在没有GPU的机器上演示的。
- 预训练权重是在ImageNet上学到的,已经内置了大量通用视觉特征(边缘、纹理、形状),你的花卉数据集只需要微调最后几层就能取得不错的效果。
- ResNet的残差结构在答辩时是一个很好的“技术亮点”,你可以清楚地解释这个结构如何解决网络退化问题,比手写CNN更容易讲出深度。
import torchvision.models as models import torch.nn as nn model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) num_ftrs = model.fc.in_features model.fc = nn.Linear(num_ftrs, 102) # 102对应Flowers102的类别数 # 冻结前几层,只微调最后一层 for param in model.parameters(): param.requires_grad = False for param in model.fc.parameters(): param.requires_grad = True device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device)这段代码的关键操作有两处:一是model.fc = nn.Linear(num_ftrs, 102),把全连接层的输出维度改成你的类别数;二是通过requires_grad控制只训练最后一层。第一个操作是通用的分类头替换模式,第二个操作则是控制在多大程度上“借用”预训练知识。如果只训练最后一层且数据量少,就几乎不会过拟合,但模型上限也低;如果解冻更多层并加大学习率,模型上限高但风险也高。
3.2 训练脚本的完整结构与两个必调的损失函数参数
训练循环看起来简单,但课程设计翻车率高,往往是因为缺少简单的检查点、没有合理使用损失函数的参数或者监控了错误的指标。下面这段代码是我平时带项目时给组员的模板,包含了“保存最佳模型”和“打印每个epoch的分类准确率”这两个关键能力:
import torch import torch.nn as nn import torch.optim as optim criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.fc.parameters(), lr=1e-3) best_acc = 0.0 epochs = 15 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() # 验证阶段 model.eval() correct, total = 0, 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.data, dim=1) total += labels.size(0) correct += (predicted == labels).sum().item() acc = 100 * correct / total print(f'Epoch {epoch+1}/{epochs}, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {acc:.2f}%') if acc > best_acc: best_acc = acc torch.save(model.state_dict(), 'best_flower_model.pth')这里有一个新手常犯的错:在验证阶段忘了加model.eval()和with torch.no_grad()。前者会关掉Dropout和BatchNorm的随机行为,后者会停止构建计算图并大幅减少显存占用。不加这两行,验证指标是不准确的。另一个隐蔽的问题是total += labels.size(0),它统计的是所有batch的样本总和,而不是只取最后那个不完整batch的数量——不过DataLoader默认drop_last=False,所以写成labels.size(0)就对了。
CrossEntropyLoss在PyTorch里默认已经把LogSoftmax和NLLLoss合并了,所以模型最后一层直接输出原始logits就行,不要再手动加Softmax。如果要处理类别不均衡的数据集,可以在CrossEntropyLoss里传weight参数——以各个类别的样本数倒数计算,这是处理数据不均衡时最直接有效的做法。
3.3 学习率和优化器:课程设计场景下的调整思路
训练时你会经常遇到一个现象:loss在降低但准确率上不去,或者验证集准确率忽高忽低。这时候调整学习率是第一步。常见的做法是先用1e-3的Adam跑几个epoch,如果loss下降太慢就调大到1e-2,如果loss震荡就调小到1e-4。
# 学习率衰减:每5个epoch衰减到原来的0.1倍 scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1) # 在训练循环每轮结束后调用 scheduler.step()Adam已经内置了一阶动量和二阶动量,所以它对学习率不像SGD那么敏感。但如果用了ImageNet预训练模型做完整微调,SGD配momentum的收敛效果往往比Adam更稳——这是一个“热身”阶段,学习率从1e-4开始,动量0.9,训练曲线会平滑很多。课程设计时间如果紧张,Adam配上StepLR是最省心的组合。
4. 避坑:花卉识别课程设计最常见的6个翻车现场
4.1 Flowers102数据集标签与类名不匹配
现象:模型训练准确率很高,但用测试图片推理时的输出结果和真实花名对不上。
原因:torchvision.datasets.Flowers102返回的是整数标签从0到101,这些索引和数据集自带的labels.txt文件是严格对应的,但和你从网上看到的类别名称列表不一定一致。花卉名在Flowers102的官方标注里是拉丁文,比如“n04356074”这样的WordNet ID,如果你自行映射到中文花名,很容易错位。
解决:直接使用dataset._labels或者下载官方label文件做一次映射表。比较保险的做法是,训练完就保存一份idx_to_name.json,推理时从该文件读取类别名,不要每次手工指定列表。
4.2 Windows下num_workers设置过高导致程序卡死或反复重启
现象:代码在for images, labels in train_loader:处停住不动,或每个epoch刚开始就报BrokenPipeError。
原因:Windows下多进程数据加载和Linux行为不同,当num_workers大于0时,如果代码不是写在if __name__ == '__main__'保护块里,或者num_workers超过了CPU核心数,就可能出现子进程崩溃。
解决:把训练代码放进if __name__ == '__main__':下,或者将num_workers直接设为0。课程设计阶段的数据量并不大,num_workers=0的加载速度完全可以接受,这是最省心的方案。
4.3 验证集准确率很高,但单张图片推理效果很差
现象:训练时在验证集上达到90%以上准确率,但拿一张手机拍的花去预测,结果是错的。
原因:验证集图像经过了和你训练集一致的Resize(224, 224)和Normalize,但你在推理时会忘记做同样的预处理,或用了不同的尺寸。这是最典型的“黑匣子”场景——模型本身没问题,问题是输入管线的格式不一致。
解决:推理代码和数据加载代码复用同一个预处理函数。单独定义一个get_transform()函数,训练、验证、推理各环节都调用它,不要在推理时重新写一遍transform。
4.4 训练loss不下降,准确率一直在1%左右
现象:第一个epoch后loss基本不变,准确率远低于类别数的倒数(例如102类时约1%)。
原因:最常见的是标签错位。比如用了ImageFolder但类别目录和实际图像不对应,或者数据集里部分图片损坏导致自动跳过。另一个原因是学习率过大导致loss直接nan。
解决:先用极小的学习率(1e-5)跑一个epoch,确认loss确实在下降,再调回正常学习率。然后逐个检查train_dataset.class_to_idx的映射,挑几张图打印标签和图像路径确认一致性。
4.5 显存不足(CUDA out of memory)
现象:训练第二个epoch时报CUDA out of memory。
原因:batch_size过大是主因,但很多人忽略的是验证阶段也占显存——如果代码加了torch.no_grad()但没加model.eval(),BatchNorm层仍然可能累积额外状态。
解决:先把batch_size减半,或者减少num_workers;验证阶段务必同时配合model.eval()和torch.no_grad()。如果还在爆,就把输入尺寸从224降到160——但注意这会改变预训练模型的输入分布,准确性会有轻微下降。
4.6 保存的模型加载后结果不一样
现象:训练时指标正常,但重新加载.pth后推理结果明显不对。
原因:加载模型时用了不同的类数量,或者使用了不同的模型结构。例如训练时是resnet18(num_classes=102),加载时却默认了resnet18(num_classes=1000)——因为torchvision默认输出维度是1000,加载权重时不会报错但最后一层对不上。
解决:保存模型时同时保存模型结构和超参数,至少保存类别数:
torch.save({ 'model_state_dict': model.state_dict(), 'num_classes': 102, 'transform': transform }, 'flower_model_checkpoint.pth')加载时从checkpoint里读取num_classes来重建模型结构,这样就不会出现维度错位。
5. 从课程设计到真正可用:推理脚本、界面与指标验证
5.1 写一个能接受单张图片的推理脚本
课程设计交到这一步,很多同学认为训练出模型就算结束了,但答辩时老师最常问的一句话是:“你这个模型实际用起来什么样?”所以一个能接收单张图片并输出“预测类别+置信度”的脚本,比多训练10个epoch都重要。推理脚本我习惯的做法如下:
import torch from PIL import Image import torchvision.transforms as transforms from torchvision import models 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]) ]) def load_model(checkpoint_path, num_classes=102): model = models.resnet18(weights=None) model.fc = torch.nn.Linear(model.fc.in_features, num_classes) checkpoint = torch.load(checkpoint_path, map_location='cpu') model.load_state_dict(checkpoint['model_state_dict']) model.eval() return model def predict(image_path, model, idx_to_name): img = Image.open(image_path).convert('RGB') img_tensor = transform(img).unsqueeze(0) with torch.no_grad(): outputs = model(img_tensor) probabilities = torch.nn.functional.softmax(outputs[0], dim=0) top_prob, top_idx = torch.max(probabilities, dim=0) return idx_to_name[top_idx.item()], top_prob.item() if __name__ == '__main__': model = load_model('best_flower_model.pth') name, prob = predict('test_rose.jpg', model, idx_to_name) print(f'预测结果: {name}, 置信度: {prob:.2%}')加上map_location='cpu'意味着这个脚本即使在演示机器上只有CPU也能正常工作,同时Image.open后接.convert('RGB')能避免某些灰度图或RGBA图在预处理时报通道数错误。置信度输出是所有课程设计演示中展现专业度的最好手段,展示的可信度明显高于只输出类别名。
5.2 用混淆矩阵验证模型的真实盲区
准确率是一个过于笼统的指标。对花卉识别这个小数据集任务来说,准确率达到92%并不能证明模型“会认花”,可能只是绕开了某些容易混淆的类别。我习惯的做法是输出一张混淆矩阵图,把它贴到课程设计报告里作为结果分析部分的核心证据。
import matplotlib.pyplot as plt import numpy as np from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay all_labels, all_preds = [], [] with torch.no_grad(): for images, labels in val_loader: images = images.to(device) outputs = model(images) _, predicted = torch.max(outputs.cpu(), dim=1) all_labels.extend(labels.numpy()) all_preds.extend(predicted.numpy()) cm = confusion_matrix(all_labels, all_preds) disp = ConfusionMatrixDisplay(confusion_matrix=cm) disp.plot(cmap='Blues') plt.savefig('confusion_matrix.png', dpi=150)混淆矩阵能一眼看出来哪些类别互相打架,比如向日葵和蒲公英如果经常混淆,说明两者的颜色和纹理特征在模型看来没有足够的区分度——这时可以增加这类别的训练样本量,或者查一下是不是数据增强把花色、背景过度扭曲了。这里的一个关键技巧:confusion_matrix的输入必须是CPU上的NumPy数组,很多人在all_labels.extend(labels)时忘了labels还在GPU上,结果直接报错。
5.3 后续值得做的两个方向:Gradio部署和细粒度分类
如果要让这个课程设计更出彩,我强烈建议用Gradio把模型包装成一个网页应用,拖一张图进去就能看到识别结果和置信度。这是目前最讨巧的展示手段——代码量不到二十行且效果直观。课程设计答辩时,能把模型跑在网页上的人基本上不会再被追问任何实现细节。
另一个方向是细粒度花卉识别。常见的Flowers102已经是一个难度不低的细粒度识别任务,但如果你的数据是自己收集的、类别之间外观差异更小时,可以尝试引入注意力机制模块比如SE Block或者CBAM,加到ResNet的每个残差块后面。这个改动会显著提升相似花朵的区分度,还能给你的答辩增加一个“为了提升性能做了什么设计”的技术亮点。不过要注意,加了注意力之后显存占用上升约10%到15%,训练时间也会变长,课程设计阶段把精力放在可视化验证上更值得。
最后说个我自己带过的真实教训:有一个组在答辩前一天还在大量调参,想把准确率从91%再推到95%以上,结果答辩当天模型过拟合在训练集上——这其实也是很多课程设计的通病。课程设计不是论文,你的目标是在有限时间内展示“完整链路+合理指标+可解释的结果”,而不是刷极限数字。把推理脚本、混淆矩阵和几组对比实验的截图准备好,比多跑50个epoch有价值得多。希望帮到你。
本文还有配套的精品资源,点击获取