基于Pytorch的花卉识别项目实战:数据集、ResNet迁移学习与论文写作指南
2026/9/8 7:51:43 网站建设 项目流程

简介:面向希望从零入门深度学习与模式识别的学习者,这份基于Pytorch实现的花卉识别项目适合作为毕业设计、课程设计或工程实训,也适合有基础者快速搭建图像分类基线。资源共17个文件,压缩包约39.78MB,核心是14个Python脚本,分别实现PCA、LDA、tSNE、AutoEncoder等降维方法,Kmeans、SOM、Kmeans++等聚类算法,以及深度与非深度图片分类;另附1个zip数据集、1份模式识别大作业PDF说明文档和1个MD说明,数据集已整理可直接使用,目录按降维、聚类、分类模块划分,便于按需阅读和运行。目前已有596人学习,通过阅读PDF原理说明并逐段运行脚本,可以完整走通“特征降维—聚类分析—分类建模”的实验流程,理解不同算法在真实花卉数据上的效果差异,并获得一套可直接复用的模板,方便后续扩展或接入新数据集。整个项目代码结构清晰,对于初学模式识别的读者尤为友好。 去年帮一个学弟调他的课程设计,模型结构没问题、数据也没问题,可验证集准确率死活卡在70%上不去。折腾了一晚上,最后发现是数据加载时忘了开shuffle,每个epoch看到的都是同一个顺序的样本。这种问题在Pytorch的花卉识别项目里太典型了——看起来是模型的问题,实际上是数据管线和训练策略的问题。

这篇文章我想把一个基于Pytorch实现的花卉识别项目完整拆开讲一遍,包括程序怎么写、数据集怎么准备、论文怎么组织。这套方案我自己带过几个学生做课程设计和毕业设计,属于“照着做就能跑通”的成熟路线。

1. 项目全貌:程序、数据集、论文怎么搭

1.1 为什么是花卉识别而不是猫狗分类

很多初学者入门Pytorch,第一个项目都是猫狗分类。但真做下来你就会发现,猫狗分类有两个问题:一是猫和狗的特征差异太大了,随便一个简单CNN都能跑到95%以上,体现不出调参和优化能力;二是这个方向做得人太多,论文和报告很难写出差异化。

花卉识别就不一样。拿牛津花卉数据集来说,有102个类别,很多花长得极其相似——比如你家楼下的雏菊和野甘菊,用肉眼看都容易弄混。这种细粒度图像分类(Fine-grained Image Classification)的场景,能让模型架构选择、迁移学习策略、数据增强方案真正发挥作用,做出来的准确率有梯度感,论文里也能写出东西。

另外一个实际原因是,花卉识别是图像分类任务里“性价比”最高的——数据集不算大,单张图片尺寸适中,用一块普通显卡甚至CPU都能在可接受的时间内跑完实验。这对学生党来说非常重要。

1.2 交付物三角:程序、数据集、论文如何配合

标题里写了“程序+数据集+论文”,这三个交付物其实对应了项目评审的三个维度:

  • 程序:证明你能把想法变成能跑的代码,包括数据加载、模型构建、训练循环、评估预测这一整条链路。
  • 数据集:证明你处理过真实数据,懂得数据清洗、预处理、类别平衡、划分策略这些基本功。
  • 论文:证明你能把实验过程升华为方法论的表达,讲清楚“为什么这么做”和“效果如何”。

我见过太多人只交程序和数据集,论文随便凑几千字,结果答辩时被问住——因为论文里的数据跟程序跑出来的对不上,或者方法论部分写得太虚。所以这篇博文里,我会把论文写作的落点也讲清楚,让它真正服务于项目的验收和展示。

2. 数据集:决定准确率上限的第一道关卡

2.1 用什么数据集合适

当前比较主流的选择有两类。第一类是牛津花卉数据集(Oxford 102 Flower),102个类别、每类40到258张图片不等,类别多、相似度高,适合做成一个“有点难度”的完整项目。第二类是经典花卉数据集(Flower Recognition),5个类别(雏菊、蒲公英、玫瑰、向日葵、郁金香),每类大约700到900张图片,数据量更充足,适合做baseline或快速验证。

