news 2026/10/1 3:04:40

蝴蝶分类数据集20类实战:从压缩包到可训练模型的完整落地路径

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
蝴蝶分类数据集20类实战:从压缩包到可训练模型的完整落地路径

简介:蝴蝶分类数据集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", encoding="utf-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。我一般用 errors="ignore" 先读进来,再打印前几行确认没有乱码。

def read_lines(path): with open(path, "r", encoding="utf-8", errors="ignore") 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 到自定义getitem

3.1 为什么不能直接用 ImageFolder

torchvision 的 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, transform=None): 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", encoding="utf-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", encoding="utf-8", errors="ignore") 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(brightness=0.2, contrast=0.2, saturation=0.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_size=0.2, stratify=labels, random_state=42 ) print("训练集样本数:", len(train_idx)) print("验证集样本数:", len(val_idx))

stratify=labels 保证每个类别的训练/验证比例一致,random_state=42 保证可复现。如果某个类别样本数少于 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 头;解决:用 errors="ignore" 并过滤空行

BOM 头是 UTF-8 文件的常见问题,表现为第一行开头多出 \ufeff。用 encoding="utf-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 两级标签,天然适合做层次分类。常见做法是模型输出两个头,一个预测 species(20 类),一个预测 genus(属的数量通常少于 20)。损失函数用加权和:loss = loss_species + alpha * loss_genus,alpha 一般设 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, alpha=0.4): loss_sp = criterion_species(outputs["species"], labels["species"]) loss_ge = criterion_genus(outputs["genus"], labels["genus"]) return loss_sp + alpha * loss_ge

backbone 可以用 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(dim=1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels["species"].numpy()) print(classification_report(all_labels, all_preds, digits=4)) 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 里有记录。这个检查脚本很简单,但能避免训练到一半才发现图片缺失的尴尬。

从那以后我每次拿到新的图像分类数据集,都强制先跑一遍结构体检、标签对齐和重复图片检查,再开始写训练代码。这三步花不到十分钟,但能挡掉后面大部分的玄学问题。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 3:03:45

基于Mask R-CNN的缺陷检测Python工程:从框到轮廓的分割实践

简介&#xff1a;针对图像缺陷检测任务打造的完整Python实现包&#xff0c;面向本科与硕士阶段进行计算机视觉、工业质检相关教研学习的学生。内容以Defect Eye缺陷检测为主线&#xff0c;覆盖数据示例、模型推理、评估验证等环节。压缩包共227个文件&#xff0c;以py源码为核心…

作者头像 李华
网站建设 2026/10/1 3:03:31

Synapse数据集实战:医学图像分割从CT预处理到多器官分割

简介&#xff1a;医学图像分割是计算机辅助诊断与手术规划的核心技术之一&#xff0c;而高质量标注数据集是训练可靠模型的基础。在实际工程中&#xff0c;面对CT影像&#xff0c;数据预处理与标注格式的标准化往往比网络结构更影响最终精度。窗宽窗位调整、体素重采样、标签映…

作者头像 李华
网站建设 2026/10/1 3:02:55

结核杆菌YOLO小目标检测数据集与部署实战

简介&#xff1a;本资源是面向医学图像分析与AI辅助诊断研究者的结核杆菌目标检测专用数据集&#xff0c;适用于YOLO系列模型训练与验证&#xff0c;助力肺结核早期筛查、自动化病原识别等实际医疗场景落地。数据包共2000个文件&#xff0c;含1265张痰液显微图像&#xff08;JP…

作者头像 李华
网站建设 2026/10/1 3:01:47

东莞GEO优化优质企业有哪些?省心不踩坑的服务商挑选全攻略

东莞GEO优化优质企业有哪些?省心不踩坑的服务商挑选全攻略 开篇&#xff1a;选东莞GEO优化服务商&#xff0c;你可能正在踩这4个大坑找东莞本地的GEO优化服务商时&#xff0c;很多企业都会踩坑&#xff1a;要么选了不懂珠三角本地化运营的外地团队&#xff0c;关键词布局脱离本…

作者头像 李华
网站建设 2026/10/1 3:01:47

江苏GEO优化服务商推荐,能提升自然流量占比且资质齐全

开篇行业痛点&#xff1a;江苏企业主在AI时代面临的四大获客难题在江苏这片经济热土上&#xff0c;无论是制造业的工厂老板&#xff0c;还是服务业的门店经营者&#xff0c;都深刻感受到传统网络营销的获客成本正在逐年攀升。过去依赖百度竞价、SEO优化的模式&#xff0c;在AI搜…

作者头像 李华
网站建设 2026/10/1 3:01:38

从阅读资料到自己写出论文:原创表达训练

从阅读资料到自己写出论文&#xff1a;原创表达训练 “同义词换了一遍&#xff0c;查重和语病却都没有改善。”是不是很多同学写论文时都遇到过这种尴尬&#xff1f;表面上看&#xff0c;好像只要把文字换成同义词就能降低重复率&#xff0c;但机械替词往往会让术语变得混乱、…

作者头像 李华