
简介蝴蝶分类数据集20类.zip 面向机器学习与图像识别方向的开发者、深度学习入门者及生物多样性研究者用于训练和测试蝴蝶物种自动识别模型。压缩包共1870个文件以1866张jpg图像为主体另含2个txt标签文件、1个json字典文件和1个系统隐藏文件整体约60.96MB规模适中便于本地加载与快速实验。其中json字典记录图片路径与物种、属名等元信息txt文件分别列出20个蝴蝶物种名称及其所属属名图像按类别组织可直接用于构建分类标签体系。数据集覆盖20个蝴蝶类别适合作为卷积神经网络等模型的训练与验证素材也可支撑物种分布、亲缘关系等生物学比较分析。目前已有118人学习下载读者可据此完成数据预处理、图像增强、模型训练与泛化能力评估等完整流程是入门图像分类与开展相关研究的实用样本集合。1. 蝴蝶分类数据集20类从压缩包到可训练模型的落地路径拿到一个图像分类数据集最怕的不是数据量小而是结构不透明。蝴蝶分类数据集20类.zip 这个包解压后能看到 Butterfly20_dict.json、species.txt、genus.txt 和 Butterfly20 文件夹结构不算复杂但真正要把它跑通成一个可训练的 ImageFolder 或自定义 Dataset中间有几个环节容易卡住。这个数据集适合三类人想练手 CNN 图像分类的算法工程师、做生物多样性图像识别的研究生、以及需要一个小规模多类数据集验证模型泛化能力的从业者。20 个类别、每类若干张蝴蝶图片配合物种名和属名两级标签既能做细粒度分类也能做层次分类实验。下面按实际拆包顺序把结构解析、标签映射、DataLoader 构建和训练前检查逐层拆开。2. 拆包先看结构Butterfly20_dict.json 与 species.txt 怎么对齐2.1 压缩包内文件的实际角色解压后第一件事不是急着写 Dataset而是把四个核心文件的关系理清楚。Butterfly20 文件夹是图片根目录里面按类别分子文件夹存放 jpg 图片文件名是数字编号比如 077.jpg、050.jpg、126.jpg。species.txt 每行一个物种名共 20 行顺序通常与文件夹编号或 dict 中的 key 对应。genus.txt 每行一个属名行数与 species.txt 一致表示每个物种所属的属。Butterfly20_dict.json 是索引字典常见结构是 key 为图片相对路径或编号value 包含 species、genus 等字段。这里有个容易翻车的地方species.txt 的行顺序不一定等于文件夹编号顺序。我一般会先做一次交叉验证用 dict 里的 species 字段去比对 species.txt 的内容确认映射关系。如果 dict 里没有 species 字段那就只能靠文件夹名和 species.txt 的行号硬对齐这时候必须打印前几条确认。2.2 用 Python 做一次结构体检在写任何训练代码之前先跑一段结构检查脚本把图片数量、类别数、标签对齐情况全部打出来。这一步花两分钟能省掉后面几小时的排查。import os import json from collections import Counter root Butterfly20 dict_path Butterfly20_dict.json # 读取字典 with open(dict_path, r, encodingutf-8) as f: meta json.load(f) print(dict 条目数:, len(meta)) print(dict 前两条:, list(meta.items())[:2]) # 统计图片文件夹结构 class_dirs sorted(os.listdir(root)) print(类别文件夹数:, len(class_dirs)) print(前五个类别目录:, class_dirs[:5]) # 统计每个类别的图片数 counts {} for c in class_dirs: cpath os.path.join(root, c) if os.path.isdir(cpath): imgs [x for x in os.listdir(cpath) if x.lower().endswith(.jpg)] counts[c] len(imgs) print(各类别图片数:, counts) print(总图片数:, sum(counts.values())) print(最少类别样本数:, min(counts.values())) print(最多类别样本数:, max(counts.values()))这段脚本的逻辑很直接先确认 dict 的条目数和结构再统计 Butterfly20 下每个子文件夹的 jpg 数量。关键参数是 root 和 dict_path按实际解压路径改。输出里重点看三个数类别文件夹数是否为 20、总图片数是否与 dict 条目数接近、最少类别样本数是否过小。如果某个类别只有个位数图片后面做分层采样或数据增强时就要特别处理。2.3 species.txt 与 genus.txt 的读取与校验species.txt 和 genus.txt 是纯文本每行一个名称。读取时注意编码常见是 UTF-8但也不排除 GBK。我一般用 errorsignore 先读进来再打印前几行确认没有乱码。def read_lines(path): with open(path, r, encodingutf-8, errorsignore) as f: lines [line.strip() for line in f if line.strip()] return lines species read_lines(species.txt) genus read_lines(genus.txt) print(species 数量:, len(species)) print(genus 数量:, len(genus)) print(species 前五行:, species[:5]) print(genus 前五行:, genus[:5]) # 校验 species 与 genus 行数是否一致 assert len(species) len(genus), species 与 genus 行数不一致如果 species 和 genus 行数不一致说明两个文件不是严格按行对应这时候不能直接 zip 成映射表。常见做法是以 species.txt 为准genus.txt 多出来的行忽略或单独处理。另一个坑是 species.txt 里可能有空行或注释行strip 之后过滤掉空行能解决大部分问题。3. 构建可复现的 Dataset从 ImageFolder 到自定义getitem3.1 为什么不能直接用 ImageFoldertorchvision 的 ImageFolder 要求根目录下每个子文件夹是一个类别文件夹名就是类别名。Butterfly20 的文件夹名是数字编号直接拿来当类别名也能跑但标签就是 0 到 19没有可读性。更重要的是如果 dict 里提供了 species 和 genus 两级标签ImageFolder 只能给一级标签做层次分类时就不够用。所以常见做法是写一个自定义 Dataset继承 torch.utils.data.Dataset在init里把图片路径、物种标签、属标签全部整理成列表getitem返回图像张量和标签字典。这样既能做单标签分类也能扩展成多任务学习。3.2 自定义 Dataset 的完整实现下面这个 Dataset 类把 dict、species.txt、genus.txt 和图片文件夹串起来。核心思路是以 Butterfly20 下的类别文件夹为基准按文件夹编号去 species.txt 和 genus.txt 里取对应名称同时用 dict 做辅助校验。import os import json from PIL import Image from torch.utils.data import Dataset import torchvision.transforms as T class Butterfly20Dataset(Dataset): def __init__(self, root, dict_path, species_path, genus_path, transformNone): self.root root self.transform transform # 读取物种和属名 self.species self._read_lines(species_path) self.genus self._read_lines(genus_path) # 读取字典 with open(dict_path, r, encodingutf-8) as f: self.meta json.load(f) # 整理样本列表 self.samples [] class_dirs sorted(os.listdir(root)) for cls_idx, cls_name in enumerate(class_dirs): cls_path os.path.join(root, cls_name) if not os.path.isdir(cls_path): continue # 用文件夹编号映射到 species 和 genus # 假设文件夹名是数字如 0、1 或 001 try: sp_idx int(cls_name) except ValueError: sp_idx cls_idx sp_name self.species[sp_idx] if sp_idx len(self.species) else unknown ge_name self.genus[sp_idx] if sp_idx len(self.genus) else unknown for img_name in os.listdir(cls_path): if not img_name.lower().endswith(.jpg): continue img_path os.path.join(cls_path, img_name) self.samples.append({ path: img_path, species: sp_name, genus: ge_name, species_idx: sp_idx, genus_idx: sp_idx, # 简化处理实际应按属名去重编号 }) # 构建物种到索引的映射 self.species_to_idx {s: i for i, s in enumerate(sorted(set(self.species)))} self.genus_to_idx {g: i for i, g in enumerate(sorted(set(self.genus)))} def _read_lines(self, path): with open(path, r, encodingutf-8, errorsignore) as f: return [line.strip() for line in f if line.strip()] def __len__(self): return len(self.samples) def __getitem__(self, idx): item self.samples[idx] img Image.open(item[path]).convert(RGB) if self.transform: img self.transform(img) label { species: self.species_to_idx.get(item[species], -1), genus: self.genus_to_idx.get(item[genus], -1), } return img, label这段代码的关键参数和逻辑说明root 是 Butterfly20 文件夹路径dict_path、species_path、genus_path 按实际文件名传。class_dirs 排序后用 int(cls_name) 尝试把文件夹名转成编号如果文件夹名不是纯数字就退化为按排序序号取 species。species_to_idx 和 genus_to_idx 用 sorted(set(...)) 构建保证索引稳定可复现。getitem返回的 label 是字典包含 species 和 genus 两个键后面训练循环里按需取用。3.3 数据增强与归一化参数怎么选蝴蝶图片的拍摄角度、背景、光照差异较大数据增强是必须的。常见做法是训练集用 RandomResizedCrop、RandomHorizontalFlip、ColorJitter验证集只做 Resize 和 CenterCrop。归一化参数用 ImageNet 的 mean[0.485, 0.456, 0.406]、std[0.229, 0.224, 0.225]这是迁移学习场景下的默认选择。train_transform T.Compose([ T.RandomResizedCrop(224, scale(0.7, 1.0)), T.RandomHorizontalFlip(), T.ColorJitter(brightness0.2, contrast0.2, saturation0.2), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_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]), ])RandomResizedCrop 的 scale 参数控制裁剪区域占原图的比例0.7 到 1.0 是比较温和的设置如果图片本身分辨率不高可以降到 0.5。ColorJitter 的四个参数分别控制亮度、对比度、饱和度和色调蝴蝶分类里色调变化不宜过大否则可能把物种特有的颜色特征抹掉所以 hue 一般不设或设很小。4. 训练前的数据划分与标签一致性检查4.1 分层划分训练集与验证集20 类蝴蝶数据如果每类样本数不均衡随机划分可能导致某些类别在验证集中没有样本。常见做法是用 sklearn 的 train_test_split 做分层采样stratify 参数传物种标签。from sklearn.model_selection import train_test_split import numpy as np # 假设 dataset 是上面构建的 Butterfly20Dataset 实例 labels [s[species_idx] for s in dataset.samples] indices np.arange(len(labels)) train_idx, val_idx train_test_split( indices, test_size0.2, stratifylabels, random_state42 ) print(训练集样本数:, len(train_idx)) print(验证集样本数:, len(val_idx))stratifylabels 保证每个类别的训练/验证比例一致random_state42 保证可复现。如果某个类别样本数少于 5分层采样会报错这时候要么合并稀有类别要么改用 K 折交叉验证。4.2 标签映射的常见错位与排查标签错位是这类数据集最隐蔽的坑。现象是训练 loss 正常下降但验证准确率始终在随机水平附近。原因通常是 species.txt 的行顺序与文件夹编号不对应或者 dict 里的 species 字段与 species.txt 不一致。解决办法是打印每个类别的文件夹名、species.txt 对应行、dict 中该类别第一条记录的 species 字段三者对照。# 标签一致性抽查 for cls_name in sorted(os.listdir(root))[:5]: cls_path os.path.join(root, cls_name) if not os.path.isdir(cls_path): continue sp_idx int(cls_name) sp_from_txt species[sp_idx] if sp_idx len(species) else N/A # 从 dict 中找一条该类别记录 sample_key None for k, v in meta.items(): if cls_name in k or cls_name in str(v): sample_key k break sp_from_dict meta[sample_key].get(species, N/A) if sample_key else N/A print(f文件夹: {cls_name} | species.txt: {sp_from_txt} | dict: {sp_from_dict})如果三者不一致以 dict 为准还是以 species.txt 为准取决于哪个字段更完整。我一般优先信 dict因为 JSON 结构更明确species.txt 可能是后期手动整理的。5. 避坑与排查蝴蝶分类数据集落地时的五个血泪经验5.1 现象DataLoader 报 Found 0 files原因路径拼接漏了子文件夹层级解决打印 os.listdir 逐层确认这个错误通常发生在用 ImageFolder 或自定义 Dataset 时root 指向了 Butterfly20 的上一级而不是 Butterfly20 本身。现象是程序不报错但 len(dataset) 为 0或者直接抛 Found 0 files。排查方法是手动执行 os.listdir(root)看返回的是不是类别文件夹列表。如果返回的是 [Butterfly20, species.txt, ...]说明 root 指错了应该再进一层。5.2 现象训练准确率很高但验证准确率极低原因同一张图片同时出现在训练集和验证集解决按图片路径去重后再划分蝴蝶数据集里可能存在重复图片或高度相似的连拍图片。如果按随机索引划分同一张图可能既在训练集又在验证集导致验证指标虚高。更隐蔽的情况是不同文件名但内容相同。常见做法是用图片的 MD5 或感知哈希去重再按去重后的列表做分层划分。import hashlib def file_md5(path): with open(path, rb) as f: return hashlib.md5(f.read()).hexdigest() seen {} duplicates [] for s in dataset.samples: h file_md5(s[path]) if h in seen: duplicates.append((s[path], seen[h])) else: seen[h] s[path] print(重复图片对数:, len(duplicates))5.3 现象species.txt 读取后行数对不上原因文件末尾有空行或 BOM 头解决用 errorsignore 并过滤空行BOM 头是 UTF-8 文件的常见问题表现为第一行开头多出 \ufeff。用 encodingutf-8-sig 可以自动去掉 BOM。空行则用 strip 后过滤。如果行数仍然不对检查是否有注释行以 # 开头按需跳过。5.4 现象训练 loss 震荡不收敛原因学习率过大或 batch size 过小解决先用小学习率 warmup 再余弦退火20 类蝴蝶数据如果每类样本在几十到几百张batch size 设 32 或 64 比较合适。学习率从 1e-4 开始配合 CosineAnnealingLR 或 StepLR。如果 loss 在前几个 epoch 就飙到 NaN先把学习率降到 1e-5 试一轮。5.5 现象验证集准确率卡在 5% 左右原因标签索引与模型输出维度不匹配解决打印 num_classes 和模型最后一层输出维度这个坑的典型表现是模型输出 20 维但标签索引范围是 0 到 19 之外的数或者标签被映射成了字符串。排查方法是取一个 batch打印 labels 的最大值和最小值确认在 [0, num_classes-1] 范围内。如果 species_to_idx 构建时用了 sorted(set(...))而 species.txt 里有重复名称set 会去重导致索引数少于 20这时候要改用有序去重或直接按行号做索引。6. 进阶技巧用 genus 标签做层次分类与模型验证6.1 从单标签到多任务species 与 genus 联合损失这个数据集提供了 species 和 genus 两级标签天然适合做层次分类。常见做法是模型输出两个头一个预测 species20 类一个预测 genus属的数量通常少于 20。损失函数用加权和loss loss_species alpha * loss_genusalpha 一般设 0.3 到 0.5。这样训练出来的模型即使 species 预测错了genus 层面也可能正确对生物多样性研究更有意义。import torch import torch.nn as nn class MultiTaskButterflyNet(nn.Module): def __init__(self, backbone, num_species, num_genus): super().__init__() self.backbone backbone self.species_head nn.Linear(backbone.fc.in_features, num_species) self.genus_head nn.Linear(backbone.fc.in_features, num_genus) self.backbone.fc nn.Identity() def forward(self, x): feat self.backbone(x) return { species: self.species_head(feat), genus: self.genus_head(feat), } # 损失计算 criterion_species nn.CrossEntropyLoss() criterion_genus nn.CrossEntropyLoss() def compute_loss(outputs, labels, alpha0.4): loss_sp criterion_species(outputs[species], labels[species]) loss_ge criterion_genus(outputs[genus], labels[genus]) return loss_sp alpha * loss_gebackbone 可以用 torchvision 的 resnet18 或 efficientnet_b0把最后一层替换成 Identity 后接两个线性头。alpha 控制属级损失的权重设太大可能让 species 精度下降设太小则 genus 头学不到东西。我一般从 0.3 开始试看验证集上两个头的准确率再调。6.2 验证方法混淆矩阵与 per-class 准确率只看总体准确率不够20 类里如果有几个类别长得像混淆矩阵能直接暴露问题。用 sklearn 的 confusion_matrix 和 classification_report把 species 的验证结果打出来。from sklearn.metrics import classification_report, confusion_matrix import numpy as np all_preds [] all_labels [] model.eval() with torch.no_grad(): for imgs, labels in val_loader: outputs model(imgs) preds outputs[species].argmax(dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels[species].numpy()) print(classification_report(all_labels, all_preds, digits4)) print(confusion_matrix(all_labels, all_preds))classification_report 里的 f1-score 低于 0.6 的类别要重点关注通常是样本少或与其他类别视觉相似。confusion_matrix 里非对角线上的大数说明这两类容易混。针对混淆严重的类别可以单独做数据增强或加类权重。6.3 一个具体技巧用 dict 里的额外字段做弱监督Butterfly20_dict.json 里除了 species 和 genus可能还有图片路径、编号等字段。如果 dict 里包含图片的拍摄地点或时间可以拿来做弱监督或域适应实验。即使没有dict 的 key 也可以用来校验图片文件名是否完整。我一般会在训练前跑一遍 dict 与文件系统的交叉检查确认 dict 里每条记录都能找到对应图片文件系统里每张图片也都在 dict 里有记录。这个检查脚本很简单但能避免训练到一半才发现图片缺失的尴尬。从那以后我每次拿到新的图像分类数据集都强制先跑一遍结构体检、标签对齐和重复图片检查再开始写训练代码。这三步花不到十分钟但能挡掉后面大部分的玄学问题。希望帮到你。本文还有配套的精品资源点击获取