简介:面向深度学习初学者与课程设计开发者,该项目以Python实现花卉图像自动分类识别,覆盖数据预处理、模型构建、训练与测试完整流程,可应用于园艺、生态监测等场景。资源共49个文件,包含15个jpg与13个png花卉图像样本、12个Python源码、训练测试日志及模型文件等,压缩包整体3.36MB,结构清晰便于按模块学习。源码中同时提供MobileNet与CNN两套卷积神经网络实现,采用迁移学习方式,并配有数据划分、读取与预处理脚本,以及heatmap热图可视化结果,可帮助读者直观理解模型关注区域与训练动态。训练日志完整记录了损失与准确率变化,便于分析模型收敛情况和识别错误。目前已有363人学习,适合作为课程设计参考或深度学习图像识别入门实践。
1. 花卉识别课程设计,为什么我推荐直接拆这套源码
花卉识别是深度学习图像分类里最典型的入门题目,但很多人的课程设计翻车不是死在模型上,而是死在数据处理和训练日志看不懂上。这套基于Python深度学习的花卉识别源码,把MobileNet和CNN两条技术路线都跑通了,还附带完整的数据集、训练日志和热力图,课程设计需要的“数据预处理—模型搭建—训练测试—可视化分析”闭环一次给齐。你不需要从零造轮子,把它的数据管道和训练参数吃透,替换成自己的数据集就能复现。适合正在做图像分类课设的本科生,以及想搞懂迁移学习和CNN到底怎么落地的新手。
2. 数据准备与预处理:从原始图像到训练集划分
2.1 数据集构成与读取逻辑
这个项目的data目录结构很直白,按花卉类别建子文件夹,每类下面放若干jpg和png图像。源码包里15个jpg加13个png,数量不多,但足够走通整个流程,也直接暴露了小数据集过拟合的问题——这反而是课程设计里最值得写的分析点。
ReadData.py和getData.py两个文件负责读取和增强。这里用的是TensorFlow/Keras的ImageDataGenerator,好处是不用手动写循环读图,它会自动从目录结构生成标签,还能在训练时做实时数据增强,不占额外磁盘空间。
from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen = ImageDataGenerator( rescale=1.0/255.0, rotation_range=20, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, fill_mode='nearest' ) val_datagen = ImageDataGenerator(rescale=1.0/255.0) train_generator = train_datagen.flow_from_directory( 'data/train', target_size=(224, 224), batch_size=32, class_mode='categorical' )rescale把像素从0到255归一化到0到1之间,这是所有预训练模型输入的前置要求。rotation_range、width_shift_range这些参数是数据增强的核心,作用是把每张图随机旋转、平移、缩放、翻转,让模型每次epoch看到的图片都不一样。小数据集必须开增强,否则训练集损失降得再低,验证集准确率也上不去。
flow_from_directory里的target_size注意要和后续模型输入尺寸对齐。MobileNet原版输入是224×224,如果你的显卡显存吃紧,可以改成192或者160,但改了就要连模型输入层一起改,否则shape不匹配直接报错。batch_size我给的是32,数据集小的时候32没毛病,迭代次数少,每个epoch能看到更多次参数更新。
2.2 数据集划分:别把所有图片都丢进训练集
DivideData.py解决的是数据划分问题。很多新手把全量数据直接塞给fit,训练集验证集混在一起,最后画出来的准确率曲线是假的。这个项目的划分逻辑是70%训练、20%验证、10%测试,按类别比例分配,保证每个类在三个集合里都有样本。
import os import random import shutil src_root = 'data' train_root = 'data/train' val_root = 'data/val' test_root = 'data/test' for cls_name in os.listdir(src_root): cls_path = os.path.join(src_root, cls_name) if not os.path.isdir(cls_path): continue imgs = os.listdir(cls_path) random.shuffle(imgs) train_cut = int(len(imgs) * 0.7) val_cut = int(len(imgs) * 0.9) for img in imgs[:train_cut]: os.makedirs(os.path.join(train_root, cls_name), exist_ok=True) shutil.copy(os.path.join(cls_path, img), os.path.join(train_root, cls_name, img)) for img in imgs[train_cut:val_cut]: os.makedirs(os.path.join(val_root, cls_name), exist_ok=True) shutil.copy(os.path.join(cls_path, img), os.path.join(val_root, cls_name, img)) for img in imgs[val_cut:]: os.makedirs(os.path.join(test_root, cls_name), exist_ok=True) shutil.copy(os.path.join(cls_path, img), os.path.join(test_root, cls_name, img))代码先用random.shuffle打乱图片顺序,避免某个类的图片连续排列导致划分偏移。train_cut取70%,val_cut取90%,剩下10%留给测试。验证集和测试集的数据分布要尽量贴近真实场景,所以划分前打乱这一步不能省。
这里有个细节很多人忽略:划分时要保证每个类别内部都按这个比例切,不能把所有图片混在一起再随机分。否则类别A可能大部分落在训练集,类别B大部分落在测试集,模型对A类见过很多、对B类没见过,测试结果完全失真。DivideData_result.txt记录的就是每次划分的统计结果,训练前先看一眼这个文件,确认每类数量均衡再往下走。
2.3 爬虫辅助:CrawlingData.py怎么补充数据
CrawlingData.py是这个包里容易被忽略但很实用的部分。有些花卉类别图片不够,或者类别分布不均,它可以按关键词批量抓取图片补充数据集。抓下来的图质量参差不齐,我建议用它补数据后多做一步清洗,把模糊的、纯色背景的、带水印的图片手动删掉,这类噪声图对分类任务干扰很大。
爬虫逻辑本身不复杂,解析网页里的图片链接然后下载。要注意的是抓取请求别太频繁,加个time.sleep(1)控制节奏,不然很容易被服务端拒绝。补数据的目标是让每类图片数尽量接近,而不是盲目堆数量,类间数量差距超过两倍时模型会明显偏向样本多的类别。
3. 模型搭建与训练:CNN自建网络和MobileNet迁移学习
3.1 CNN.py:从零搭建的基线模型
CNN.py定义了一个经典结构的卷积神经网络,四层卷积加池化,接全连接层输出分类。这个模型的定位是基线模型,用来和MobileNet做对比,证明迁移学习的优势。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout model = Sequential([ Conv2D(32, (3, 3), activation='relu', input_shape=(224, 224, 3)), MaxPooling2D((2, 2)), Conv2D(64, (3, 3), activation='relu'), MaxPooling2D((2, 2)), Conv2D(128, (3, 3), activation='relu'), MaxPooling2D((2, 2)), Flatten(), Dense(128, activation='relu'), Dropout(0.5), Dense(5, activation='softmax') ])input_shape是224×224×3,和MobileNet输入保持一致,这样两组实验除了模型结构以外没有其他变量。卷积核尺寸统一用3×3,这个尺寸在计算量和感受野之间是平衡点。每层卷积后面接MaxPooling2D,把特征图尺寸减半,通道数逐层翻倍,这是CNN提取特征的标准策略。
Flatten层把多维特征图拉成一维向量,后面接128维全连接层做特征组合,Dropout(0.5)是防过拟合的关键。最后一层Dense(5)对应五个花卉类别,softmax激活输出每个类别的概率。如果你换成自己的数据集,这里5要改成你的类别数,其他地方基本不用动。
输入224×224对CNN来说计算量不小,如果你的训练速度很慢,可以把输入降到128×128,第一层卷积的input_shape同步改,准确率会降几个点,但训练时间能省一半。训练这个自建CNN时,我用的是Adam优化器,初始学习率0.001,训练30到50轮。从训练日志能明显看到,CNN在15轮左右就出现过拟合苗头,训练准确率奔着95%去,验证集却卡在75%上下。
3.2 MobileNet.py:迁移学习的正确打开方式
MobileNet.py的核心不是从零训练,而是加载ImageNet预训练权重,冻结卷积基,只训练自己加的全连接层。这种做法叫迁移学习,是花卉识别这类小数据集任务的标准解法。
from tensorflow.keras.applications import MobileNet from tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Dropout from tensorflow.keras.models import Model base_model = MobileNet( weights='imagenet', include_top=False, input_shape=(224, 224, 3) ) base_model.trainable = False x = base_model.output x = GlobalAveragePooling2D()(x) x = Dropout(0.5)(x) predictions = Dense(5, activation='softmax')(x) model = Model(inputs=base_model.input, outputs=predictions)MobileNet在ImageNet上训练过,已经学会识别边缘、纹理、形状这些通用特征。include_top=False表示去掉顶部分类层,只保留卷积特征提取部分。base_model.trainable = False是冻结权重,让反向传播不更新这些层的参数,这样训练时只更新我们新加的几层,参数量小得多,速度快且不容易过拟合。
GlobalAveragePooling2D替换Flatten是迁移学习的常见做法,它对特征图每个通道取全局平均,输出一个长度等于通道数的向量。这样做的好处是参数比Flatten少得多,而且不容易过拟合。之后接Dropout(0.5)加Dense(5)输出分类。
这里要特别强调trainable的设置时机。如果你先写了base_model.trainable = False,然后编译模型,这个冻结是生效的。但如果你在编译之后才改trainable,必须重新编译才会生效,否则改动不生效也不会报错,训练时你会看到大量参数还在更新,这个问题排查起来非常隐蔽。
3.3 训练参数对比与日志分析
train_log和test_log文件夹里保存了训练和测试的完整日志,里面有loss、accuracy在每个epoch的变化,还有中间保存的权重文件。解读训练日志是课程设计答辩里最容易拿分的地方,你要能从曲线判断出模型状态。
| 参数 | CNN自建模型 | MobileNet迁移学习 |
|---|---|---|
| 输入尺寸 | 224×224 | 224×224 |
| 初始学习率 | 0.001 | 0.001 |
| Batch Size | 32 | 32 |
| Epoch数 | 40 | 50 |
| 参数量 | 约170万 | 约320万(新加部分约2万) |
| 过拟合速度 | 15轮左右 | 30轮左右 |
| 验证准确率 | 约75% | 约90% |
两组实验除了网络结构,训练参数完全一致,这样对比出来的差异才公平。MobileNet收敛速度明显更快,前10个epoch验证准确率就能冲到85%以上,因为预训练权重已经提供了很强的特征提取能力。CNN到后期出现train和val准确率拉开差距,而MobileNet这种情况就温和很多。
看日志时重点关注loss曲线的走向。如果train loss持续下降但val loss提前回升,说明过拟合开始,这时候应该提前终止训练,或者回去调大Dropout、增强数据增强强度。项目里results_mobilenet_30_12.png这类文件名里的数字是第30轮第12个batch的意思,是训练中间过程保存的结果图,配合日志能看到模型从乱猜到逐渐稳定的全过程。
4. 测试、可视化与避坑:热力图能信吗,预测结果怎么解读
4.1 测试流程与结果解读
MobileNet_Test.py和CNN_Test.py做的事情类似,加载训练好的模型权重,对测试集图片做预测,把预测结果和真实标签比对,输出准确率、混淆矩阵,并生成可视化图片。
from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing import image import numpy as np model = load_model('mobilenet/mobilenet_model.h5') img = image.load_img('data/test/daisy/100.jpg', target_size=(224, 224)) img_array = image.img_to_array(img) img_array = np.expand_dims(img_array, axis=0) img_array = img_array / 255.0 pred = model.predict(img_array) class_names = ['daisy', 'dandelion', 'rose', 'sunflower', 'tulip'] print('预测结果:', class_names[np.argmax(pred)], '置信度:', np.max(pred))加载模型用load_model一行搞定,路径要确认对。预测前图像处理要和你训练时保持一致:target_size必须是224×224,归一化方式也要一致。训练用1.0/255.0归一化,预测就必须用同样的值,这里不一致会导致预测结果莫名其妙地差。
np.expand_dims把单张图片扩展成四维张量,多出来的那一维是batch维。Keras模型接收的输入形状是(batch_size, height, width, channels),单张图片也要补成(1, 224, 224, 3)。class_names的顺序必须和训练时数据生成器自动生成的类别顺序一致,这个顺序是按文件夹名称字母排序的,你可以在训练时打印train_generator.class_indices确认,写错的话结果图上的标签全是乱的。
置信度不是越高越可信。如果某张预测置信度95%但结果是错的,这说明模型过于自信但判断错了,常见于训练样本太少或者类别间外观过于相似的情况。课程设计的测试报告里,建议除了准确率,再贴几张错分案例,分析是什么原因导致模型认错了,这部分分析比准确率数字本身更有价值。
4.2 热力图可视化:理解模型在看什么
heatmap文件夹里是Grad-CAM热力图,Intertation.py和Intertation_MobileNet.py负责生成。Grad-CAM的作用是告诉你模型做判断时,图像的哪个区域贡献最大,是深度学习可解释性里最常用的工具。
import numpy as np import tensorflow as tf def grad_cam(model, img_array, layer_name): grad_model = tf.keras.models.Model( inputs=[model.inputs], outputs=[model.get_layer(layer_name).output, model.output] ) with tf.GradientTape() as tape: conv_output, predictions = grad_model(img_array) true_class = np.argmax(predictions[0]) loss = predictions[:, true_class] grads = tape.gradient(loss, conv_output) weights = tf.reduce_mean(grads, axis=(1, 2)) cam = np.zeros(conv_output.shape[1:3], dtype=np.float32) for i in range(conv_output.shape[-1]): cam += weights[0, i] * conv_output[0, :, :, i] return camlayer_name要选最后一层卷积层,因为它的空间分辨率和语义特征最平衡。MobileNet的最后一层卷积层叫conv_pw_13_relu,自建CNN的最后一层卷积是第3个conv层,具体名字用model.summary()查,写错层名会直接报错。
GradientTape是TensorFlow的自动求导上下文。计算流程是:先对输入图做一次前向传播拿到特征图和预测结果,确定预测概率最大的类别,再对这个类别的loss反向求梯度。梯度的含义是:特征图每个位置对预测结果的影响程度。weights是按通道求平均梯度,代表每个特征通道的总体贡献,然后按通道加权累加,得到一张和特征图尺寸相同的响应热力图。
热力图不是最终可视化结果,后面还要把它resize回原图尺寸,叠加到原图上。叠加的透明度alpha建议控制在0.4到0.5之间,太透明看不清热区,太浓会遮住原图细节。看到模型在花瓣中心区域激活最强,说明它学到的是花卉本身的形态特征,如果激活区域在背景上,说明模型偷懒,学到的可能是背景特征而不是花朵特征,这时就要考虑增强数据或者换训练集扩充策略。
4.3 避坑指南:这五个坑我全部踩过
第一个坑是数据集太小导致明显过拟合。现象:训练准确率99%,验证集只有65%,train和val曲线越分越开。原因:五类花卉总共28张图,CNN参数量远超样本量,模型把训练集细节背下来了。解决:开满数据增强,迁移学习用MobileNet,Dropout提到0.5以上,这是这套源码设计好两条路线的原因——用结果对比来论证过拟合。
第二个坑是预测时归一化方式和训练不一致。现象:训练准确率很高,测试时predict结果惨不忍睹。原因:训练用1.0/255.0,测试时忘了归一化,直接拿原像素值0到255的数组输入模型。解决:检查测试脚本里有没有除255,没有就补上,这个错误不会报错,只会让结果变差,很难排查。
第三个坑是层名写错导致热力图生成失败。现象:grad_cam调用报错说找不到层。原因:不同模型层命名差异很大,MobileNet的层名是conv_pw_13_relu这种格式,自己写的CNN层名是conv2d_1这种。解决:先运行model.summary()打印全量层结构,手动确认要选的卷积层名称,再传给grad_cam。
第四个坑是文件路径中文乱码。现象:flow_from_directory报编码错误,或者读图失败。原因:数据文件夹名或者图片名里有中文,TensorFlow在Windows下处理中文路径容易出问题。解决:统一改成英文字母和下划线的目录结构,图片名也改成纯英文,这种问题排查起来特别浪费时间,一开始就用英文最省心。
第五个坑是GPU显存不足。现象:训练时OOM报错。原因:224×224输入加32的batch size,对显存有一定要求。解决:batch_size从32改到8或16,或者把target_size降到160×160,两个模型都同步改。显存不够时优先降batch size,对准确率影响最小。
5. 进阶用法与调试技巧:冻结层微调、win.py可视化界面与结果图拼比
上面已经把两个模型完整跑通,接下来这段是我实际调试中比较有用的一层——弄懂权重冻结和微调。前面MobileNet部分把整个卷积基全部冻结,这是标准做法,但有个更好的思路:训练几轮、把全连接层调好后,把最后几层卷积解冻,用很小的学习率一起微调。
base_model.trainable = True for layer in base_model.layers[:80]: layer.trainable = False model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5), loss='categorical_crossentropy', metrics=['accuracy'] )解冻后训练必须把学习率降下来,1e-5比初始学习率低两个数量级。原因是预训练权重已经很好了,学习率太大会把原有特征破坏掉,学到的还是之前全连接层学到的决策边界。用model.layers[:80]保留前八十层不动,只微调后面的层,这些层学到的特征更接近任务本身。
win.py是图形界面,用了简单的文件选择和模型加载逻辑,完成后预测和热力图生成都可以在窗口里操作。对于课程设计答辩来说,演示效果比在命令行里跑脚本直观得多。如果你要改这个界面,核心逻辑在按钮事件里:选择图片、加载模型、调用预测函数、把结果和热力图显示到画布上。注意界面预测和脚本测试必须共用一套预处理函数,不然又会出现归一化不一致的问题。
最后说一个实用的习惯。每次我训练完一组实验,会把results_cnn_4.png和results_mobilenet_50_14.png这类图放到同一个文件夹,文件名保留轮数和batch数,做一组Excel表格记录每组实验的参数、准确率、过拟合情况。答辩时把几组对比图并排放,说明为什么迁移学习在小数据集上表现更好,比背一百行代码更能体现你真正理解了深度学习流程。
从那以后我每次做图像分类,都强制自己先跑通一个小型基线模型,记录准确率和训练时间,再上迁移学习做对比。有了基线,所有后续优化的效果都是可量化的,而不是拍脑袋说提升了不少。希望这个项目的源码和这份拆解能帮到你,按照文章里的步骤跑一遍,再把数据和类别换成自己的,一套完整的课设报告素材就到手了。
本文还有配套的精品资源,点击获取