简介:一套面向深度学习与毕业设计场景的花卉识别系统完整项目,围绕郁金香、玫瑰、蒲公英、向日葵四种花卉,提供可直接运行的卷积神经网络实现,适合高校学生用于课程设计、毕业设计以及科研项目初期的原型搭建与思路验证。压缩包内共有两千个文件,主体为近两千张花卉图片数据集,另包含若干源码文件、说明文档、交互页面和辅助脚本,压缩包整体约二百八十兆字节,目录结构按功能划分,便于快速定位与后续维护。目前已有七十二人学习,代码经多环境严格测试,运行稳定,配套演示视频完整展示模型训练与预测流程,设计文档则涵盖环境配置、核心逻辑和常见问题排查,能帮助初学者减少踩坑成本。具备一定技术基础的读者还可自行修改模型结构或增加花卉类别,从而扩展到其他图像分类任务,是一份兼顾教学、答辩与项目实践的可复用资源。
1. 一张图片四分类,TensorFlow 花卉识别系统到底在解决什么
输入一张郁金香、玫瑰、蒲公英或向日葵的图片,让程序告诉你它属于哪一类,这就是 TensorFlow 花卉识别系统要做的事。它常被当作 CNN 实战的第一道练手项目,因为任务规模刚好:数据集不需要上百万张,模型用几十行 Keras 代码就能搭起来,却能完整覆盖数据加载、数据增强、卷积网络构建、训练调参、模型评估与推理导出这条链路。
网上流传的“源码+数据集+演示视频”包里,源码质量参差不齐,有的停在 TensorFlow 1.x,用着tf.contrib旧接口,解压后第一关就把人卡住。与其到处找包,不如从零把同一套实现重新走一遍。本文不假设你手上有任何现成代码,只要求有一批可分类的花图片。文章会讲清楚目录结构怎么设计、CNN 结构图怎么画、训练要盯哪些参数,以及最后如何用一条命令完成单图预测。正在做课程设计,或者刚学完卷积原理想动手的读者,跟着这条路走完就能得到一个可演示的四分类系统。
2. 数据先行:TensorFlow 花卉识别系统的数据集整理与增强
2.1 从散图到标准目录结构
训练开始前的第一件事不是写模型,而是整理目录。TensorFlow 里两个常用的图片加载接口image_dataset_from_directory和flow_from_directory遵守同一个约定:把图片按标签分文件夹,文件夹名就是类别名。对四分类花卉任务,目录结构通常长这样:
data/ train/ tulip/ rose/ dandelion/ sunflower/ validation/ tulip/ rose/ dandelion/ sunflower/ test/ tulip/ rose/ dandelion/ sunflower/训练集、验证集、测试集各自独立。训练集用于更新权重,验证集用于观察是否过拟合、决定何时保存模型,测试集只在最终评估时用一次。很多人图省事不建测试集,拿验证集当测试集反复调参,最后报出来的精度水分很大。验证集被“看上”的次数越多,它对真实泛化能力的代表性就越差。
如果下载的数据集是四类混在一个目录里,先写脚本按 8:1:1 切分。切分之前一定要先洗牌,因为很多数据集按采集时间排列,前一半是晴天中午拍的,后一半是阴天拍的,不洗牌会让人为地把训练集和验证集分布拉开。
import os import random import shutil classes = ['tulip', 'rose', 'dandelion', 'sunflower'] for cls in classes: src = os.path.join('origin', cls) files = [f for f in os.listdir(src) if f.lower().endswith(('.jpg', '.jpeg', '.png'))] random.shuffle(files) n = len(files) for i, f in enumerate(files): if i < int(n * 0.8): phase = 'train' elif i < int(n * 0.9): phase = 'validation' else: phase = 'test' dst = os.path.join('data', phase, cls, f) shutil.copy(os.path.join(src, f), dst)脚本按int(n * 0.8)和int(n * 0.9)两个边界切分,保证四个类在三个阶段的比例一致。这里用copy而不是move,原始数据留着更安全;如果磁盘紧张,换成shutil.move也可以。class_indices这个属性会在flow_from_directory执行后自动生成,顺序按文件夹名的字母序排列,例如dandelion是 0,rose是 1,这个顺序在后面的预测阶段必须对齐。
新版 Keras 官方更推荐image_dataset_from_directory,但ImageDataGenerator在绝大多数课程源码里还在用,目录约定完全相同,迁移成本很低。
2.2 数据增强组合与关键参数
四类花各几百张图,对 CNN 来说数据量偏小。一个简单卷积神经网络也有几十万个参数,训练集不够时模型很容易把背景、拍摄角度、光线这些不相关因素一起记住,最终表现是训练准确率接近 100%,验证准确率卡在 60%。缓解过拟合最有效的办法是数据增强:在训练过程中对图片随机做旋转、平移、缩放、翻转,让模型每个 epoch 看到的都是同一张花的不同形态。
from tensorflow.keras.preprocessing.image import ImageDataGenerator train_gen = ImageDataGenerator( rescale=1.0 / 255, rotation_range=20, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.15, zoom_range=0.2, horizontal_flip=True, fill_mode='nearest', ) train_flow = train_gen.flow_from_directory( 'data/train', target_size=(224, 224), batch_size=32, class_mode='categorical', shuffle=True, )rescale把 0 到 255 的像素值压缩到 0 到 1,几乎任何模型都要做这一步,否则数值范围过大会让梯度更新不稳定。rotation_range控制随机旋转角度,对花这类方向不敏感的目标,20 度以内的旋转既模拟了拍摄角度变化,又不会把花旋转到语义上反常识的状态。width_shift_range和height_shift_range让花朵在画面里“跑动”,模拟目标不在画面正中央的情况。zoom_range模拟远近不一的拍摄距离。horizontal_flip对花特别安全,因为左右翻转不会改变花的类别。
关键参数汇总如下:
| 参数 | 含义 | 推荐值 | 实际作用 |
|---|---|---|---|
| rescale | 像素归一化 | 1.0/255 | 把数值压到 0 到 1,必填 |
| rotation_range | 随机旋转角度 | 20 | 模拟拍摄角度变化 |
| width/height_shift_range | 平移比例 | 0.2 | 模拟花朵偏离画面中心 |
| zoom_range | 缩放范围 | 0.2 | 模拟远近拍摄 |
| horizontal_flip | 水平翻转 | True | 花翻转后仍是同一种花 |
| fill_mode | 变换后空白填充 | nearest | 旋转平移带来的空洞用最近像素补上 |
验证集和测试集的生成器只写rescale,不加任何随机变换。验证集的作用是评估模型在真实分布上的表现,如果把增强也加进去,每次验证时输入都不同,得到的高高低低的验证准确率就不是一个可比较的稳定指标。
2.3 验证集单独留好,再用小脚本查一遍坏图
训练前确认一下每个类别在各个目录里的图片数量。如果郁金香只有 200 张,玫瑰有 700 张,训练会不自觉偏向多数类。缓解办法有两个:多数类保持原样,少数类用更强的增强;或者在训练时给少数类更高的class_weight。还有一个被忽略的坑是损坏图片,某些 zip 包里的 jpg 文件是零字节的,或者文件头损坏,flow_from_directory读取时要么抛异常,要么直接跳过,等你发现时训练已经跑偏了。
from PIL import Image import os bad = [] for root, dirs, files in os.walk('data/train'): for f in files: path = os.path.join(root, f) try: img = Image.open(path) img.load() except Exception: bad.append(path) print('损坏图片:', bad)脚本遍历训练目录下所有文件,Image.open负责读取文件头,load()里真正解码像素数据。能通过这两步的文件基本可以安全交给 TensorFlow 处理。如果bad列表不为空,用os.remove清掉这些文件再开始训练。
3. 用 CNN 卷积神经网络搭模型:结构图与 Keras 实现
3.1 先画一张 CNN 结构图,把卷积和池化的关系理清
搭模型之前先在纸上画结构图。输入是一张 224x224x3 的彩色图片,经过若干组“卷积、批归一化、池化”的组合,特征图的宽高不断缩小,通道数不断加深。粗略流程是:224x224x3 经过第一组卷积变成 224x224x32,池化后变成 112x112x32;第二组卷积后变成 112x112x64,池化后 56x56x64;第三组再变成 56x56x128,池化后 28x28x128。最后接全局平均池化,把所有特征图压成一个一维向量,经过两层全连接,输出 4 个概率。
这种结构是经典的 VGG 风格叠法,没有残差连接,也没有深度可分离卷积,但对四分类花卉任务完全够用,而且能让原理说得清楚。第一层卷积只能看到 3x3 范围内的局部像素,学到的是边缘、色块、方向这类底层纹理;郁金香的杯状轮廓、玫瑰的层叠卷曲、向日葵中央的管状花盘、蒲公英的放射状细丝,这些差异在浅层表现为边缘方向和颜色组合的不同。越到深层,感受野越大,卷积层才能把局部纹理组合成“花瓣”“花盘”这类语义部件。
有个常见的误区是模型越深越好。数据只有几百张时,加深网络带来的收益远小于过拟合带来的损失。四分类小任务里,三组卷积已经足够,重点是控制参数量,而不是堆层数。
3.2 用 TensorFlow Keras 写出可训练的花卉分类模型
结构图定下来之后,Keras 代码非常短。下面这段模型直接定义了完整的 CNN 卷积神经网络:
import tensorflow as tf from tensorflow.keras import layers, models model = models.Sequential([ layers.Input(shape=(224, 224, 3)), layers.Conv2D(32, (3, 3), activation='relu', padding='same'), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activation='relu', padding='same'), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activation='relu', padding='same'), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.GlobalAveragePooling2D(), layers.Dense(128, activation='relu'), layers.Dropout(0.5), layers.Dense(4, activation='softmax') ]) model.summary()Conv2D((3, 3))是 3x3 卷积核,padding='same'让卷积输出保持和输入一样的宽高。每经过一次MaxPooling2D((2, 2)),特征图宽高各自减半。BatchNormalization放在卷积层输出后面,让每层输出分布更稳定,收敛速度明显加快。GlobalAveragePooling2D把每张特征图取全局平均值,替代先展平再连全连接层的旧方案,参数更少,还自带一点正则效果。Dropout(0.5)在训练时随机丢弃一半神经元,等价于同时训练多个子网络再取平均,是控制过拟合最直接的手段。
Dense(4, activation='softmax')输出 4 个值,对应 dandelion、rose、sunflower、tulip 四个类别的概率。用model.summary()可以确认每层输出尺寸是不是符合预期:28x28x128 的特征图经过全局平均池化后变成 128 维向量。这个模型总参数量大约 84 万,对 CPU 训练来说也能在十几分钟内完成一轮像样的验证。
如果想进一步提高精度,更快的路线是把主干换成 ImageNet 预训练的 MobileNetV2,冻结其卷积基座,只训练新增的分类头。这种做法是迁移学习的标准套路,但建议先把这个自定义 CNN 跑通,因为冻结层数、Batch Normalization 的参数行为、输入归一化方式都会影响迁移效果。自定义模型能稳定过 90% 再换骨干,问题排查起来会简单得多。
3.3 郁金香和玫瑰为什么容易互相误判
训练结束之后,算一次混淆矩阵,大概率看到郁金香和玫瑰互相误判。原因是两类花的颜色高度重叠,红色玫瑰和红色郁金香在花瓣纹理不明显的情况下,模型很容易被颜色主导。蒲公英和向日葵也容易出现类似问题,因为它们都是黄色放射状花盘结构。模型在这种情况下学的不是“花的形状”,而是“颜色加中心区域形态”。
这时候首先检查的是数据本身,而不是模型结构。如果训练图大多是单朵花特写、绿色背景、正常光照,模型会倾向于把背景也学进特征里。把rotation_range从 20 提到 30,把zoom_range从 0.2 提到 0.3,一般能明显降低这类误判。颜色分布接近的类别,需要在增强里加入轻微的颜色扰动,例如用brightness_range调整亮度,让模型不再依赖某一特定色调。
4. 训练与调参:让模型稳定收敛的完整配置
4.1 编译参数与优化器选择
模型结构只决定表达能力,能不能稳定收敛靠的是编译参数和训练策略。四分类任务使用categorical_crossentropy作为损失函数,配合 softmax 输出。优化器选择 Adam,对学习率不那么敏感,适合作为小项目的第一选择。关键是学习率的初始值:1e-3对这个小模型来说偏大,loss 会在前几个 epoch 震荡;1e-5又收敛太慢,四类花任务通常从1e-4起步最合适。
model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4), loss='categorical_crossentropy', metrics=['accuracy'], )metrics里先只放accuracy。精确率、召回率这类指标等训练完在测试集上统一计算就好,没必要在训练循环里每个 epoch 都算,耗时且对判断收敛帮助有限。
训练环境方面,TensorFlow 2 搭配 Python 3.9 到 3.11 比较稳妥。常见做法是先建一个独立环境,避免把系统 Python 搞乱:
conda create -n tf-flower python=3.9 -y conda activate tf-flower pip install tensorflowGPU 版本需要注意 CUDA 和 cuDNN 的版本匹配,TensorFlow 2.10 之后在 Windows 上不再提供官方 GPU 支持,Linux 下用conda install cudatoolkit cudnn对齐比较省心。数据集只有几百张、模型只有几十万参数时,CPU 跑完全没问题,GPU 只是把每轮 epoch 耗时从几分钟压缩到几十秒。
4.2 回调函数与训练代码
小数据集训练最怕两个问题:不知道该跑多少个 epoch,以及最佳模型被后续过拟合的权重覆盖。回调函数就是为这两个问题设计的。
callbacks = [ tf.keras.callbacks.EarlyStopping( monitor='val_loss', patience=10, restore_best_weights=True, ), tf.keras.callbacks.ModelCheckpoint( 'flower_cnn.keras', monitor='val_accuracy', save_best_only=True, verbose=1, ), tf.keras.callbacks.ReduceLROnPlateau( monitor='val_loss', factor=0.5, patience=5, min_lr=1e-6, ), ] history = model.fit( train_flow, steps_per_epoch=train_flow.samples // train_flow.batch_size, epochs=100, validation_data=valid_flow, validation_steps=valid_flow.samples // valid_flow.batch_size, callbacks=callbacks, )EarlyStopping监控val_loss,连续 10 个 epoch 不下降就停止训练,并把权重回滚到验证集最优时刻。ModelCheckpoint保存最佳模型,save_best_only=True保证磁盘上始终是验证集上最好的版本。ReduceLROnPlateau在 loss 停滞时把学习率减半,让训练从“大步搜索”切换到“精细收敛”。
steps_per_epoch用整数除法计算,避免最后一个不完整 batch 带来的训练步数不均匀。模型文件用.keras后缀保存,这是 TensorFlow 2 新格式,包含模型结构和权重,加载时不需要再单独写结构代码。history对象里保存了每个 epoch 的 loss 和 accuracy,之后画训练曲线、判断过拟合都靠它。
4.3 训练常见坑和调试手段
第一类坑是内存不足。batch_size从 32 调到 16 或 8 是第一反应,其次是检查target_size是否被设置得过大。224x224 和 512x512 的内存占用相差数倍,而四分类花卉任务在 224 尺寸下已经足够。
第二类是类别不均衡。某个类别图片特别少时,在fit里传入class_weight:
class_weight = { 0: 1.2, 1: 1.0, 2: 1.2, 3: 1.0, }这里的键对应class_indices里的顺序。数字越大,该类样本在损失函数里的权重越高,模型会更多地“关注”样本少的那一类。
第三类是验证准确率一直不涨。先看训练准确率高不高:如果训练准确率远高于验证准确率,是过拟合,优先加大 Dropout、缩小 Dense 层宽度、增强数据;如果训练和验证准确率都低,是欠拟合,需要增加卷积层通道数或把学习率调小后再跑。另外建议优先观察val_loss而不是val_accuracy,四分类任务下准确率经常连续几个 epoch 卡在同一个整数百分比上,看着像没收敛,实际上val_loss还在小幅下降。
5. 单图预测与模型导出:识别系统的最后一步
5.1 写一个输入图片路径就返回类别名的脚本
训练得到的.keras文件还差最后一公里:一个能直接调用的预测入口。下面的脚本加载模型,处理一张图片,返回预测类别、置信度和完整概率向量。注意class_names的顺序必须和训练时flow_from_directory生成的一致,否则输出名会和概率对不上。
from tensorflow.keras.models import load_model from PIL import Image import numpy as np class_names = ['dandelion', 'rose', 'sunflower', 'tulip'] model = load_model('flower_cnn.keras') def predict_image(img_path): img = Image.open(img_path).convert('RGB').resize((224, 224)) x = np.asarray(img, dtype=np.float32) / 255.0 x = np.expand_dims(x, axis=0) probs = model.predict(x, verbose=0)[0] idx = int(np.argmax(probs)) return class_names[idx], float(probs[idx]), probsresize((224, 224))必须和训练时的target_size完全一致,否则输入维度不匹配。convert('RGB')统一把 RGBA 或灰度图转成三通道,避免单通道图在预测时报 shape 错误。np.expand_dims给单张图增加一个 batch 维度,因为模型接收的输入形状是(batch_size, 224, 224, 3)。模型加载放在函数外面,避免每张图都重复加载一次。
5.2 顺手导出 TensorFlow Lite 模型
如果识别系统要放到手机或嵌入式设备上,可以顺手用TFLiteConverter导出轻量模型:
converter = tf.lite.TFLiteConverter.from_keras_model(model) tflite_model = converter.convert() with open('flower_model.tflite', 'wb') as f: f.write(tflite_model)导出后的模型可以用官方 C++ 或 Java API 在移动端调用。转换本身不会改变模型参数量,体积也不会自动变小;真正缩小体积的手段是训练后量化,例如把权重从 float32 降到 float16,这需要在转换时设置优化选项。量化后的精度会有一点损失,对四分类的花卉识别任务来说通常不明显,但要在测试集上重新验证一遍才能放心。
本文还有配套的精品资源,点击获取