ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python花卉识别课程设计:迁移学习训练与答辩可视化全流程指南

Python花卉识别课程设计:迁移学习训练与答辩可视化全流程指南 简介这是一份面向高校课程设计或深度学习入门者的花卉识别工程围绕花卉图像自动分类这一任务涵盖数据读取与划分、模型构建、训练测试和结果可视化等环节。压缩包共49个文件、约3.36MB主体为15个jpg与13个png花卉样本、12个Python源码并包含Git忽略文件、Markdown说明、训练与测试日志等辅助内容可作为从数据预处理到CNN/MobileNet迁移学习实践的可运行参考。工程在传统卷积神经网络之外还提供MobileNet模型测试与热图可视化代码便于对比不同结构的识别效果并通过训练日志和结果图定位过拟合等问题帮助复现实验与分析模型关注区域。资源目前已有363人学习对正在完成图像识别类课程设计或希望快速上手深度学习流程的读者是一份结构清晰、即下即用的实战资料。1. 花卉识别课程设计值不值得选为什么这套 Python 深度学习源码比管理系统更有答辩优势对 Python 深度学习入门者来说花卉识别几乎是课程设计里「下限最低、上限却很高」的题目。数据是公开的、任务是一个标准图像分类问题、结果能可视化答辩时评委看得懂你也讲得清不像隔壁那种 Java 课程设计案例源码的管理系统除了增删改查很难聊出深度。但别高兴太早每年都有学生拿着完整源码跑不出效果loss 不降、val acc 卡在 30%、换一张真实照片就识别错。问题基本出在两个地方——数据划分不干净以及迁移学习的学习率配置不对。这篇就把从数据准备到训练、评估、答辩可视化的完整路径讲透。2. 先定方案再碰代码迁移学习 CNN 的选型逻辑与最小模型替换2.1 花卉识别本质是细粒度图像分类普通分类思路会吃亏先想清楚这个题目在技术上属于哪一类。花卉识别不是一个常见的「猫 vs 狗」粗粒度分类102 类花卉里大量存在外观高度相似的品种比如玫瑰、月季、蔷薇放在一起普通人分辨都要犹豫模型要能抓到花瓣纹理、花蕊结构、叶片边缘这些局部差异。这类问题在深度学习里叫细粒度图像分类难点在于类间差异小、类内差异反而大同一朵花不同角度、不同光照下差别可能比不同品种还明显。也就是说你不能指望一个随机初始化的浅层网络能直接学会这种分辨能力。这也是为什么同样用 CNN有人三小时训出 90% 准确率有人训到虚脱还在 40% 徘徊——差距不在「你有没有用深度学习」而在你有没有用对别人已经训练好的视觉先验。2.2 从零训练、冻结微调、全面微调三种方案的代价与上限课程设计阶段主流做法有三种我把它们在 102 类花卉数据集上的表现预期、训练时长和踩坑程度列出来方案数据需求训练时长单卡预期 top-1 准确率主要风险从零训练小型 CNN至少 5 万张需自采或大量增强3–6 小时40%–60%过拟合、训练不收敛冻结骨干 只训练分类头每类 20 张即可起步20–40 分钟60%–75%上限低特征不够细分预训练模型全面微调每类 30–80 张1–2 小时85%–95%学习率设错会灾难性遗忘我一般会直接推荐第三种。理由很朴素课程设计要的是在一个有限数据集上把结果做出来而不是重新发明视觉特征提取器。ImageNet 上训出来的 ResNet 已经学会了边缘、纹理、形状这些通用特征花卉识别要做的只是在这个基础上「微调」——让它更关注花瓣和花蕊的差异这比从零开始学要省太多时间也更稳。2.3 骨干网络选 ResNet18 还是 MobileNet课程设计场景下的取舍骨干网络的选择会影响你后面所有代码和答辩内容。对课程设计场景我的默认答案是 ResNet18。残差结构在答辩时非常好讲——跳连接解决了深层网络退化问题这一句话就能让评委看出你确实理解了网络设计另外 ResNet18 参数量约 1100 万在 CPU 上做单张推理也就一两秒现场演示不会卡到尴尬。MobileNet 的优势是轻量、适合部署到手机或树莓派但如果你的选题没有部署要求它反而不如 ResNet 好讲。选型的同时我习惯先写一段「模型替换分类头」的最小代码验证网络能跑通再继续。这里用 PyTorch 2.x torchvision 的新 APIimport torch.nn as nn import torchvision.models as models from torchvision.models import ResNet18_Weights num_classes 102 # Oxford 102 花卉数据集 model models.resnet18(weightsResNet18_Weights.IMAGENET1K_V1) num_ftrs model.fc.in_features # ResNet18 这里应该是 512 model.fc nn.Linear(num_ftrs, num_classes) print(model)这段代码先把 ImageNet 上预训练好的权重加载进来再把最后一层全连接替换成自己的分类头。model.fc.in_features这个写法请一定保留不要硬编码 512——万一你换成 ResNet50它的 fc 输入是 2048写死就要改代码。num_classes102对应数据集类别数如果你用的是 17 类花卉或自采数据改成实际类别数即可。跑完打印模型结构确认 fc 输出的尺寸变了选型就基本落地了。3. 把数据集盘清楚Oxford 102 的目录组织、8:1:1 划分与数据增强参数3.1 数据从哪来公开数据集与自采数据集的现实差距花卉识别用得最多的是 Oxford 102 Flowers102 个类别、每类几十到两百多张总量约八千多张图像是花卉在自然环境下的照片不是实验室白底图于是天然带上了光照、遮挡、背景干扰刚好能暴露模型真实水平。官方原始划分是每类前 10 张做训练、后 10 张做验证、其余做测试如果你按这个划分得到的指标可以直接跟论文对比。另一个选择是 17 类花卉数据集类别少、每类 80 张适合做快速 demo但答辩深度不够。自采数据集是最容易翻车的路线。手机拍 50 张一张张裁剪标注听起来可行实际做下来你会被「同一朵花的不同角度到底算不算一个样本」这种问题折磨。我见过一个人用爬虫批量抓花图结果类别标签错了一半训练时模型把「菊花」里混入的背景全学进去了。如果不是老师明确要求自采建议用公开数据集把精力留给网络和参数。3.2 目录结构与 ImageFolder让 torchvision 直接读数据的规范PyTorch 的torchvision.datasets.ImageFolder要求目录严格按照「根目录 / 类别名 / 图片」组织。很多课程设计源码拿到手跑不通就是因为数据路径没对上。我常用的结构是这样dataset/ ├── train/ │ ├── rose/ │ │ ├── rose_001.jpg │ │ └── rose_002.jpg │ ├── tulip/ │ └── sunflower/ ├── val/ └── test/train、val、test 三个目录互不交叉每个子目录名是类别名。这样直接用datasets.ImageFolder(dataset/train)就能加载类别名会自动按字母序映射成数字标签。图片格式建议统一成 JPG尺寸不必提前裁剪ImageFolder会在加载时做 Resize但要注意把所有图片统一成 RGB 三通道灰度图混进来会在后面归一化时报错。3.3 划分脚本按类别比例拆分阻断同源图片泄漏如果下载的原始数据是一整个目录我一般先写一个划分脚本按 8:1:1 把每类图片随机分到 train/val/test。注意这里有个容易忽略的泄漏点train 和 val 里如果出现几乎一样的图模型在验证集上的成绩就是假的。自拍数据尤其要小心——同一朵花的连拍图先按「拍摄批次」分桶再整桶划分不能一张张随机打散。import os import random import shutil random.seed(42) # 固定随机种子保证每次划分结果一致 src dataset/raw_images # 原始数据类目录下放了全部图片 dst dataset ratio_train, ratio_val 0.8, 0.1 for class_name in os.listdir(src): class_dir os.path.join(src, class_name) if not os.path.isdir(class_dir): continue images os.listdir(class_dir) random.shuffle(images) n_total len(images) n_train int(n_total * ratio_train) n_val int(n_total * ratio_val) splits { train: images[:n_train], val: images[n_train:n_train n_val], test: images[n_train n_val:], } for split_name, imgs in splits.items(): out_dir os.path.join(dst, split_name, class_name) os.makedirs(out_dir, exist_okTrue) for img in imgs: shutil.copy2(os.path.join(class_dir, img), os.path.join(out_dir, img)) print(划分完成) for split_name in [train, val, test]: cnt sum(len(os.listdir(os.path.join(dst, split_name, c))) for c in os.listdir(os.path.join(dst, split_name))) print(split_name, cnt)random.seed(42)保的是可复现你改一次增强策略后再跑不会因为划分变了分不清是数据问题还是网络问题。shutil.copy2保留原图元数据后续做格式转换时不至于丢信息。还有一个细节test目录在整个训练过程中任何人都不能碰包括你自己——所有调参、看曲线、看混淆矩阵都只参考 val 的结果test 留给最后一次性评估。3.4 数据增强参数翻转、旋转、裁剪、归一化的实操数值花卉识别里的增强和猫狗分类同一个套路但参数要克制。花瓣这类纹理一旦旋转超过 30 度或裁剪太狠看起来就不像花模型学到的全是增强噪声。我用得比较顺手的一套from torchvision import transforms train_transforms transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transforms transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])训练集用RandomResizedCrop模拟花朵大小不一scale(0.6, 1.0)意思是裁剪区域占原图的 60% 到 100%比例低于 0.5 会让花朵主体被切掉大半。水平翻转对花卉完全合理但垂直翻转不要开——花倒过来在自然界几乎不存在模型没必要学这种不真实的变化。旋转控制在 15 度内ColorJitter 三个分量都取 0.2避免颜色失真太夸张。验证集和测试集只做Resize(256) CenterCrop(224)不做任何随机增强这样才能拿到稳定、公平的指标。归一化的 mean 和 std 用的是 ImageNet 标准值和预训练权重当时的预处理保持一致这个不能改改了你前面的迁移学习基本白做。4. 跑通训练主流程损失函数、分层学习率与最佳权重的保存4.1 预训练权重自带预处理weights.transforms() 帮你对齐输入我们手动写了 Normalize但 torchvision 新版本其实给了更省事的写法.transforms()方法能直接取到和该预训练权重配套的预处理流程输入尺寸、Resize 方式、归一化均值方差全部对齐比自己拼手写的更不容易出错。比如weights ResNet18_Weights.IMAGENET1K_V1 preprocess weights.transforms() # 包含 Resize、ToTensor、Normalize model models.resnet18(weightsweights) model.fc nn.Linear(model.fc.in_features, num_classes)这有个隐含坑要注意如果你用了weights.transforms()那么 val 集预处理里那个Resize(256) CenterCrop(224)就不再适用因为 transforms 对象内部可能用的是Resize(232)之类的自己的配置。数据加载时用同一个preprocess处理 train/val 的图片但训练集在它之前还要叠增强顺序是「先增强再走 preprocess」。由此说句实在话版本差异是课程设计源码最容易翻车的点之一。网上能找到不少「免费 python 源码大全」里的老代码用models.resnet18(pretrainedTrue)这种写法在旧版 torchvision 里能用但新版会直接报参数不匹配的警告甚至错误。拿到源码第一件事不是跑而是看它 import 的模型 API 是否匹配你环境里的 torchvision 版本。4.2 损失函数为什么选 CrossEntropyLoss软标签与类别不均衡训练分类任务损失函数默认就是交叉熵。CrossEntropyLoss 内部已经帮你把全连接输出的 logits 做 softmax 再算对数损失你不需要在模型里额外接 softmax 层。它对分类错误施加的惩罚是非线性的模型越「自信」地分错loss 越大梯度越猛这对细粒度分类其实有利——能逼着网络把最容易混淆的花卉类别分开。如果类别不均衡比如某类只有 20 张另一类 180 张要传weight参数criterion nn.CrossEntropyLoss(weightclass_weights)class_weights可以按1 / 类别样本数归一化得到。但我的课程设计经验是只要每类样本数差距不超过 5 倍不设权重也行省得引入新的调参变量差距很大时再设。4.3 分层学习率预训练层与新分类头不能用一个学习率这是整个训练流程里最容易踩、也最值得讲的一个参数设置。预训练层已经收敛得很好你拿着一张大学习率去把整个 ResNet 连同新 fc 层一起更新几个 epoch 下来预训练特征就被冲掉了表现就是 val acc 卡住不动。标准做法是给新分类头设大一点的学习率给预训练层设小学习率import torch.nn as nn param_groups [ {params: model.fc.parameters(), lr: 1e-3}, # 新分类头 {params: [p for n, p in model.named_parameters() if fc not in n], lr: 1e-4}, # 预训练层 ] optimizer torch.optim.SGD(param_groups, momentum0.9, weight_decay1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size4, gamma0.5)参数推荐值说明分类头学习率1e-3新层从零学给大步长快速收敛预训练层学习率1e-4微调旧特征步长太大会灾难性遗忘优化器SGD, momentum0.9Adam 也行但 SGD 迁移学习表现通常更稳weight_decay1e-4轻微的正则缓解过拟合schedulerStepLR, 每 4 轮减半后期学习率下降让 loss 落得更平named_parameters()会把每个参数的名字和值遍历出来名字里含fc的只有新分类头剩下的都是预训练层这个过滤逻辑简洁且不会漏。SGD 配合动量收敛慢但稳Adam 收敛快但会在迁移学习后期出现震荡。课程设计如果用 Adam答辩被问「为什么不用 SGD」确实不好答用 SGD 就没有这个问题。4.4 训练循环细节按 val acc 保存最佳权重别等最后一个 epoch最后一个 epoch 的模型不一定最好尤其是有 scheduler 在动态降学习率时往往是中间某个 epoch 的 val acc 最高。我坚持用「每轮验证一次val acc 创新高就覆盖保存 best_model.pth」的方式best_acc 0.0 epochs 20 for epoch in range(epochs): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() model.eval() correct, total 0, 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc 100.0 * correct / total print(fepoch {epoch1}: loss{running_loss/len(train_loader):.4f}, val_acc{val_acc:.2f}%) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) print(f保存最佳模型val_acc{val_acc:.2f}%)device在脚本开头统一写torch.device(cuda if torch.cuda.is_available() else cpu)没有独显也能在 CPU 上跑只是慢。Train 和 eval 两个模式切换别忘——model.eval()会关掉 BN 和 dropout 的随机行为只保留其中一批是推理脚本常用的坑。如果本机没有 NVIDIA 显卡可以到云平台租一块 T4 跑训练本地只做推理演示这是课程设计阶段性价比很高的做法。训练结束后加载权重时注意格式model.load_state_dict(torch.load(best_model.pth, map_locationcpu))一般按官方推荐用weights_onlyTrue更安全。5. 避坑花卉识别源码最常见的 5 个翻车现场5.1 训练中途报解码错误数据读到一半崩了现象train 循环跑了一半突然报OSError: broken image file程序直接退出。原因数据集里混入了损坏的图片、格式伪装成 jpg 但不是真 jpg 的文件或者下载时截断的图片。解决不要在 load 时靠运气提前遍历清洗。写个小脚本把所有图片用PIL.Image.open()打开并调用img.verify()失败的直接移除再把扩展名统一转成 jpg。这个 5 分钟的活能省掉后面所有训练中断的烦恼。另外 DataLoader 设num_workers4时异常信息会乱建议调试期先num_workers0跑通了再调高。5.2 val acc 卡在 30% 上不去学习率把预训练权重冲掉了现象train loss 正常下降但 val acc 在第 5 个 epoch 后一直徘徊在 30% 左右怎么调都不动。原因预训练层使用了和分类头一样的较大学习率前面的卷积特征被破坏等价于从随机初始化的特征开始训练而这套数据量根本撑不起从零训练。解决回到 4.3 的分层学习率方案。我见过有的免费源码大全里的旧代码干脆不做分层统一lr1e-3改完分层后常见结果是 val acc 从 30% 跳到 85% 以上。这是最典型的「源码没坏参数不对」的情况。5.3 增强参数开太大训练集准确率 99%验证集反而下降现象train acc 很快到 99%val acc 却比预期的低 5-8%。原因数据增强强度过高模型在「被旋转 90 度的花」和「被裁掉一半的花」上反复拟合而这些样本跟真实分布差距太大学出来的特征反而干扰了正常图片的分类。解决把RandomRotation(15)改小到 10、关闭ColorJitter的 hue 分量或者减少 RandomResizedCrop 的 scale 下限到 0.7优先保真实分布。判别依据是误差 e 两个曲线train loss 持续低于 val loss 且随 epoch 拉大先怀疑增强过头而不是先怀疑网络能力。5.4 验证集准确率很高拿手机拍一张真实照片却识别错现象val acc 95%把模型部署到本地手机随手拍一张菊花预测结果错得离谱。原因一是数据泄漏train 和 val 里出现了同源图片val acc 虚高二是模型学的是背景而不是花尤其是公开数据集里花的位置居中、背景固定模型很容易把「绿色背景 中心色块」当成判别特征。解决划分时按 3.3 的方式阻断同源图评估时额外准备一些网上搜来的真实照片放到一个单独的test_real/目录里专门验证泛化能力。如果你发现模型对真实照片错得有规律基本可以判定它把背景当成了特征后面第六章的 Grad-CAM 可视化就能直接证明这一点。另外注意千万别用 opencv 的imread读图直接塞给模型——它读出来是 BGR 通道顺序颜色一错模型在当前任务上的表现立刻崩。5.5 显存不够、训练极慢batch size、num_workers 与 CPU 版 PyTorch现象设置batch_size64直接 OOM或者训练一个 epoch 要 40 分钟。原因显存不够时处理方式不是硬扛而是往下调。ResNet18 224 输入16G 显存的卡跑 batch 64 没问题8G 卡建议降到 324G 卡降到 8不行就用梯度累积。但首先要排查你是不是装成了 CPU 版 PyTorch——很多人在 VSCode 里配 Python 环境时直接pip install torch装的就是 CPU 版训练速度差十几倍跟参数没有半点关系。解决检查torch.version.cuda是否存在确认 GPU 可用后再谈调参。另外 DataLoader 的num_workers在 Windows 上设 0 或 2 比较稳设 8 反而可能因为进程开销把速度拖慢还容易触发内存错误。6. 答辩加分动作混淆矩阵与 Grad-CAM 特征可视化6.1 混淆矩阵定位模型最容易混淆的那几对花训练完先别急着收工跑一遍 test 集把混淆矩阵画出来。这对细粒度分类几乎是必做项因为评委一定会问「你这个模型最可能在什么情况下犯错」。用 sklearn 一句话算出矩阵再按行归一化方便对比from sklearn.metrics import confusion_matrix import numpy as np cm confusion_matrix(y_true, y_pred) cm_norm cm.astype(float) / (cm.sum(axis1, keepdimsTrue) 1e-8) np.fill_diagonal(cm_norm, 0) # 把对角线清零只看错误分布 idx np.unravel_index(np.argsort(cm_norm, axisNone)[-5:], cm_norm.shape)np.fill_diagonal把正确的预测清零后剩下最大的几个元素就是模型混淆最严重的位置。我通常会把前 5 对混淆类名打印出来再单独找出这些类各自的 test 图片来看十次里有八次会发现是「背景相似」或「同色系花」在互相干扰这正好是下一步可视化要验证的结论。6.2 Grad-CAM 热力图证明模型在「看花」而不是「看背景」答辩现场最能镇场的就是 Grad-CAM。它通过目标类别对最后一层卷积特征的梯度加权把模型「重点看哪里」画成热力图叠加在原图上。如果热力图集中在花瓣和花蕊上说明模型学到的确实是花如果热力图一片绿全是背景那 val acc 高也只是假象。核心实现不复杂靠 hook 取特征与梯度feature_map, grad_map {}, {} def forward_hook(module, input, output): feature_map[value] output def backward_hook(module, grad_input, grad_output): grad_map[value] grad_output[0] h_f model.layer4.register_forward_hook(forward_hook) h_b model.layer4.register_full_backward_hook(backward_hook) out model(img_tensor.unsqueeze(0)) # img_tensor 是预处理后的单张图 model.zero_grad() out[0, pred_class].backward() feat feature_map[value][0] # [C, H, W] grad grad_map[value][0] # [C, H, W] weights grad.mean(dim(1, 2), keepdimTrue) cam torch.relu((weights * feat).sum(dim0)) cam (cam - cam.min()) / (cam.max() - cam.min() 1e-8)要点在最后两步grad.mean(dim(1,2))把每个通道的梯度平均成一个权重再与特征图逐通道加权求和得到的就是「对目标类别最关键的二维空间位置」最后取 relu 只保留正向贡献归一化到 0-1 后可以直接用 matplotlib 的imshow叠加到原图上。答辩时多准备三张图正常花的热力图、混淆对里看错的那张图的热力图、以及一张背景干扰严重但识别对的图。我自己的习惯是所有结果必须我自己先用纯背景图暗测一遍——比如拿一张「花很小、背景很大」的照片跑 Grad-CAM如果热力图不在花上我不会把这张图放进答辩 PPT。视觉证据比任何准确率数字都更能回答「你的模型到底学到了什么」。希望这些参数和习惯能帮你少走几趟弯路。本文还有配套的精品资源点击获取
返回列表