如果是课程设计,我建议直接用5类的经典花卉数据集——类别少、每类样本多,容易出效果。如果是毕业论文,建议用牛津102类——类别多、挑战大,论文的对比实验更有说服力。

需要注意,这两个数据集的图片尺寸都不统一,原始图片从几百像素到上千像素都有。直接塞进模型是不行的,必须做统一预处理。

2.2 本地目录组织方式与ImageFolder加载

在Pytorch里加载图片分类数据集,最省事的方案就是用torchvision.datasets.ImageFolder。但它要求数据按特定目录组织:每个类别一个子文件夹,子文件夹名就是类别标签。

flower_dataset/ ├── train/ │ ├── daisy/ │ ├── dandelion/ │ ├── rose/ │ ├── sunflower/ │ └── tulip/ ├── val/ │ ├── daisy/ │ ├── dandelion/ │ ├── rose/ │ ├── sunflower/ │ └── tulip/ └── test/ ├── daisy/ ├── dandelion/ ├── rose/ ├── sunflower/ └── tulip/

组织好目录后,加载代码非常简单:

from torchvision import datasets, transforms train_dataset = datasets.ImageFolder( root='flower_dataset/train', transform=train_transform )

这里有个小坑:ImageFolder默认按文件夹名称的字母顺序分配标签索引,不能想当然地认为“第一个文件夹就是0号类别”。务必在训练前打印train_dataset.class_to_idx确认一下标签对应关系,否则后面做单张图片预测时很容易张冠李戴。

2.3 三个必须处理的细节:尺寸、归一化、数据增强

尺寸。工程上最常用的做法是把图片统一缩放到224x224——这是ResNet、VGG这些经典模型的标准输入尺寸。Pytorch的transforms.Resize(256)然后transforms.CenterCrop(224)是比较稳妥的组合方式,先等比缩放到短边256,再从中心裁剪224,这样不会把图片拉伸变形。

