花卉识别系统开发实战:深度学习图像分类全流程解析

发布时间:2026/10/5 5:23:24
花卉识别系统开发实战:深度学习图像分类全流程解析
简介这是一套基于Python深度学习的花卉识别系统设计源码面向图像识别开发者、深度学习初学者与毕业设计选题人群以花卉图像分类为核心解决从数据预处理、模型训练到推理部署的完整流程问题。压缩包共1048个文件大小37.71MB以py源码和pyc字节码为主附带dll动态链接库、pyd扩展、exe可执行程序便于运行与二次开发同时包含txt说明文档、jpg花卉样本、xml配置文件及虚拟环境配置可支撑环境搭建和快速启动。资源已有665人学习下载。整体来看源码提供TensorFlow/PyTorch风格的神经网络特征提取与分类实现并打包venv依赖隔离环境与开发IDE配置配合示例图片和说明文档可帮助读者理解花卉识别系统的工程结构、调试思路和部署细节适合作为课程设计、项目实战或入门深度学习的参考资料。1. 从识别一朵花到识别一片花田花卉识别系统到底在解什么题基于Python深度学习的花卉识别系统设计源码拆开看是三件事一份能跑通的图像分类代码、一套干净可复用的数据处理流程、一个能接收新图片并给出预测结果的闭环。它对应的场景很具体——公园里的植物科普牌、花卉市场的拍照识花、果园的开花统计甚至高校的课程设计本质上都在做同一件事对花的照片做多分类并给出可以解释的置信度。这里先说一个反直觉的结论在这个系统里深度学习模型选型从来不是决定成败的关键数据处理、训练参数和源码环境才是。模型不收敛、预测不准、换台电脑跑不起来大多不是网络的锅。2. 找数据比写模型更磨人花卉数据集的选择、清洗与增强花卉识别系统在整个深度学习流程里的位置其实很靠前——模型结构可以借鉴现成源码训练框架有成熟模板唯独数据没人能替你准备。我见过不少项目翻车在生产环节模型在公开数据集上准确率很高一换到真实拍摄环境就大面积误判。原因往往不是网络结构不好而是训练数据与实际场景之间的差距没被正视。所以这一章先停一下把数据集这一步讲透。2.1 公开数据集与自采数据的取舍先看类别和背景再动手常见的起步组合是公开数据加少量自采数据。公开数据集中Oxford 102 Flowers是实测最顺手的去处102个类目、每类的图片量从几十张到两百多张不等绝大多数图片主体清晰、背景不过分杂乱适合用来验证整条流程Kaggle上还有一个流传很广的5类花卉数据集类间区分度高适合第一次把源码跑通。自采数据的价值在于让系统贴近真实场景——你最终要识别的是游客随手拍的花而不是已经抠好图的标本照。选数据集时我会先做两件看起来不重要的检查一是看每个类目的图片数量分布二是看背景复杂度。花卉识别最怕的不是类目多而是同类花在不同背景下的表现差异太大如果你的应用场景是户外自然光训练集里都是棚拍白底图那模型学到的可能是白背景花的联合特征而不是花本身。这一点在深度学习里叫分布偏移花卉识别中它几乎是默认存在的所以我的习惯是公开数据打底自采数据按最终使用场景补至少每类几十张。2.2 数据清洗删错图、查重复、按类目对账拿到数据后第一件事不是写模型而是清洗。花卉数据集的脏数据主要有三类错图、重复图、混入的非花图。错图最常见——采集时把花旁边的叶子剪进了框标注成了花的名字重复图则是同一张照片在采集过程中被存了多份或者是数据集内部本身就冗余非花图混入会让模型学会看到什么都往那类靠这类噪声在类别多、图片少的小数据集上尤其致命。我会用一段小脚本先做一次快速筛查把明显的错图挑出来。检查手段用感知哈希原理是把图片缩小到8x8的灰度图计算每个像素与均值的比较得到一个64位指纹汉明距离小于阈值就判定为重复import os from PIL import Image def phash(image, hash_size8): image image.convert(L).resize((hash_size, hash_size)) pixels list(image.getdata()) avg sum(pixels) / len(pixels) return sum(1 i for i, p in enumerate(pixels) if p avg) def find_duplicates(folder, threshold5): fingerprints {} for root, _, files in os.walk(folder): for name in files: if not name.lower().endswith((.jpg, .jpeg, .png)): continue path os.path.join(root, name) try: fp phash(Image.open(path)) except Exception: print(无法读取建议人工检查:, path) continue for other_fp, other_path in fingerprints.items(): if bin(fp ^ other_fp).count(1) threshold: print(疑似重复:, path, -, other_path) break fingerprints[fp] path if __name__ __main__: find_duplicates(flowers)这段代码的核心是把图片压缩成低频灰度图再做逐像素比较。threshold5表示两张图的哈希指纹最多允许5个比特不同超过这个值就认为不是同一张图如果你想更保守可以调到3代价是会把一些拍了花的不同角度误判成重复。注意感知哈希只对几乎一样的图有效同一朵花的不同拍摄角度它查不出来那种情况要靠人工抽样或更重的特征比对。清洗完成后还有一个容易忽略的对账动作统计每个类目的图片数量低于某一下限的类目要先补齐。对花卉识别来说每类最少40张才有基本可训练性低于20张的类目建议直接合并进相近类目或删除不然训练时这个类的loss会反复震荡。这个对账结果会直接影响后面训练时的类别不平衡处理。对账之后不要急着训练再花十分钟抽样看每个类目的图片质量。我会把每个类目拼成一张网格图快速过一遍重点看有没有过暗、过曝、严重模糊的图。这类图不会让模型崩但会挤占有效的学习样本尤其是总数只有几十张的小类目两三张模糊图就可能拉低一整类。2.3 数据增强的力度翻转到什么程度才算有效增广数据增强是花卉识别里成本最低、收益最稳的一步。花的照片天然有旋转不变性和尺度变化所以旋转、翻转、随机裁剪都符合这个任务的先验。我用PyTorch的torchvision.transforms时训练集和验证集用的是完全不同的流水线from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3), 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]) ])几个参数值得单独说。RandomResizedCrop里的scale(0.6, 1.0)表示裁剪区域占原图的60%到100%比默认的(0.08, 1.0)更保守——花卉识别中花的主体通常已经比较大裁得太狠会把花瓣细节切掉反而不利于学纹理特征。RandomRotation的degrees15只做小角度旋转因为大部分花卉照片是垂直拍摄或轻微倾斜旋转超过30度会产生大量黑边黑边区域会让模型学到角落处有噪声这种无关特征。ColorJitter的三个0.3控制颜色抖动幅度这对花卉识别很重要因为同一品种在不同光照下颜色差异很大适度抖动颜色可以让模型少依赖色调、多依赖结构。验证集不加任何随机变换只做Resize加CenterCrop这是为了让评估结果稳定可复现。这里还要提醒一句Normalize的mean和std用的是ImageNet预训练统计值只要你接下来用的模型是从ImageNet预训练权重初始化的就必须沿用这一组数值不能自己另算否则迁移学习的效果会大打折扣。另外验证集怎么划也决定了你对系统能力的判断。网上很多源码直接random split这在花卉识别里不算好做法——同一个场景连拍的图会同时出现在训练和验证里验证准确率虚高。我习惯按拍摄来源分组比如同一个公园、同一天采集的图全放进训练集或全放进验证集才能反映真实场景的泛化能力。初次实现为了流程简单可以用random split但最终交付前记得重划一份。数据准备到这里就齐了。一个小建议把清洗和增强的脚本写进项目的data目录里作为独立模块不要放在Jupyter notebook里随手跑。后面换数据集、调参数的时候你会发现这个决定能省下大量重复劳动。这一步做完才到真正让人兴奋的模型部分。3. 用迁移学习把模型拉起来ResNet 与 EfficientNet 的选型和改造数据就位之后模型设计反而是整个源码里最标准化的部分。花卉识别这类任务不需要你发明网络结构主流的做法是在预训练模型上做微调这也是《动手学深度学习》里图像分类微调那一章的标准路线。看清楚下面三个问题模型部分就能一次跑通。3.1 为什么从预训练权重开始而不是自己从头训花卉识别是典型的小数据图像分类任务公开数据集加上自采数据规模也就是几千到几万张。这种规模从头训练一个深层CNN效果基本不会好——深度学习模型对数据量的胃口远大于此从头训的ResNet50在几千张图上会陷入严重过拟合验证集准确率可能连60%都上不去。而ImageNet预训练权重已经让模型学会了边缘、纹理、花瓣形状这些通用特征你需要做的只是在它的基础上做二次学习。所以我的选型习惯是起步用ResNet50或EfficientNet-B0它们处在效果够用、显存友好、训练快的平衡点上。ResNet50的优势是生态成熟PyTorch官方torchvision里直接有预训练权重任何版本的深度学习源码都绕不开它EfficientNet-B0在同样参数量下准确率略高但输入分辨率、缩放系数的设定更敏感后面对训练参数的调试要求更高。第一次做选ResNet50能把更多精力留在数据处理和参数调优上。3.2 改造分类头把1000类换成你的类目预训练模型的最后一层是1000类的全连接层要换成我们自己类目数量的全连接层。这个改动很小但有一个容易犯的错误只改了最后一层前面的卷积层全部冻结这样训练时只有最后一层在更新模型学不到花卉特有的高层特征。常见做法是先不冻结让全部层参与训练只是把学习率设得比分类头小一些import torch.nn as nn from torchvision import models def build_model(num_classes, pretrainedTrue): model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1 if pretrained else None) in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(p0.2), nn.Linear(in_features, num_classes) ) return model这段代码做了两件事读取ResNet50的预训练权重然后把最后一层替换成Dropout加线性层。Dropout放在全连接前作用是抑制训练阶段分类头对特征组合的过拟合这在训练集只有几千张的花卉任务上尤其有用。in_features取自原模型的fc层输入维度ResNet50是2048EfficientNet-B0是1280这个值不要自己硬编码写死会导致换模型时改错。替换分类头时我把原来的单层fc换成了一个两层结构。实际训练下来单层Linear和DropoutLinear在验证集上的差距有时不大但在训练后期、当损失进入平台期时Dropout版本往往能多撑几个epoch不出现过拟合。如果你的训练集自采比例高、图片噪声大这个设计带来的收益会更明显。3.3 完整训练脚本从数据加载到 checkpoint 保存模型定义好接下来是把训练过程串起来的脚本。我用PyTorch时习惯把训练脚本拆成三个模块数据加载、训练循环、checkpoint管理。下面是一份能直接跑的简化版重点看train_one_epoch和保存策略import torch import torch.nn as nn from torch.utils.data import DataLoader def train_one_epoch(model, loader, optimizer, criterion, 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) _, preds outputs.max(dim1) correct (preds labels).sum().item() total labels.size(0) return total_loss / total, correct / total def train(model, train_loader, val_loader, epochs, device): criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) best_acc 0.0 for epoch in range(epochs): train_loss, train_acc train_one_epoch( model, train_loader, optimizer, criterion, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) scheduler.step() if val_acc best_acc: best_acc val_acc torch.save({model: model.state_dict(), epoch: epoch, best_acc: best_acc}, best_checkpoint.pth) print(fepoch {epoch1}/{epochs} ftrain_loss {train_loss:.4f} train_acc {train_acc:.4f} fval_acc {val_acc:.4f})几个参数按经验说。优化器用AdamW而不是Adamweight_decay1e-4是常规起点AdamW把权重衰减和梯度更新解耦对小数据集训练更稳。学习率lr3e-4是从迁移学习中常用的起点如果你的数据集只有几类或几百张图可以降到1e-4如果自采数据很干净提到5e-4也没问题。CosineAnnealingLR让学习率按余弦曲线衰减到接近0相比固定学习率它在训练后期更不容易震荡。evaluate函数在val集上跑一遍相同的forward流程只改model.eval()和torch.no_grad()这部分代码模式固定不难写。checkpoint保存这里我有血泪经验只保存state_dict是不够的把epoch、best_acc、优化器状态一起存进去后面中断恢复时才能准确续训。特别是花了几小时训练到第80轮因为断电从头再来那种感觉想必你不想体验。保存格式用一个dict打包恢复时再按同样的结构解出来这是最省心的做法。evaluate函数的具体实现很简单把train_one_epoch里optimizer相关的三行删掉加上model.eval()和torch.no_grad()即可。val集上的预处理必须是val_transform不能复用训练集的随机变换不然每次评估的结果都不一样模型选择也就失去了依据。关于训练轮数迁移学习在几千张花卉图上通常15到30轮就能达到平台期不需要一上来就设100轮。模型在小数据上收敛很快真正需要的是在第10轮左右观察val_acc是否还在升——如果连续3轮不升就该考虑减学习率或停止训练。这个判断会在下一章展开。4. 训练参数是门玄学也是门手艺学习率、batch size 与评估指标的配合模型结构确定后剩下的训练环节最考验耐心。花卉识别是小数据集任务参数的微小差别会直接体现在验证集准确率上而且每个参数都不是独立的学习率调大可能要配合更大的batchbatch改小又影响batch normalization的统计量。这一章把最核心的三个旋钮讲清楚。4.1 学习率的设定与热身策略为什么3e-4不是银弹学习率是让模型翻车频率最高的参数。lr过大loss可能在第一个epoch就冲到十几个lr过小训练推不动loss从1.8到1.6要磨上几十轮。我习惯以3e-4为起点跑前5个epoch观察两个信号loss是否在稳定下降、val_acc是否在上升。如果loss在快速下降但val_acc不动往往是学习率偏大导致在泛化边界震荡如果loss几乎不动需要先排查是不是学习率太低。一个值得投入的做法是热身策略warmup。训练初期模型权重刚从预训练状态出来某些层的梯度可能异常大直接上大学习率容易把预训练特征冲坏。常见做法是先让学习率从接近0线性爬到目标值前5个epoch完成热身再进入正常的余弦衰减import torch import math class WarmupCosineScheduler(torch.optim.lr_scheduler._LRScheduler): def __init__(self, optimizer, warmup_epochs, total_epochs, eta_min1e-6): self.warmup_epochs warmup_epochs self.total_epochs total_epochs self.eta_min eta_min super().__init__(optimizer) def get_lr(self): epoch self.last_epoch 1 if epoch self.warmup_epochs: return [base_lr * epoch / self.warmup_epochs for base_lr in self.base_lrs] progress (epoch - self.warmup_epochs) / (self.total_epochs - self.warmup_epochs) return [self.eta_min 0.5 * (base_lr - self.eta_min) * (1 math.cos(math.pi * progress)) for base_lr in self.base_lrs]这段代码把学习率分成两个阶段前warmup_epochs个epoch从0线性上升到设定值之后按余弦曲线从目标值平滑降到eta_min。warmup_epochs在花卉这种小数据集上设3到5就够设太长会浪费训练时间total_epochs要和总的训练轮数一致否则余弦曲线的终点对不上。使用时代替step是因为这个调度器以epoch为单位更新和训练循环里的for epoch结构保持一致。实际训练中我遇到过看起来玄学的现象同样一组参数跑三次结果有1到2个点的波动。这不是代码问题是数据加载顺序、随机增强、Dropout共同作用的结果。所以调参时只看单次结果没有意义至少跑两次取均值再把最优的那份checkpoint作为最终结果。4.2 batch size、图像尺寸与显存的三角关系用梯度累积绕过显存墙batch size的选择直接受显存限制。以ResNet50、输入224x224为例常见的对应关系大致如下batch size单卡显存占用约适用情况83-4 GB显存紧张需配合梯度累积165-7 GB最常用的起点329-12 GB训练更稳但学习率需同步调大这个表里的数值是训练实测中常见的量级不是精确值实际占用还受torch版本、混合精度开关影响。如果batch只能设为8不必焦虑用梯度累积把8个mini-batch的梯度攒起来再更新一次效果等效于batch32但要记得把学习率按比例上调因为有效batch变大了。图像尺寸也是一个可以动的变量。224x224是ResNet预训练的标准输入换成320x320通常能提高1到2个点但显存占用接近翻倍、训练时间明显拉长。我的建议是先用224把整体流程跑通等确认数据、模型、参数都没问题再考虑放大分辨率作为冲刺手段。数据集背景复杂、花瓣纹理细节多的场景放大尺寸的收益会更明显。4.3 损失函数与评估指标Accuracy不够用的时候看什么分类任务默认用CrossEntropyLoss在类别数量均衡时没有问题。但花卉识别非常容易出现类别不均衡公开数据集中有些类目40张有些200张自采数据更是集中在常见的玫瑰、菊花、月季上。这种情况下模型会倾向把不确定的样本预测为多数类整体Accuracy看着还行少数类的召回率却很难看。解决办法有两个方向。第一是在损失函数上做加权给样本少的类更高的loss权重第二是在数据加载时做类别重采样让每个batch里各类别的比例尽量接近。第一种实现简单适合初步改进import torch def build_criterion(labels, device): num_classes labels.max().item() 1 counts torch.bincount(labels, minlengthnum_classes).float() weights counts.sum() / (num_classes * counts 1e-6) return torch.nn.CrossEntropyLoss(weightweights.to(device))这段代码用每个类别的图片数倒数做权重类别样本数越少、权重越大loss计算时少数类的错误会被放大梯度更新也更偏向拉高少数类的准确率。weights计算里的1e-6是为了防止某类图片数为0时除零。要注意的是加权CrossEntropy会改变loss的绝对数值你之前观察的loss降到1以下的经验值会失效判断收敛时要重新看val_acc和val loss。评估指标上单看Accuracy不够。我每次训练完都会打印一份按类别的混淆矩阵和宏平均F1。宏平均F1把所有类别的F1先平均再汇总不像Accuracy那样会被多数类掩盖能直接反映小类别的表现。即使在验证集上准确率到了90%如果某一个类别的F1只有0.4说明这个类在真实场景里大概率会大面积误判需要回头补数据或检查该类图片的质量。训练告一段落后不要急着部署先回头做一轮系统性的排查。以下是花卉识别项目里最容易翻车的四个环节。5. 花卉识别最常见的四个坑过拟合、样本不均、部署差距与源码依赖排查这一章集中写我踩过的坑。每一条都按现象、原因、解决三步讲清楚适合你训练遇到问题时回来对照排查。5.1 过拟合loss在降验证集却纹丝不动现象训练loss从2.1一路降到0.3训练准确率接近100%但val_acc停在65%左右甚至在后期缓慢下降。这是我做花卉识别最常碰到的第一道坎新手最容易在这里浪费两三天。原因模型容量太大、训练数据太少、数据增强不够。ResNet50有几千万参数几千张图片喂进去网络完全有能力背下训练集到了验证集就露馅。判断标准很简单训练准确率和验证准确率的差距超过15个点基本就是过拟合在作祟。解决优先加大数据增强的力度把RandomRotation从15度提到30度、ColorJitter幅度从0.3提到0.4其次检查模型分类头确认已经加上了Dropout最后用到早停连续5个epoch val_acc没有刷新记录就停止训练保留best_checkpoint.pth。早停不是在偷懒而是在防止验证集过拟合——模型继续训练只会把验证集也背下来让最终部署效果更糟。5.2 类别不平衡百合比萱草多三倍怎么破现象整体准确率不错但某个类别的召回率只有20%。我在处理一个20类的花卉数据时模型几乎把所有的萱草都预测成了百合因为百合的图片数是萱草的三倍。原因CrossEntropy在类别先验不均衡时最优策略就是把所有不确定样本都押到多数类上这是数学上的必然不是模型偷懒。你看到模型的预测偏向多数类恰恰说明它学会了数据分布。解决先用上一章的加权CrossEntropy做第一轮修复如果还不行对样本少的类做重采样让每个epoch里每类看到的图片数量接近。重采样的做法是给每个样本按类别权重赋一个采样概率少数类的概率放大多数类缩小DataLoader里的WeightedRandomSampler就是干这个的。做重采样时注意别把增强后的图当成新样本写回硬盘那只会增加训练时间泛化效果提升有限在线增强就够用了。5.3 训练指标好但现场翻车部署环境与预处理不一致现象验证集准确率92%把手机拍的花传上去预测连续错三四张甚至把叶子识别成了花。原因三个最常见的不一致。一是预处理不一致训练时做了CenterCrop推理时代码里没写直接resize到224就送进模型二是图片方向差异训练集里大多是横幅现场照片是竖幅特写裁剪后的构图完全不同三是光线条件训练集是晴天拍的现场是阴天或灯光下。解决把推理脚本里的预处理流水线原样复制训练时的val_transform一个函数都不要删然后从真实场景补照片加入训练集至少每类补20张现场图最后在代码里加一个评测技巧对同一张图做五点裁切四角加中心分别预测取平均作为最终结果def five_crop_predict(model, image, device): base T.Resize(256)(image) crops T.FiveCrop(224)(base) normalize T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) probs [] for crop in crops: t normalize(T.ToTensor()(crop)).unsqueeze(0) with torch.no_grad(): probs.append(torch.softmax(model(t.to(device)), dim1)) return torch.mean(torch.cat(probs), dim0)这个函数先把图缩放到256再用FiveCrop切成左上、右上、左下、右下、中心五块每块单独过模型最后对概率求平均。花的主体通常偏离画面中心五点裁切能让预测不那么依赖花正好在正中间这个先验。注意传进来的image要先Resize到256不能直接送原图否则五个裁切块可能落到完全不同的位置Normalize参数也必须和训练时一致不能混用训练集的随机增强。5.4 源码环境依赖python安装版本与依赖锁死问题现象从网上拿到的花卉识别源码在自己机器上跑不起来最常见的报错是ModuleNotFoundError: No module named torchvision或者是torch版本和CUDA版本不匹配import torch时直接报错。原因写源码的人用的python安装版本、torch版本和你本机不一致。我见过用python 3.8写的源码在python 3.10下因为API弃用直接崩也见过requirements.txt里不锁版本pip安装时pull到了最新版torch结果和本机驱动对不上。解决新建独立的conda环境不要往系统python里乱装东西。最稳妥的起步命令是这样的conda create -n flower_env python3.8 -y conda activate flower_env pip install torch2.0.1 torchvision0.15.2 pip install -r requirements.txtpython 3.8是目前深度学习源码兼容性最好的版本之一torch和torchvision的版本号是配套的必须一起指定只装其一会在运行时报缺算子。requirements.txt里的关键包版本也要显式钉死。还有一个容易被忽视的坑项目里如果有自定义的utils.py不要和torchvision等包重名否则import的路径会被当前目录下的同名文件劫持报一堆莫名其妙的AttributeError。6. 部署成能用的系统从 py 脚本到接口验证的一步到位训练结束不等于系统完成。最后一步是把模型包装成一个能接收图片、返回结果的接口。我一般用FastAPI做推理服务代码量小、自带文档页面调试时直接在浏览器里测试最方便from fastapi import FastAPI, UploadFile from PIL import Image import torch, torchvision.transforms as T from model_builder import build_model app FastAPI() labels [rose, tulip, sunflower] # 按训练时的类目顺序填写 model build_model(num_classeslen(labels)) state torch.load(best_checkpoint.pth, map_locationcpu) model.load_state_dict(state[model]) model.eval() transform T.Compose([ T.Resize(256), T.CenterCrop(224), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) app.post(/predict) async def predict(file: UploadFile): img Image.open(file.file).convert(RGB) tensor transform(img).unsqueeze(0) with torch.no_grad(): probs torch.softmax(model(tensor), dim1) top1 probs.argmax().item() return {label: labels[top1], confidence: float(probs.max().item())}这段代码先按训练时的类目数量重建模型结构再把checkpoint里的权重加载进来。注意保存时我用的是dict结构所以读取时先取[model]再load_state_dict顺序不能反如果你当初只保存了state_dict这里直接load_state_dict(torch.load(...))就行。加载完成后model.eval()一定要加它会关闭Dropout和BatchNorm的训练统计推理结果才稳定。部署后验证有个笨但有效的办法拿手机到楼下花园拍十来张真实照片不走你训练用的文件夹单独建一个eval_site目录逐张测。把预测错误的图单独保存起来按错误类型分类——是预处理问题、还是类目相似、还是数据没覆盖每一类对应一个修复动作。这个复盘习惯救了我很多次比盯着验证集准确率有用得多。我的教训是验证集准确率只是起点不是终点源码能跑通也只是起点能回答你这系统到底误判在哪才是交付的底线。希望你做完这个花卉识别系统后也把难例收集当成常规动作而不是事后补救。希望帮到你。本文还有配套的精品资源点击获取