基于CNN的11种花卉识别:从模型训练到Web与PyQt5双端部署
发布时间:2026/10/19 8:17:58 锦皓数字建站

我做毕设那会儿选的是基于CNN卷积神经网络的11种花卉识别题目。除了模型训练本身还要同时交付Web端和PyQt桌面端两个小程序。一开始我以为难度全在模型上做完才发现真正磨人的是数据整理、模型导出、两个前端调用同一个推理模块这一整条链路。这篇就是完整的过程复盘从选题逻辑、数据预处理、CNN训练到Flask后端和PyQt5界面开发的细节都会写到。如果你也在做类似的图像分类毕设或者刚入门想找一个能从前端打到后端的完整案例这篇应该能直接当参考。1. 项目起步题目拆解与技术栈选型1.1 为什么定“11种花卉”而不是更多或更少选这个题目之前我大概翻了同类毕设的常见做法。有的是猫狗识别有的是树叶分类但花卉识别在效果展示上有个天然优势——结果直观用户上传一张照片立刻出答案答辩时演示成本很低。我最终把类别数定在11种主要考虑了三个因素。第一数据来源要可控。公开的花卉分类数据集大多在10类上下直接下载能省掉大量采集时间。如果自己补拍10类左右的花也比较容易凑齐。第二11类对模型来说是一个“多分类入门难度”既不会像二分类那样显得简单也不至于像100类那样需要调很久。第三用于演示时类别多了用户记不住类别少了又没有“多分类”的说服力。如果你也想复刻这个思路类别数量建议控制在10到15种之间。太少体现不了CNN多分类的能力太多会让数据标注和模型训练周期变长对毕业论文的时间节奏不友好。1.2 CNN对比传统机器学习方案优势在哪做之前我其实犹豫过要不要用SVM加手工特征。当时我在某课程实验里跑过HOG特征加SVM的分类效果也还行但换一个数据集就要重新设计特征提取器泛化能力一般。而CNN卷积神经网络走的是端到端学习卷积层自动提取边缘、纹理、花瓣形态等特征不需要人工设计特征算子。我后来做了一个简单的对比实验作为论文里的对照组传统SVM加HOG在11类花卉测试集上准确率大约86%同一个数据集上简单CNN能到94%左右。这里面的差异来源很直接花卉图像的类别差异往往体现在局部纹理和颜色分布上CNN的多层卷积对这种局部特征的组合表达更充分。毕设论文里加这样一个对比也能让“为什么用深度学习”这个论述站得住脚。1.3 Web端和PyQt端的角色分工与技术选型我一开始想过只做Web端但题目明确要求“web和pyqt”两个形态所以就得考虑一套推理代码怎么复用。整体技术栈最终定成了这样模块技术选型理由模型训练TensorFlow KerasAPI简单文档多适合快速迭代图像处理OpenCV PillowOpenCV做增强和读取Pillow负责格式统一Web端Flask HTML/CSS/JavaScriptFlask轻量和TensorFlow同属Python生态桌面端PyQt5 Qt Designer组件成熟可以直接拖拽界面模型导出SavedModel H5兼顾推理灵活性和迁移便利性Web后端没有选Django因为项目不需要数据库和后台管理Django太重了。桌面端没有选Tkinter因为Tkinter做出来的界面比较简陋PyQt5在视觉和控件丰富度上都更合适。两个前端共用同一个模型文件和同一个预处理函数核心推理逻辑只写一份这也是整个项目里我认为最值钱的设计。2. 数据集准备最容易翻车的环节2.1 数据来源与类别平衡数据集是整个项目的地基但也是我第一次翻车的地方。当时从某公开数据集下载了一部分花卉图片另外自己补拍了几类花。下载完我做了个统计发现类别严重不平衡数量最多的类别有800多张最少只有200多张。这种情况下直接开训CNN会把样本多的类别学得很好样本少的类别准确率惨不忍睹。我当时的处理分两步第一步把大类别随机抽到与中等类别接近的数量避免模型倾向第二步对小类别做数据增强。你没看错先降采样再增强而不是一股脑全用因为大类图片太多时训练周期也长均衡后的数据集在效果上往往更好。2.2 图像预处理尺寸、归一化、通道顺序预处理这一步看起来简单实际上坑很深。我统一把图片resize到128x128像素值除以255归一化到[0,1]区间。训练集、验证集、测试集按6:2:2划分划分前先shuffle确保同类图片不是连续排在一起的。这里有个容易踩的坑OpenCV读进来的图片是BGR通道顺序而TensorFlow的decode操作默认按RGB读取。如果你用OpenCV读图又没有做cv2.cvtColor(img, cv2.COLOR_BGR2RGB)转换训练出来的模型在验证集上可能表现还行但在实际预测时颜色分布就不对准确率会掉一大截。这个问题很隐蔽因为模型不会报错只会“默默地变笨”。2.3 数据增强的“度”怎么把握数据增强是解决小样本问题的常规手段但不能乱用。常用的增强操作包括水平翻转、小角度旋转、亮度抖动、缩放裁剪等。我刚开始把旋转角度设到了30°跑出来的模型训练集准确率很高验证集却一般后来发现是旋转太大导致花蕊形状被破坏反而给模型引入了错误样本。最后我调整成水平翻转概率0.5、旋转10度以内、亮度调整0.8到1.2之间。这个幅度既增加了样本多样性又不至于让花卉形态失真。数据增强一定要在训练流程里做而不是提前把所有增强后的图片存下来。用Keras的ImageDataGenerator或tf.data在训练时实时生成增强数据可以省硬盘空间还能让每个epoch看到不同的变体相当于隐式地增加了训练轮次的效果。3. 模型训练从baseline到稳定收敛3.1 卷积神经网络结构怎么设计很多同学一上来就上预训练模型但我觉得毕设题目里“基于CNN卷积神经网络”这几个字意味着你自己搭建的模型结构必须能讲清楚。我最终用的是经典卷积池化堆叠结构import tensorflow as tf from tensorflow.keras import layers, models def build_flower_cnn(input_shape(128, 128, 3), num_classes11): model models.Sequential([ layers.Conv2D(32, (3, 3), activationrelu, input_shapeinput_shape), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activationrelu), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(256, activationrelu), layers.Dropout(0.5), layers.Dense(num_classes, activationsoftmax) ]) return model这个结构没有多复杂但每一层都能在论文里解释清楚卷积层负责提取局部纹理池化层负责降维并保留主要特征全连接层做特征组合Dropout抑制过拟合最后一个softmax输出11个类别的概率分布。我同时用迁移学习做了一组对照实验用的是MobileNetV2的预训练权重做特征提取。对比下来MobileNetV2的准确率能到97%自己搭的简单CNN也能到94%。我个人建议主体模型用自己的结构把迁移学习作为对比实验写进论文这样既证明你理解了CNN原理又展示了探索能力。3.2 训练配置与关键参数训练参数我用了很常规的一套适合绝大多数入门级图像分类model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), losscategorical_crossentropy, metrics[accuracy] ) early_stop tf.keras.callbacks.EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue ) history model.fit( train_generator, validation_dataval_generator, epochs50, callbacks[early_stop] )表里的这些参数对应关系值得细看参数取值经验解释learning_rate0.001Adam优化器的常用起始值太小收敛慢太大loss会震荡batch_size32中等数据量下性价比高显存不够就降到16epochs50配合早停使用实际大概在25轮左右就收敛了EarlyStoppingpatience5验证集loss连续5轮不降就停避免无效训练有同学问为什么不用更大的学习率。我试过0.01训练到第三轮loss就变成了NaN。后来排查发现Adam在0.01的学习率下对这个数据规模的模型来说步子太大了权重直接爆炸。如果你遇到loss为NaN优先把学习率降到0.0001试试同时检查数据里有没有像素值异常。3.3 训练阶段常见的三个坑第一个坑是训练准确率极高但验证准确率低。这基本就是过拟合。我最初不加Dropout时训练集能达到99%验证集只有85%。加了Dropout(0.5)之后验证集立刻回到92%以上。第二个坑是验证集准确率忽高忽低。后来发现是batch_size太小8梯度下降的方向不够稳定。把batch_size提高到32后曲线平滑了很多。第三个坑是模型保存格式。Keras默认支持.h5和SavedModel两种格式训练结束后我两种都导出过。.h5是一个单文件部署时拷一个文件就行SavedModel是一个目录包含变量文件和结构定义适合TensorFlow Serving。对Web端和PyQt端来说用.h5最省事。但要注意两个前端加载模型时必须保持训练时的预处理逻辑一致否则再好的权重也会“水土不服”。4. Web端识别小程序落地4.1 Flask后端接口设计Web端我拆成了前端页面和后端接口两部分。后端用Flask写了两个路由一个GET /返回上传页面一个POST /predict接收图片并返回识别结果。模型文件在服务启动时加载一次千万不能在每次请求里重复调用load_model。我之前图省事把加载写在了predict函数内部本地调试时没问题并发一上来内存就吃紧。正确的做法是在模块级别加载import os import numpy as np from PIL import Image from flask import Flask, request, jsonify from tensorflow.keras.models import load_model app Flask(__name__) model load_model(flower_model.h5) # 类别列表要和训练时的顺序完全一致 CLASS_NAMES [类别0, 类别1, 类别2, 类别3, 类别4, 类别5, 类别6, 类别7, 类别8, 类别9, 类别10] def preprocess_image(image_bytes): img Image.open(image_bytes).convert(RGB) img img.resize((128, 128)) arr np.array(img) / 255.0 arr np.expand_dims(arr, axis0) return arr app.route(/predict, methods[POST]) def predict(): file request.files.get(image) if file is None: return jsonify({error: no image uploaded}), 400 try: array preprocess_image(file.stream) probs model.predict(array)[0] idx int(np.argmax(probs)) confidence float(probs[idx]) return jsonify({ class: CLASS_NAMES[idx], confidence: round(confidence, 4) }) except Exception as e: return jsonify({error: str(e)}), 500 if __name__ __main__: app.run(host0.0.0.0, port5000)这里有几个细节值得注意。用PIL而不是OpenCV来处理前端传来的图片是因为PIL对常见图片格式的兼容性更好而且convert(RGB)可以强制统一通道格式。前端的格式只有jpg、png还好万一有人传了bmp或者webpOpenCV处理起来就比较麻烦。我还给返回值加了个confidence字段前端可以直接展示置信度增加演示效果。4.2 前端页面上传、预览、异步请求前端我写了个单页面包含上传控件、图片预览区域、识别结果展示区域。核心逻辑是监听change事件读取本地图片用URL.createObjectURL生成预览图然后用fetch把图片POST到/predict。div h3花卉识别/h3 input typefile idimageInput acceptimage/* / br / img idpreview alt预览图 stylemax-width: 300px; display: none; / div idresult/div /div script document.getElementById(imageInput).addEventListener(change, function(e) { const file e.target.files[0]; if (!file) return; const preview document.getElementById(preview); preview.src URL.createObjectURL(file); preview.style.display block; const formData new FormData(); formData.append(image, file); fetch(/predict, { method: POST, body: formData }) .then(res res.json()) .then(data { if (data.error) { document.getElementById(result).textContent 识别失败 data.error; } else { document.getElementById(result).textContent 预测结果 data.class 置信度 (data.confidence * 100).toFixed(2) %; } }) .catch(err { document.getElementById(result).textContent 请求出错 err.message; }); }); /script这段代码写起来不难但有一个细节如果前端和后端不在同一个域名下比如你用不同端口启动前端静态服务器fetch会触发跨域限制。Flask里解决跨域最简单的方式是用flask-cors扩展在predict接口上加个cross_origin()装饰器就行。如果你只是想在本机演示让Flask同时托管HTML文件和接口就不会有跨域问题。4.3 Web部署调试中的实际问题我在这个环节踩了两个坑。第一个坑是上传图片后接口返回“图片无法识别”。排查后发现问题出在前端某些手机照片是EXIF旋转过的PIL读取后可能方向不对虽然我代码里没有专门处理EXIF但用convert(RGB)后统一尺寸大部分情况下就能绕过这个问题。如果遇到竖拍照片方向异常可以在预处理里用PIL的ImageOps.exif_transpose做一次翻转修正。第二个坑是Flask启动后浏览器访问一直转圈。排查了一圈发现是电脑上另一个程序占了5000端口。后来我习惯在app.run之前打印一行提示print(Server running at http://127.0.0.1:5000)避免启动成功却不知道访问地址的尴尬。5. PyQt桌面端实现5.1 界面布局与交互控件PyQt桌面端的界面我用了Qt Designer拖拽生成。主要控件就四个一个QLabel用于显示选中的图片一个“选择图片”按钮一个“开始识别”按钮一个QTextBrowser用于显示识别结果。界面布局大概是这样------------------------------------------- | [QLabel 图片显示区域] | | | ------------------------------------------- | [选择图片按钮] [开始识别按钮] | ------------------------------------------- | [QTextBrowser 识别结果] | -------------------------------------------如果用代码直接写界面也可以但Qt Designer生成.ui文件再转成.py调整布局直观很多而且毕业设计里截一张界面设计图也方便写论文。5.2 推理逻辑和主线程分离PyQt端最容易忽略的问题就是界面卡死。如果直接在按钮的点击事件里调用model.predict高分辨率图片预处理加推理可能要一两秒这一两秒内窗口会变成“无响应”状态用户会以为程序崩溃了。解决办法是引入QThread。把推理过程放到子线程里主线程只负责更新界面。我封了一个简单的工作线程类from PyQt5.QtCore import QThread, pyqtSignal import numpy as np from PIL import Image import os class PredictThread(QThread): result_ready pyqtSignal(str) def __init__(self, model, image_path, class_names): super().__init__() self.model model self.image_path image_path self.class_names class_names def run(self): try: img Image.open(self.image_path).convert(RGB) img img.resize((128, 128)) arr np.array(img) / 255.0 arr np.expand_dims(arr, axis0) probs self.model.predict(arr)[0] idx int(np.argmax(probs)) conf float(probs[idx]) msg f预测结果{self.class_names[idx]}置信度{conf * 100:.2f}% except Exception as e: msg f识别失败{e} self.result_ready.emit(msg)主窗口里连接信号self.predict_thread PredictThread(model, image_path, class_names) self.predict_thread.result_ready.connect(self.update_result) self.predict_thread.start()注意QThread对象要保存为实例属性不能是局部变量否则会被Python垃圾回收线程跑一半就没了。这个坑我调试了一个多小时报错信息里完全看不出来是回收导致的。5.3 PyQt端打包成exe的注意事项PyQt项目做完后一般会想打包成exe方便答辩时在任意电脑上演示。我用的是PyInstaller命令很简单pyinstaller -F -w main_window.py-F表示打包成单个exe文件-w表示不显示命令行窗口。这里有两个坑。第一模型文件flower_model.h5不在代码目录时会加载失败。最简单的解决方法是把模型放在exe所在目录的固定子文件夹里代码里用相对路径拼接base_dir os.path.dirname(os.path.abspath(__file__)) model_path os.path.join(base_dir, models, flower_model.h5)第二PyQt5打包后在别人电脑上可能出现“缺DLL”的报错。一般装上对应版本的windows-curses或者把PyInstaller升级到新版能解决如果还不行就去PyInstaller生成的build目录看日志它通常会标明缺失的是哪个动态库。还有一个提升答辩效果的小技巧把运行窗口标题改成自己的毕设题目标题界面左上角再放一个小的结果置信度条。这些细节虽然不增加功能但能让评委觉得完成度很高。6. 实测结果与踩坑复盘6.1 各端实测准确率模型训练完成后的验证集准确率在94%左右测试集准确率93.6%。Flask接口和PyQt端共用同一份测试集图片做推理两个端的结果完全一致说明两端共用推理逻辑的设计是成功的。实际测试过程中我挑了几张网络上找的花卉照片识别的置信度普遍在85%到98%之间。这说明模型在“同类图片”上泛化能力可以但我也发现了两个明显的短板白玫瑰和百合经常被搞混因为两者花瓣颜色接近、形态也类似黄色郁金香容易被识别为向日葵颜色特征在模型判断中占了太大比重。6.2 从毕设答辩角度复盘整个项目整个项目做完后我把技术要点梳理成了一条主线数据预处理、CNN结构设计、训练调参、模型导出、Web部署、桌面端打包。这条线几乎覆盖了深度学习项目从0到1的完整链路答辩时讲起来逻辑非常顺。我答辩时重点展示了三个东西一是混淆矩阵直接说明模型在哪些花卉类别上容易出错以及我为什么做数据增强二是两个端的实时识别演示现场传了一张没见过的图片识别正确时效果很直观三是训练曲线训练集和验证集的loss下降过程能体现早停机制的作用。如果你也想复刻这个项目可以从扩展方向入手再加深一点深度。比如加上摄像头实时识别用OpenCV打开摄像头每一帧都调用模型预测或者把模型转换成TFLite格式部署到手机上。这些扩展不算难但能让项目从“毕设水平”向“项目经验”再靠一步。6.3 给后来者的几点经验最核心的体会是别把训练模型当成全部部署环节的坑远比你想象的多。我在Web端和PyQt端上花的时间几乎和训练一样多而且大多是踩完坑才意识到。模型保存和加载格式要保持一致最好给自己写一个统一的预处理函数两个端都调用同一个函数确保训练时的图像尺寸、归一化方式、通道顺序完全一致。任何一端预处理逻辑和训练时不一致识别效果都会打折扣。数据集质量永远是第一位。我的模型在验证集准确率卡在93%左右上不去时补了几张被错误归类的图片到训练集重训后准确率就提升到了94%。图片质量、类别平衡、标注准确这些基础工作比调参更重要。如果你时间紧先把Web端跑通再加PyQt端。因为Web端调试迭代快而PyQt端一旦涉及界面和线程问题排查成本相对更高。两个端共用推理模块的设计刚好能让你在完成Web端后PyQt端只写界面代码就够了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。