ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

DenseNet201迁移学习花朵识别:数据预处理到模型微调实战

2026/9/11 22:59:06 拓冰建站 浏览量
DenseNet201迁移学习花朵识别:数据预处理到模型微调实战 简介这是一份基于深度学习TensorFlow框架的花朵识别项目源码包面向需要完成毕业设计、课程设计或期末大作业的在校学生也适合入门图像分类的开发者。项目采用DenseNet201预训练模型实现从数据预处理、模型训练到花朵分类识别的完整流程。压缩包共238个文件包括196张花朵样本图片jpeg/jpg用于训练与测试、19个Python源码文件负责模型构建与预测、DenseNet201.h5预训练权重文件、flower_info.db花名数据库以及XML配置、PNG图像等辅助文件整体大小约68.89MB目录结构清晰便于按模块查看与二次开发。代码经过完整测试下载后无需修改即可直接运行读者可快速跑通“数据加载—模型训练—花朵分类”全流程也可在此基础上调整网络参数或替换数据集进行功能扩展能有效帮助理解深度学习图像分类项目的工程化组织方式。目前已有1811人学习下载适合作为高完成度的课程项目参考。1. 使用 DenseNet201 做花朵识别为什么迁移学习起步就能跑出效果拿到这个项目压缩包时第一反应不是去读代码而是先看有没有模型文件。看到DenseNet201.h5就知道作者已经把最耗时间的事情做完了。花朵识别属于细粒度图像分类不同种类的花瓣颜色、纹理、花蕊形态差异非常小靠几张图从头训练一个深度卷积网络几乎必然过拟合。而 DenseNet201 在 ImageNet 上预训练过前几层已经学会了边缘、纹理、形状等通用视觉特征只需要把最后分类头换掉再在小规模花卉数据上微调就能取得很稳的效果。这也是 TensorFlow 生态里做毕业设计、课程作业最省力的路线。后面几章我会从项目文件拆起讲清楚数据怎么从数据库读出、DenseNet201 怎么微调、训练参数怎么定以及模型训练完以后怎么验证、怎么复用。2. 项目结构与数据准备flower_info.db 的标签映射与图像解码2.1 压缩包里每个文件是干什么的解开python实现基于深度学习TensorFlow框架的花朵识别项目源码.zip之后能看到一个混合了.h5、.db、.iml和若干.jpeg的目录。很多人看到.iml会疑惑这里直接说清楚.iml是 PyCharm 的模块描述文件保留它只是方便在 IDE 里直接打开项目对运行没有任何影响。文件作用DenseNet201.h5训练好的模型权重可能包含完整网络结构也可能是纯权重flower_info.dbSQLite 数据库存放花朵类别 ID 与类别名称的映射关系xxx.jpeg多个样例图片用于快速验证预测效果flower_info.imlPyCharm 项目模块信息IDE 使用.gitignoreGit 忽略规则与本机运行无关flower_info.db是容易被忽略但很关键的文件。模型输出的只是类别序号比如0、1、2没有名称想要在屏幕上显示“玫瑰”“向日葵”或者项目报告里要求的自定义类别名必须从数据库里反查。我一般拿到项目会先用 Python 打开它确认表结构和字段名避免后面写死字段导致查询报错。2.2 读取 flower_info.db 构建类别映射先写一个最小脚本看看库里到底有什么import sqlite3 conn sqlite3.connect(flower_info.db) cur conn.cursor() # 列出所有表 cur.execute(SELECT name FROM sqlite_master WHERE typetable;) tables cur.fetchall() print(tables:, tables) # 查看每张表的字段 for t in tables: cur.execute(fPRAGMA table_info({t[0]});) print(t[0], cur.fetchall()) conn.close()这段代码里我用sqlite_master查询表名再用PRAGMA table_info拿字段定义。常见情况下花朵分类表结构是flower_classes(id, name)如果项目改动过也可能叫class_names。字段可能不同所以先探一下再往下写。拿到结构后构建一个 Python 字典后面预测时把模型输出的整数索引转成可读名称import sqlite3 def load_class_map(db_pathflower_info.db): 读取类别表返回 {id: name} 字典id 必须与模型训练时的类别序号一致。 conn sqlite3.connect(db_path) cur conn.cursor() # 以最常规的 flower_classes(id, name) 为例 cur.execute(SELECT id, name FROM flower_classes ORDER BY id) class_map {int(row[0]): row[1] for row in cur.fetchall()} conn.close() return class_map if __name__ __main__: print(load_class_map())id转成int是因为 SQLite 返回的行数据里字段类型可能是字符串而 Keras 预测结果的下标是整数不转换会匹配不上。项目里如果字段名不同比如是class_id, class_name把SELECT部分换掉即可。这一步是数据与模型之间的桥漏了它后面打印出来的就是一串数字报告没法写。2.3 图像解码load_img 与数据流水线花朵图片来自真实拍摄尺寸不一DenseNet201 的输入要求是(224, 224, 3)。常见做法是直接用tf.keras.utils.load_img做加载和缩放因为它内部用 PIL返回后还能方便地转成 numpy 数组from tensorflow.keras.utils import load_img, img_to_array img load_img(a668e078bd0892334c0a3767cb8f5cdc.jpeg, target_size(224, 224)) x img_to_array(img) # 缩放至 [0,1] x x / 255.0 print(x.shape)target_size(224, 224)会强制把图片等比拉伸到目标尺寸虽然花朵有时会变形但单目标分类任务里影响很小。img_to_array得到的是(224, 224, 3)的 float 数组除以 255 是为了匹配预训练模型的输入分布。DenseNet201 预训练时数据本来就是[0,1]范围不做这一步输出结果基本是错的。如果训练数据量较大我会改用tf.keras.preprocessing.image.ImageDataGenerator或者tf.data.Dataset做批量加载。前者写起来简单适合几百张图的课程设计from tensorflow.keras.preprocessing.image import ImageDataGenerator datagen ImageDataGenerator(rescale1./255, validation_split0.2) train_gen datagen.flow_from_directory( flower_dataset, target_size(224, 224), batch_size32, subsettraining, class_modecategorical )rescale必须放在生成器里不能只对单张测试图做然后训练图忘了做这样训练与推理的预处理不一致模型表现会谜之变差。validation_split0.2会在目录里按文件名随机取 20% 做验证文件夹名就是类别名这也是项目里没有单独类别文件时最常用的组织方式。3. DenseNet201 迁移学习参数详解冻结层、微调策略与训练配置3.1 加载预训练模型include_top 与 weightsDenseNet201 在 TensorFlow 中可以直接从tensorflow.keras.applications加载。加载时两个参数最关键weights和include_top。weightsimagenet表示使用 ImageNet 预训练权重include_topFalse表示不要最后的 1000 类全连接层只保留卷积特征提取部分from tensorflow.keras.applications import DenseNet201 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout base_model DenseNet201(weightsimagenet, include_topFalse, input_shape(224, 224, 3)) base_model.trainable Falseinclude_topFalse后base_model 输出的是卷积特征图形状通常是(7, 7, 1920)1920 维通道数是 DenseNet201 最后一个 dense block 的结构决定的。把这个特征图直接拉平再接 Dense 层会有 7×7×1920 ≈ 94080 个输入维度参数爆炸所以我一般会在中间加GlobalAveragePooling2D把每个通道的 7×7 平均成一个值变成 1920 维向量既降维又保留全局信息。base_model.trainable False是迁移学习第一阶段的通用做法。冻结的层只做前向传播不参与梯度更新这样训练压力小而且预训练特征不会被小数据集破坏。如果你的数据集只有几千张甚至几百张这个设置能显著抑制过拟合。3.2 自定义分类头与微调结构分类头加几层、每层多大需要根据类别数决定。假设项目里识别 5 类花那么最后 Dense 层输出 5 个神经元用 softmax 激活model Sequential([ base_model, GlobalAveragePooling2D(), Dropout(0.5), Dense(256, activationrelu), Dropout(0.3), Dense(5, activationsoftmax) ]) model.summary()两个 Dropout 是防止全连接层过拟合的关键。Dropout(0.5)表示在训练时随机丢弃一半神经元我一般放在 GAP 之后和最后的 softmax 之前。中间这个Dense(256)不是必须的如果类别数很少比如 5 类以内也可以直接 GAP 后接输出层。加这个层是为了让模型有更强的非线性拟合能力但代价是参数量增加训练需要更多数据。数据集如果只有每类几十张图建议去掉中间层只保留一个 Dropout 加 Dense 输出。3.3 编译、回调与训练参数配置编译时选优化器和学习率非常重要。常见选择是 Adam初始学习率用1e-4而不是默认的1e-3。预训练模型的特征已经很好学习率太大一步就会把调整好的权重冲乱from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau model.compile( optimizerAdam(learning_rate1e-4), losscategorical_crossentropy, metrics[accuracy] ) checkpoint ModelCheckpoint( best_flower_model.h5, monitorval_accuracy, save_best_onlyTrue, modemax ) early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) reduce_lr ReduceLROnPlateau( monitorval_loss, factor0.5, patience4, min_lr1e-6 ) history model.fit( train_gen, validation_dataval_gen, epochs50, callbacks[checkpoint, early_stop, reduce_lr] )配置里各参数的含义如下参数值作用learning_rate1e-41e-4微调阶段的安全学习率避免破坏预训练特征monitorval_accuracyval_accuracy以验证集准确率作为模型保存的评判指标patience1010验证损失连续 10 轮不降则停止训练factor0.50.5验证集指标停滞时学习率减半min_lr1e-61e-6学习率下限避免降到零save_best_onlyTrue保证磁盘上只保留验证集最优的那个模型而不是最后一轮。课程设计交报告时你一般希望交“最优状态”不是“训练结束状态”这个回调就是干这个的。EarlyStopping的restore_best_weights能让你在回调触发后模型权重自动回滚到验证集最好的那轮省得手动去 reload 权重。3.4 二阶段微调解冻部分 Dense Block第一阶段训练到验证准确率平台期后可以进入微调阶段。微调不是直接base_model.trainable True然后全量跑那样前面层梯度变化大不仅慢而且容易灾难性遗忘。更稳的做法是从后往前冻结前面若干层base_model.trainable True # 从第 200 层开始让后面的层参与训练 for layer in base_model.layers[:200]: layer.trainable False model.compile( optimizerAdam(learning_rate1e-5), losscategorical_crossentropy, metrics[accuracy] ) model.fit(...) # 继续训练这里我取了 200 层作为例子具体层数要看base_model.layers的长度。DenseNet201 共有 200 多层把前面 200 层冻结、只让最后几个 dense block 和 transition 层更新能针对性调整高层语义特征同时避免底层通用特征被扰动。注意重新compile是必须的trainable改动后要重新编译否则不生效。二阶段学习率要更小一般取1e-5 ~ 5e-6因为我这里已经做了一轮训练权重已经接近局部最优学习率大了会在最优值附近震荡。4. 训练与验证中的踩坑记录过拟合、显存不足与模型重载4.1 过拟合训练集准确率 98%验证集 70%这是迁移学习项目最常遇到的症状。在 TensorFlow 中如果发现训练曲线上accuracy持续上升而val_accuracy停滞不动就说明模型开始死记硬背训练集了。常见应对方案有三个。第一个是加强数据增强。在ImageDataGenerator里加随机翻转、旋转、缩放datagen ImageDataGenerator( rescale1./255, rotation_range30, width_shift_range0.2, height_shift_range0.2, shear_range0.2, zoom_range0.2, horizontal_flipTrue, validation_split0.2 )rotation_range30表示随机旋转 0~30 度zoom_range0.2表示随机缩放 20%horizontal_flipTrue做水平翻转。这些变换能让模型看到更多“变体”变相扩大数据集。但要注意验证集不能做增强只能用rescale否则验证指标会失真。第二个是提高 Dropout 比率。把分类头的Dropout(0.5)提到Dropout(0.6)让模型不那么依赖某一个特征。第三个是直接降低全连接层维数比如把 256 降到 128。很多时候项目里给的源码在“裸运行”时过拟合先调这三处验证准确率会上升 5~10 个百分点。4.2 显存不足batch_size 和图像尺寸的调整训练时报ResourceExhaustedError是 TensorFlow 常见问题。尤其是在 Windows 上跑显存只有 4GB 时batch_size32直接爆掉。我一般先看nvidia-smi确认显存占用然后把batch_size改成 16 或 8。还有一个技巧是减少验证集批大小因为验证时不更新梯度但每个 batch 依然会占用相同显存train_gen datagen.flow_from_directory( flower_dataset, target_size(224, 224), batch_size16, # 从 32 调下来 subsettraining, class_modecategorical )如果改成 8 仍然爆显存再考虑把输入尺寸降到(192, 192)或(160, 160)但这时必须同时修改训练和验证的target_size并且之后预测新图片也要用同样尺寸。DenseNet201 对 192×192 输入依然可用只是精度会略微下降不过影响不大。不要试图通过allow_growthTrue解决一切它只是让显存按需增长实际峰值没变。4.3 模型重载自定义层与 custom_objects训练好的DenseNet201.h5如果是用model.save()保存的直接load_model就能恢复。如果保存时只存了权重那就要先重建结构再加载。还有一个小坑如果在模型里加了自定义层或者用了 Lambda 层重载时 TensorFlow 会因为找不到自定义类而报错。我遇到过项目里模型用了自定义F1Score指标加载时就必须这样写from tensorflow.keras.models import load_model model load_model( DenseNet201.h5, custom_objects{F1Score: F1Score} )custom_objects里填的是自定义类名和类对象的映射。如果你是从网上下载的项目重载时报错第一件事就是把模型文件里涉及的所有自定义函数、类都收集起来按这个格式填进去。项目本身提供的这个DenseNet201.h5如果是最新版直接load_model能过说明没有额外自定义层。5. 复用已有模型单图预测、批量评估与类别日志核对5.1 单张图片预测并输出类别名称模型训练好之后最常被问到的就是“我拿一张新图片怎么知道模型认为它是什么花”。这里给出一个完整可跑的推理脚本把前面章节的数据库映射和预处理整合起来import sqlite3 import numpy as np from tensorflow.keras.models import load_model from tensorflow.keras.utils import load_img, img_to_array def predict_single(model_path, image_path, db_pathflower_info.db): # 读取类别映射 conn sqlite3.connect(db_path) cur conn.cursor() cur.execute(SELECT id, name FROM flower_classes ORDER BY id) class_map {int(i): name for i, name in cur.fetchall()} conn.close() # 加载模型 model load_model(model_path) # 预处理尺寸、归一化、添加 batch 维度 img load_img(image_path, target_size(224, 224)) x img_to_array(img) / 255.0 x np.expand_dims(x, axis0) # (1, 224, 224, 3) probs model.predict(x)[0] idx int(np.argmax(probs)) confidence float(probs[idx]) return class_map.get(idx, funknown_{idx}), confidence name, conf predict_single(DenseNet201.h5, 48c812f2d14b43d500767fb5881e7a28.jpeg) print(f预测结果: {name}, 置信度: {conf:.2%})numpy.expand_dims非常关键因为 Keras 模型默认按 batch 处理数据单张图片必须扩出一个维度变成(1, 224, 224, 3)。class_map.get(idx, funknown_{idx})防止类别 ID 对不上时程序崩溃而是返回一个带下划线的未知标签这样你可以回查数据库。置信度打印成百分比方便直接判断这个预测可不可信。5.2 批量验证从验证集算 Top-1 准确率如果想快速检验项目自带的模型是不是真能用不要一张张测把目录下的样例图片全部跑一遍import os from tensorflow.keras.models import load_model model load_model(DenseNet201.h5) correct 0 total 0 for img_name in os.listdir(test_imgs): if not img_name.lower().endswith((.jpeg, .jpg, .png)): continue # 假设文件名格式是 类名_编号.jpg按下划线取真值 true_label img_name.split(_)[0] pred_label, _ predict_single(DenseNet201.h5, os.path.join(test_imgs, img_name)) total 1 if true_label pred_label: correct 1 print(f{img_name}: 预测{pred_label}, 真实{true_label}) print(fTop-1 准确率: {correct/total:.2%})我在这里用文件名的前缀作为真实标签这是一种常见的临时验证手段。项目里如果没有带标签的测试集你也可以从flower_info.db中读取类别 ID 与图片文件名的对应关系来做。批量脚本跑完你就能知道这个DenseNet201.h5在当前图片上准确率大致是什么水平而不是盲目相信 README 里的描述。5.3 回查数据库确认类别 ID 对齐最后强调一个容易被坑的细节训练出的模型类别序号和flower_info.db里的 ID 不一定一致。TensorFlow 的flow_from_directory会自动按文件夹名称的字母序生成类别索引比如文件夹叫daisy, dandelion, rose索引就是0,1,2。而数据库里 ID 可能按1,2,3编排或者按中文名排序顺序完全不同。所以推理之前一定要用一张你已知类别的图片先测一下把模型输出的 idx 与数据库映射结果打印出来对比print(模型输出 idx:, idx, 数据库名称:, class_map.get(idx))如果发现对不上要么修改数据库查询语句按字母序处理要么在训练时手动把类别索引写入数据库。做完这一步整个项目的闭环才算打通训练有模型验证有指标推理有名称写课程设计文档时每一张测试图都能给出“图片路径、预测类别、置信度”三要素。本文还有配套的精品资源点击获取