ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CNN花卉识别大作业:从PyTorch模型搭建到GradCAM可视化完整指南

CNN花卉识别大作业:从PyTorch模型搭建到GradCAM可视化完整指南 简介面向计算机视觉课程设计与期末大作业的CNN花卉图像识别完整工程包基于Python与TensorFlow实现适合计算机相关专业学生作为毕业设计、课程设计或期末项目参考。项目经导师指导并获得99分评审代码完整、可运行包含模型训练、测试、GUI交互等核心模块覆盖从数据处理到可视化识别的全流程。压缩包共13个文件大小10.82MB以6个Python脚本为主另含设计报告Word、答辩PPT、环境配置YAML及说明文档方便快速搭建运行环境并理解项目结构。已有148人学习下载适合需要完整项目方案或希望动手实践深度学习图像分类的学习者。包内含可直接运行的源码、预训练模型、详细设计报告与答辩资料可帮助降低入门门槛快速复现实验并掌握CNN图像分类的基本方法。1. 计算机视觉大作业选CNN花卉识别从零跑通到写报告一份能直接交差的完整闭环每到期末计算机视觉课的群里都会被同一种问题刷屏大作业选什么题、模型跑不起来怎么办、报告怎么写才不像糊弄。花卉图像识别是这类大作业里出现频率最高的题目之一原因很直接——数据集公开好拿、类别语义清晰而且CNN做花卉分类的效果下限很高哪怕网络结构简单也能在几十个epoch里看到精度往上涨。但也正因为大家都选它作业能不能拿高分拼的往往不是模型有多花哨而是你有没有把数据处理、训练细节、结果分析和设计报告这条链路完整走通。这篇笔记就按一套可复现的完整方案来讲从数据集组织与预处理开始到PyTorch里的CNN模型搭建与训练再到推理验证、混淆矩阵和GradCAM可视化最后落到设计报告怎么写才经得起答辩追问。中间穿插我会踩的坑——环境版本、RandomSplit的隐患、迁移学习里那层fc怎么替换都是作业里最容易翻车的地方。适合正在做“PythonCNN图像识别”课程设计、需要一份能自己讲清楚完整方案的人。2. CNN模型选型与数据集准备类别文件夹的讲究和归一化参数不能拍脑袋2.1 花卉识别任务里CNN的优势与边界花卉分类在图像识别里属于细粒度分类的入门版本。说它“入门”是因为花的类别之间虽然存在花瓣形状、颜色、纹理的差异但这种差异在CNN眼里属于中等难度——不需要像车型识别那样去捕捉轮毂和车灯的细节也不需要像医学影像那样依赖低对比度的局部纹理。一个常规的卷积堆叠加上全局池化和全连接层就能学到“花瓣颜色分布”“叶片轮廓”“花心区域纹理”这类判别性特征。但边界也清晰如果只用两三层普通卷积而不做BatchNorm、不做数据增强遇见光照变化大、背景复杂、花瓣遮挡严重的图片泛化精度会明显掉下来。原因在于花卉数据集的图片大多来自网络爬取背景是草地、手部、纸张甚至别的花朵模型很容易把背景颜色当成类别线索。这也就解释了为什么很多作业里训练精度能到99.8%验证集却一直卡在91%上下——模型在记背景而不是在认花。CNN的另一个天然优势是层级特征的可解释性比较友好。浅层卷积响应的是边缘和色块深层卷积响应的是花瓣区域和花心结构这让GradCAM类激活图在写报告时能讲出“模型关注了花瓣边缘而非背景”这样的结论比直接丢一个黑盒精度更有说服力。选这个方向做课程设计本质上是选了一个“任务难度适中、可视化手段成熟、报告好展开”的组合。2.2 数据集组织按类别建文件夹别在划分上偷懒花卉识别最常见的数据组织方式是每个类别一个子文件夹文件夹名就是类别标签。这种结构正好对上torchvision里的datasets.ImageFolder它会把一级子目录名按字母序映射成从0开始的整数标签省去自己写标签映射表的功夫。常见的公开花卉数据集大多已经按这种结构打包解压后大概是flower_photos/roses/*.jpg、flower_photos/daisy/*.jpg这样的布局。拿到数据集后第一件事不是写模型而是确认图片完整性和类别分布。我一般会先跑一段统计脚本打印每个类别下的图片数量重点看有没有样本数量一个类别上百张、另一个类别只有几十张的失衡情况。另一个高频坑是文件夹里混入了隐藏文件或非图片文件ImageFolder加载时会报错或者把异常文件跳过导致训练集和验证集的类别数对不上。先做一轮后缀名过滤是值得的。import os import random from collections import Counter from PIL import Image data_dir ./flower_photos exts {.jpg, .jpeg, .png, .bmp} for cls_name in os.listdir(data_dir): cls_path os.path.join(data_dir, cls_name) if not os.path.isdir(cls_path): continue valid [f for f in os.listdir(cls_path) if os.path.splitext(f)[1].lower() in exts] print(cls_name, len(valid)) bad [] for f in valid: try: Image.open(os.path.join(cls_path, f)).verify() except Exception: bad.append(f) if bad: print( 损坏图片:, bad)这段脚本的作用有两个第一统计每个类别下真正可用的图片数量判断类别是否均衡第二逐张执行verify()检测文件头是否完整把半截下载的损坏图片直接找出来。注意verify()只做基础完整性校验不保证图片一定能被解码但如果这里都不过训练时DataLoader会随机抛异常且很难定位。筛完坏图后如果某个类别剩余图片太少我的建议是直接从该类里复制几张做简单翻转扩充或者干脆删掉这个类别二者选其一别带着残缺数据硬训练。2.3 预处理参数Resize到224不是玄学归一化有标准答案预处理管线我固定用四件套Resize、ToTensor、Normalize加上训练时才启用的数据增强。尺寸选224×224不是因为越大越好而是因为ImageNet预训练的CNNResNet、VGG、EfficientNet系列默认输入尺寸就是224×224沿用这个尺寸可以无缝加载预训练权重做迁移学习。如果自建小网络用128×128能省不少训练时间精度差距在花卉这种任务上通常不超过三个点。归一化的mean和std我直接沿用ImageNet的统计值[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225]。原因不是“大家都这么写”而是如果你计划用ImageNet预训练权重那么输入的分布必须和预训练时的分布保持一致否则前面几层卷积的统计特性就被打破了。就算完全从零训练沿用这套数值也远好过自己拿单张图去算均值后者的统计量没有代表性。from torchvision import datasets, transforms from torch.utils.data import DataLoader, random_split train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) full_dataset datasets.ImageFolder(rootdata_dir, transformtrain_transform) n_train int(0.8 * len(full_dataset)) n_val len(full_dataset) - n_train train_ds, val_ds random_split(full_dataset, [n_train, n_val]) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers2) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers2)这里有一个很多人忽视的坑random_split是按顺序切分整个数据集如果ImageFolder按类别文件夹排序加载那么前80%的样本可能只覆盖了前几个类别。更稳妥的做法是先拿到图片路径和标签按类别做分层抽样保证训练集和验证集里每个类别的比例一致。手动实现分层抽样很简单读取出full_dataset.imgs按标签分组组内按8:2比例切分再各自构造Subset。这一步做干净了后面验证集的精度数字才有参考意义。3. 用PyTorch搭建CNN自建小网络到ResNet迁移训练脚本可抄但要懂参数3.1 自建网络还是迁移学习先看显存和训练时间再决定以花卉识别的大作业规模自建网络完全可行但我要先泼一盆冷水从零训练的CNN在几百张到几千张图片的小数据集上精度上限通常在85%到92%之间而同样的数据量用ImageNet预训练模型微调很容易做到95%以上。这个差距不是模型结构的问题而是数据量不够支撑深层卷积学到足够泛化的特征。花卉图片的背景差异极大从零训练的模型会把大量参数浪费在拟合背景噪声上。自建网络的适用场景是老师明确要求不能使用预训练模型或者你手头只有CPU、训练时间有限。一个够用的自建网络不需要堆太多层结构上照抄LeNet到VGG的思路即可——卷积提取特征、全局池化降维、全连接分类。BatchNorm要加它能显著稳定训练过程。Dropout在最后的全连接层前加一层抑制过拟合。下面这个结构在5类花卉、总样本2000到5000张的条件下训练50个epoch能稳定在88%到93%之间。import torch.nn as nn class SimpleFlowerCNN(nn.Module): def __init__(self, num_classes5): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Dropout(0.4), nn.Linear(128, num_classes) ) def forward(self, x): return self.classifier(self.features(x))输入224×224的彩色图经过三个卷积池化块之后特征图尺寸依次缩到112、56、28通道数从32涨到128。AdaptiveAvgPool2d(1)的作用是把任意尺寸的特征图池化成1×1这样全连接层的输入维度就固定了。最后一个全连接层输出5个值交给CrossEntropyLoss时不需要手动过SoftmaxPyTorch的nn.CrossEntropyLoss内部已经包含了log_softmax和NLLLoss。3.2 迁移学习的骨架替换ResNet18只改最后一层如果条件允许我更推荐直接上迁移学习。PyTorch里加载ResNet18预训练权重只需要一行难点在于知道该改哪里。ResNet18的特征提取部分是conv1到layer4分类部分是最后的fc全连接层它原来输出1000类我们要改成花卉的类别数。注意改的位置是model.fc而不是model.classifier不同模型命名不一样ResNet叫fcVGG和MobileNet叫classifier换模型时先打印model看清结构再动手。import torch import torch.nn as nn import torch.optim as optim from torchvision import models device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) model.fc nn.Linear(model.fc.in_features, 5) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-4) scheduler optim.lr_scheduler.StepLR(optimizer, step_size6, gamma0.5)model.fc.in_features会自动取到原全连接层的输入维度512不需要硬编码。ResNet18在单张GTX 1060级别的GPU上batch_size取32一个epoch跑3000张图大约十几秒CPU跑则要慢五到十倍如果只有CPU建议把batch降到16或8。这里我把学习率设成1e-4而不是常见的1e-3是因为迁移学习场景下预训练权重已经在一个较好的局部最优附近学习率太大容易直接把这个位置破坏掉后期想收回来就很难。3.3 训练循环与调参你只需要一个能跑起来的循环训练循环本身是机械的但有几个习惯值得从一开始就养成。第一每个epoch同时输出训练集和验证集的loss与精度这样能第一时间发现过拟合还是欠拟合。第二每个epoch结束把模型权重和优化器状态存成checkpoint文件名带epoch号这是后期调参的后悔药。第三验证时务必要把模型切到eval()模式否则BatchNorm和Dropout在推理时的行为是错的拿到的验证集精度会偏低。def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total 0.0, 0, 0 for images, labels in loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) _, pred torch.max(outputs, 1) correct (pred labels).sum().item() total labels.size(0) return total_loss / total, 100.0 * correct / total best_acc 0.0 epochs 30 for epoch in range(1, epochs 1): train_loss, train_acc train_one_epoch( model, train_loader, criterion, optimizer, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) scheduler.step() print(fEpoch {epoch:02d}: train_loss{train_loss:.4f} train_acc{train_acc:.2f}% fval_loss{val_loss:.4f} val_acc{val_acc:.2f}%) if val_acc best_acc: best_acc val_acc torch.save({model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict()}, fcheckpoint_best_{val_acc:.2f}.pth)evaluate函数的结构和训练函数几乎一样唯一区别是去掉反向传播、在torch.no_grad()上下文里执行并且先把模型切到eval()。学习率调度器StepLR每6个epoch把学习率乘以0.5用途是让训练后期步长变小在最优解附近精细搜索。如果你的训练loss下降非常慢可以检查是不是optimizer.zero_grad()忘了写这是新手最容易翻车的地方忘掉它会导致梯度跨batch累积loss曲线变成锯齿状。4. CNN训练避坑五个高频翻车现场按现象对号入座4.1 验证集精度忽高忽低像抽风一样跳变现象训练集loss平稳下降验证集精度每个epoch波动很大这轮92%下轮84%再下一轮又91%。原因最常见的是训练集和验证集划分没有分层。random_split按顺序切分导致某些类别在验证集里分布不均衡一个小batch里如果恰好全是难分类的样本精度就会剧烈下坠。另一个原因可能是验证集图片太少统计波动本来就大。解决改成按类别分层采样。先读取出所有图片路径和标签按标签分组后组内随机划分保证两个数据集类别比例一致。如果调整后仍然波动检查数据增强是不是对验证集也生效了——验证集必须用不带随机翻转和旋转的transform否则每次评估都在不同的输入分布上测试。4.2 报错Expected 3 channel but got 1 channel现象数据加载阶段或训练第一个epoch就报错提示输入通道数和预期不符有时还会出现RGBA与RGB的转换问题。原因花卉数据集里混入了灰度图或带透明通道的PNG图。灰度图只有1个通道RGBA图有4个通道而卷积第一层是Conv2d(3, ...)要求固定3通道输入。解决在预处理里做通道统一。灰度图用transforms.Grayscale(3)转成三通道RGBA图先转成RGB再进管线。更稳妥的做法是在ImageFolder之前写一个图片清洗函数对每张图做通道判断并转换后另存避免训练到一半突然崩。这个问题在自媒体图片和网页爬取的数据里出现概率极高值得提前处理。4.3 Loss曲线降到0.5附近就不再下降现象训练到中期训练loss卡在0.4到0.6之间不动epoch再涨也没用验证集精度跟着停在90%左右。原因学习率设得不够合适。1e-4级别的学习率在迁移学习场景下一般够用但如果用的是自建网络且batch_size较小梯度方向噪声偏大固定学习率会让参数在最优解附近来回震荡无法收敛到更小loss。解决换用ReduceLROnPlateau或者手动把学习率降一半再训练。常见做法是以验证集loss为监控指标连续3个epoch不下降就把学习率乘0.5。也可以试试把Adam换成带动量的SGD学习率设成0.01在自建网络上有时收敛更稳但需要更多epoch才能看到效果。4.4 训练精度超99%验证集精度反而跌现象训练集loss逼近0精度99.8%验证集精度在92%左右就不再上升偶尔还往下掉。原因这是标准的过拟合。模型容量足够大训练集又小CNN把识别背景、花盆边缘这些训练集特有信息当成了判别特征。没有Dropout、没有数据增强、训练epoch过多三者凑齐就会触发这种情况。解决三步同时做。一是训练集加RandomHorizontalFlip、RandomRotation和ColorJitter相当于变相扩充数据打断模型对背景的依赖二是在全连接层前加Dropout自建网络建议0.5ResNet迁移时可以把分类器部分换成两层全连接并加Dropout三是保存验证集精度最好的checkpoint而不是最后一个epoch的权重用torch.save的时机选择来兜底。4.5 换了一台电脑代码就跑不动或结果对不上现象在自己机器上训练好的模型权重到别人机器上加载后验证精度明显下降或者直接无法加载。原因最常见的两个——一是CUDA版本和PyTorch版本不匹配导致无法使用GPUCPU推理速度慢到让人误以为卡死二是训练时模型和数据都在GPU加载权重时没有先model model.to(device)而device变成了CPU张量设备不匹配直接报错。解决代码里统一用torch.device(cuda if torch.cuda.is_available() else cpu)模型和数据都显式.to(device)。加载checkpoint时用map_locationtorch.device(cpu)兼容没有GPU的机器。另外建议记录下训练用的Python版本和PyTorch版本课程设计报告里写清楚运行环境答辩时被问到“复现不了怎么办”也能直接回答。5. 推理与结果验证混淆矩阵和GradCAM让报告有图有真相5.1 单张图片推理从加载权重到输出类别训练完的模型最终要在课堂展示或答疑时做现场推理这时候的代码应该简洁、可靠、不出错。我一般写一个独立的推理脚本不依赖训练时的完整代码只加载模型结构和权重对单张图片走一遍预处理后输出各类别概率。import torch from torchvision import models, transforms from PIL import Image class_names [daisy, dandelion, rose, sunflower, tulip] transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) model models.resnet18(weightsNone) model.fc torch.nn.Linear(model.fc.in_features, len(class_names)) checkpoint torch.load(checkpoint_best_95.20.pth, map_locationcpu) model.load_state_dict(checkpoint[model_state_dict]) model.eval() img Image.open(test_rose.jpg).convert(RGB) x transform(img).unsqueeze(0) with torch.no_grad(): logits model(x) probs torch.softmax(logits, dim1).squeeze(0) top3 torch.topk(probs, 3) for i, idx in zip(top3.indices, top3.values): print(f{class_names[i]}: {idx.item():.4f})注意加载权重用的是load_state_dict(checkpoint[model_state_dict])而不是直接torch.load返回的整个字典因为训练时我们存的是包含优化器状态的大字典。model.eval()不能省它影响BatchNorm层的统计行为。推理脚本里Resize必须和训练时保持一致如果训练用了RandomCrop这样的尺寸变化推理时就不要加否则输入分布对不上。5.2 混淆矩阵一眼看出模型到底在混淆哪两类验证集精度只是一个笼统的数字答辩时老师大概率会问“模型哪里还有不足”。这时候混淆矩阵比精度数字更有说服力。用sklearn.metrics里的confusion_matrix统计后配合matplotlib画成热力图能直观看到比如“玫瑰和郁金香经常被混在一起”这样的结论。import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay all_preds, all_labels [], [] model.eval() with torch.no_grad(): for images, labels in val_loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) disp ConfusionMatrixDisplay(confusion_matrixcm, display_labelsclass_names) disp.plot(cmapBlues, xticks_rotation45) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi200)如果混淆矩阵显示两类之间错误特别集中值得去数据里翻几张被分错的样本贴在报告里分析是不是这两类花外观相似度本来就高比如白色玫瑰和白色郁金香在花瓣形状上有重叠。这种分析是设计报告加分的核心来源比堆一堆指标强得多。混淆矩阵建议用验证集全量数据计算样本太少画出来的图不具备代表性。5.3 GradCAM可视化看模型到底在看哪里毕业论文级别的可视化里GradCAM几乎是必备内容。它的原理不复杂取最后一个卷积层的特征图用类别得分对特征图的梯度做加权平均得到每个空间位置对分类结果的重要性再放大叠加到原图上形成热力图。PyTorch里实现一个简版GradCAM只需要几十行代码。class GradCAM: def __init__(self, model, target_layer): self.model model self.features None self.gradients None target_layer.register_forward_hook(self.save_features) target_layer.register_full_backward_hook(self.save_gradients) def save_features(self, module, input, output): self.features output.detach() def save_gradients(self, module, grad_input, grad_output): self.gradients grad_output[0].detach() def generate(self, x, class_idxNone): output self.model(x) if class_idx is None: class_idx torch.argmax(output, dim1).item() self.model.zero_grad() output[0, class_idx].backward() weights self.gradients.mean(dim(2, 3), keepdimTrue) cam torch.relu((weights * self.features).sum(dim1, keepdimTrue)) cam torch.nn.functional.interpolate( cam, sizex.shape[2:], modebilinear, align_cornersFalse) cam cam.squeeze().numpy() cam (cam - cam.min()) / (cam.max() - cam.min() 1e-8) return camGradCAM的做法是正向传播拿到特征图和得分反向传播拿到梯度然后对特征图按通道加权求和经过ReLU过滤负贡献后缩放到原图尺寸。热力图上红色区域就是模型分类时的主要依据。值得注意的是register_full_backward_hook是较新版本的接口旧版PyTorch用的是register_backward_hook换版本时注意兼容。如果反向传播报错检查输入张量x是否设置了requires_grad模型推理时默认不计算梯度GradCAM需要梯度所以必须正向传播前确保x.requires_grad True。6. 设计报告怎么写才能通过查重和答辩结构与两个加分点课程设计的报告其实有固定套路问题定义、数据说明、模型设计、实验分析、总结展望这个框架不会错但大部分人的报告死于两个毛病——数据和实验部分用套话填充模型设计部分没有和代码对应起来。报告里的每一张图都应该能在你的代码里找到出处混淆矩阵是哪次验证集结果、GradCAM哪张图对应哪个类别都要写清楚否则答辩时一问就露馅。第一个加分点是放一组“数据增强前后对比”实验。同一模型、同一下午设置只切换不带增强和带增强两套训练把验证精度曲线叠在一起画出来能非常直观地说明数据增强对泛化能力的作用。这个实验成本不大只要提前把训练结果存成CSV或npy画图时直接读取即可。第二个加分点是把错误案例专门做一页贴三到五张验证集里被分错的图片配合GradCAM热力图分析错在哪里——是背景干扰、遮挡严重还是目标太小。这种分析比罗列十个表格的精度都更有说服力。报告的代码附录我建议只放核心训练循环和模型定义不要整段贴几百行的数据处理代码老师不会看。项目说明里把运行环境、数据集来源、每个文件的作用写清楚让对方能照着你的脚本跑一遍。我自己的习惯是交作业前一定用一台干净机器重新跑一次推理脚本确认从解压数据集到跑出结果没有少装依赖。哪怕是课程作业能复现本身就是最硬的交付物希望这些经验帮到你至少别再和我在同一处翻车。本文还有配套的精品资源点击获取
返回列表