资讯动态

基于Python与CNN的花卉绽放识别:从数据标注到模型训练全流程解析

发布时间:2026/10/23 4:52:28 来源:尧图企业网站定制
识别花卉是否绽放本质是“花有没有开”的二分类问题。很多准备做计算机毕业设计的人第一眼看到“基于Python的CNN卷积神经网络深度学习识别花卉是否绽放”这个题目都以为就是建两个文件夹一类放绽放的花一类放花苞然后丢进深度学习框架里训练。听起来很轻松实际把整个流程跑通之后才会发现这个题目最麻烦的并不是卷积神经网络本身而是大量“半开不开”的花要怎么标、怎么学。我按实际调试的路线从问题定义、数据整理、网络设计、训练调参一直讲到评估和答辩展示尽量做到可以直接照着做、能复现也能在原有基础上改出自己的东西。1. 选题拆解为什么“是否绽放”比“是哪种花”更难在写模型之前最值得先做的事是搞清楚这个题目到底在解决什么问题。如果不把问题定义清楚后面所有数据标注和训练都是糊涂账。1.1 给“绽放”一个可量化标准花从花苞到盛开是一个连续发育过程中间夹着大量半开、侧开、微开的状态。如果让不同的人标注对“半开”的判断很容易产生分歧。比如花瓣已经裂开缝隙但花心还完全包着这算绽放还是没绽放有人觉得“已经开了”有人觉得“还没完全开”。如果不统一口径数据集的标签就会互相矛盾模型训练时一会儿学A一会儿学B效果自然好不了。我在实际项目中采用的口径是花瓣大部分展开、花心清楚可见才算“绽放”花瓣仍包住花蕊或者只有边缘裂开一条缝都算“未绽放”。已经出现萎蔫、垂头、颜色发暗的残花也要归到“未绽放”这一类。这个标准不算完美但好处是足够清晰标注者拿到规则后能快速上手一张图几秒钟就能判断。实测下来明确标注规范之后不同标注者之间的分歧率能明显下降。这个细节很多人忽略但它直接决定数据质量而数据质量又决定CNN模型的天花板。1.2 边界样本不是噪音是训练信息另一个容易踩的坑是看到“半开不开”的图就扔掉觉得这是脏数据。实际上这些边界样本恰恰是整个题目最有价值的部分。如果模型只见过“完全绽放”和“完全没绽放”两种极端情况那它在真实场景里遇到半开的花大概率会懵。正确的做法是把边界样本单独分一个子集用来观察模型输出概率。比如某张半开花模型预测绽放概率是0.42离0.5比较近说明它知道这朵花有点特殊但没有把握。如果你看到一批这样的照片概率都集中在0.4到0.6之间说明模型没有真正学到花从闭合到展开的形态变化很可能是在靠颜色、背景纹理硬猜。这时候不要急着调网络结构先回去补数据和统一标注更有效。这和“哪种花”的分类不一样花品种分类拼的是花形、叶形、颜色组合的差异而“是否绽放”拼的是同一朵花在不同发育阶段的细微形态差异后者对数据分布更敏感。2. 数据集标注策略和脏数据才是第一个拦路虎数据是这类小项目的生命线。模型用什么结构、用什么优化器都是后话先把数据集弄得干净、分布合理CNN才有东西可学。2.1 图像收集路线公开数据集与自拍结合我建议不要只依赖某一种数据来源。公开花卉数据集有一个典型问题它通常是为了“品种分类”而整理的同一朵花会从多个角度拍好几张但“是否绽放”这个标签不一定齐全。所以更稳妥的组合方式是取一部分公开数据集里明确能区分开合状态的图片再自己拍摄一批花苞、半开、盛开、凋谢的照片补进去。自己拍的时候需要注意三件事。第一是同一棵花的拍摄时间尽量拉长比如从花苞期到盛花期每隔两三天拍一组这样能覆盖连续的中间状态。第二是背景要多样化不要总在同一个花坛同一个角度拍路边的、阳台上的、楼下的颜色和光线都要换。第三是数量要平衡绽放和未绽放两类图片尽量各在一千张以上。如果某一类只有两三百张模型很容易偏向样本多的类别。图片格式统一成JPG尺寸不用太大训练前统一缩放即可。2.2 标注规范和复核机制文件目录要按类别组织比如训练集里放blooming和not_blooming两个子目录这样很多框架用ImageFolder就能直接读。真正影响质量的是标注规范本身。我写过一个简单的“绽放判定手册”大概内容就是花瓣是否展开、花心是否可见、有没有明显萎蔫符合前两条就标绽放否则标未绽放。手册配了十张示例图标注时照着示例看效率很高。标注完成后我通常还会抽一百张图让另一个人独立标一遍然后跟原始标签对比。如果一致率低于95%就把不一致的图挑出来重新讨论看是标准不够清晰还是图片本身太模棱两可。这个过程虽然麻烦但能让训练集和验证集之间的判断逻辑一致。很多人训练时发现验证集准确率忽高忽低其实不是模型不稳定而是标签本身在“打架”。2.3 数据增强和按“个体”划分数据集数据增强方面我用的是随机水平翻转、小角度旋转、随机裁剪缩放、亮度对比度抖动、饱和度抖动。这里有个度的问题花瓣颜色本身是很重要的特征所以饱和度抖动不要调得太狠否则模型会把“颜色鲜艳”和“绽放”错误地绑定。随机旋转角度控制在正负20度以内太夸张的旋转在花卉摄影里不常见。更关键的是划分方式。如果同一朵花的照片同时出现在训练集和验证集里模型相当于提前“见过”了这个个体验证准确率会虚高不少。正确做法是先按花的个体做分组同一株花、同一组连拍的照片尽量放进同一个集合再按大致7:2:1划分训练、验证、测试。比如一朵月季从花苞到盛开拍了五张这五张要放在同一侧而不是随机散落到各个集合。我见过网上不少教程对这一点完全没有交代实际做下来随机划分和按个体划分的验证集准确率可以差五到十个百分点这个差距在毕设答辩里很容易被问住。3. 模型设计从零搭建CNN还是迁移学习数据集准备好之后模型这一环就很清晰了。我同时做了两个版本一个是自己搭的轻量CNN用来讲原理、跑通流程另一个是迁移学习版本用来刷指标和做对比实验。3.1 自建轻量CNN的结构与解释自建CNN的好处是每一层都知道它在干什么答辩的时候被问参数含义不会慌。我用的是一个非常朴素的结构三层卷积每层后面接BatchNorm和ReLU用最大池化降分辨率最后接全局平均池化和一个输出单元。输入图片统一缩放到224x224。import torch import torch.nn as nn class BloomNet(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.global_pool nn.AdaptiveAvgPool2d((1, 1)) self.classifier nn.Sequential( nn.Dropout(0.5), nn.Linear(128, 1) ) def forward(self, x): x self.features(x) x self.global_pool(x) x x.flatten(1) x self.classifier(x) return x这个网络参数很少CPU也能训练特别适合毕设演示。三次最大池化会把224x224的图逐步降到28x28全局平均池化之后得到128维特征再全连接到1个神经元。BatchNorm的作用是让每层输入分布稳定加快收敛Dropout则是防止分类头过拟合的一种常规手段。如果你后续想加内容可以在最后一层卷积后再堆一层卷积或者把全连接改成两个全连接灵活性很高。3.2 迁移学习什么时候用、怎么冻结自建CNN在上千张数据量下能跑到大约88%到92%的准确率但再往上会感觉吃力。如果想让指标更好看我建议做一版迁移学习对照实验。用预训练在大型图像数据集上的残差网络作为骨干把最后分类头换成1个输出节点即可。这个思路简写如下import torch.nn as nn from torchvision import models backbone models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) backbone.fc nn.Linear(backbone.fc.in_features, 1)数据量不大时不要一上来就全量微调。我的建议是冻结骨干的前几层只训练后面部分和分类头。比如把骨干的前5个模块参数requires_gradFalse后面的层保持可训练。这样模型既能利用预训练学到的边缘、纹理、形状特征又不会因为小数据而过拟合。如果数据量到了一两千张以上可以考虑全量微调但要把学习率调小通常是1e-4到1e-5量级。同样是跑二分类小样本场景下迁移学习通常比自建CNN高3到8个百分点稳定性也会更好。但从理解原理的角度我始终建议先搭一版自建CNN把loss降下去、把整个流程跑通再切换到迁移学习。这样答辩时你能讲清楚两套方案的差异而不是只会调接口。3.3 输出层为什么最后一层只有一个神经元很多初学者习惯性用两个神经元加Softmax做二分类。这个做法没有错但这个任务我用的是单神经元加Sigmoid输出的是一个0到1之间的绽放概率大于0.5判为绽放小于0.5判为未绽放。在PyTorch里训练时配合BCEWithLogitsLoss不需要自己手动计算Sigmoid数值上更稳定。具体来说模型输出的原始值叫logit不是概率。训练时直接用nn.BCEWithLogitsLoss()(logit, target)计算损失推理时用torch.sigmoid(logit)转换成概率。标签用0和1表示1代表绽放0代表未绽放。这是二分类里很标准的做法处理代价低调阈值也方便。4. 训练阶段损失函数、优化器与调参实战网络搭好后训练这一步看着门槛不高但有几个参数直接影响项目能不能顺利跑完。我把实际调过的关键点列出来你可以直接参考。4.1 损失函数、优化器与学习率设定损失函数就用二分类的BCEWithLogitsLoss。优化器我一般先用Adam跑通流程初始学习率设1e-3batch size设32或64。如果用的是迁移学习骨干初始学习率建议降到1e-4因为预训练模型已经比较“成熟”学习率太大会把之前学到的特征冲坏。学习率调度上用CosineAnnealingLR比较省心也可以简单的StepLR每10个epoch降一半。我实测下来30到50个epoch就能收敛没必要训练太久。训练前要固定随机种子保证实验能复现这对写毕业设计报告很重要不然今天跑91%明天跑89%又解释不了为什么。4.2 训练日志和过拟合判断训练过程中不要光盯着准确率要同时看训练损失和验证损失。下面是我一版模拟训练日志比较典型EpochTrain LossVal Acc10.61271.2%30.47182.5%50.35387.6%80.24191.3%100.19789.8%第8个epoch时验证准确率最高之后训练loss继续往下掉验证准确率却开始下滑这就是过拟合的典型信号。这时候不需要继续硬训练直接保存第8轮的模型权重就可以。早停的阈值可以设在5到10个epoch验证指标连续不提升就停止省时间又能防止过拟合。4.3 类别不平衡和损失加权很多实际场景里“绽放”和“未绽放”的样本并不均衡花苞期的照片可能比盛开期少。如果不处理模型会倾向于预测样本多的类别。最简单的处理方式是给损失函数加一个正样本权重让模型少犯错时对少样本类别更敏感。在PyTorch里可以用pos_weight实现比如nn.BCEWithLogitsLoss(pos_weighttorch.tensor([1.5]))。这个值一般取“负样本数量/正样本数量”代码里可以自动算出来。也可以用WeightedRandomSampler在采样时让少样本类别被抽中的概率更高。我个人的习惯是先加pos_weight效果不明显再换采样器因为采样器会让每个epoch看到的重复样本变多训练时长增加不少。5. 结果评估准确率之外的混淆矩阵和典型误判模型训练完很多人习惯只看验证集准确率但这一个指标在二分类里信息量太少。尤其是“绽放/未绽放”这个场景两类错误的意义完全不同必须拆开看。5.1 精确率、召回率、F1与AUC假设测试集里有328张实际绽放的图和390张实际未绽放的图模型预测结果是真正绽放且预测绽放的有312张把未绽放误判为绽放的有58张把绽放漏判为未绽放的有16张真正未绽放且预测未绽放的有332张。按这些数据算精确率312 / (312 58) 84.3%表示模型预测为绽放的图里确实绽放的比例。召回率312 / (312 16) 95.1%表示实际绽放的图里模型找回的比例。F1分数约89.4%是精确率和召回率的调和平均。特异性332 / (332 58) 85.1%表示未绽放图被正确识别的比例。AUC大约0.93说明整体排序能力不错。从这个结果能看出来模型更倾向于“宁可多报绽放、少漏报绽放”所以精度被误报拉低。至于这种倾向好不好取决于应用场景。如果是用来提示赏花时间漏报会让人觉得不够灵敏如果是用来筛选花田成熟度误报又会带来额外工作量。所以不能只用一个准确率糊弄过去最好把混淆矩阵打印出来看看错在哪类。5.2 典型误判案例半开、背景、小目标与残花我整理了一批典型的误判照片大概能分成四类。第一类是半开状态花瓣已经露出明显颜色但花心没有完全展开模型有时会犹豫。这类样本大量出现在0.4到0.6的概率区间处理方式是多收集、多标注让模型见过更多“中间状态”。第二类是背景干扰。如果背景里有红色塑料袋、同色的墙面、甚至远处另一朵颜色类似的虚化花朵模型会把背景纹理误当成花瓣区域。这种问题可以通过热力图看出模型关注了错误区域然后针对性增加带复杂背景的训练样本。第三类是花朵在画面中占比太小。比如几米外拍的一小丛花花瓣特征被压缩到几十个像素人眼也要仔细辨别模型更容易乱猜。这类图不要硬训练可以先用一个目标检测框把花裁出来再分类或者至少保证测试时花朵占比不要太悬殊。第四类是残花与花苞形态接近。菊花、向日葵之类的花开败之后舌状花开始下垂绒毛部分露出来从侧面看跟正在闭合的花苞状态很相似。这类样本需要单独拉出来看如果数量太少就考虑把它们单独作为一个“其他状态”类别否则二分类模型学得很痛苦。5.3 阈值调整和后处理默认情况下模型输出概率大于0.5就判为绽放。但根据上面的混淆矩阵和实际需求阈值是可以移动的。如果希望放过更多边界开花可以把阈值调到0.4如果希望只输出比较确定的绽放可以把阈值升到0.6。这个操作在推理阶段很简单画一条PR曲线或ROC曲线就能辅助选阈值。另外一个实用后处理技巧是如果一张测试图里有多朵花先裁剪成多个局部区域再分别预测最后取平均概率会比整张图直接判断稳定很多。因为整张图里花的状态不一致时模型容易被数量多的一侧带跑。我在实际测试中用这个方式把边界样本的稳定性提升了几个百分点而且实现成本很低。6. 毕设落地的代码框架与答辩演示模型效果调到可接受范围后剩下的就是工程整理和展示。很多同学有一个误区只丢一个训练好的模型文件和一段训练脚本给指导老师看。其实毕业设计更看重的是项目结构清晰、实验有对比、展示有深度。6.1 项目文件组织方式我建议使用下面这种目录结构flower_bloom/ ├── data/ │ ├── train/ │ │ ├── blooming/ │ │ └── not_blooming/ │ ├── val/ │ └── test/ ├── src/ │ ├── dataset.py │ ├── model.py │ ├── train.py │ ├── evaluate.py │ └── infer.py └── checkpoints/dataset.py负责读取图片、做数据增强、返回训练用的pytorch Datasetmodel.py里面放自定义网络和迁移学习网络的构建逻辑train.py负责训练、验证、保存最优模型evaluate.py输出准确率、混淆矩阵、分类报告infer.py接收单张图片输出概率和最终判断。checkpoints目录用来放每个epoch的模型权重也可以只保留最佳模型。这个结构看起来很常规但好处是分工清楚每一步都能被复现。6.2 训练脚本与推理的关键环节训练脚本里最需要注意的是“写对训练循环的四个状态”训练模式、验证模式、保存时机、早停条件。简单写出来就是model.train() for images, labels in train_loader: logits model(images) loss criterion(logits, labels.unsqueeze(1).float()) optimizer.zero_grad() loss.backward() optimizer.step()验证的时候要记得model.eval()并且用with torch.no_grad()包裹否则推理模式下的随机失活和BatchNorm统计会不稳定。保存模型时我建议同时保存state_dict和超参数信息比如记录当时的epoch、val_acc、optimizer状态这样之后恢复实验很方便。推理脚本则很简单加载权重对图片做和训练时相同的预处理得到sigmoid概率再用阈值判断。预处理不一致是常见的部署翻车原因比如训练时把图片缩放到224x224推理时却忘了做同样的变换导致结果跟验证阶段差距很大。6.3 答辩展示的三个加分项真正到了答辩展示环节我只推荐三个方向。第一讲数据难点和标注规范。把“什么算绽放”的口径、边界样本、半开状态的样例放出来比单纯展示94%准确率更有说服力。因为答辩组老师通常更关心你有没有想清楚问题边界。第二展示训练曲线和混淆矩阵。一张loss下降的图、一张混淆矩阵图、几个典型误判案例就能把项目从“跑通”提升到“系统分析”的层次。特别是误判案例只要讲清楚错在哪里、可不可以解决就已经展示了你的思考能力。第三做一个“未见过照片”的小测试。在提交前当场用训练集之外的实拍照片测试模型把输出概率展示出来。这个动作虽然简单但比任何口头描述都直观。我见过不少准备充分的同学在这个环节加分明显因为这说明项目不是背题而是真正面对了未知数据分布。答辩里还有一个很常见的问题“为什么没用目标检测先把花定位出来”你可以回答本任务专注于二分类目标检测是后续扩展方向并且你已经观察到小目标容易误判未来可以用检测加分类的两阶段方案提升稳定性。这种回答既没有否定当前工作又展示了对项目局限的认知。最后分享一个小技巧。无论验证集准确率多高提交前请单独去路边、阳台、校园绿化带拍上一两百张和训练集无关的新照片把这些当成“模型没见过的新世界”来测。我试过很多次凡是这轮测试掉链子的模型基本都是训练集背景太单一导致的要么花太小要么背景有大量同色色块。把这些照片整理成一个小型测试集再让模型跑一遍你能很清楚地看到自己这个项目距离“真实场景可用”还差多少。这个环节做好了答辩时讲出来的深度完全不一样。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