train_transform = transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), 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(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

归一化。上面的meanstd是ImageNet数据集的统计值,迁移学习时使用预训练模型,必须沿用这个归一化参数。如果用错或者漏掉归一化,准确率会大幅下降——而且这种情况特别难排查,因为损失函数看起来在正常下降,指标就是上不去。

数据增强。训练集上的RandomResizedCropRandomHorizontalFlipRandomRotation都是为了增加数据的多样性,相当于免费扩充了训练集。验证集不能用任何随机增强,只用固定裁剪,这样才能保证评估结果稳定可靠。

2.4 划分策略:训练/验证/测试三层划分

很多初学者的项目只有训练集和测试集,这是不对的。合理的做法是分为训练集、验证集、测试集三部分:

  • 训练集:用于更新模型参数。
  • 验证集:用于调整超参数、判断是否过拟合、决定是否保存模型。
  • 测试集:最终评估模型泛化能力的“考场”,只能测一次,绝不能用来调参。

比例上,5类花卉数据集可以按8:1:1划分;102类数据集因为部分类别样本少,建议按7:2:1或者直接保持官方划分。划分后还要检查每个类别在三个集合中的比例是否大致均衡,避免出现某一类全部进了训练集、验证集里完全没有的情况。

3. 模型构建:ResNet迁移学习的正确打开方式

3.1 为什么不用自己搭CNN

从零搭建一个CNN网络在Pytorch里很简单,几层卷积池化就完事。但真实项目里,我不建议这么做,原因是:普通CNN在小数据集上特征提取能力太弱,很难学到有区分度的细粒度特征,最终准确率往往在75%到85%之间打转,论文里写出来也不好看。

相比之下,用预训练的ResNet做迁移学习,相当于起点就站在了别人训练好的肩膀上。预训练模型已经在大规模数据集上学到了通用的边缘、纹理、形状特征,我们要做的只是“微调”最后一层或最后几层去适配花卉分类任务。工程上限和效果都明显更好。

3.2 代码实现:加载预训练模型并替换分类头

以ResNet18为例,加载预训练权重后替换全连接层:

import torch.nn as nn from torchvision import models def create_model(num_classes=5, pretrained=True): model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 获取ResNet最后一层全连接层的输入维度 in_features = model.fc.in_features # 替换成适配自己数据集分类数的全连接层 model.fc = nn.Linear(in_features, num_classes) return model

如果追求更高准确率,可以换成ResNet50,参数多了将近4倍,对显卡显存的要求也更高。从我实测来看,5类花卉任务ResNet18和ResNet50的最终准确率差距大约在2到3个百分点,但ResNet50的训练时间几乎是ResNet18的三倍。课程设计用ResNet18足够,想冲高精度或者写论文做对比实验再用ResNet50。

3.3 冻结与解冻:两种训练策略的取舍

迁移学习有两种训练策略,这是很多教程没讲透的地方。

策略一:冻结backbone,只训练分类头。把前面所有层设成不可训练,只有最后一层全连接层参与梯度更新。这种方式速度极快,即使CPU也能在几分钟内完成训练,适合快速验证数据管线是否通畅。但准确率上限有限,因为预训练特征是通用特征,没有根据花卉数据做适配。

for param in model.parameters(): param.requires_grad = False for param in model.fc.parameters(): param.requires_grad = True

策略二:全量微调。所有层都参与训练,但通常会设置不同的学习率——backbone用较小的学习率,分类头用较大的学习率。这是推荐的做法,能够兼顾训练效率和最终精度。

optimizer = torch.optim.Adam([ {'params': model.conv1.parameters(), 'lr': 1e-5}, {'params': model.bn1.parameters(), 'lr': 1e-5}, # ... 前面的层统一小学习率 {'params': model.fc.parameters(), 'lr': 1e-4} ])

实操中更简单的做法是给backbone和分类头各设一个参数组:

backbone_params = [] fc_params = [] for name, param in model.named_parameters(): if 'fc' in name: fc_params.append(param) else: backbone_params.append(param) optimizer = torch.optim.Adam([ {'params': backbone_params, 'lr': 1e-5}, {'params': fc_params, 'lr': 1e-4} ])

3.4 训练超参数怎么定

超参数这块,我直接给一组实测过比较稳妥的配置,5类花卉数据集在ResNet18上大概30到40个epoch就能收敛:

  • batch size:32到64之间。显存不够就减到16。
  • 初始学习率:1e-4到1e-3之间,建议从1e-4起步。
  • 优化器:Adam,betas=(0.9, 0.999),默认权重衰减建议设weight_decay=1e-4
  • 损失函数nn.CrossEntropyLoss()

Loss值在训练初期从大约1.5到1.6往下掉是正常的,因为随机初始化分类头的输出熵较高。如果初始loss高于2.3,说明权重初始化出了问题或者标签有误。

4. 训练循环:从损失曲线到避坑实录

4.1 一个完整的训练循环怎么写

Pytorch的训练循环本质上就是四个步骤:前向传播、计算损失、反向传播、更新参数。我习惯把训练和验证分别封装成函数,结构更清晰:

def train_one_epoch(model, train_loader, criterion, optimizer, device): model.train() running_loss = 0.0 correct = 0 total = 0 for inputs, labels in train_loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * inputs.size(0) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() epoch_loss = running_loss / total epoch_acc = correct / total return epoch_loss, epoch_acc def validate(model, val_loader, criterion, device): model.eval() running_loss = 0.0 correct = 0 total = 0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) loss = criterion(outputs, labels) running_loss += loss.item() * inputs.size(0) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() epoch_loss = running_loss / total epoch_acc = correct / total return epoch_loss, epoch_acc

注意两个细节:训练阶段必须写model.train(),验证阶段必须写成model.eval()。这个切换会改变Dropout和BatchNorm的行为——忘了切回eval模式,验证结果会忽高忽低;忘了切回train模式,模型训练效果会不对。另外,验证阶段整个循环都要包在torch.no_grad()里,否则会占用额外显存计算不必要的梯度。

4.2 损失曲线怎么判断

训练信息整理成一张表格或者直接打印每个epoch的数值都可以。我习惯在每个epoch结束时打印一行,格式如下:

Epoch [10/40] Train Loss: 0.4372, Train Acc: 85.62% | Val Loss: 0.3581, Val Acc: 89.40%

判断训练状态的核心指标是训练集loss和验证集loss的差值变化:

  • 两个loss都在下降,验证集准确率同步上升——一切正常。
  • 训练loss持续下降,但验证loss在某个epoch后开始反弹——过拟合的典型信号。
  • 两个loss都降不下去——学习率可能过大或过小,也可能是模型结构配置有问题。
  • 验证loss大幅震荡——检查batch size是否太小或学习率是否偏高。

4.3 学习率怎么调整

固定的学习率从头train到尾往往不够理想。更稳妥的做法是配合学习率调度器,在训练后期逐步降低学习率,让模型在最优解附近做精细调整。

scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.1, patience=5 ) # 每个epoch验证结束后调用 scheduler.step(val_loss)

ReduceLROnPlateau的机制是:当验证loss连续patience个epoch没有下降时,就把学习率乘以factor。这是我最常用的方案,不用预设固定的衰减节点,一切以验证集表现说话。

另一种做法是StepLR,每N个epoch固定衰减一次,但它的缺点是不知道模型是否真的需要衰减,属于“闭眼操作”,效果不如OnPlateau。

4.4 训练中容易踩的三个坑

第一个坑:忘了开shuffle。这直接导致每个epoch内batch的构成完全一样,模型会对batch内部的伪模式过拟合。训练集和验证集的DataLoader都应该设置shuffle=True(验证集也可以是False,因为不需要随机性,但训练集必须为True)。

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=2)

