
简介这是一份面向机器学习、图像识别与生物多样性研究的蝴蝶分类数据集包含20个常见蝴蝶类别每类提供多角度实拍图片可用于训练卷积神经网络、验证图像分类算法也便于生物学家开展物种比较与生态分析。压缩包共1870个文件整体约60.96MB主体为1866张JPG图片另含物种名称文本、所属属名文本和JSON属性字典目录结构清晰方便按类别读取与映射标签能显著减少数据预处理的负担。目前已有118人学习/下载。使用这份数据可直接获得带标注的20类蝴蝶样本及配套元数据省去自行采集、筛选和整理图片的时间配合JSON字典和文本标签能快速搭建图像分类训练流程适合高校实验、算法练习或小型科研项目作为初始数据集使用也可用于图像增强、模型迁移学习与分类效果对比等进一步实践。1. 蝴蝶分类数据集20类.zip拿到手先别急着解压下载完“蝴蝶分类数据集20类.zip”后大多数人做的第一件事就是双击解压然后开始敲训练代码。我当初也这么干过结果数据集质量参差不齐有的文件夹里塞满了同一只蝴蝶的不同角度照片有的类只有三十来张图zip 包解压到一半还会遇到整包损坏的情况。这个数据集本质上是一个细粒度图像分类任务20 个类别的昆虫图像区别往往只在翅膀纹理、斑点和颜色分布上比猫狗分类难得多也因此很适合拿来验证迁移学习、样本均衡和调参流程还经常被用作 yolov8 训练自己数据集时的预研究素材。这篇笔记会按你实际操作的顺序来讲先验货拆包再做分类训练再谈如何转成目标检测数据最后把常踩的坑逐个排掉。2. 打开 zip 前先验货把压缩包拆成可训练的干净目录2.1 先别急着双击解压用文件列表判断数据真实结构拿到任何一个数据集 zip第一件事不是解压而是先看里面的目录长什么样。你双击打开压缩包看到的“假目录”和真正解压后的结构经常不一致有些来源的压缩包会在根目录套一层空文件夹有些则直接把一百多个文件平铺在最外层解压后全部混在一起后面写ImageFolder时就直接报错。在 Linux 或 macOS 下用unzip -l列出压缩包内文件列表即可Windows 下用 7-Zip 的打开方式查看也是一样的效果。# 列出压缩包内所有文件前 40 行足够看清楚目录结构 unzip -l butterfly_classification_20class.zip | head -n 40 # 如果只想看每个类目录下的文件数量用 awk 按路径层级统计 unzip -l butterfly_classification_20class.zip | awk NR3 {print $4} | awk -F/ {print $2} | sort | uniq -c | sort -rn | head -n 30第一段命令里unzip -l只读压缩包中央目录不解压任何文件速度很快。第二段命令把每行第四条字段也就是文件名拆开取第二层目录名去重计数这样能直接看到哪一类图片多、哪一类图片少。如果$2的位置不对换成$3再看一次取决于压缩包根目录层数。这个操作的核心目的只有一个在还没动数据之前先确认类目录数量、每类大概样本量、文件命名规律以及有没有混入 Mac 系统生成的__MACOSX垃圾目录和.DS_Store文件。2.2 解压与完整性校验zip 的脾气比你想象的大确认结构没问题后再执行真正的解压。最常见的问题是解压到一半报CRC failed尤其是从百度网盘这类渠道下载的大文件下载过程被客户端截断、重传都会造成压缩包尾部数据缺失。mkdir -p butterfly20 unzip -q butterfly_classification_20class.zip -d butterfly20/ echo $?-q让 unzip 安静解压不逐条打印文件-d指定解压目标目录。命令结束后$?为 0 表示解压过程没有遇到错误。但这只代表当前这次解压顺利不代表压缩包本身完全健康换一个工具解压可能又会报错。稳妥的做法是解压前先做一次整体测试unzip -t butterfly_classification_20class.zip-t会逐条读取压缩包条目并计算 CRC 校验值与原文件的校验值比对。任何一条输出bad CRC都说明文件在传输或转存过程中损坏了直接重新下载别浪费时间尝试修复。zip -FF这类修复命令在某种程度上能抢救但多数情况会把损坏条目直接丢掉对于训练数据集来说不如重新下载来得干净。这里额外说一个包相关的坑部分渠道会把 zip 设置成伪加密你看得到文件列表但解压时提示需要密码。这类情况下密码通常是发布方写在下载页面或随附说明里的不是什么技术难题把发布页面的说明翻出来找一下。如果确认来源没有设置密码而你碰到了密码提示说明你下载的副本被转手处理过果断换一个来源更可靠。2.3 目录重排与 train/val 拆分一个脚本解决数据划分解压完成后如果你看到的是干净的 20 个类目录那还算幸运。但很多数据集解压后会有重复嵌套目录、文件扩展名不统一、图片文件放在子目录深层等问题。为了后面训练不被路径问题打断先统一整理成train/类名/图片.jpg和val/类名/图片.jpg的标准结构。import os import shutil import random random.seed(42) src_root butterfly20 out_root butterfly20_split val_ratio 0.2 classes [d for d in os.listdir(src_root) if os.path.isdir(os.path.join(src_root, d))] if len(classes) 0: print(未在根目录找到任何类目录请检查嵌套层级) exit(1) for c in sorted(classes): imgs [f for f in os.listdir(os.path.join(src_root, c)) if f.lower().endswith((.jpg, .jpeg, .png, .bmp))] if len(imgs) 0: print(f警告: {c} 目录下没有图片) continue random.shuffle(imgs) val_n max(1, int(len(imgs) * val_ratio)) for i, img in enumerate(imgs): sub val if i val_n else train dst_dir os.path.join(out_root, sub, c) os.makedirs(dst_dir, exist_okTrue) shutil.copy2( os.path.join(src_root, c, img), os.path.join(dst_dir, img), ) print(f{c}: 共 {len(imgs)} 张train {len(imgs) - val_n}val {val_n})这里用shutil.copy2而不是os.rename原因是保留原压缩包作为备份划分后如果发现比例不合适或某类有问题还能重新拆分。val_ratio 0.2是 20 类小数据集的常见选择如果每类图片只有 40 到 50 张可以考虑改成 0.15避免验证集只剩个位数样本。random.seed(42)保证每次运行脚本的划分结果一致否则调试时每次数据分布都不一样模型复现无从谈起。2.4 解压后必须做的三道体检重复图、异常尺寸、无效文件拆包和划分只是第一步。我见过太多次训练到一半突然报image file is truncated或者验证集里莫名其妙出现两张一模一样的图。所以在开始训练前花五分钟跑一遍体检脚本能省下后面好几天的排查时间。import hashlib import os import collections def md5_of(path): h hashlib.md5() with open(path, rb) as f: for chunk in iter(lambda: f.read(65536), b): h.update(chunk) return h.hexdigest() md5_map collections.defaultdict(list) for root, _, files in os.walk(butterfly20_split): for f in files: if f.lower().endswith((.jpg, .jpeg, .png, .bmp)): p os.path.join(root, f) md5_map[md5_of(p)].append(p) dups {k: v for k, v in md5_map.items() if len(v) 1} print(f重复图片组数: {len(dups)}) for k, paths in list(dups.items())[:5]: print(fmd5: {k}) for p in paths: print( , p)这个脚本按 64KB 分块计算 md5避免一次性把大图读进内存。查出来的重复文件看它们是同类重复还是跨类重复同类重复说明数据集有冗余跨类重复说明原始来源贴错了标签这种图对齐到正确类别的成本太高直接删掉最省事。另一种常见现象是文件扩展名是.jpg但实际内容是 PNG 格式PIL 打开时一般能自动识别不用太担心但如果用的是某些依赖扩展名判断格式的老代码就会被坑到。最后看一眼图片尺寸分布如果大部分图都在 200x200 以下后面的训练尺寸就不能设得太大否则上采样会引入大量伪影。提示所有体检步骤都在拆分后的目录上做不要在原始压缩包目录上操作。拆分的意义就是给数据一个干净的副本体检数据再脏也不影响原始包。3. 首次训练跑通用 ResNet18 在 PyTorch 上训练 20 类蝴蝶分类模型3.1 数据读取ImageFolder 读取、增强策略与批次参数目录结构已经是train/类名/*.jpgPyTorch 的torchvision.datasets.ImageFolder可以直接读不需要自己写 Dataset 类。这一步看似简单但增强策略的参数选择很影响最终效果蝴蝶数据集不是普通物体分类类间差异细腻增强不能太狠也不能没有。from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(10), 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_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_ds datasets.ImageFolder(butterfly20_split/train, transformtrain_tf) val_ds datasets.ImageFolder(butterfly20_split/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(类名映射:, train_ds.class_to_idx) print(训练集样本数:, len(train_ds), 验证集样本数:, len(val_ds))Resize((256, 256))与后面模型输入尺寸配合蝴蝶翅膀纹理需要一定的分辨率低于 224x224 会把细节抹掉。RandomRotation(10)只旋转 10 度因为蝴蝶照片通常不是完全水平就是完全倒置旋转角度太大会出现大量不自然的“头朝下”样本这个数据集里出现 90 度旋转标本的概率很低。ColorJitter的饱和度和亮度扰动幅度控制在 0.2用于模拟不同光线环境下的拍摄差异。验证集不做任何随机增强只做 Resize 和归一化保证每张图都能稳定复现预测结果。num_workers4适合 CPU 核数在 8 以上的机器如果你的机器只有 4 核改成 2 反而比 4 更效率因为线程切换开销大于并行收益。pin_memoryTrue只对 GPU 训练有意义它让数据在内存中锁页减少 host 到 device 的拷贝时间纯 CPU 训练时这个参数没有作用但留着也不影响。3.2 模型选择与超参数为什么用预训练 ResNet1820 类蝴蝶分类ImageNet 预训练模型完全够用。我建议从 resnet18 起步而不是 resnet50原因有两个一是这类公开数据集的图片分辨率普遍不高深网络的特征提取能力提升有限训练时间却增加两三倍二是蝴蝶翅膀纹理的判别信息在浅层和中层特征里就有明显分布resnet18 的 512 维特征足以承载 20 类分类的决策面。当然如果你显存充裕且数据量超过每类 300 张换 resnet50 会有一定收益差别主要在尾部几条类别上。import torch import torch.nn as nn from torchvision import models device torch.device(cuda if torch.cuda.is_available() else cpu) print(使用设备:, device) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 20) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30)models.ResNet18_Weights.IMAGENET1K_V1是 torchvision 官方推荐的预训练权重引用方式比旧写法pretrainedTrue更明确代码审查时不会出现版本歧义。最后一层全连接从 1000 类改成 20 类输出维度与类别数对齐val_ds.class_to_idx的索引顺序就是输出通道的含义训练时务必确认这个映射和最终类别列表一致。学习率从1e-4起步而不是常见的1e-3。细粒度分类任务的特征差异本身较微细预训练权重在前几层已经学到通用边缘和纹理过大的学习率会在微调初期就把这些通用特征重置掉。如果想保守一点可以先把 backbone 冻结只训练fc层几个 epoch然后再解冻整体微调但这样要多写几行调度代码对中等数据集收益一般。CosineAnnealingLR配合 30 个 epoch 的 T_max让学习率从 1e-4 平滑降到接近 0最后几个 epoch 的精细收敛对细粒度分类很有帮助。3.3 训练循环指标看什么、loss 不降怎么办训练代码本身不难难的是知道每一步在干什么、loss 出现异常时如何反应。下面的循环把训练和验证分开写每个 epoch 打印一次两边的准确率方便观察是否过拟合。for epoch in range(30): model.train() train_loss, train_correct, train_total 0.0, 0, 0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() out model(imgs) loss criterion(out, labels) loss.backward() optimizer.step() train_loss loss.item() train_correct (out.argmax(dim1) labels).sum().item() train_total labels.size(0) model.eval() val_correct, val_total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) out model(imgs) val_correct (out.argmax(dim1) labels).sum().item() val_total labels.size(0) print(fepoch {epoch:02d} | ftrain_loss {train_loss / len(train_loader):.4f} | ftrain_acc {train_correct / train_total:.4f} | fval_acc {val_correct / val_total:.4f}) scheduler.step()每行输出四个指标训练 loss、训练准确率、验证准确率。这里的关键是观察训练准确率和验证准确率的间距。如果训练准确率已经到 95% 而验证准确率一直在 60% 上下这就是典型的过拟合需要回到增强策略里增强扰动或者调整 dropout。如果两者都很低比如都不到 40%先怀疑类别映射是否错位再去检查数据增强是不是过度破坏了图像。如果训练 loss 在前两三个 epoch 里不降反升最可能的原因是学习率偏大把预训练权重冲坏了把学习率降到 3e-5 重新跑一轮基本能恢复。训练收敛后建议保存两份模型一份包含model.state_dict()遍历自保一份包含完整模型结构加权重方便后面做推理验证。保存时用torch.save加后缀_final.pt避免覆盖中途 checkpoint。4. 从分类数据到目标检测给蝴蝶图上画框并转成 YOLO 格式4.1 分类与检测的差距为什么需要目标检测很多人在分类模型跑通之后下一步就想把手头这套 20 类蝴蝶数据用到 yolov5 或 yolov8 上。这里必须先想清楚分类数据集的图片是“主体居中、背景干净”的裁剪图训练出来的模型只能回答“这张图是什么蝴蝶”回答不了“蝴蝶在哪”。而真实场景里你拿手机拍一张花园照片里面可能有三五只蝴蝶分布在不同位置有大有小还互相遮挡这类任务必须先用目标检测框出每个目标再配合分类逻辑才能完整解答“哪只蝴蝶在哪个位置、是什么种类”。如果你手头的素材是标本照蝴蝶永远居中那分类模型足够用但要做野外识别或农业监测就绕不开标注这一步。4.2 标注工具与标注规范LabelImg 如何标出可用数据把分类数据集转成检测数据集客观难点在于分类图没有标注框。公开数据集里确实有一部分图是含目标位置信息的但绝大部分只提供类别标签。拿到这类素材后常见做法是使用 LabelImg 或 LabelStudio 手动补标一张张图。分类数据集里少则两三千张、多则上万张全部标完不现实优先选择那些包含多个目标或者目标占画面比例过小的图来标这些图才是检测任务的真正难点。标注时有几个原则要提前定下来尤其是框的尺度一致性。有的标注者习惯把框贴得很紧有的习惯留一圈背景最终模型收敛效果完全不同。蝴蝶检测框建议稍微贴紧翅缘但不要把翅膀尖切掉。蝴蝶标本如果平铺在展翅板上四翅张开框的四个边角通常会有大块空白这种空白属于正常现象不要为了减小框的面积而切成八边形。用 LabelImg 保存时选择 PascalVOC 格式它生成的是 XML 文件每个框记录xmin, ymin, xmax, ymax四个坐标值后续转换脚本只需要处理这一种格式就能覆盖大多数情况。4.3 XML 标注转 YOLO txt转换脚本与类别 ID 对齐YOLO 训练需要的标签是纯文本每一行表示一个目标格式为class_id x_center y_center width height坐标全部归一化到 0 到 1 之间除以图片宽高。转换脚本是绕不开的下面把 XML 转 TXT 的完整逻辑写出来注意处理浮点精度。import xml.etree.ElementTree as ET import os # 这个顺序必须与后续训练 yaml 中的 names 完全一致 class_names [ Iphiclides_podalirius, Papilio_machaon, Vanessa_atalanta, # ... 共 20 个按 val_ds.class_to_idx 顺序抄过来 ] def convert_xml_to_yolo(xml_path, txt_path, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in class_names: print(f跳过未定义类别: {name} in {xml_path}) continue cls_id class_names.index(name) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 坐标值先做一次边界检查防止越界负值 xmin max(0.0, min(xmin, img_w)) xmax max(0.0, min(xmax, img_w)) ymin max(0.0, min(ymin, img_h)) ymax max(0.0, min(ymax, img_h)) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines))脚本需要三个外部输入图片宽高、XML 路径、输出 txt 路径。图片宽高可以在标注后从图片本身读取这里建议直接用 PIL 解析不要看 XML 里的size节点因为部分标注工具写入的尺寸和实际图片不一致。转换完成后给每一张图片生成同名 txt 文件目录结构为butterfly20_detect/ ├── images/train/xxx.jpg ├── images/val/xxx.jpg ├── labels/train/xxx.txt └── labels/val/xxx.txt这样组织后训练 yaml 文件里train写images/trainval写images/valYOLO 会按路径前缀自动找到同级labels目录下的同名 txt。4.4 最小验证训练前检查标签与图片是否对齐不要直接开训。先花两分钟写个检查脚本遍历所有 labels 下的 txt 文件核对三个方面文件是否为空、类别 ID 是否超出 0-19、归一化坐标是否在 0 到 1 区间内。python -c import os for root, _, files in os.walk(butterfly20_detect/labels): for f in sorted(files): p os.path.join(root, f) with open(p) as fp: lines [l.strip() for l in fp if l.strip()] print(p, len(lines)) for l in lines[:3]: parts l.split() assert 0 int(parts[0]) 20, f类别ID越界: {l} for v in parts[1:]: assert 0 float(v) 1, f坐标越界: {l} 这个检查脚本本身不承担训练任务但它能一次暴露三类问题漏标导致 txt 为空、标注时类名写错导致 ID 越界、标注框超出图像边界导致坐标大于 1。任何一类问题进入训练集都会让验证指标变得不可信损失下降曲线也会异常抖动。5. 蝴蝶分类数据集的 5 个常见坑与避坑方法5.1 现象unzip 解压中断报 CRC failed 或提示密码从网盘下载的 zip 包经常出现解压到 70% 时突然报CRC failed此时已经解压出来的文件是不完整的但文件系统里路径和大小都对肉眼根本看不出来。还有一种情况是点击解压时提示输入密码而你确认发布方没有给过密码。原因前者是文件传输过程损坏下载软件断点续传后没有重新校验文件后者大概率是伪加密有些分享者为了阻止在线预览手动改过 zip 的加密位这个 zip 并非真正加密。解决解压前先执行unzip -t做完整性测试CRC 报错就删除压缩包重新下载不要带病使用。密码问题先回下载页翻说明正规渠道的伪加密密码都会写在明显位置翻不到就重新换一个下载来源不要在损坏的包上浪费时间。5.2 现象训练了好几个 epoch某几个类别准确率始终是 0训练完成后查看每个类别的准确率发现有四五个类别在验证集上一次都没预测对而另外十几个类别的准确率高达 95%。整体准确率看上去还有 80%掩盖了局部完全不可用的事实。原因样本量极度不均衡。20 个类里有些类拥有 500 张图有些类只有 30 张交叉熵损失被大类主导小类学不到特征。解决先做类别重采样用WeightedRandomSampler给样本少的类提高采样权重或者直接对样本少的类别做更强的数据增强和复制扩充。更简单的做法是在 Loss 里加权重from torch.nn import CrossEntropyLoss counts [520, 480, 35, 30, ...] # 按 class_to_idx 顺序写每类样本数 max_count max(counts) weights torch.tensor([max_count / c for c in counts], dtypetorch.float).to(device) criterion CrossEntropyLoss(weightweights)权重公式是“最大类样本数除以当前类样本数”样本越少的类别 loss 放大倍数越高。介于束手无策和动手重采样之间这个手段最直接也最容易被忽略。5.3 现象验证集准确率 95%放到实际照片上立刻掉到 50%模型在划分出来的 val 集上跑得很好但拿手机随手拍的照片一测就翻车。原因训练集和验证集之间存在数据泄漏。这个数据集许多来源是通过图库爬虫收集的同一个物种的图片可能来自同一个图库页面或者同一只个体的连拍照片被同时分进了两个集合。模型记住的是画面里的背景、光线和特定个体特征而不是“蝴蝶”这个类别本身。解决拆 train/val 之前先用 md5 去重把完全相同或高度相似的图片归并到同一边。再进一步如果数据是按物种收集的检查验证集里是否出现了训练集同一个个体的不同姿态图。这类泄漏无法完全根除但至少做到 md5 层面无重复。判断泄漏是否存在的快速方法训练准确率 95% 且验证准确率 95%但野外拍 10 张只对 5 张那你的模型大概率是在背样本。5.4 现象中文标签或特殊字符导致路径读取失败有的数据集直接使用中文名作为类文件夹名例如“柑橘凤蝶”训练时datasets.ImageFolder正常读取但保存 checkpoint 或写入日志时编码报错某些环境下还会在os.path.join拼接路径时出现斜杠问题导致验证集图片加载失败。原因不同系统默认编码不一样中文字符在 Linux 容器里以 UTF-8 存储在 Windows 里可能被转成 GBK 或乱码路径一旦错位就会整批报错。解决数据集解压后统一把类文件夹重命名为英文小写加下划线。用 Python 批量重命名时注意替换空格、括号、这类在路径里比较危险的特殊字符。类别与中文标签的对应关系单独维护一份class_names.txt训练结束后人工对照查看时可读性也更好。5.5 现象验证准确率高但 Grad-CAM 热图显示模型在看背景用 Grad-CAM 或者同类可视化工具观察模型决策依据发现高亮区域集中在蝴蝶旁边的花蕊、叶脉甚至标本白底上而不是蝴蝶翅膀本体。原因数据收集时同一类蝴蝶的背景高度一致。比如某类蝴蝶大量来自标本馆扫描图、某类大量来自同一位摄影师的野外作品背景风格的共性比蝴蝶特征的共性更容易被模型抓到。解决给训练集加随机裁剪放大让蝴蝶在画面里的占比更大压缩背景面积再做背景替换增强如对图像随机区域做高斯模糊模拟不同景深效果。最彻底的办法是检测模型加分类头联合训练先框出蝴蝶再分类让模型无法跳过着框这个过程直接看全图。如果你手头只有分类模型那就把输入裁剪范围缩小让蝴蝶占更多像素。6. 最后一项验证混淆矩阵与野外照片模型训练完成后验证集准确率只是一个整体标量它反映不了哪两类之间在互相混淆。这个数据集是 20 类细粒度分类最值得看的是一张 20x20 的混淆矩阵它直接告诉你“黄凤蝶被错认成什么了”。写几行脚本在验证集上跑一遍预测生成矩阵图import numpy as np import torch from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt all_preds [] all_labels [] model.eval() with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.to(device) out model(imgs) all_preds.extend(out.argmax(dim1).cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) disp ConfusionMatrixDisplay(cm, display_labelsval_ds.classes) disp.plot(xticks_rotation45) plt.tight_layout() plt.savefig(butterfly_confusion_matrix.png, dpi150)保存生成的混淆矩阵图仔细看对角线之外颜色较深的格子。如果有某两类的混淆颜色明显深于其他格说明这两个类在大众特征上高度重叠可行的处理办法是把这两个类的训练图拿出来逐张对比找到人眼能分辨但模型分辨不了的那部分特征针对性增加这类图片或考虑把这两个类合并成一个“近似种”类目。混淆矩阵验证完之后还要经历一次野外照片测试。从网上找一些数据集里没有出现过的蝴蝶摄影图最好包含背景杂乱、蝴蝶占比小、姿态侧翻的难例。写一个批量预测脚本把 Top3 概率打印出来看正确类别是否落在前三from PIL import Image topk 3 for img_path in [field_1.jpg, field_2.jpg, field_3.jpg]: img Image.open(img_path).convert(RGB) tensor val_tf(img).unsqueeze(0).to(device) with torch.no_grad(): prob torch.softmax(model(tensor), dim1)[0] top torch.topk(prob, ktopk) names [val_ds.classes[i] for i in top.indices.tolist()] scores [f{p:.3f} for p in top.values.tolist()] print(f{img_path}: {list(zip(names, scores))})这个脚本的价值在于它不依赖你划分好的 val 集而是面向真实环境验证模型的泛化能力。我现在做任何细粒度识别任务都会留出 20 到 30 张从源数据分布之外的渠道收集的照片训练完只测一次测完就不再回头调参防止模型往这批测试样本上过拟合。蝴蝶分类数据集的训练之路走完之后你会发现真正难的不是把准确率从 80% 提到 90%而是面对分布外样本时保持住一个不撒谎的准确率预期。希望这些拆包、训练和验证方法能帮你少走弯路。本文还有配套的精品资源点击获取