资讯动态

基于TensorFlow实现CNN花卉识别Demo:从数据集到模型部署全流程

发布时间:2026/10/17 4:33:46 来源:尧图企业网站定制
很多人第一次碰 TensorFlow都是被“图片分类 Demo”这种项目勾过来的我也不例外。这些年我前前后后写过好几个版本的花卉识别小系统从最早照着教程复制代码、“能跑通”就行到后来能跟别人解释清楚每一个设计决策中间踩过的坑比想象中的多得多。这篇文章就围绕“基于 TensorFlow 实现 CNN 花卉识别 Demo”展开把从数据集、模型结构、训练技巧到推理脚本、模型保存的完整链路拆开讲顺便把我在实际操作中踩过的那些怪坑也一并交代。这个项目的定位适合两类人一类是刚学完 TensorFlow 基础想拿一个可视化、有趣味性的图像分类项目练手的人另一类是正在准备课程设计或小组项目需要一套可解释、可复现的完整方案的人。花卉识别这个主题特别适合做 Demo因为“花”这个类别天然地比猫狗分类更有视觉层次感对结果好坏的判断也更直观。先说一句最重要的话做这种 Demo目标不是把准确率硬怼到 99%而是把“数据—模型—训练—推断”这条链路彻底打通并且知道每一步为什么这么设计。很多人在意那一个百分点却忘了整套流程的完整性和可解释性才是 Demo 的真正价值。1. 为什么拿“花卉识别”练手先给项目定个调子1.1 从产品视角倒推Demo 要解决的是“分类问题”还是“识别问题”很多人把“花卉识别”默认理解成“拿手机拍一朵花它告诉我这是什么花”。但严格拆解起来这句话背后其实藏了三件不同的事分类、识别、细粒度识别。分类指这张图属于哪个预设类别这是大多数入门 Demo 做的工作。识别则更广泛它意味着“图里有没有花、花在图片的哪个位置、画面中可能同时存在几种花”。细粒度识别则要区分同一类目下的相似品种比如玫瑰和月季、薰衣草和鼠尾草视觉差异非常小。明确这个定位非常关键。因为常见花卉数据集的图片往往是一朵花占据画面主要区域、背景相对干净模型的任务就是判断主体类别属于标准单标签分类。如果把“识别”和“分类”这两个概念混着用做出来的系统边界会很尴尬模型预测“这朵花最像玫瑰”用户却拿一张包含玫瑰、雏菊和蒲公英的合影来测试模型当然会困惑因为它在训练时根本没见过这种多物体样本。这一点是我给所有初次做图像分类项目的朋友的第一条建议先想清楚你的 Demo 是分类演示工具还是实际识别工具。前者能做到单类准确率高就很成功后者则需要思考目标检测、多标签分类、数据采集等一系列更复杂的问题。1.2 一个可复现 Demo 的最小闭环产品需求清了再来看技术组成。一个最小可用的花卉识别 Demo我把它拆成五个环节数据至少包含 4 类以上花卉图片每个类别建议 500 张以上模型自建 CNN 或基于预训练模型的迁移学习方案训练包含数据切分、数据增强、回调函数和训练日志推断能接受单张图片输入输出 Top-K 预测结果及置信度模型管理保存、加载后续可能的格式转换。你不需要在一开始就加入摄像头实时识别、云端部署、后台管理系统这些花活。Demo 的价值在链路不在功能数量。把上面五个环节走通后续任何扩展才有基础。我见过很多初学者直接跳进“用摄像头实时识别公园里的花”这个大坑结果大把时间耗在摄像头视频流预处理上模型本身准不准都没验证过。2. 数据集选型与预处理真正决定天花板的部分2.1 公开数据集与自采数据怎么权衡花卉识别教程里最常见的做法是采用公开的经典花卉数据集通常包含五个类目每类约几百到上千张图片。这类数据集的优点是类别平衡、每张图基本都是“一朵花居中、背景相对干净”的标准构图、不需要自己清洗大量垃圾数据适合快速验证 pipeline。但你必须清醒地认识到一个现实公开数据集里的图大多是高质量照片和你手机随手拍出来的“阳光下背光、画面里有手抖虚影、花瓣被风吹糊”的图分布差异非常大。模型在验证集上表现再好拿到真实场景里也可能翻车。我的建议是把公开数据集当作训练主集另外在自采数据阶段增加一批“脏数据”作为额外的测试集。这批数据不参与训练只在最后测试真实泛化能力时使用看看模型面对手机照片的真实表现。这一步能让你理解一个重要的经验准确率数字不能完全代表 Demo 的真实可用性。如果你准备自采数据以下几个采集纪律非常实用花的主体要占据画面 20% 以上的面积避免整张图都是绿色背景尽量覆盖不同光照、不同季节、不同拍摄角度单类图片至少 300 张太少时训练过程很容易过拟合类别粒度要一致要么统一按物种分要么统一按品种分不要混着来。2.2 预处理链路踩过的坑rescale 重复加倍问题TensorFlow 2.x 处理图像预处理至少有三种常见姿势ImageDataGenerator 老 API、image_dataset_from_directory 工具函数、以及直接在模型内部添加 Rescaling 或 Normalization 层。这三种姿势混着用最容易踩到一个非常隐蔽的坑。我自己的亲身经历是数据加载时用了 ImageDataGenerator并设置了 rescale1./255然后在模型第一层又加了一个 layers.Rescaling(1./255)。结果就是像素值被缩放了两次模型收敛速度极为缓慢训练损失曲线在好几个 epoch 里几乎一动不动。当时我一度怀疑是学习率设得不对来回调了好几轮最后仔仔细细检查完整条数据流才发现是预处理叠加了。正确原则其实只有一条预处理逻辑全链路统一。如果使用 image_dataset_from_directory 读取数据就配合 Normalization 层或手工做归一化不要在多个环节重复动手。另外用这个函数读数据时会默认把文件夹名称当作类别标签所以文件夹命名尽量不要用中文和空格否则后续模型保存和推理脚本编码时会冒出来一堆奇怪的编码问题。2.3 数据划分与验证集纪律数据划分听起来简单但翻车的人特别多。最常见的问题有两种一是读完所有图片之后直接整个喂给 model.fit根本不做验证集二是用 train_test_split 时不设 random_state导致每次运行实验验证集都不同模型效果根本无法横向比较。我现在固定采用的做法如下第一次划分时就用分层抽样确保每个类别在训练集与验证集中的比例一致把划分结果保存成索引文件所有实验复用时使用同一批数据训练阶段绝不碰测试集测试集只用于最终效果报告。分层抽样这一点尤其重要假设某个花类只有 200 张图随机划分时它可能只分到 80 张进训练集比别的类少了三分之一模型在这个类别上的表现就会明显偏弱。使用 sklearn 的 train_test_split 并传入 stratifylabels 参数就能很好地解决这个问题。3. 网络结构设计不要一上来就堆 VGG193.1 从零训练的 CNN 基线结构适合理解原理的版本对于花卉分类这种中小规模数据集一个三到四组卷积模块的 CNN 已经足以作为基线模型。先上代码这是我最常用的一个基础结构import tensorflow as tf from tensorflow.keras import layers model tf.keras.Sequential([ layers.Rescaling(1./255, input_shape(224, 224, 3)), layers.Conv2D(32, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activationrelu, paddingsame), layers.GlobalAveragePooling2D(), layers.Dense(64, activationrelu), layers.Dropout(0.5), layers.Dense(num_classes, activationsoftmax) ])这个结构里有几个细节值得展开细说。第一最后一层特征图用 GlobalAveragePooling2D 而不是 Flatten。很多教程还在用 Flatten 直接拉平再接 Dense但 GlobalAveragePooling2D 能大幅减少全连接层的参数量同时让模型对花在画面中的位置不那么敏感。举个例子一张玫瑰花偏左一张玫瑰花居中卷积特征图的最大激活位置会不同但全局平均池化会把整个特征图各通道的空间信息都汇总起来位置漂移的影响会小很多。第二Dropout 放在了最后一个全连接层之前这是抑制过拟合的常用手段。它在训练时随机丢掉一半神经元推理时自动关闭不需要你手工做任何处理这是 Keras 封装好的行为。第三输入尺寸统一成 224×224。这不是因为花卉数据集在这个尺寸上表现最优而是主流预训练模型几乎都按这个尺寸设计。你保留这个尺寸就保留了以后切换 MobileNetV2 或 ResNet 的兼容性不用因为输入尺寸变了又重新预处理一遍数据。如果只想跑自建 CNN降到 160×160 会明显加快速度但准确率通常也会掉一点需要自己权衡。3.2 为什么不建议初学者从零训练一个大网络从零训练 CNN 能让你掌握卷积、池化、全连接这些核心概念但它对数据量的要求很高。花卉图片里每个类别的实际特征非常多花瓣形状、颜色渐变、花蕊分布、叶片纹理、背景植物……如果只给每个类别几百张图从零学习这些低层特征就很容易出现过拟合。所以只要你把“快速得到一个准确率不错的 Demo”作为目标迁移学习几乎是必然更优解。用预训练在 ImageNet 上的模型作为特征提取器冻结底层卷积参数只训练后面的分类头十几轮就能达到从零训练几十轮都很难达到的效果。实现起来其实非常短base_model tf.keras.applications.MobileNetV2( input_shape(224, 224, 3), include_topFalse, weightsimagenet ) base_model.trainable False model tf.keras.Sequential([ base_model, layers.GlobalAveragePooling2D(), layers.Dense(128, activationrelu), layers.Dropout(0.5), layers.Dense(num_classes, activationsoftmax) ])两者的差别你可以这么理解从零训练是“从像素开始学习怎么看到轮廓再学习怎么把轮廓组合成语义特征”需要大量样本迁移学习是“借一双已经学会看世界的眼睛你只需要教它如何分辨不同种类的花”。对中小数据集来说后者抗过拟合的能力强得多而且训练时间短很多。我实测过一组数据同样是 5 类花卉各 500 张图片自建 CNN 往往要 30 个 epoch 以上才能接近 85% 验证准确率而 MobileNetV2 迁移学习差不多在 15 轮以内就能到 90% 以上训练时间还要短不少。3.3 结构之外的几个设计细节BatchNormalization 到底加不加在自建 CNN 里我建议加它能让训练过程稳定不少尤其在学习率稍微偏大时能避免损失值剧烈震荡类别数量很少比如只有 2-3 类时视觉差异往往比较大分类头不用做复杂一个 Dense 直接到 softmax 就够加太多全连接层反而容易过拟合输出层的 softmax 配合 CategoricalCrossentropy 使用如果你的标签是整数没有做 one-hot 编码记得改用 SparseCategoricalCrossentropy否则会报维度不匹配的错误。4. 训练环节的实操参数与过拟合对抗4.1 优化器、学习率与损失函数的选择训练阶段最需要调的就是优化器参数。我的基准配置是 Adam 优化器初始学习率 0.001。做迁移学习时因为分类头比较小学习率可以设置在 0.0003 到 0.001 之间。从零训练 CNN 时建议配合学习率衰减策略TensorFlow 里可以直接使用 CosineDecay 或 ReduceLROnPlateau。这里给你一个我总结的经验参考表很实用场景batch size初始学习率备注自建 CNN输入 224×224320.001显存不够降到 16自建 CNN输入 160×160640.001尺寸变小可适当加大 batch迁移学习 MobileNetV2320.0005分类头轻量学习率不宜过大迁移学习 微调最后几层320.0001微调阶段学习率必须更小关于 batch size 和学习率的关系可以套用一个很直观的经验batch size 变大时梯度噪声变小学习率往往可以适当调大如果 batch size 只有 8学习率仍设 0.001训练通常会震荡得很厉害。我自己做过一个测试把 batch size 从 32 换到 64学习率没动第一轮损失确实降得更快但到中后期验证损失出现了轻微震荡把学习率降到 0.0005 后曲线平滑了很多。4.2 观察曲线的方式别只盯着训练集准确率很多初学者训练时只盯着训练 accuracy看到 0.98 就高兴得不行。实际上训练准确率再高都没有意义关键要看验证集的 loss 和 accuracy 是否同步变化。我经常使用的判断信号有以下几个训练 loss 持续下降验证 loss 先降后升这是过拟合的经典信号说明模型开始背诵训练集了训练 loss 下降但验证 loss 几乎不动可能是数据增强太强或者学习率从一开始就偏高也可能是训练集和验证集分布差异太大验证准确率突然掉了一大截之后慢慢恢复大概率是某个 batch 里混进了脏数据或者学习率衰减策略过于激进损失变成 NaN先检查预处理有没有除以 0再确认标签是否干净最后看训练数据里是否有全黑或损坏图片。这里要重点推荐一个工具EarlyStopping。它不只是验证 loss 不再下降时停止训练配合 ModelCheckpoint 使用它会在泛化性能最好的点保存模型。很多人每次训练结束只存最后一个 epoch 的权重但最后一个 epoch 往往不是验证集表现最佳的位置尤其是当你用了学习率衰减和随机数据增强之后临近结束时的权重波动反而更大。4.3 数据增强与样本不均衡数据增强对于小数据集几乎是必需品。TensorFlow 2.x 里可以直接在模型内部添加增强层data_augmentation tf.keras.Sequential([ layers.RandomFlip(horizontal), layers.RandomRotation(0.2), layers.RandomZoom(0.2), ])把这个增强模块放到模型输入之后即可。训练时增强模块生效验证和推理时自动关闭Keras 对这类层有默认的推断行为处理你不需要写额外逻辑去开关它。但增强不是越猛越好。力度过大比如旋转角度超过 45 度、随机裁剪比例过大会让模型学到很奇怪的特征验证 loss 迟迟不降。我踩过一个典型坑给花朵图片增加 RandomBrightness(0.5)结果大量图片曝光过度到连花瓣轮廓都看不清训练损失直接开始震荡不收敛。样本不均衡在经典公开数据集里不明显但自采数据时经常碰到。处理方式有两个一是对每个 batch 做加权采样二是对少数类施加更强的增强。我优先建议前者因为它不改变数据分布只是让少数类在每个 batch 里被抽到的概率更高。keras 里可以用 class_weight 参数传一个类别权重字典两行代码搞定。5. 从训练到可演示 Demo 的最后一公里5.1 推理脚本的输入输出设计模型训练完毕不代表一个 Demo 能用了。能跑通一张图片的推理脚本才算跨过最后一公里。我平时保留一个很简单的推理脚本结构大概长这样import tensorflow as tf import numpy as np from PIL import Image model tf.keras.models.load_model(flower_model.h5) class_names [daisy, dandelion, rose, sunflower, tulip] def predict_image(path, top_k3): img Image.open(path).convert(RGB).resize((224, 224)) arr np.array(img) / 255.0 # 注意和训练时保持一致 batch np.expand_dims(arr, axis0) probs model.predict(batch, verbose0)[0] indices np.argsort(probs)[::-1][:top_k] return [(class_names[i], float(probs[i])) for i in indices] if __name__ __main__: print(predict_image(test_flower.jpg))这个脚本里最容易出问题的点是“输入尺寸和归一化方式必须与训练全局一致”。如果你在训练时用的是 image_dataset_from_directory没有在数据流里归一化而是模型第一层加了 Rescaling 层那么推理代码直接把 0-255 的整数数组喂进去就行不要再多加一步除以 255。反过来也一样。最稳妥的设计思路是把预处理逻辑尽可能放进模型内部这样推理脚本只需要处理尺寸和通道顺序不会因为外部环境不同而行为各异。5.2 图片加载方式的坑PIL、OpenCV、TensorFlow 三种方案我见过一个非常隐蔽的问题训练时用 tf.keras.utils.load_img 或 tf.io.decode_jpeg 读图推理时用 OpenCV 的 cv2.imread 读图同一张图预测结果却出现了明显差异。这不是模型的问题而是 OpenCV 默认读出来的是 BGR 通道顺序训练模型的输入是 RGB 通道通道错位严重破坏了原始特征表达。我的建议是统一使用 PIL Image 打开图片并且显式调用 convert(RGB) 保证通道一致行为和结果都最直接可控。如果项目里确实需要依赖 OpenCV至少要在读取后加一行 cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 做转换再进入模型。还有一个很多人忽略但特别影响效果的小点推理时要把图片 resize 到训练尺寸resize 算法最好也保持默认。PIL 默认使用 LANCZOS 重采样TensorFlow 的 resize 默认是 bilinear虽然差异通常很细微但既然可以做到完全一致就没必要在这个变量上冒险。5.3 交互界面从命令行到轻量 Web命令行版其实已经能完整说明问题但如果你想给别人演示一个最简单的 Web 接口比写桌面窗口程序省事得多。我用 Flask 写过一个小演示核心就两个路由一个返回 HTML 表单用于上传图片一个接收 POST 请求、调用预测函数、把 Top-K 结果拼到页面里。整个服务端逻辑控制在 100 行以内。在跑的过程中注意几个点文件读取要设置安全大小避免超大图片把内存打爆上传文件格式要限制后缀只允许 jpg、jpeg、png模型初始化要放到路由函数外面只加载一次不要每个请求都重新 load_model否则并发几个请求之后机器直接卡死。我不建议一开始就折腾摄像头实时识别。实时视频流本质上是把“单帧推理”套进循环同时还要处理“画面里有没有花、有花时主体在哪、取帧频率怎么控制、延迟能不能接受”这些额外问题和模型本身是两套复杂度。先用单图把模型能力验证扎实再考虑扩展实时方向。6. 模型保存、加载与后续迁移很容易被忽略的工程细节6.1 H5、SavedModel、Keras 格式怎么选TensorFlow 2.x 中 model.save 默认保存成 SavedModel 目录指定扩展名为 .h5 时则保存为 Keras 格式。对本地 Demo 来说两者都能用但如果你后续要把模型放到服务端加载或者用其他工具转换SavedModel 格式更规范一些。实际使用中最常见的故障是在 2.x 版本训练并保存的 h5 文件换到另一个环境后 load_model 报错“Unknown layer”或者无法反序列化自定义层。这个问题的根源往往是模型里使用了自定义层或者自定义损失函数而这些定义在新环境里找不到。我的规避原则有两个一是尽量只使用 Keras 内置层避免自定义类带来的兼容性问题二是如果确实写了自定义层保存加载时显式传入 custom_objects 字典。更保险的做法是把自定义逻辑封装成一个普通函数但内部仍然基于内置层去实现这样就不会产生专属类名的依赖。下面整理一个格式对比表方便按场景选择格式保存内容典型使用场景注意事项.h5架构 权重 优化器状态本地快速加载、模型较小跨版本兼容性稍弱SavedModel完整模型目录 signature服务端部署、TFLite 转换目录结构需要整体拷贝.kerasKeras 3 新格式完整序列化新项目推荐依赖 Keras 3 及配套版本6.2 推理阶段的显存和资源问题训练完模型后如果直接在训练脚本里调用 predict同一个进程通常没问题。但单独写一个推理脚本时很多人会遇到模型一加载就报显存溢出的问题。这里有个 TensorFlow 2.x 的已知特性它默认会占用几乎全部 GPU 显存即使你只是单张图推理也可能因为显存分配策略导致 OOM。对 Demo 来说设置显存按需增长更合适gpus tf.config.experimental.list_physical_devices(GPU) if gpus: tf.config.experimental.set_memory_growth(gpus[0], True)如果机器没有 GPUCPU 推理也不是不可接受。MobileNetV2 在普通 CPU 上处理单张 224×224 图片大约几百毫秒完全够演示使用。如果你自建了三四个卷积模块CPU 上单张处理也基本在一两秒以内。6.3 版本迁移与轻量化思路TensorFlow 升级节奏很快项目能跑通是一回事换一台机器能不能跑通是另一回事。我的建议是把关键依赖版本写进 requirements.txt至少记录 tensorflow、numpy、Pillow 三个包的实际测试版本。不要在没验证的情况下跑到新环境直接 pip install 最新版。别人来找我问报错时十个有八个都是版本不一致导致的非常浪费时间。如果后续你想把模型塞进手机或者网页可以考虑转换为 TFLite。转换时尽量把输入尺寸固定为 224×224并把归一化逻辑也一并固化进模型这样目标端的预处理会简化成“加载图片 resize 喂模型”三步。轻量化转换之后准确率会有小幅下降但对花卉识别这种类别间差异较大的任务通常影响相对有限。最后说一个我现在的固定习惯做任何图像分类 Demo我都会先从“数据分布是否看得见”开始验证而不是先调网络结构。动手训练前先抽 20 张图可视化一遍数据增强效果和预处理结果确认这一环节没有异常再进训练。这个习惯帮我挡掉了至少十次“模型莫名不收敛”的排查时间。准备跑通你的花卉识别 Demo 的话这一步时间非常值得预留。

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

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

免费获取报价 →
↑