第二个坑:数据归一化参数不一致。训练和验证用不同的imagenet均值方差,等于同一个分布被强行拆成两套标准,验证准确率虚低。前面已经强调过,校验一下代码里两个transform是否一致。

第三个坑:设备不匹配。模型和数据一个在GPU一个在CPU,会直接报错或者极其缓慢。统一的写法是:

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) # 每个batch的数据都这样处理 inputs, labels = inputs.to(device), labels.to(device)

5. 评估、预测与模型落地

5.1 混淆矩阵与分类报告

准确率只是第一层指标,到答辩或者写论文阶段,专家一定会问“哪几类容易混淆”。这时候混淆矩阵和分类报告比准确率有说服力得多。

from sklearn.metrics import classification_report, confusion_matrix import numpy as np import torch def evaluate_model(model, test_loader, device, class_names): model.eval() all_preds = [] all_labels = [] with torch.no_grad(): for inputs, labels in test_loader: inputs = inputs.to(device) outputs = model(inputs) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) report = classification_report(all_labels, all_preds, target_names=class_names) return cm, report

从花卉识别的角度来看,混淆矩阵里对角线越亮越好,一旦发现某两类(比如向日葵和雏菊)频繁互相误判,说明这两种花在训练集里的视觉特征太接近,可以考虑为这两类重点补充训练数据,或者加大数据增强的强度。

5.2 单张图片预测的实现细节

模型训练完成后,要么做一个简单的预测脚本,要么接一个Gradio界面做可视化。但单张图片预测有一个高频坑:预测阶段的预处理必须和验证集完全一致。很多人训练时用RandomResizedCrop,预测时也用随机增强,结果每次都不同——这明显不对。

预测时只能用验证集的预处理方式:

def predict_image(image_path, model, device, class_names): from PIL import Image # 注意:这里用val_transform,不是train_transform image = Image.open(image_path).convert('RGB') image = val_transform(image).unsqueeze(0).to(device) model.eval() with torch.no_grad(): outputs = model(image) probs = torch.softmax(outputs, dim=1) top_prob, top_class = torch.max(probs, dim=1) return class_names[top_class.item()], top_prob.item()

还另外一个细节:单张图片进入网络前必须加一个batch维度,unsqueeze(0)就是做这件事。漏掉这一行,Pytorch会提示维度不匹配。

5.3 模型保存与加载

模型训练好后,保存策略也有讲究。有两种方式:

# 方式一:保存完整模型(不推荐) torch.save(model, 'flower_model.pth') # 方式二:保存状态字典(推荐) torch.save(model.state_dict(), 'flower_model.pth')

