简介:围绕深度学习模型在花卉种类识别中应用的期刊论文PDF,面向计算机视觉、机器学习方向的研究者、学生及竞赛团队,聚焦解决花卉这类非刚性物体因形态多样而难以自动分类的问题。论文基于ImageNet数据库中的花卉图像样本完成训练与测试,提出带多个隐层的深度卷积神经网络,并通过与传统神经网络、支持向量机对比,验证该算法可将花卉识别率提升10%以上;同时阐述了多隐层特征学习、卷积池化与反向传播等关键机制。压缩包内共1个PDF文件,大小约1.78MB,便于收藏、打印与批注阅读;目前已有2440人学习/下载。读者可从中获取完整模型架构、实验设置、对比结果及深度学习在非刚性物体识别中的原理分析,既可作为论文参考文献,也适合作为图像识别项目的方法参考。
1. 先别急着跑模型:花卉种类识别为什么是深度学习图像分类的经典练兵场
你可能觉得认花很简单——但把一张月季和一张玫瑰的照片放在一起,让模型区分,它比 ImageNet 分类更考验基本功。基于深度学习模型的花卉种类识别,本质上是一个细粒度图像分类任务:类间差异小(不同品种可能只差在花瓣纹理)、类内差异大(同一朵花在不同光照、角度下形态完全不同)、背景干扰强(花盆、土壤、叶子经常成为模型偷懒的线索)。这个标题背后是一条完整的落地链路:数据集准备、模型选型、迁移学习训练、超参调优、导出部署,每一步都有坑。它特别适合两类人:做深度学习毕设的学生,以及刚入门深度学习图像识别、想跑通一个完整实战项目案例的开发者。下面我就按这条链路,把每一步的选型理由、可复现命令和翻车现场一次讲透。
2. 数据先行:公开数据集与自建图片库的取舍
2.1 公开数据集怎么选:102类花卉与自采数据的边界
花卉识别不是从零开始攒图。深度学习项目里,公开数据集的最大价值是给你一个可复现的基准。经典选择是 Oxford 102 Flower,这是牛津大学视觉几何组发布的公开数据集,包含 102 个类别,每类图片数量在 40 到 258 张之间,全部拍摄于真实自然环境,背景杂乱,类别标签使用花的学名。做学术对比用它很合适;但如果你最终要部署到某个具体场景,比如校园花圃或植物园导览,公开数据和真实环境之间存在明显的分布漂移——公开集里的英国花园花种和国内南方常见的花种重叠度有限。
我的建议是分两步走:先用公开数据集把整个训练和评估流程跑通,拿到一个可用的基线准确率,再补充自采数据做微调。自采数据不需要贪多,每个类 30 到 50 张就够迁移学习使用,但必须覆盖不同拍摄角度、不同时间段的光照和同一品种的不同生长状态。特别提醒:一个常见误区是只拍花的正面特写,这样模型学到的是“正对镜头的花”,换个俯拍角度立刻翻车。采集时要有意识地把背景复杂度拉高——草地、水泥地、花盆、人的手指都可能入镜,这才是真实场景。
另外,公开数据集的标签通常是学名,比如 Rosa rugosa 这类拉丁文,你训练时直接用数字索引做标签就行,但部署阶段要维护一张“索引到中文俗名”的映射表。这张表建议从一开始就存成 JSON,不要等训练完了再补映射,否则最后交付时对不上号是常态。
2.2 目录按类组织与数据集划分脚本
不管数据来自公开集还是自采,第一步都是把它整理成 PyTorchtorchvision.datasets.ImageFolder可以直接读取的目录结构。约定很简单:根目录下分train/、val/、test/三个文件夹,每个文件夹下按类别建子目录,子目录里放对应类别的图片。ImageFolder 会把子目录名当作标签,按字母序映射成数字索引,省去手写标签文件的麻烦。
数据集划分这一步极容易出问题,尤其是验证集里混进训练图。直接对全部文件做一次随机打乱再切分,会让每一个类别的图片同时出现在训练集和验证集里,造成精度虚高。正确的做法是按类别分组,逐类 shuffle,保证每个类别在三份数据里都按比例存在。下面这个脚本就是干这件事的:
import random from pathlib import Path import shutil def split_dataset(raw_dir, target_dir, ratios=(0.7, 0.15, 0.15)): raw_dir = Path(raw_dir) target_dir = Path(target_dir) random.seed(42) # 固定随机种子,保证每次运行结果一致 for class_dir in raw_dir.iterdir(): if not class_dir.is_dir(): continue classes = sorted(class_dir.glob("*.jpg")) + sorted(class_dir.glob("*.png")) random.shuffle(classes) # 只打乱当前类别内部顺序 n_train = int(len(classes) * ratios[0]) n_val = int(len(classes) * ratios[1]) for phase, part in zip( ["train", "val", "test"], [classes[:n_train], classes[n_train:n_train + n_val], classes[n_train + n_val:]] ): dest = target_dir / phase / class_dir.name dest.mkdir(parents=True, exist_ok=True) for img in part: shutil.copy2(img, dest / img.name) # copy2 保留文件元信息 if __name__ == "__main__": split_dataset("raw_images", "flowers_split", ratios=(0.7, 0.15, 0.15))这里的核心逻辑有两个:一是random.seed(42)固定随机种子,让划分结果可复现,排查问题时不会出现“上次跑和这次跑验证集不一样”的困惑;二是每个类别独立 shuffle,而不是对所有图片统一 shuffle。后者会导致某些冷门类别在验证集里一张图都没有,或者验证集与训练集出现同源重复,这两种情况都会让验证集失去意义。
ratios=(0.7, 0.15, 0.15)适用于图片总量充足的情况。如果每个类别只有 40 到 80 张图,建议改成(0.8, 0.1, 0.1),并且保证验证集每个类至少 5 张,否则验证集准确率的波动会非常大。数据集总量不大时,优先用copy2复制文件而不是移动,因为你很可能在准备阶段反复调整划分比例,把原图移动之后想重分就得重新找数据源。
2.3 增强策略:让模型别把背景当花
花卉识别的增强策略要针对它的两个特点来设计:一是颜色敏感,二是背景干扰强。花的颜色是分类的重要线索,但同一品种在不同光照下颜色饱和度差异很大,所以颜色增强要做,但不能做过头;背景干扰则需要通过随机裁剪来强迫模型关注主体。
训练集的增强流水线我一般这样配:
from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0), ratio=(0.75, 1.333)), transforms.RandomHorizontalFlip(), transforms.RandomApply([ transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.05) ], p=0.6), 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(224, scale=(0.7, 1.0))的意思是裁剪面积占原图面积的 70% 到 100%,这个范围是我在花卉数据上调试后比较稳的取值——小于 0.7 容易把花瓣边缘截掉,模型会误以为“花瓣残缺”是某个类别的特征。ratio=(0.75, 1.333)控制裁剪宽高比,对花卉来说不需要过度极端,否则花朵整体形态被拉伸变形。ColorJitter的hue参数我只给到 0.05,超过这个值会出现把黄色花变成橙色花的问题,而颜色恰好是花卉分类最敏感的维度。
验证集只做Resize(256)加CenterCrop(224),不做随机增强,这是为了保证验证结果可复现、可对比。注意Normalize用的均值和标准差必须是 ImageNet 的统计量,因为后面要加载在 ImageNet 上预训练的模型权重,输入分布不一致会直接拉低迁移学习的效果。还有一个容易忽略的细节:ToTensor()会把像素值从 0-255 归一化到 0-1 区间,Normalize再按通道做标准化,这两个变换的顺序不能颠倒。
增强部分最忌讳的是无脑堆强度和种类。随机旋转、随机灰度化、高斯噪声这些在你数据量少的时候看似能“无中生有”,实际会把模型推向极度依赖形状线索,反而忽略颜色纹理。先只用裁剪、翻转和颜色扰动跑一个基线,再加其他增强,对比着看验证集精度变化,比一次性上全套增强可靠得多。
3. 模型选型与迁移学习:为什么 ResNet18 比自建 CNN 更靠谱
3.1 从零训练 vs 迁移学习:算力账单说明一切
花卉数据集通常只有几千到几万张图,从零训练一个卷积神经网络需要百万级数据和数天 GPU 时间,这在个人项目里根本不现实。迁移学习是把在 ImageNet 上预训练好的特征提取器搬过来,让模型用已经学会的边缘、纹理、颜色特征来适配花卉分类。这是深度学习图像识别领域目前公认最优的做法——花的数据量只够训练一个分类头,不够训练一个特征提取器。
这里顺带说一个最近常被问到的点:LLM 是否属于深度学习,以及能不能用大模型来做图像分类。结论很直接:花卉种类识别是典型的细粒度图像分类,CNN 加迁移学习是被验证过的最短路径,这个场景里大模型没有用武之地。目标类别只有几十到一百出头,输入是静态图像,不需要语言理解,强行套大模型只会增加推理成本,精度还不一定比得上 ResNet。
动手之前,先把 PyTorch 环境配好。建议 Python 3.9 以上、PyTorch 2.x,有 NVIDIA 显卡就装对应 CUDA 版本的 torch。具体配置方式网上有大量教程,这里不展开。我默认你已经有能跑import torch的环境,下面的代码才谈得上落地。
3.2 用 PyTorch 加载预训练 ResNet18 并替换分类头
ResNet18 是这个任务里性价比最高的选择。太浅的 CNN 特征表达能力不够,ResNet50 参数太多,在小数据集上容易过拟合,且推理速度慢。ResNet18 只有 11M 左右的参数量,一张普通消费级显卡就能跑,推理一张图在 CPU 上也是毫秒级。
加载并改造模型的代码非常简单,但有两处地方新手容易出错:
import torch import torch.nn as nn import torchvision.models as models num_classes = 102 # Oxford 102 是 102 类,自建数据集按实际类别数改 model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) # 打印模型结构,确认 fc 层已替换 print(model.fc)第一,weights参数在新版 torchvision 里必须使用ResNet18_Weights.IMAGENET1K_V1这种枚举写法,旧版本里直接用pretrained=True会给出弃用警告,后续版本可能直接移除。第二,model.fc是 ResNet18 最后的全连接层,输入维度是 512,输出维度原来是 1000(ImageNet 类别数),现在改成num_classes。注意in_features一定要从原模型的fc.in_features取,不要硬编码成 512,万一你换用 ResNet34 或 ResNet50,这个值的来源是唯一的。
预训练权重首次加载时会从网络下载到 torch 的默认缓存目录,之后每次都从本地读取。如果你下载总是失败,请自查网络环境,这是国内做深度学习入门最常见的一道坎,没有捷径可走。
3.3 冻结与解冻:梯度只走该走的路
替换完分类头之后,直接对整个模型做端到端训练是可以跑通的,但效果不会好。原因是新初始化的fc层梯度很大,反向传播时会带动前面所有层的预训练权重一起剧烈更新,几个 epoch 就能把 ImageNet 上学到的通用特征破坏掉。正确的做法是两阶段训练:先冻结骨干网络,只训练分类头,等分类头收敛后再解冻部分深层,用小学习率微调。
# 第一阶段:冻结除 fc 层外的所有参数 for name, param in model.named_parameters(): if name.startswith("fc"): param.requires_grad = True else: param.requires_grad = False # 为不同参数组设置不同学习率 optimizer = torch.optim.AdamW([ {"params": model.fc.parameters(), "lr": 1e-3}, ]) # 第二阶段(在初步收敛后):解冻 layer4,降低学习率微调 for name, param in model.named_parameters(): if name.startswith("fc") or name.startswith("layer4"): param.requires_grad = True else: param.requires_grad = False optimizer = torch.optim.AdamW([ {"params": model.fc.parameters(), "lr": 1e-3}, {"params": model.layer4.parameters(), "lr": 1e-4}, ])named_parameters()返回的参数名对应模型结构的层级前缀,fc是最后的全连接层,layer4是 ResNet18 最后一个残差模块。第二阶段的逻辑是:layer4输出的特征已经非常接近语义信息,直接跟分类任务挂钩,所以解冻它做微调;layer1到layer3学的是通用边缘和纹理,保持冻结,避免小数据量下的过拟合。
这里给一个我常用的参数组合:第一阶段训练 10 个 epoch,fc 层学习率 1e-3;第二阶段解冻 layer4,学习率 1e-4,训练 15 个 epoch。注意两个阶段的优化器都要配weight_decay,一般设1e-4,防止小数据集下权重范数增长过快。
4. 训练配置与超参调优:让 loss 曲线说真话
4.1 损失函数、优化器与学习率调度器
分类任务的标准损失函数是交叉熵。nn.CrossEntropyLoss()在 PyTorch 里已经集成了LogSoftmax和NLLLoss,所以模型的最后一层直接输出原始 logits 就行,不要再手动加 softmax,否则等于做了两次 softmax,梯度信号会被扭曲。
优化器我推荐 AdamW,候选是 SGD 加 Momentum。两者对比:
| 配置项 | 推荐值 | 理由 |
|---|---|---|
| 损失函数 | nn.CrossEntropyLoss() | 单标签多分类的标准配置,内部已做 log_softmax |
| 优化器 | AdamW, lr=1e-3, weight_decay=1e-4 | 收敛快,对学习率不敏感,适合迁移学习前段 |
| 学习率调度器 | CosineAnnealingLR(T_max=20) | 比 StepLR 的阶梯下降更平滑,后期不容易震荡 |
| batch_size | 32 或 64 | 根据显存选,尽量大,但验证集精度反而因此略降是正常现象 |
| epochs | 第一阶段 10,第二阶段 15 | 迁移学习场景下 25 个 epoch 足够收敛 |
AdamW 和 Adam 的差别在于权重衰减的实现方式。Adam 的权重衰减和梯度动量耦合在一起,正则化效果不干净;AdamW 解耦之后,同等weight_decay下的表现更稳定。SGD 加 momentum 到后期精度上限更高,但需要手动调学习率衰减策略,新手容易在“学习率该在第几个 epoch 降”这个问题上纠结。用 AdamW 加 CosineAnnealing,只需设好初始学习率和T_max就可以放手跑。这里的T_max我设置为 20,意思是在 20 个 epoch 内学习率从初始值余弦衰减到接近 0,正好覆盖第二阶段的训练长度。
4.2 训练循环与验证逻辑:每个 epoch 记录什么
训练代码的核心骨架如下。注意这里没有用 PyTorch Lightning 等框架封装,目的是把底层逻辑讲清楚,你自己封装或者用现成框架时也知道每一步在做什么:
import torch 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) logits = model(images) loss = criterion(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() * images.size(0) correct += (logits.argmax(dim=1) == labels).sum().item() total += labels.size(0) return total_loss / total, correct / total def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total = 0.0, 0, 0 with torch.no_grad(): for images, labels in loader: images, labels = images.to(device), labels.to(device) logits = model(images) loss = criterion(logits, labels) total_loss += loss.item() * images.size(0) correct += (logits.argmax(dim=1) == labels).sum().item() total += labels.size(0) return total_loss / total, correct / totalmodel.train()和model.eval()切换的是 Dropout 和 BatchNorm 层的行为。训练模式下 Dropout 随机失活一部分神经元,BatchNorm 使用当前 batch 的均值和方差;评估模式下 Dropout 不生效,BatchNorm 使用训练阶段积累的全局统计量。忘记切换是后期推理结果不稳定的头号元凶。torch.no_grad()关闭梯度记录,评估阶段显存占用会降低不少,也避免无意中把验证集的梯度传回模型。
每个 epoch 记录四个指标:训练损失、训练准确率、验证损失、验证准确率。判断训练是否正常,主要看训练损失是否持续下降、验证损失是否跟着下降。如果训练损失降而验证损失不降甚至上升,说明过拟合已经开始;如果两者都不降,先怀疑学习率,再怀疑数据问题。total_loss累加时乘了images.size(0),最后除以总样本数,得到的是每个 epoch 的平均损失,不同 epoch 之间才有可比性。
4.3 早停与模型保存:别把最后一轮权重当宝贝
训练过程中很多人习惯训练完直接把最后一轮的model.state_dict()拿去部署,这是不合适的。最后一轮不一定是最优的一轮,验证集上最好的模型往往出现在中间某个 epoch。需要加一个基于验证集准确率的早停和保存机制:
best_val_acc = 0.0 patience = 10 bad_epochs = 0 for epoch in range(num_epochs): train_loss, train_acc = train_one_epoch( model, train_loader, criterion, optimizer, device) val_loss, val_acc = evaluate( model, val_loader, criterion, device) if val_acc > best_val_acc: best_val_acc = val_acc bad_epochs = 0 torch.save({ "epoch": epoch, "model_state_dict": model.state_dict(), "optimizer_state_dict": optimizer.state_dict(), "best_val_acc": best_val_acc, }, "best_flower_model.pth") else: bad_epochs += 1 if bad_epochs >= patience: print(f"epoch {epoch}: early stop, best val acc {best_val_acc:.4f}") breaktorch.save保存的是 dict,里面包含模型权重、优化器状态和训练进度。只保存model.state_dict()也可以,但带上优化器状态之后,如果你想从断点继续训练,就具备了完整的恢复条件。patience=10的意思是连续 10 个 epoch 验证集准确率没有刷新纪录就停止训练。数据增强开启时,验证集准确率会有正常的上下波动,patience 太小容易在波谷处误杀训练,10 是一个相对安全的默认值。保存下来的 checkpoint 文件大小受模型参数量影响,ResNet18 的 state_dict 在 45MB 量级,你看到的“文件大小”和“参数量”不是一个概念——参数量决定的是模型容量,文件大小由参数量乘以每个参数的存储字节数决定。
5. 避坑记录:花卉识别训练最常见的 5 个翻车现场
5.1 图片路径带中文或空格,DataLoader 在 Windows 上直接报错
现象:ImageFolder加载图片时抛出FileNotFoundError,或者训练到一半突然中断,错误信息指向某张路径含中文的图片。在 Windows 上尤其常见,默认编码不是 UTF-8,中文路径和全角空格会直接让 torchvision 的图片读取器失效。
原因:torchvision 底层调用 Pillow 读取文件,Pillow 在 Windows 上对非 ASCII 路径支持不稳定;另外,目录名如果在中文环境创建,默认就是 GBK 编码,与 Python 的 UTF-8 字符串比较时产生偏差。
解决:在数据集准备阶段就把所有目录和文件名改为纯英文小写加下划线。文件重命名规则可以简单粗暴一点:类别索引_四位序号.jpg,比如001_0001.jpg。在 2.2 节的划分脚本里,加一步强制重命名的逻辑,不要手工改,数据量大时手工改名必然出错。
5.2 验证集里混进了训练图,验证准确率虚高还不自知
现象:验证集准确率轻松上 98%,模型部署到真实场景后识别效果一塌糊涂,跟训练时的表现完全对不上。
原因:最典型的是划分数据集时没有按类别分组,而是把所有图片混在一起随机切分。随机切分本身没有错,错的是没有考虑同源图片——比如同一朵花在不同角度拍了 5 张,随机切分后 3 张进训练集、2 张进验证集,这两张验证图对模型来说“见过”的高度相似版本,验证准确率自然虚高。
解决:严格按照 2.2 节的脚本按类别分组后切分,并且切分完成后做一次抽样检查:随机挑 5 张验证集图片,去训练集里找相同文件名的副本,用 MD5 哈希对比内容是否一致。这一步成本很低,但能拦住 90% 的精度造假问题。
5.3 照搬 ImageNet 训练配置,学习率设 0.1,loss 直接飞掉
现象:第一个 epoch 的 loss 从 1.7 涨到 50 以上,后面直接变成 NaN。新手容易把开源 ImageNet 训练仓库里的学习率配置照搬过来,但那是从零训练且使用超大 batch size 的配置。
原因:迁移学习场景下,预训练模型的骨干网络权重已经很稳定,初始学习率过大会让梯度更新一步跨过最优点,破坏之前学到的好特征。尤其是新替换的fc层随机初始化,梯度量级远大于骨干网络,两者共用同一个学习率时,骨干网络会被带崩。
解决:fc 层学习率设为 1e-3,骨干网络解冻部分设为 1e-4。如果 loss 在前两个 epoch 不降反升,把学习率整体降到 1e-4。打印每个 epoch 的实际学习率,确认调度器生效,排查时先排除学习率问题再动网络结构。
5.4 长尾分布:少数类别只有 10 张图,多数类别有 200 张
现象:混淆矩阵里冷门类别几乎全错、recall 偏低,模型对所有类别都倾向于预测头部那几个数量多的类别。
原因:交叉熵损失的梯度被数量多的类别主导,模型学到“猜常见的类别能降低整体损失”,因此对少数类别的决策边界收敛得极差。公开数据集和自采数据都存在这个问题,采集时对常见花多拍、稀有花少拍是天然倾向。
解决:最轻量级的手段是用WeightedRandomSampler对采样概率按类别样本数的倒数加权。代码只需在 DataLoader 里加一个参数:
from torch.utils.data import WeightedRandomSampler class_counts = [len(imgs) for imgs in per_class_images] weights = [1.0 / count for count in class_counts] sample_weights = [weights[label] for _, label in dataset.samples] sampler = WeightedRandomSampler(sample_weights, num_samples=len(sample_weights), replacement=True) loader = DataLoader(dataset, batch_size=32, sampler=sampler)注意sample_weights的长度必须与数据集样本数一致,默认每个样本的权重等于它所属类别的样本数倒数。replacement=True表示允许重复采样,否则num_samples超过数据集长度时会报错。加权后少数类在同一个 epoch 里被采样更多次,相当于做了过采样,但不会改变图像本身。
5.5 推理阶段忘了 model.eval(),同一张图结果每次都不同
现象:部署到推理脚本后,同一张输入图片跑三次,输出概率各不相同;多进程推理时结果还和进程数有关。
原因:模型里含有 Dropout 和 BatchNorm。训练模式下 Dropout 随机失活,BatchNorm 使用当前 batch 统计量,二者都会让输出带有随机性;推理时没切到 eval 模式,这些层的训练行为被错误保留。
解决:推理代码里必须同时使用model.eval()和torch.no_grad()。前者切换层的行为模式,后者停止梯度追踪,两者缺一不可。评估函数里已经写了这两个调用,最容易漏的是自己在写预测脚本时没有沿用封装好的函数。习惯性把这些逻辑封装成同一个函数,避免每次写推理都重新实现一遍。
6. 从模型到可用:导出、可解释性验证与批量预测
6.1 导出 ONNX 模型,脱离 PyTorch 环境推理
训好的模型如果只在 PyTorch 环境里跑,交付给嵌入式设备或者移动端时会被框架绑死。导出成 ONNX 是当前兼容性最好的一条路,用一行代码就能完成:
model.to("cpu").eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "flower_model.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, opset_version=11 )dynamic_axes把 batch 维度设为动态,这样导出后的模型既能一次推理一张,也能一次推理一批。注意导出前必须eval(),否则模型里 Dropout 的行为会被固化进 ONNX 图,导出成功但推理结果是错的。
6.2 用 Grad-CAM 验证模型是在看花还是在看背景
准确率数字高不一定意味着模型学到了正确的语义。我之前的项目里,模型在验证集上准确率 96%,但把热力图叠加到原图上一看,它盯的是花盆边缘而不是花瓣纹理。Grad-CAM 类方法能定位模型决策时关注的区域,做法是对最后一个卷积层的输出做梯度加权,生成热力图。实践中可以参考pytorch_grad_cam这类现成库,目标层传入model.layer4[-1],把热力图和原图叠加保存到visual/目录下批量查看。如果热力图集中在花瓣、花芯区域,说明模型学到了正确的特征;如果集中在花盆、土壤,说明数据里背景信息太强,需要重新设计增强策略或补充多背景数据。
6.3 批量预测脚本与部署映射
最后给一个批量预测的核心片段,方便你直接落地到评估脚本里:
def predict_image(model, image_path, class_names, device): from PIL import Image img = Image.open(image_path).convert("RGB") img = val_transform(img).unsqueeze(0).to(device) with torch.no_grad(): logits = model(img) probs = torch.softmax(logits, dim=1) top_probs, top_idx = probs.topk(3, dim=1) return [(class_names[i], p) for i, p in zip(top_idx[0], top_probs[0])]class_names是从 JSON 里加载的索引到中文花名的映射表。注意这张表的顺序必须和训练时ImageFolder的类别索引一致——ImageFolder按字母序排序,你只需要在训练前把这份映射导出一次,之后不要再改原始图片目录里的子目录名,否则索引全乱。
我早期的做法是训练完直接看准确率,觉得数字好看就收工,直到一次真实拍摄测试翻车才发现模型学的是背景。从那以后,Grad-CAM 可视化成了我每个分类项目的标配验证步骤,成本不高,但能提前暴露数据集的偏置问题。希望这个流程对你有帮助。
本文还有配套的精品资源,点击获取