简介:基于Python与TensorFlow 2.3实现的花卉识别系统,是一款面向高校期末大作业、课程设计及毕业设计的完整项目,适合具备一定深度学习和Python基础的开发者参考或直接使用。项目围绕数据划分、模型训练、数据读取、模型测试及可视化窗口等模块展开,整体识别率约97%,具备清晰的目录结构和完整可运行代码。资源共27个文件,包含Python源码、H5模型权重、XML配置、PNG/JPG图片样本、文本说明及依赖清单等,压缩包约239.31MB。已有251人学习下载,可作为花卉分类任务从数据处理到模型部署的完整参考。除核心代码外,还提供数据集说明、readme文档、训练与测试脚本及模型文件,便于理解实现思路并快速复现实验。项目界面简洁、操作便捷,能够识别多种花卉,具有较好的实际应用与演示价值。
1. 用 Python + TensorFlow 2.3 做花卉识别:期末大作业为什么这个组合最稳
期末大作业选 Python + TensorFlow 2.3 做花卉识别,这个组合看起来普通,实际是最稳的一条路:不自己从头搭卷积网络,用现成的 MobileNetV2 预训练模型做迁移学习,把大部分时间花在数据整理和参数调整上,而不是被梯度消失、过拟合这些理论坑拖住。这篇笔记按一套最省事、也经得起答辩追问的课设流程来写,从环境部署、数据集下载、模型构建到最后的图形界面,把每一步细节和踩坑标出来。适合刚入门 Python 想拿一个完整 CV 项目练手、或者正在准备期末大作业答辩的人。照着跑一遍,就能拿到可演示、可解释、有曲线有结论的结果。
2. 搭建开发环境:Python 版本选型与 TensorFlow 2.3 的兼容边界
2.1 从 Python 安装开始:版本差一个小数点都不行
TensorFlow 2.3 的坑不在代码,在环境。它的官方轮子明确支持 Python 3.5 到 3.8,我建议直接用 Python 3.7,这是网上能找到的 python 安装教程里报错最少的一个版本。装 Python 不推荐直接去官网装完就用,那样多个项目共用一套 site-packages,装到后面全是版本冲突。我一般用 Miniconda 或 Anaconda 建独立环境,文件夹就叫 flower,以后重复实验直接删掉重来,比手动卸包省心得多。
conda create -n flower python=3.7 -y conda activate flower pip install tensorflow==2.3.0创建环境时把 python=3.7 显式写在命令里,能避免 conda 默认解析到最新 Python 导致后面装 TF 时提示找不到匹配版本。pip 安装如果网络慢,可以在命令末尾追加-i https://pypi.tuna.tsinghua.edu.cn/simple,用国内镜像源下载会快很多,这也是目前最稳妥的 python 下载方式。
装完以后打开 VSCode,按 Ctrl+Shift+P 调出命令面板,选择 Python: Select Interpreter,把解释器指到 flower 环境。这个步骤就是许多人常说的 vscode python 环境配置。配置错会出现一种很迷惑的现象:在终端里import tensorflow能成功,在 VSCode 里点运行却报 ModuleNotFoundError。本质是编辑器用的解释器跟终端不是同一个,选对虚拟环境后这个问题就消失了。
2.2 安装 TensorFlow 2.3:CPU 版、GPU 版与 CUDA 匹配
TensorFlow 2.3 的安装分 CPU 和 GPU 两条线。CPU 版是最省事的,不管有没有独立显卡都能跑,缺点是训练稍慢;GPU 版在 Windows 上建议直接安装带 GPU 后缀的包,并且要匹配 CUDA 10.1 和 cuDNN 7.6,版本错一位就会在 import 阶段报缺少 DLL 的错误。
import tensorflow as tf print(tf.__version__) # 期望输出 2.3.0 print(tf.test.is_gpu_available()) # CPU 环境输出 False 也正常这段验证代码要放在项目最前面,因为期末大作业后面所有代码都依赖这一个 import 能通过。如果输出版本号不是 2.3.0,说明你当前环境装的是别的版本,后续某些 API 调用会有差异。is_gpu_available()输出 False 不代表环境坏了,CPU 也可以完成整个训练流程,只是慢一些。
GPU 环境最容易翻车的点是显卡太新。比如 RTX 30 系显卡普遍需要 CUDA 11 以上,而 TensorFlow 2.3 固定要求 CUDA 10.1,两者在驱动层面不兼容。遇到这种情况我建议别花时间折腾降级驱动,直接改用 CPU 版先把流程跑通,答辩时老师关注的是识别效果和实验过程,不会因为你用 CPU 训练就扣分。能调通 GPU 算加分项,调不通也完全不影响交付。
提示:把 conda 环境和 pip 包严格锁定后,整个大作业期间不要再轻易执行
pip install --upgrade,TensorFlow 2.3 的 API 跟 2.10 之后版本有明显差异,升级容易把已经跑通的代码弄坏。
3. 准备花卉数据集:102 类牛津花卉的下载、划分与标签对账
3.1 数据集选哪个:Oxford 102 还是自己爬图
花卉识别题目的公开数据集主要有两个流派:一个是直接用 Oxford 102 Flowers,包含 102 个类别、约 8189 张图片,每类 40 到 258 张不等,标注文件是官方提供的,答辩时引用它最有说服力;另一个是自己用爬虫收集十几类花,优点是类别少、图好凑,缺点是要自己清洗大量重复图和错误标签,反而比调模型更费时间。
对期末大作业来说,我强烈建议直接用 Oxford 102。类别多会让模型看起来更有分量,官方标注还能省掉一整套人工校对工作。如果你的题目只要求识别 10 类花,也不用换数据集,按下面的脚本对 Oxford 102 抽一个子集就行,模型最后 Dense 层的 102 改成你的类别数即可。
| 对比项 | Oxford 102 Flowers | 自爬图片 |
|---|---|---|
| 类别数 | 102 类固定 | 自己定 |
| 标注准确性 | 官方人工标注 | 需要自己洗数据 |
| 版权风险 | 学术使用无风险 | 图片来源复杂 |
| 答辩说服力 | 强,有标准 benchmark | 弱,需解释数据来源 |
3.2 下载与按类别分目录:数据准备的 Python 脚本
下载到的 Oxford 102 压缩包解压后是一个名为 102flowers 的文件夹,里面图片的命名规则是image_00001.jpg到image_08189.jpg,文件名本身不携带类别信息。类别标签存放在 102labels.mat 文件里,是 MATLAB 的 mat 格式,要用 scipy 读取。所以数据准备的第一步是解析标签,把图片按类别放进独立文件夹。
import os import shutil import numpy as np from scipy.io import loadmat data_root = "flower_data" raw_imgs = "102flowers" mat_path = "102labels.mat" target = os.path.join(data_root, "by_label") labels = loadmat(mat_path)["labels"].flatten() os.makedirs(target, exist_ok=True) for img_id, label in enumerate(labels, 1): src = os.path.join(raw_imgs, f"image_{img_id:05d}.jpg") dst_dir = os.path.join(target, f"label_{int(label):02d}") os.makedirs(dst_dir, exist_ok=True) shutil.copy2(src, os.path.join(dst_dir, f"image_{img_id:05d}.jpg")) print("总类别数:", len(os.listdir(target)))这段代码的核心是loadmat(mat_path)["labels"].flatten()。mat 文件读出来是二维数组,.flatten()把它压成一维,之后就能按图片序号索引。循环里img_id从 1 开始,对应文件名image_00001.jpg;label是类别编号,label_{int(label):02d}格式化成两位数文件夹名,保证 class 排序稳定。最终输出 102 个文件夹,如果输出数值不是 102,优先检查 mat 文件路径是不是写对了。
按类别分好目录后,还要按训练集和验证集划分。我一般固定按每类图片数量的 15% 抽到 val 目录,并且设置随机种子保证每次划分结果一致,否则你训练到一半重新跑脚本,验证集变了,之前的训练曲线就失去对照意义。
import random import os import shutil from glob import glob split_dir = os.path.join(data_root, "split") for mode in ["train", "val"]: os.makedirs(os.path.join(split_dir, mode), exist_ok=True) for cls in os.listdir(target): cls_path = os.path.join(target, cls) imgs = glob(os.path.join(cls_path, "*.jpg")) random.seed(42) random.shuffle(imgs) n_val = max(3, int(len(imgs) * 0.15)) for i, img in enumerate(imgs): mode = "val" if i < n_val else "train" dst = os.path.join(split_dir, mode, cls, os.path.basename(img)) os.makedirs(os.path.dirname(dst), exist_ok=True) shutil.copy2(img, dst) print("训练集图片数:", sum(len(f) for _, _, f in os.walk(os.path.join(split_dir, "train"))))这里有两个参数值得注意。n_val = max(3, int(len(imgs) * 0.15))里的max(3, ...)保证每类至少进验证集 3 张图,防止某些类别数量太少导致验证集为空;random.seed(42)则是玄学但管用的东西,答辩时老师要求你重跑一次实验,有固定种子就能复现给老师看。最后一行用 os.walk 统计训练集总数,正常情况下应该是 7000 张左右。
3.3 标签与文件名对不上的排查
数据准备最常见的异常是图片数量跟 labels 数组对不上。Oxford 102 官网的图片包和标签包是分开下载的,有时网盘里下载的压缩包版本不一致,labels 数组是 8189 个元素,实际图片却只有 8180 张,运行脚本时会在后半段报 FileNotFoundError。
排查方法是数一遍解压目录里的文件数量,然后在脚本里打印 labels.shape 和实际图片列表长度做对比。如果数量差得很小,一般是压缩包传输出问题,重新从课程资源站或同学处拷贝一份完整压缩包即可,不用自己补造图片。如果数量差得离谱,比如图片有 1 万张,说明下载的是更高版本的花卉数据集,那份数据集标签格式可能不一样,需要重新下载匹配的 labels 文件。
提示:数据准备整个阶段不要用移动硬盘直接作为目标目录,复制 8000 多张小图在 USB 上可能只要几分钟,但后期训练要反复读取文件,放到本地 SSD 上会让每个 epoch 快很多。
4. 构建模型并训练:迁移学习的选型与三个关键参数
4.1 为什么用 MobileNetV2 而不是自己搭 CNN 或 VGG16
很多同学拿到这个题目第一反应是自己搭一个 CNN:几层 Conv2D、几层 MaxPooling、最后接 Flatten 和 Dense。这个思路本身没问题,但对期末大作业来说性价比很低,因为从零训练的模型在 8000 张图上很难收敛到 80% 以上准确率,而迁移学习可以轻松做到。答辩老师一定会问「为什么不用经典网络」,答案很简单:VGG16 有 1.38 亿参数,ResNet50 也有 2500 万,而 MobileNetV2 只有约 350 万参数,在 CPU 和低端显卡上都能快速训练,同时精度表现又不差。
| 模型 | 参数量 | ImageNet 预训练 | 训练速度 | 期末作业适配度 |
|---|---|---|---|---|
| 自搭 CNN | 几百万到上千万 | 无 | 快 | 低,收敛难 |
| VGG16 | 1.38 亿 | 有 | 慢 | 中,答辩可讲 |
| MobileNetV2 | 约 350 万 | 有 | 快 | 高,首选 |
选择迁移学习的另一个理由是逻辑清晰:它在 ImageNet 上已经学会了通用特征提取,花卉的边缘、花瓣纹理、颜色分布这些底层特征可以直接复用,你要训练的部分只是最后的分类器。这个思路在答辩时一两句话就能讲明白,比解释多层卷积的感受野好讲得多。
4.2 搭建模型:两段式结构的核心代码
迁移学习模型的搭建分为两步:先加载预训练骨干网络并冻结它,再在顶部拼接自己的分类层。这能避免预训练权重在训练初期被随机初始化的分类层反向传播破坏掉。
import tensorflow as tf from tensorflow.keras import layers, models base_model = tf.keras.applications.MobileNetV2( include_top=False, weights="imagenet", input_shape=(224, 224, 3), ) base_model.trainable = False # 冻结骨干网络 model = models.Sequential([ base_model, layers.GlobalAveragePooling2D(), layers.Dropout(0.2), layers.Dense(102, activation="softmax"), ]) model.compile( optimizer=tf.keras.optimizers.Adam(1e-3), loss="categorical_crossentropy", metrics=["accuracy"], ) print(model.summary())这段代码里include_top=False去掉 MobileNetV2 原有的 ImageNet 分类头,只保留卷积部分;GlobalAveragePooling2D把输出的 7×7×1280 特征图压缩成 1280 维向量,相比 Flatten 更省参数,也更不容易过拟合;Dropout(0.2)给分类层加一点随机失活;最后Dense(102)输出 102 个类别的概率分布。base_model.trainable = False是关键,它让骨干网络的权重在 fit 过程中不被更新,第一轮训练只学习新加的分类层参数。
如果要展示自己有更多思考,可以在骨干网络冻结训练一轮之后,解冻后面一部分层做微调。常见做法是把base_model.layers[:100]保持冻结,只让靠后的层参与训练,学习率降到 1e-5。这一步在答辩时作为「如何进一步提升精度」的加分项很有用。
4.3 数据增强与训练参数:这些数字是我踩坑后定下来的
数据只有 8189 张,训练集约 7000 张,对 102 类分类任务来说每类平均不到 70 张,直接硬训练很容易过拟合。数据增强是解决这个问题的标准手段,TensorFlow 2.3 里用 ImageDataGenerator 就能实现,不需要额外安装包。
from tensorflow.keras.preprocessing.image import ImageDataGenerator train_gen = ImageDataGenerator( rescale=1./255, rotation_range=20, width_shift_range=0.2, height_shift_range=0.2, zoom_range=0.2, horizontal_flip=True, ) val_gen = ImageDataGenerator(rescale=1./255) train_ds = train_gen.flow_from_directory( "flower_data/split/train", target_size=(224, 224), batch_size=32, class_mode="categorical", ) val_ds = val_gen.flow_from_directory( "flower_data/split/val", target_size=(224, 224), batch_size=32, class_mode="categorical", )rescale=1./255把所有像素值从 0 到 255 归一化到 0 到 1,使用预训练模型时这一步是必须的,因为 ImageNet 预训练权重就是在归一化后的输入上训练出来的。rotation_range=20表示图片最多旋转 20 度,width_shift_range和height_shift_range为 0.2 表示水平垂直各平移 20%,zoom_range控制随机缩放。验证集不能用任何增强,只用归一化,这样才能真实反映模型在未见过的图片上的表现。
训练阶段的回调配置也直接影响结果,我通常同时启用三个回调:
callbacks = [ tf.keras.callbacks.ModelCheckpoint( "flower_model.h5", save_best_only=True, monitor="val_accuracy", ), tf.keras.callbacks.ReduceLROnPlateau( factor=0.5, patience=3, verbose=1, ), tf.keras.callbacks.EarlyStopping( patience=6, restore_best_weights=True, ), ] history = model.fit( train_ds, validation_data=val_ds, epochs=30, callbacks=callbacks, )ModelCheckpoint 靠monitor="val_accuracy"把验证集表现最好的权重保存下来,训练完直接用这个文件做推理;ReduceLROnPlateau 在验证集连续 3 个 epoch 不提升时把学习率减半,避免后期震荡;EarlyStopping 在连续 6 个 epoch 无提升时提前停止,防止浪费时间。batch_size 在 4G 显存下建议 32,CPU 训练也设 32,显存不足就降到 16,不需要硬撑。
5. 避坑指南:期末大作业里最常见的 5 个翻车点
5.1 环境与依赖的 2 个坑
网上那些免费 python 源码大全里的花卉识别代码大多是 TensorFlow 1.x 的写法,直接抄进 2.3 环境会报各种 AttributeError。如果你找参考代码,先确认代码里有没有tf.keras或keras.layers字样,出现tf.Session()、placeholder的十有八九是旧项目,不要浪费时间。
现象一:Windows 下安装完 TensorFlow 2.3 后 import 报错,提示 DLL load failed。
原因:Python 版本与 TF 轮子不匹配,或者缺少 Microsoft Visual C++ Redistributable 运行库。TensorFlow 2.3 在 Windows 上的编译依赖特定 VC 运行库,很多干净系统默认没装。
解决:先确认当前 Python 是 3.6 到 3.8,然后安装最新的 VC_redist.x64.exe,重启后重新 import。如果还不行,卸掉 tensorflow 重新pip install tensorflow==2.3.0,这次注意看输出的文件名有没有 cp37 或者 cp38 标记。
现象二:GPU 环境已经装好,tf.test.is_gpu_available()返回 False。
原因:CUDA 版本不是 10.1,或者 cuDNN 版本不是 7.6。TensorFlow 2.3 对这两个版本卡得很死,装成 CUDA 10.2 或者 cuDNN 8.x 都没用。
解决:直接放弃 GPU,换pip install tensorflow==2.3.0CPU 版。CPU 版训练 30 个 epoch 大约 20 到 30 分钟,完全在可接受范围内,期末大作业的演示视频也可以提前录好,不耽误。
5.2 数据与训练的 3 个坑
现象三:训练时准确率很高,但用自己的测试图预测结果却一塌糊涂。
原因:有可能验证集和训练集没有正确分开,图片在 copy 时混进了同一个目录;也有可能预测时的预处理和训练时不一致,比如用 OpenCV 读图得到 BGR 三通道,训练却用的 RGB,输入数据的颜色通道顺序反了。
解决:检查by_label和split两个目录是否完全独立,写预测脚本时统一用tf.keras.preprocessing.image.load_img读图,它默认输出 RGB。如果非要用 cv2.imread,就手动做一次通道转换,img = img[:, :, ::-1]这类数组切片就是干这个的。
现象四:模型训练了 10 个 epoch,准确率一直停在 1% 左右,完全不收敛。
原因:最常见的是标签与输出层不匹配。flow_from_directory的class_mode="categorical"要求 Dense 层输出维度等于类别文件夹数量,如果你的 by_label 目录里有 102 个 folder,Dense(102) 是对的;但如果你选了 10 类花的子集,Dense 还写 102,就会梯度混乱。
解决:打印model.summary()最后一行检查 Dense 输出数,再打印train_ds.class_indices确认文件夹顺序与类别对应。改完后如果还不收敛,把 ImageDataGenerator 的加强参数全部关掉只留 rescale 试一版,确认能跑通再逐步加增强。
现象五:matplotlib 画混淆矩阵时中文标题变成方块。
原因:matplotlib 默认字体不支持中文,系统里也没有合适的字体映射。
解决:在画图代码最前面加两行配置:
plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False这是最简单且通用的做法,Windows 自带 SimHei 字体,设置后中文能正常显示。答辩报告里如果有训练曲线和混淆矩阵截图,这一步一定要提前处理,否则截图里全是方框,观感很差。
6. 从模型到作品:包一个预测界面并导出 exe
6.1 用 PySide2 写最小预测界面
训练完成的 h5 文件只是模型,期末大作业要交付的是一个能用的系统。用 PySide2 或 PyQt5 做一个单窗口界面,包含一个「选择图片」按钮和一个「识别结果」标签,代码量不大,但演示效果好。预测部分的预处理必须跟训练完全一致,这一步最容易把之前所有工作毁掉。
import sys import numpy as np import tensorflow as tf from PySide2.QtWidgets import QApplication, QLabel, QPushButton, QVBoxLayout, QWidget, QFileDialog model = tf.keras.models.load_model("flower_model.h5") def predict_image(path): img = tf.keras.preprocessing.image.load_img(path, target_size=(224, 224)) arr = tf.keras.preprocessing.image.img_to_array(img) / 255.0 arr = tf.expand_dims(arr, axis=0) pred = model.predict(arr)[0] return int(np.argmax(pred))这段代码里tf.expand_dims把单张图片从 (224, 224, 3) 扩成 (1, 224, 224, 3) 的批次格式,是因为model.predict要求第一维是样本数,即使只有一张也要补上这个维度。/ 255.0对应训练时的rescale,如果省掉这一步,预测准确率会明显下降。界面其他部分用 QVBoxLayout 组装两个控件就行,核心逻辑就这五行的量。
6.2 导出 exe 与写答辩报告的验收清单
交付给老师时,很多学校要求能独立运行的 exe。用 PyInstaller 打包 TensorFlow 程序体积会到 500MB 左右,这是正常现象,不用费劲精简。打包命令很简单:
pip install pyinstaller pyinstaller -F -w app.py-F表示打成单个 exe 文件,-w表示不显示控制台窗口。打包完成后务必在另一台没有 Python 环境的机器上双击测试,TensorFlow 的模型文件路径、图片路径这类相对路径问题要在打包前处理成os.path.abspath或嵌入资源文件。
答辩报告里建议放三样东西:训练过程的 accuracy 曲线,证明模型收敛过程正常;混淆矩阵,展示哪些类容易混淆并给出你的分析;至少一组参数对比,比如「增强前 78% vs 增强后 86%」,这比最终准确率数字本身更有说服力。
我自己做这个题目时最深的教训是:环境搭了两天,模型训练只花了半小时。后来每次启动新项目都先检查 Python 版本和三方库版本,再也不会在环境上反复横跳。这个坑你提前知道,就能把时间花在真正加分的数据分析和调参上。希望帮到你。
本文还有配套的精品资源,点击获取