推荐方式二,因为状态字典不依赖模型文件的类定义位置,换环境加载也可以正常读取。加载时先创建模型结构,再载入权重:

model = create_model(num_classes=5, pretrained=False) model.load_state_dict(torch.load('flower_model.pth', map_location=device)) model = model.to(device)

必须注意的一点:训练结束后想要额外跑5个epoch继续精调,加载权重时要相应调整优化器的状态。如果只是做预测,加载权重就够了,不需要恢复优化器。

6. 论文写作:把项目变成能打分的成果

6.1 论文每个章节的核心内容

很多人的论文写得像“代码说明书”,这是很吃亏的。一个合格的基于Pytorch的花卉识别论文,应该包含以下核心章节,每部分的核心内容我标注一下:

  • 引言/绪论:花卉识别的实际应用背景(植物分类辅助、生态监测、园艺管理等),以及当前深度学习做图像分类的技术趋势。核心是说明“为什么选这个题”。
  • 相关工作:综述传统的图像特征方法(颜色直方图、SIFT特征)和基于深度学习的卷积神经网络方法,把ResNet、VGG、MobileNet等经典网络介绍一遍。核心是体现你对领域现状有了解。
  • 数据集描述:放数据集的统计信息——一共多少类、每类多少张、图像尺寸范围、训练/验证/测试划分比例。最好画一个各类别样本数量的柱状图。
  • 方法:这是核心章节。写清楚网络结构选型、迁移学习策略、数据增强方案、训练超参数设置。每个选择都要配“为什么”,比如“选择ResNet18是因为在计算资源有限的情况下,残差结构能有效解决深层网络退化问题”。
  • 实验结果:放训练损失曲线、准确率曲线、混淆矩阵、分类报告,做多个模型的对比实验(比如ResNet18 vs ResNet50 vs MobileNetV3)。核心是“用数据说话”。
  • 结论:总结项目成果、分析不足、提出改进方向。

6.2 实验对比表怎么设计

论文里最有分量的往往是一张清晰的对比表。可以参考这个格式:

模型预训练权重参数量训练时间(分钟)测试集准确率
ResNet18ImageNet11.2M1894.2%
ResNet50ImageNet23.5M4596.8%
MobileNetV3ImageNet4.2M1293.1%

这张表能同时说明三件事:不同模型的性能差异、计算开销差异、以及你选择最终模型的原因。表格里的数据一定要来自真实实验记录,绝对不能编——答辩时专家可能会当场盯着你的训练曲线提问。

6.3 答辩时的常见问题和应对

做这类项目被问得最多的问题有三个:

“为什么用迁移学习,而不是从头训练?”

回答思路:花卉数据集规模相对较小,从头训练深度网络容易过拟合;ImageNet预训练模型已经学到了通用的图像特征,迁移后只需要在少量数据上适配,能显著提升训练效率和最终精度。

“ResNet50效果更好,为什么不用它?”

回答思路:在当前的硬件条件下,权衡了训练时间、显存占用和精度增益,认为ResNet18在精度和效率之间取得了更好的平衡。同时论文中做了对比实验,给出了量化数据——这就是对比实验的价值。

“花卉识别在实际中有什么应用?”

回答思路:植物分类App、园艺自动识别、生态多样性监测、教育科普工具等。如果论文里多写几个真实的应用场景,这个问题基本就稳了。

最后:这套方案还能怎么扩展

这套基于Pytorch的花卉识别项目做完之后,往上扩展的思路其实很多。比如把单标签分类升级成多标签识别(一张图里同时有多朵花),或者引入目标检测框架同时定位和分类图片中的多朵花,再进一步还可以做基于注意力机制的可视化分析——用Grad-CAM生成热力图,展示模型到底依据哪些区域做出判断。这些方向都是在现有代码基础上的自然延伸,训练逻辑和数据管线的核心代码大部分都能复用。

如果你正准备做课程设计或者毕业设计,花几天时间把这条链路完整跑通,收获的绝对不止一个模型文件,而是从数据到训练再到评估的全流程工程能力——这是走一遍才拿得到的东西。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询