TensorFlow CNN花卉识别实战:四分类模型构建与调参

发布时间:2026/9/11 9:35:12
TensorFlow CNN花卉识别实战:四分类模型构建与调参 简介这是一套面向高校学生与深度学习入门者的花卉识别实战资源以 TensorFlow 构建卷积神经网络完成郁金香、玫瑰、蒲公英、向日葵四类常见花卉的图像分类。压缩包总大小约两百八十兆字节内含完整源码、数据集与图片素材文件总数达两千个其中一千九百九十一张 JPG 图片以类别划分可直接用于模型训练两个 Python 脚本分别承担模型构建与预测推理另有多份文档说明与 HTML、JS 文件用于搭建简单的可视化展示页面并配有演示视频便于对照学习整体流程。资源整体结构清晰适合毕业设计、课程设计以及项目初期立项演示。当前已有七十二人学习下载代码经过多人、多环境测试运行稳定配套设计文档和演示视频能有效降低复现门槛帮助读者掌握从数据预处理、网络搭建、训练评估到前端展示的完整链路还可在此基础上修改扩展实现更多花卉类别的识别任务。1. 为什么用 CNN 做花卉识别而不是传统图像方法花卉识别看上去是图像分类里最亲切的任务但真要区分郁金香、玫瑰、蒲公英和向日葵传统方法并不轻松。郁金香有杯状花冠玫瑰是层叠花瓣蒲公英是放射状绒球向日葵有巨大的中央花盘颜色分布又有大量重叠单靠颜色直方图或 HOG 特征设计分类器很容易把黄玫瑰和向日葵混淆。CNN 用卷积核逐层学习边缘、纹理、花瓣排列等特征对这类细粒度差异的区分能力要强得多。这也是这个 TensorFlow 花卉识别项目选择 CNN 做四分类的直接原因。资源包里除了源码还带四类花的图片数据集、演示视频和设计文档完整跑通后对课程设计、毕业设计来说是比较扎实的素材。适合有 Python 基础、想系统过一遍 TensorFlow 2.x 图像分类流程但不想一上来就啃 ResNet 和迁移学习源码的读者。你可以把这份代码里的四类花换成其他类别只需要换目录和标签数量整体训练流程不用动。2. 数据集组织与 TensorFlow 数据管道四类花卉图片的读取与增强把压缩包解压后你会发现图片文件基本都在同一个目录下文件名保留着 Flickr 原始照片 ID并没有按类别放进子目录。资源包里还附带一个index.html一般是图片来源页或预览索引和训练过程没有直接关系。TensorFlow 的image_dataset_from_directory直接依赖目录结构生成标签所以第一步是把图片整理成下面这种结构。如果资源包里的数据已经分好类这一步可以跳过但多数时候拿到手还是要重新梳理一遍。2.1 数据目录结构与标签映射flower_dataset/ ├── train/ │ ├── tulip/ # 郁金香 │ ├── rose/ # 玫瑰 │ ├── dandelion/ # 蒲公英 │ └── sunflower/ # 向日葵 ├── validation/ │ ├── tulip/ │ ├── rose/ │ ├── dandelion/ │ └── sunflower/ └── test/ ├── tulip/ └── ...这里有个容易被忽略的点image_dataset_from_directory会按照目录名的字母序给类别编号所以四类花的标签顺序是 dandelion、rose、sunflower、tulip而不是你在页面上看到的郁金香、玫瑰、蒲公英、向日葵。后面做预测时类别数组必须按这个顺序写否则输出索引和真实类别会对不上。整理图片时注意不要按文件名简单均分。Flickr 上下载的图里经常有同一拍摄者的连拍照片如果这些相似图片同时进了训练集和验证集验证准确率会虚高。我一般会先用文件名的 ID 前缀做一次分组再按组拆分避免同源图片串集。这个细节在课程设计答辩时提到会是加分项。2.2 用 image_dataset_from_directory 加载数据加载部分直接使用 TensorFlow 的高层 APIimport tensorflow as tf IMG_SIZE (224, 224) BATCH_SIZE 32 train_ds tf.keras.preprocessing.image_dataset_from_directory( flower_dataset/train, image_sizeIMG_SIZE, batch_sizeBATCH_SIZE, label_modeint, shuffleTrue, seed42, ) val_ds tf.keras.preprocessing.image_dataset_from_directory( flower_dataset/validation, image_sizeIMG_SIZE, batch_sizeBATCH_SIZE, label_modeint, shuffleFalse, )参数里image_size会把所有图片统一缩放到 224×224不管原图是横幅还是竖幅都会直接拉伸。对于花朵这种主体居中的图片拉伸带来的形变影响有限但如果后续换其他数据集先做居中裁剪再缩放会更稳妥。label_modeint表示标签是整数索引对应后面损失函数SparseCategoricalCrossentropy如果这里改成categorical标签就变成 one-hot 编码损失函数也必须换成CategoricalCrossentropy。shuffle对训练集必须开启验证集建议关闭这样每个 epoch 的验证顺序一致指标可比性更强。seed固定后数据打乱顺序可复现调参时不会被随机性干扰。返回的train_ds是一个tf.data.Dataset每次迭代出一个(images, labels)组合images 的形状是(32, 224, 224, 3)labels 形状是(32,)。如果训练集和验证集是从同一个大目录拆出来的也可以直接在image_dataset_from_directory里传validation_split0.2配合subsettraining和validation使用效果相同但要求数据目录只包含训练样本。2.3 数据增强与归一化参数设计花卉识别里翻转和旋转不会改变花的类别所以数据增强对这个项目提升非常明显。TensorFlow 2.x 推荐把增强层直接写在模型里这样训练时生效推理时自动关闭不需要在数据管道里额外处理。data_augmentation tf.keras.Sequential([ tf.keras.layers.RandomFlip(horizontal), tf.keras.layers.RandomRotation(0.1), tf.keras.layers.RandomZoom(0.15), tf.keras.layers.RandomContrast(0.1), ]) rescale tf.keras.layers.Rescaling(1./255)RandomFlip(horizontal)做水平翻转郁金香和向日葵翻转后仍然是合理照片。RandomRotation(0.1)的 0.1 表示旋转范围是 ±36°超过这个角度蒲公英的放射状特征会被裁掉。RandomZoom(0.15)做 15% 的缩放模拟拍摄距离变化。RandomContrast(0.1)只给 10% 的对比度波动玫瑰花瓣纹理密集对比度扰动太大会让卷积核学到不稳定的高光。这里有一个很常见的矛盾点如果模型里已经放了Rescaling(1./255)数据管道里就不要再除以 255如果在tf.data里用了map(lambda x: x/255)模型里就不要放Rescaling。两种方式都能收敛但混用时像素值范围不是模型预期训练曲线会变得很奇怪。我在排查别人代码时见过最多次的错误就是在两边同时归一化导致 softmax 输出始终接近均匀分布。后面预测阶段还会再踩到一次。3. 搭建 CNN 卷积神经网络从卷积层到全连接层的参数设计数据管道准备好后就进入这个项目的核心CNN 本身。四分类的花卉图片不需要非常深的网络一个包含四个卷积块的模型加上数据增强已经能在验证集上得到不错的准确率。更深的网络如 ResNet50 在这个数据规模下反而容易过拟合训练时间也长。下面的结构是课程设计里比较标准的一套既完整展示了卷积、池化、全连接三种核心层参数量又不至于大到 CPU 跑不动。3.1 输入形状与卷积核的选择模型输入是(224, 224, 3)对应image_dataset_from_directory里设置的image_size。如果数据加载时改成(128, 128)这里必须同步改否则训练会报维度错误。卷积核全部使用 3×3 小卷积核好处是参数量少两个连续 3×3 卷积的感受野等于一个 5×5 卷积非线性表达能力却更强。paddingsame会让卷积输出保持和输入相同的空间尺寸配合MaxPooling2D每次把特征图缩小一半信息逐步向通道维度集中。3.2 模型结构Conv2D MaxPooling2D Dropout Densefrom tensorflow.keras import layers, models def build_cnn(input_shape(224, 224, 3), num_classes4): model models.Sequential([ layers.Input(shapeinput_shape), layers.Rescaling(1./255), layers.Conv2D(32, (3, 3), paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dropout(0.5), layers.Dense(128, activationrelu), layers.Dense(num_classes, activationsoftmax) ]) return model模型里第一层放了Rescaling(1./255)与数据增强层共用同一个Sequential时要注意增强层也要包含在模型最前面否则训练和推理的预处理逻辑会不一致。Conv2D的卷积核数量从 32 逐步增加到 128浅层偏向学习边缘和颜色块深层需要更多通道表达花瓣纹理。MaxPooling2D的(2, 2)窗口把特征图宽高减半四个池化层后224×224变成14×14Flatten 后是14*14*12825088维向量所以后面接的 Dense(128) 是主要参数量来源。Dropout(0.5)放在 Flatten 之后目的是让全连接层不要过度依赖某个局部神经元这里是训练时随机屏蔽一半神经元推理时全部恢复。如果训练集很小可以把 Dropout 提高到 0.6但不要超过 0.7否则模型会欠拟合。最后一个 Dense 层用 softmax 输出四个类别的概率。如果不追求展示 Flatten 过程可以把最后一个池化层换成GlobalAveragePooling2D参数量会从 345 万降到几十万训练更快精度不一定下降。但课程设计通常要求体现 CNN 的完整结构所以我保留 Flatten Dense 的写法。3.3 编译损失函数、优化器与评价指标model build_cnn() model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losstf.keras.losses.SparseCategoricalCrossentropy(), metrics[accuracy] ) model.summary()SparseCategoricalCrossentropy对应整数标签如果你的标签是 one-hot 编码就换成CategoricalCrossentropy。Adam优化器默认学习率 1e-3对这个项目来说可以先用这个值跑 5 个 epoch 观察损失下降速度如果 loss 下降非常慢检查是不是归一化写重复了如果 loss 震荡明显把学习率降到 1e-4。summary()输出每一层参数数量下面是这个结构的参数量表。层输出形状参数量Input(224, 224, 3)0Rescaling(224, 224, 3)0Conv2D MaxPooling(112, 112, 32)896Conv2D MaxPooling(56, 56, 64)18,496Conv2D MaxPooling(28, 28, 128)73,856Conv2D MaxPooling(14, 14, 128)147,584Flatten Dropout(25088,)0Dense(128,)3,211,264Dense(4,)516合计约 3.45M从表格能直观看到三个卷积块的总参数量只有约 24 万最后一个全连接层占了超过 321 万这就是 CNN 在图像任务上比纯全连接网络节省参数的根本原因。如果你想压缩模型可以把 Dense(128) 去掉直接接 softmax准确率会略降但模型文件从十几 MB 降到不到两 MB。这个取舍在移动端部署时很关键。4. 训练中的过拟合控制与调参学习率、Batch Size 和 Early Stopping模型定义完成后训练环节往往是差距最大的部分。一开始跑这个花卉项目最容易看到的局面是训练集准确率很快到 95% 以上验证集却卡在 80% 上下波动。这不是网络结构错了而是过拟合控制没跟上。四类花的数据集总量不大每类图片可能只有几十到几百张模型很容易记住训练图里的背景颜色和拍摄角度。下面这套训练配置是专门为这种小数据集设计的。4.1 训练参数设置与回调函数callbacks [ tf.keras.callbacks.EarlyStopping( monitorval_accuracy, patience10, restore_best_weightsTrue, ), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr1e-6, ), tf.keras.callbacks.ModelCheckpoint( best_model.h5, monitorval_accuracy, save_best_onlyTrue, ), ] history model.fit( train_ds, validation_dataval_ds, epochs50, callbackscallbacks, )EarlyStopping的patience10表示连续 10 个 epoch 验证集准确率没有超过历史最佳时停止训练restore_best_weightsTrue会把模型权重恢复到验证集最优的那个 epoch。ReduceLROnPlateau监测验证集损失5 个 epoch 不下降就把学习率减半直到最低 1e-6。这两个回调一起用时训练通常会在第 25 到第 40 个 epoch 之间停下来而不是真的跑满 50。ModelCheckpoint只保存验证准确率最高的权重文件名叫best_model.h5方便后面加载。注意这里 monitor 一个用val_accuracy一个用val_loss。因为准确率是离散的在小验证集上会跳跃用损失作为学习率衰减的指标更平滑。如果你只监测准确率可能在连续多个 epoch 都没有变化的情况下错过学习率调整时机。4.2 过拟合判断与典型特征训练过程中不要只看准确率要看训练集和验证集的差距。下表是四分类花卉项目里最常见的几种情况。现象判断调整方向训练准确率 95%验证准确率 75%~85%过拟合增大 Dropout、加强数据增强、减少 Dense 层训练和验证都低于 70%欠拟合或学习率不合适增加卷积核数量、降低学习率、检查归一化验证损失先降后升准确率徘徊开始过拟合提前停止、减少训练轮数验证准确率曲线剧烈抖动batch size 太小或增强太强把 batch size 从 32 调到 64减弱旋转角度在这个项目里如果验证集准确率卡在 80% 左右而训练集已接近 100%最直接的两个操作是把 Dropout 从 0.5 提到 0.6同时把RandomRotation从 0.1 调到 0.15。我测试下来这两个改动叠加通常能带来 3~5 个百分点的提升。要注意的是不要因为验证集准确率不高就盲目加深网络四个卷积块在这个数据规模下已经足够再加深只会让过拟合更严重。batch size 的影响也比较微妙。BATCH_SIZE32是默认选择显存占用小但梯度噪声大。如果验证曲线抖动明显先尝试把 batch size 提高到 64这时每个 step 更新的方向更稳定学习率可以相应调大一点比如从 1e-3 调到 2e-3。如果显存不足就保持 32靠 ReduceLROnPlateau 拉平训练曲线。4.3 训练曲线可视化与保存import matplotlib.pyplot as plt acc history.history[accuracy] val_acc history.history[val_accuracy] epochs range(1, len(acc) 1) plt.plot(epochs, acc, labelTrain Accuracy) plt.plot(epochs, val_acc, labelVal Accuracy) plt.xlabel(Epoch) plt.ylabel(Accuracy) plt.legend() plt.savefig(training_curve.png)画出来的曲线要保存成图课程设计报告里直接使用即可。我一般还会把history.history[loss]和val_loss放到另一张图里用于观察过拟合出现的具体 epoch。如果保存模型用的是ModelCheckpoint训练结束后不要直接用model变量而是重新load_model(best_model.h5)因为训练流程跑完后model里的权重是最后一个 epoch 的不一定是最优状态。演示视频里通常也会把加载模型后的预测结果单列出来就是为了验证保存和加载的一致性。5. 用训练好的模型识别郁金香单张图片预测与导出技巧5.1 加载模型并做推断模型训练完最终要落到单张图片预测上。用load_model加载.h5文件它会自动重建模型结构和权重import numpy as np from tensorflow.keras.preprocessing import image model tf.keras.models.load_model(best_model.h5) img image.load_img(test_images/tulip_001.jpg, target_size(224, 224)) img_array image.img_to_array(img) img_array np.expand_dims(img_array, axis0) # 增加 batch 维度 pred model.predict(img_array, verbose0)load_img里的target_size必须和数据加载时的image_size一致。这里的expand_dims是最容易漏的一步模型训练时输入是四维张量(batch, height, width, channels)单张图片只有三维必须在最前面加一维变成(1, 224, 224, 3)。如果模型定义时已经包含了Rescaling层这里不需要手动除以 255如果模型里没有则要在predict前加img_array img_array / 255.0。5.2 类别映射与置信度class_names [dandelion, rose, sunflower, tulip] score tf.nn.softmax(pred[0]) top np.argmax(score) confidence float(np.max(score)) print(f预测类别: {class_names[top]}, 置信度: {confidence:.2%})class_names的顺序必须和训练目录的字母序一致也就是 dandelion、rose、sunflower、tulip。如果训练时目录是中文名那这里的列表就要改成与之一致但建议不要用中文目录跨平台且容易在解码上出问题。因为模型最后一层是 softmaxpred[0]已经是概率分布直接用np.argmax取最大概率的索引。如果你的模型在定义时没有加 softmax输出的是 logits预测时就一定要先做 softmax 再取 argmax。提示如果你把增强层写在模型里预测时它会被自动关闭不需要额外处理。5.3 常见坑图片预处理不一致单张图片预测时最容易出问题的是预处理链路和训练时不统一。第一个坑是归一化重复模型里有Rescaling层外面又做了除以 255像素值整体缩小 255 倍softmax 输出会变得非常平四类概率都在 0.25 附近。可以对比输入图片的像素均值判断训练时模型接收的像素均值在 0~1 之间如果手写预处理后均值只有 0.0039说明除以了两次 255。第二个坑是图片通道顺序TensorFlow 默认是 RGB但如果你用 OpenCV 读图得到的是 BGR会把郁金香的红色花和绿色叶子交换通道导致预测完全错乱。检查方法是用img_array[0, 0, :]打印第一个像素的 RGB 数值与图片实际颜色对照。第三个坑是target_size不匹配训练时用 224×224预测时写成 229×229虽然模型能跑但特征分布和训练分布不一致准确率明显下降。检查时可对照模型的输入层形状model.input_shape[1:3]会给出训练时实际使用的目标尺寸。本文还有配套的精品资源点击获取