简介:一套基于Python深度学习的花卉识别课程设计源码,面向机器学习与计算机视觉初学者,围绕花卉图像自动分类识别提供完整工程,涵盖数据预处理、数据集划分、卷积神经网络与MobileNet模型构建、训练、测试及结果可视化等关键环节,适合课程设计、毕业设计或深度学习入门实践。压缩包共49个文件,整体仅3.36MB,包含12个Python源文件、15个jpg与13个png花卉样本,并配有训练与测试日志、热力图结果、Markdown说明文档及Git忽略文件,目录结构清晰,便于按模块阅读和调试。该资源已有362人学习,具有一定的参考热度。读者可获得可直接运行的MobileNet与CNN双模型代码、数据读取与数据集划分脚本,借助热力图和训练日志理解模型关注区域与性能变化,从而快速掌握图像分类任务从数据准备到模型评估的完整实践流程。
1. 花卉识别课程设计拿到手后,先别急着跑模型
这套“基于Python深度学习的花卉识别课程设计源码”里真正值钱的不是最终训练好的权重,而是它把一条完整链路都摊开了:CrawlingData.py 管数据采集,getData.py 和 ReadData.py 管数据加载,DivideData.py 管训练验证划分,MobileNet.py 和 CNN.py 两条模型路线并行,heatmap 文件夹里还留着 Grad-CAM 热图,train_log 和 test_log 里是训练测试的过程记录。换句话说,你拿到的不只是一个“能出结果的模型”,而是一套可以拆开来逐段讲解的机器学习项目标本,适合课程设计、期末答辩,也适合刚入职想接手小数据集图像分类任务的工程师。
这套东西最有意思的地方在于:数据集本身很小(daisy 菊花一类,jpg 加 png 也就几十张),数据量压不住大模型,但代码里却同时给出了“用预训练 MobileNet 做迁移学习”和“从零搭 CNN”两条路子,正好能回答「小数据集做深度学习该怎么办」这个高频问题。下方正文按数据准备、MobileNet 迁移学习、CNN 对照实验、热图可解释性四个层面把项目拆开,每个环节都给可直接改跑的命令和参数。
2. 数据准备链路:getData、ReadData 与 DivideData 到底各管哪一段
2.1 三个数据脚本的分工边界
在动手跑训练之前,得先把 getData.py、ReadData.py、DivideData.py 这三个文件的分工理清楚。很多初学者拿到项目后会误以为这三个脚本功能重叠,实际上它们是按「采集 → 加载 → 切分」三段式设计的。
getData.py 管数据从外部进入本地项目的动作。项目里有 CrawlingData.py,说明菊花图像很可能是通过爬虫方式采集的,而 getData.py 则负责把散落在不同目录的原始图片统一整理到 data 文件夹下,并完成初步过滤,比如去掉损坏图片、剔除尺寸异常的样本。ReadData.py 更偏向模型训练前的读取接口,它的输出通常是一个可以直接喂给 Keras 或 PyTorch 的 Dataset 对象,内部封装了图片解码、resize、归一化。DivideData.py 做的事情最容易被忽视:把数据集按固定比例切成训练集、验证集和测试集,并且保证每一个子集里类别分布均匀。
实际排错时,如果训练时发现 val_loss 抖动得很厉害,第一步不是改模型,而是回头检查 DivideData.py 的执行结果,看验证集里是不是出现了类别缺失。我一般会建议把 DivideData.py 抽出来单独跑,像下面这样验证切分结果:
python DivideData.py cat DivideData_result.txt如果 DivideData_result.txt 里输出的各类别数量差距超过 15%,就要重新设置随机种子,或者在划分时加上按类别采样的逻辑。划分数据这件事看起来不起眼,但它直接决定了后续训练日志里 val_accuracy 曲线的可信度。
2.2 数据增强参数怎么设置才不破坏小数据集
小数据集训练深度学习模型,数据增强是做迁移学习和从零训练之前绕不开的一步。常见做法是直接用 tf.keras.preprocessing.image.ImageDataGenerator,在读取图片的同时完成随机水平翻转、随机旋转、缩放和亮度扰动。参考项目里 data 文件夹的图像尺寸,下面这组参数是小数据集下比较稳的起点配置:
from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen = ImageDataGenerator( rescale=1.0 / 255.0, rotation_range=30, # 随机旋转角度范围 width_shift_range=0.2, # 水平平移比例 height_shift_range=0.2, # 垂直平移比例 shear_range=0.2, # 错切变换强度 zoom_range=0.2, # 随机缩放范围 horizontal_flip=True, # 水平翻转 fill_mode='nearest' # 填充新像素的方式 )rotation_range 设 30 而不是更大的 45,是因为菊花的花瓣具有明显的放射状对称性,过大的旋转角度配合 zoom_range 会让训练样本出现大量形变过度的伪样本。width_shift_range 和 height_shift_range 控制在 0.2 以内,防止目标花朵被平移出视野中心。fill_mode 用 nearest 而不是 constant,是因为 constant 会用纯色填充边缘区域,给模型引入不存在的边界特征。验证集和测试集只做 rescale,绝对不做增强,否则验证指标会被数据增强带来的分布偏移污染。
2.3 图像尺寸选择对 MobileNet 输入层的影响
项目里 results 文件夹下的文件命名出现了 results_mobilenet_10_6、results_mobilenet_30_12、results_mobilenet_50_14 这类组合,从命名规律推测,前面的数字对应当前的训练轮数(epoch),后面的数字对应 batch size。这种「轮数_批大小」的命名方式在课程设计里很常见,它记录的是同一模型在不同训练配置下的表现。
但不管是训练多少轮,输入图像尺寸首先要满足 MobileNet 的 constraint。Keras 内置的 MobileNet 和 MobileNetV2 都要求输入高度和宽度不小于 32,且最好是 32 的倍数,常见选择是 128、160、192、224。小数据集下我一般建议用 160 而不是 224,因为 224 会让模型在前向传播时计算量陡增,而 160x160 的输入在花卉这类前景集中的图像上损失的信息量很小。ReadData.py 里对应的 resize 逻辑可以直接参考,其中 target_size 按训练配置保持一致。
3. MobileNet 迁移学习:为什么是它,以及 MobileNet.py 里的训练逻辑
3.1 MobileNet 与 ResNet、VGG 的选型权衡
花卉识别这个任务里有 15 个 jpg 和 13 个 png 图像,总样本量很小,这种情况下选择模型的第一原则不是精度上限,而是参数量与数据量的匹配。VGG16 的参数量约 1.38 亿,ResNet50 约 2500 万,MobileNet 在 224x224 输入下参数量仅约 420 万。用 VGG16 在这点数据上微调,全连接层非常容易过拟合,训练日志里会出现 train_acc 迅速升到 0.98、val_acc 卡在 0.5 上下的分裂曲线。
MobileNet 的关键在于深度可分离卷积(depthwise separable convolution),它把标准卷积拆分成逐通道卷积和 1x1 逐点卷积两步,计算量约下降 8 到 9 倍。对课程设计场景,MobileNet 的另一个隐性优势是:即便不加载预训练权重,单靠 CPU 也能完成一轮完整的训练。这是 VGG 做不到的。项目里同时存在 MobileNet.py 和 CNN.py,正好可以用来做「预训练模型 vs 从零训练」的对照实验,这是答辩时很有说服力的材料。
3.2 MobileNet.py 搭建:冻结基座与自定义分类头
MobileNet.py 走的常见路线是加载在 ImageNet 上预训练过的 MobileNet 权重,冻结底层的卷积基座,只训练新加的全连接分类头。这种迁移学习策略下,训练轮数不用太多,10 到 30 轮之间通常就能收敛。参考项目的 results_mobilenet_10_6.png、results_mobilenet_30_12.png,这个轮数区间的实验结果是可以直接对比的。
下面是一段可直接替换进 MobileNet.py 的核心代码:
import tensorflow as tf from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Dropout from tensorflow.keras.models import Model base_model = MobileNetV2( include_top=False, # 去掉 ImageNet 自带的 1000 类分类头 weights='imagenet', # 加载预训练权重 input_shape=(160, 160, 3), pooling=None ) base_model.trainable = False # 冻结基座 x = base_model.output x = GlobalAveragePooling2D()(x) # 将特征图压缩为 1D 特征向量 x = Dropout(0.3)(x) # 抑制全连接层过拟合 predictions = Dense(4, activation='softmax')(x) # 4 个花卉类别 model = Model(inputs=base_model.input, outputs=predictions) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001), loss='categorical_crossentropy', metrics=['accuracy'] )include_top=False 是为了丢弃 MobileNetV2 顶部的千分类器,换上适配本项目类别数的 4 神经元全连接层。GlobalAveragePooling2D 比 Flatten 更适合小数据集,它对空间位置信息做了平均,显著减少参数量,同时增强平移鲁棒性。Dropout(0.3) 放在全连接层之前,比增大 L2 正则更直接。learning_rate 用 0.0001 而不是默认的 0.001,是因为预训练特征已经很好了,过大的学习率会破坏已经学到的底层纹理特征。
3.3 训练日志里到底该看什么,train_log 的解读顺序
项目里有 train_log 文件夹,里面存储的是训练过程的损失和准确率记录。用 Keras 训练时,常见做法是配合 CSVLogger 回调把每一轮指标写到文件,方便后续画曲线。
python MobileNet.py 2>&1 | tee train_log/mobilenet_train.log训练完成后,先打开日志,看 val_loss 是否出现先降后升的转折点。target 是看 val_loss 最低点出现在哪个 epoch,那个 epoch 对应的权重就是最佳模型,而不是最后一个 epoch 的权重。项目里 heatmap 文件夹有 heatmap_mobilenet_50_14.png 这类文件,注意如果 50 轮时热图覆盖区域比 10 轮时更分散,说明训练时间过长导致模型学了一些背景噪声,这时应该回头用 30 轮左右的权重重新生成热图。保存模型时用model.save('mobilenet_flower.h5')同时保存结构和权重,在 Intertation_MobileNet.py 里加载后做单张预测。
4. CNN.py 对照实验:从零搭建卷积网络,看小数据下手工特征提取的边界
4.1 四层卷积加全连接的网络结构解析
为了和 MobileNet 形成对照,项目里用 CNN.py 实现了一个从零训练的浅层卷积网络。这个网络的设计思路值得借鉴:输入图像喂入后先经过两层 Conv2D-ReLU-MaxPooling,再接两层卷积加全局平均池化,最后用全连接层输出。不用 BatchNormalization 而是直接靠小尺寸卷积核堆叠,理由是数据量太小,BN 层的均值和方差估计本来就不稳定。
参考实现如下:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense model = Sequential([ Conv2D(32, (3, 3), activation='relu', input_shape=(160, 160, 3)), MaxPooling2D(2, 2), Conv2D(64, (3, 3), activation='relu'), MaxPooling2D(2, 2), Conv2D(128, (3, 3), activation='relu'), MaxPooling2D(2, 2), Flatten(), Dense(256, activation='relu'), Dense(4, activation='softmax') ]) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])第一层 32 个卷积核是对 160x160 输入的合理起点,因为菊花图像的大花瓣纹理在浅层就能被有效捕捉,不需要像 ImageNet 那样在第一个卷积层就堆到 64 个。第二层和第三层逐步从 64 翻到 128,配合三次 MaxPooling,特征图从 160 缩到 20x20,空间信息被压缩成高层语义。最后一层 256 维全连接是特意留的容量冗余,给足拟合空间——但这一层也最危险,如果发现 train_acc 高而 val_acc 低,第一件事就是把这层砍到 128。
4.2 训练超参数对比:epoch、batch size 与优化器的配合
项目命名里 results_cnn_50.png、results_cnn_4.png 暗示 CNN 路线上跑过 50 轮和 4 轮的不同实验。对从零训练的 CNN 来说,50 轮是必要的,因为它的卷积核是从随机初始化开始学习,不像 MobileNet 有预训练基础。batch size 的选择要看训练设备,CPU 环境下 batch size 设为 8 或 16 更稳,GPU 环境可以提到 32。
history = model.fit( train_generator, steps_per_epoch=len(train_generator), epochs=50, validation_data=val_generator, validation_steps=len(val_generator), callbacks=[tf.keras.callbacks.ReduceLROnPlateau( monitor='val_loss', factor=0.5, patience=3, verbose=1 )] )ReduceLROnPlateau 会在 val_loss 连续 3 轮不下降时自动把学习率缩小一半,这个回调对从零训练的 CNN 尤其关键。因为手工设计的网络容易在小数据集上陷入局部最优,等比降低学习率往往比重新随机初始化更有效。训练结束后的 logs 里如果 val_accuracy 始终低于 0.6,说明问题出在网络容量和数据量的匹配上,而不是训练技巧,这时再调轮数已经没有意义。
4.3 MobileNet 与 CNN 结果曲线的横向对比方法
把 train_log 和 test_log 组合起来,可以画出两条曲线做横向对比。MobileNet 的 val_loss 起点通常比 CNN 低很多,这是因为预训练特征对图像纹理有先验知识;CNN 的 val_loss 起点高,但下降速度在 10 轮前很快。比较时重点看两个指标:一是最终 val_accuracy 差值,二是达到 80% 精度所需的 epoch 数。
大致可以参考下表整理进课程设计报告:
| 对比维度 | MobileNet 迁移学习 | 从零搭建 CNN |
|---|---|---|
| 达到 80% 验证精度的轮数 | 10 轮左右 | 25 轮以上 |
| 参数量 | 约 260 万(冻结基座) | 约 250 万 |
| 对 CPU 训练的友好度 | 高,收敛快 | 中,训练时间长 |
| 过拟合风险 | 低 | 高,需配合 Dropout |
| 适合答辩展示的卖点 | 迁移学习有效性 | 手工特征提取理解 |
如果 MobileNet 在 30 轮时的 val_accuracy 还不如 CNN 在 50 轮时的结果,先不要急着怀疑模型,检查预处理是否一致。常见坑是 CNN.py 用了归一化,而 MobileNet.py 忘记对输入做 MobileNetV2 预处理器要求的分通道标准化。
5. Grad-CAM 热图:把黑盒模型拆开看,答辩和 debug 都好用
5.1 在 Intertation_MobileNet.py 里生成热图
项目里 heatmap 文件夹下的 heatmap_mobilenet_10_6.png、heatmap_cnn.png 就是 Grad-CAM 的可视化输出。Grad-CAM 的核心思想是用类别得分对最后一层卷积特征图求梯度,梯度大的区域就是模型分类时重点关注的像素区。对花卉识别来说,一份合格的 Grad-CAM 热图应当把高亮区域集中在花蕊和花瓣边缘,如果高亮区跑到叶片背景上,说明模型学到的特征有偏差。
在 Intertation_MobileNet.py 中加载训练好的模型后,核心代码如下:
import numpy as np import tensorflow as tf from tensorflow.keras.models import Model last_conv_layer = model.get_layer('block_16_project') # MobileNetV2 最后一个卷积层 grad_model = Model( inputs=model.input, outputs=[last_conv_layer.output, model.output] ) with tf.GradientTape() as tape: conv_output, preds = grad_model(img_array) # img_array 需为 4D 张量 class_idx = np.argmax(preds[0]) class_score = preds[:, class_idx] grads = tape.gradient(class_score, conv_output) pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2)) heatmap = tf.reduce_sum(tf.multiply(pooled_grads, conv_output[0]), axis=-1) heatmap = np.maximum(heatmap, 0) # ReLU 截断,过滤负贡献区域 heatmap /= np.max(heatmap) # 归一化到 0~1 区间block_16_project 是 MobileNetV2 中倒数第二个特征提取层,之所以不选最后一层卷积层,是因为越靠近输出的特征图语义越强,但空间分辨率越低,热图会显得过于模糊。代码先将 forward 过程包进 GradientTape 记录梯度,然后针对预测概率最大的类别计算 loss 对特征图的梯度,pooled_grads 是全局平均后的梯度权重,代表每个通道对分类结果的贡献度,最后用 ReLU 截断只保留正贡献区域。
5.2 通过热图反推训练日志中的异常
如果 MobileNet 在 10 轮时生成的热图高亮区域还很集中,到了 50 轮反而变得零散,同时 train_log 里 val_acc 缓慢下滑,这是过拟合的典型信号。模型在后半段训练中记住了训练集里的背景纹理,这些纹理分布在花盆、桌面和水印上,正好被 Grad-CAM 捕获。应对策略是回到 MobileNet.py,把 Dropout 从 0.3 调到 0.5,或者提前停止在热图最集中的那个 epoch。
另一个高频问题是 CNN 的热图区域比 MobileNet 明显更大,覆盖了整朵花而不只是花蕊。这是因为自建 CNN 的感受野在 160x160 输入下只堆了三层,每层 3x3 卷积加池化,最终特征图上的每个像素对应原图约 20x20 的区域,模型缺乏对细节纹理的敏感度。想要热图更聚焦,做法是在 CNN.py 里把第二层卷积核数从 64 提到 96,让中间层特征更丰富。
最后建议在测试集上重新生成热图,而不是只有训练集的可视化结果。把 heatmap_mobilenet_50_14.png 这类文件名对应好图像 ID、轮数、batch size 三个信息,交给答辩老师时能直接说清楚模型在哪一类菊花上容易混淆、热图特征指向哪里,这份可视化比一堆精度数字更能体现对深度学习模型行为的理解。
本文还有配套的精品资源,点击获取