蝴蝶分类数据集实战:从zip解压到PyTorch迁移学习全流程 📅 发布时间:2026/9/15 1:56:17 👁 浏览次数: 简介蝴蝶分类数据集20类.zip是一份面向机器学习、图像识别与生物多样性研究的中小型图像数据集共包含20类蝴蝶物种的标注图片与配套元数据适合用来训练CNN等视觉分类模型也可为昆虫学相关教学与研究提供基础样本。压缩包共1870个文件其中以1866张JPG图片为主体按类别组织辅以species.txt、genus.txt提供物种与属名标签Butterfly20_dict.json则记录图片路径、物种及描述性信息整体约60.96MB结构简洁便于直接加载与预处理。目前已有118人学习/下载。对研究者而言使用该数据集时可结合图像增强与标准化等手段提升模型泛化能力同时可基于属名与物种信息辅助分析蝴蝶亲缘关系、地理分布与保护问题对入门学习者它也是一个规模适中、标注清晰的分类练手项目。1. 蝴蝶分类数据集 20 类的第一步不是训练而是解压探测刚拿到蝴蝶分类数据集20类.zip 压缩包时很多人会直接解压然后丢给训练脚本结果读到一半报错或者类别标签和实际目录对不上。这类 zip 数据集通常由拍摄者手工整理内部可能有中文名、嵌套目录甚至夹杂.DS_Store和损坏图片。直接用 ImageFolder 加载之前暴力解压很容易留下隐患。常见的踩坑点集中在三处压缩包内有顶层嵌套目录导致 root 路径写错、中文目录名编码乱码、以及类别目录数量与“20类”对不上。要解决这些问题得分清 zip 内部路径确实可以做、应该做和必须做的边界。对刚接触分类任务的人来说这个 20 类数据集足够跑通从解压到验证的全链路也适合先验证数据管线再决定投入多少训练资源。2. 解压前先看清目录结构zip 包里的路径决定了标签怎么读解开 zip 之前先看内部文件列表这个动作能省掉一次错误训练。常见做法是先用unzip -l列出文件而不是直接unzip这样能确认顶层是否嵌套了一层外目录类别目录是挂在哪一级以及有没有混入非图片文件。有的压缩包还会把训练集和验证集分别放在train/、val/两个顶层目录下这会影响后续 ImageFolder 的 root 参数设置。看清目录结构后再决定是直接解压使用还是写一个整理脚本把图片统一迁移到单根目录。2.1 用 unzip -l 确认压缩包内是否有顶层嵌套目录先执行目录探测命令不实际解压快速梳理 zip 内部结构unzip -l 蝴蝶分类数据集20类.zip | head -n 40unzip -l输出每一行都是一个内部条目第一列是压缩后大小最后一列是文件路径。head -n 40限制输出行数避免被上千条图片条目刷屏。重点观察路径前缀比如出现butterfly_20/Amphrysus/xxx.jpg说明解压后第一层是butterfly_20第二层才是类别目录。若路径以__MACOSX/开头这个压缩包在 macOS 上生成里面是系统元数据训练前要过滤掉。若出现Thumbs.db或.DS_Store同样需要在数据集整理时剔除。下一步执行实际解压unzip -q 蝴蝶分类数据集20类.zip -d butterfly_20-q安静模式避免每个文件都打印一行-d butterfly_20指定解压目标目录。如果解压后直接在butterfly_20/下看到 20 个类别目录最理想ImageFolder 的 root 直接指向这里。如果多出一层butterfly_20/butterfly_20/root 要指向内层而不是外层。2.2 用 Python zipfile 解压并处理中文文件名Windows 上生成的 zip中文文件名常按 GBK 编码存储而 Python zipfile 默认按 UTF-8 解码解出来的目录名会直接乱码。若蝴蝶数据集的类别名包含中文推荐用 Python 解压顺便把文件名编码修正一遍import zipfile from pathlib import Path def safe_extract(zip_path, dest_dir): with zipfile.ZipFile(zip_path, r) as zf: for info in zf.infolist(): raw info.filename try: filename raw.encode(cp437).decode(gbk) except UnicodeDecodeError: filename raw # 已经是 UTF-8 名称 if filename.startswith(__MACOSX) or filename.startswith(.): continue target Path(dest_dir) / filename if info.is_dir(): target.mkdir(parentsTrue, exist_okTrue) continue target.parent.mkdir(parentsTrue, exist_okTrue) with zf.open(info) as src, open(target, wb) as dst: dst.write(src.read())逐个文件处理而不是用extractall()是为了在写入磁盘前对路径做清洗和过滤还能顺带统计图片总数。cp437是早期 zip 规范中非 UTF-8 文件名的默认编码Windows 中文环境下通常是 GBK所以先把原始字节按 cp437 还原再按 gbk 解码。UnicodeDecodeError时说明文件名本身就是 UTF-8保持原样即可。这套逻辑在混合编码场景下比extractall()可靠得多。解压完成后检查一下类别目录数量find butterfly_20 -maxdepth 1 -type d | wc -lmaxdepth 1只统计第一层目录减去.所在行后得到实际类别数。如果结果是 21说明 20 个类别之外多出一个目录需要打开看是说明文件还是人为建的多余目录。2.3 把类别名映射成数字索引的常见做法ImageFolder 在初始化时会自动按目录名排序生成class_to_idx映射排序方式决定类别顺序。20 类蝴蝶的类别名通常是拉丁学名比如Amphrysus、Argynnis paphia可能带空格。训练完成后做推理需要把索引映射回原始名称因此最好先导出一份映射表固定下来。下面这段代码扫描目录并打印映射import json from pathlib import Path dest_dir Path(butterfly_20) class_names sorted([p.name for p in dest_dir.iterdir() if p.is_dir() and not p.name.startswith(.)]) class_to_idx {name: idx for idx, name in enumerate(class_names)} with open(class_names.json, w, encodingutf-8) as f: json.dump(class_to_idx, f, ensure_asciiFalse, indent2) print(len(class_names), 个类别) for name, idx in list(class_to_idx.items())[:5]: print(idx, name)排序方式直接决定class_to_idx的赋值0 号类别和其他 19 个类别的顺序会影响将来混淆矩阵的坐标轴显示。保存 JSON 是让训练脚本和推理脚本共用同一份标签文件的可靠手段。下表是映射表落到文件后的示例结构实际以解压后的目录为准class index目录名备注0Amphrysus翅膀有明显放射状斑纹1Argynnis paphia豹纹蝶常见于林缘2Papilio machaon黄黑相间的大型凤蝶3Pieris rapae菜粉蝶白底黑斑需要注意class_names里若混入.DS_Store这类隐藏目录not p.name.startswith(.)会把它过滤掉。若发现打印出来的类别数不是 20回到 2.1 节重新检查 zip 内部路径。3. 加载数据集用 ImageFolder 还是自定义 Datasettorchvision 的 ImageFolder 对这类目录即标签的结构支持很直接它要求根目录下一级就是类别目录返回的样本是(PIL.Image, 类别索引)。但 ImageFolder 不会做任何尺寸统一和归一化这些要交给 transform 去处理。若 zip 解压后出现train/和val/双层结构需要分别对训练集和验证集创建两个 ImageFolder 实例。对 20 类蝴蝶这种中小规模数据集ImageFolder 足够不需要自定义 Dataset只有解压后目录完全打平的情况下才考虑手动构建数据集对象。3.1 先整理成目录即标签的形式如果解压后目录结构与 ImageFolder 不合比如图片分散在多个子目录写个短脚本统一复制到新根目录比反复改 Dataset 代码省事得多。用复制而非移动保留原始文件防止整理过程出现问题后无法复原。import shutil from pathlib import Path src_root Path(butterfly_20) # 原始解压目录 dst_root Path(butterfly_20_sorted) # 整理后的目标目录 valid_suffix {.jpg, .jpeg, .png} for class_dir in src_root.iterdir(): if not class_dir.is_dir(): continue dst_class_dir dst_root / class_dir.name dst_class_dir.mkdir(parentsTrue, exist_okTrue) for img_path in class_dir.rglob(*): if img_path.suffix.lower() not in valid_suffix: continue shutil.copy2(img_path, dst_class_dir / img_path.name)src_root.iterdir()遍历第一层目录把每一类作为独立目标目录。rglob(*)递归取到所有文件再用suffix过滤非图片文件这样即使原始结构里类别目录下还有子目录也能把图片全部平铺到新类目下。copy2保留文件元数据图片的 EXIF 信息不会丢失。整理完成后butterfly_20_sorted就能直接作为 ImageFolder 的 root 使用。如果原始 zip 内部路径本来就是train/和val/分割好的则分别对butterfly_20/train和butterfly_20/val做 ImageFolder不需要合并。两者目录内的类别子目录名称必须完全一致否则验证集的class_to_idx与训练集对不上评估时标签错位的问题很难排查。3.2 预处理管线与归一化参数的选择transform 决定喂给模型的输入长什么样。对 20 类蝴蝶这种自然影像数据集ImageNet 预训练权重的归一化参数可以直接复用因为 torchvision 自带的模型权重就是在这些参数下训出来的from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), 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_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)让模型看到蝴蝶的局部纹理模仿遮挡和姿态变化。ColorJitter 的三个参数控制在明度、对比度、饱和度上的扰动幅度数值过大会让颜色失真。验证集不做随机增广统一Resize(256)再CenterCrop(224)保证评估结果可复现。若数据集原图分辨率普遍低于 300 像素Resize(256)会强制放大导致模糊这时可以改成Resize((224, 224))直接变形或者直接跳过 Resize 只做 CenterCrop以实际验证集精度为准。提示不要擅自改动 Normalize 的均值和标准差除非你的图片颜色分布明显偏离 ImageNet比如红外图或多通道图。对蝴蝶这种可见光图片改参数只会让预训练权重失效。3.3 直接从 zip 内读图不落地解压的加载方案如果服务器磁盘空间紧张不想解压出完整副本可以用自定义 Dataset 直接从 zip 内读取图片。这种做法省磁盘但吃 CPU因为每次__getitem__都要执行一次压缩包内文件读取和解压。对于几千张规模的 20 类数据集这个方案限于磁盘不足时的过渡场景完整训练不建议长期使用。import io import zipfile from PIL import Image from torch.utils.data import Dataset class ZipImageDataset(Dataset): def __init__(self, zip_path, member_prefix, transformNone): self.zf zipfile.ZipFile(zip_path) self.names [n for n in self.zf.namelist() if n.startswith(member_prefix) and n.lower().endswith((.jpg, .png))] self.transform transform def __len__(self): return len(self.names) def __getitem__(self, idx): img_bytes self.zf.read(self.names[idx]) img Image.open(io.BytesIO(img_bytes)).convert(RGB) if self.transform: img self.transform(img) return imgmember_prefix传入 zip 内的类别目录前缀例如butterfly_20/Amphrysus/。zf.read每次都会从压缩包内定位并解压目标文件瓶颈在 IO 和解压速度。若换成不压缩的 store 模式 zip读取速度会明显提升但这要求打包时指定zipfile.ZIP_STORED无法事后修改。这个方案的核心价值是让数据流先跑通待确认磁盘或数据加载方式后再落地到完整解压。3.4 过滤损坏图片的简单方法蝴蝶数据集里常混有拍摄中断或传输损坏的图片PIL 在打开时不一定立即报错但训练到中途会偶发崩溃。最直接的方法是在训练前快速扫描一遍全部图片from PIL import Image from pathlib import Path def validate_images(root_dir): bad_paths [] valid_suffix {.jpg, .jpeg, .png} for img_path in Path(root_dir).rglob(*): if img_path.suffix.lower() not in valid_suffix: continue try: with Image.open(img_path) as im: im.verify() except (IOError, SyntaxError) as e: bad_paths.append(str(img_path)) print(损坏:, img_path, e) return bad_paths bad validate_images(butterfly_20_sorted) print(损坏图片数量:, len(bad))verify()检查文件头和编码器是否一致不完整解码速度比load()快很多。验证通过不代表一定能成功读取所有像素个别文件到ToTensor()时才暴露问题因此训练循环里最好手动加一层异常处理。若bad列表非空把这些文件删掉或者在 Dataset 的__getitem__里遇到异常时跳过该样本两种方式选一种统一处理。4. 迁移学习做 20 类分类训练参数与分类评估观测点20 类蝴蝶属于细粒度分类类别间差异集中在翅膀纹理和颜色分布上直接从头训练小网络效果不理想常见做法是加载 ImageNet 预训练权重后做迁移学习。torchvision 的models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1)会自动下载对应权重免去手动查找 URL 的麻烦。对 20 类这种规模ResNet18 参数量合适训练速度快验证集精度也足够支撑完整评估流程。4.1 加载预训练模型并冻结特征提取层加载权重后先冻结部分层再微调能有效防止小数据集过拟合。常见的做法是冻结前三个残差层只训练 layer4 和全连接层import torch import torchvision.models as models device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) for param in model.layer1.parameters(): param.requires_grad False for param in model.layer2.parameters(): param.requires_grad False for param in model.layer3.parameters(): param.requires_grad False num_ftrs model.fc.in_features model.fc torch.nn.Linear(num_ftrs, 20) model model.to(device)用model.fc.in_features动态读取原全连接层输入维度而不是硬编码 512这样换 ResNet50 时不需要改动这行代码。冻结参数的原则低层特征是通用的边缘、纹理、颜色结构对蝴蝶任务同样有效高层特征才与具体任务强相关因此只放开 layer4 和 fc 让模型适应蝴蝶特有的形态特征。若发现验证集精度不增长可以逐步放开 layer3 参与训练但学习率要降到 1e-5 量级。4.2 训练循环中 data_loader 与 loss 的配合细节20 类分类任务用nn.CrossEntropyLossImageFolder 返回的类别索引可以直接作为标签不需要额外做 one-hot。训练循环的关键是验证阶段切换 eval 模式并关闭梯度计算import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets train_dataset datasets.ImageFolder(butterfly_20_sorted/train, transformtrain_transform) val_dataset datasets.ImageFolder(butterfly_20_sorted/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) best_acc 0.0 for epoch in range(30): model.train() running_loss 0.0 for inputs, labels in train_loader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() model.eval() correct 0 total 0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc 100 * correct / total if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) print(fEpoch {epoch1}: loss{running_loss:.4f}, val_acc{val_acc:.2f}%)每个 epoch 先走训练集再走验证集用model.eval()关闭 Dropout 和 BatchNorm 的统计更新。torch.max(outputs, 1)返回每个样本预测类别的索引predicted labels做逐元素比较后求和得到正确数。保存state_dict()而非完整模型是为了后续加载时只依赖模型定义不携带优化器状态。下表是这套训练流程中常用的超参数范围参数推荐值说明batch_size328GB 显存可用显存不足时降到 16lr1e-4冻结低层时适用全量微调降到 1e-5weight_decay1e-4L2 正则抑制过拟合epochs30配合早停看验证集 loss 是否回升num_workers4本机 IO 瓶颈明显时可增加到 830 个 epoch 是经验值实际训练中若验证集精度连续 5 个 epoch 不增长不用等满 30 轮直接停掉或调低学习率。4.3 用混淆矩阵和 classification_report 做分类评估val_acc 单一指标会掩盖类别间混淆。20 类蝴蝶中两种花纹相近的种类错误可能集中在固定几个类之间用 sklearn 的classification_report能直接看到每个类别的 precision、recall 和 f1-scorefrom sklearn.metrics import classification_report, confusion_matrix import numpy as np all_preds [] all_labels [] model.eval() with torch.no_grad(): for inputs, labels in val_loader: inputs inputs.to(device) outputs model(inputs) _, predicted torch.max(outputs, 1) all_preds.extend(predicted.cpu().numpy()) all_labels.extend(labels.numpy()) class_names [v[0] for v in sorted(train_dataset.class_to_idx.items(), keylambda kv: kv[1])] report classification_report(all_labels, all_preds, target_namesclass_names, digits3) print(report) cm confusion_matrix(all_labels, all_preds) np.save(confusion_matrix.npy, cm)classification_report中 precision 是预测为该类且预测正确的比例recall 是真正属于该类且被找回的比例f1-score 是两者调和平均。如果某个类别 recall 明显低于其他类说明它的图片大量被误判成视觉相近的邻居类。confusion_matrix.npy保存下来后可以用 pandas 转成 DataFrame 输出为 CSV 或画热力图定位具体的混淆对。这个环节在细粒度分类中价值很大单纯的 val_acc 无法指出哪些类别需要补充训练样本或调整类别权重。5. 数据管道冒烟测试与样本均衡处理正式提交 GPU 训练前先跑一个小批量数据子集验证从 zip 到 loss 反向传播的完整链路。做法是每个类别取少量图片构造一个微型训练集跑 1 到 2 个 epoch确认数据集加载、transform、模型 forward 和 backward 全部通畅。indices [] for cls_idx in range(20): cls_indices [i for i, (_, label) in enumerate(train_dataset.samples) if label cls_idx] indices.extend(cls_indices[:10]) subset_train torch.utils.data.Subset(train_dataset, indices) subset_loader DataLoader(subset_train, batch_size8, shuffleTrue, num_workers0)train_dataset.samples是 ImageFolder 内的(path, class_index)列表按类别索引取前 10 张组成 200 张图片的微型数据集。num_workers在冒烟测试阶段设为 0避免多进程加载引入偶发死锁干扰问题定位。如果这 200 张图片能正常跑完一个 epoch 且 loss 下降再切到完整数据。若数据集中某些类别样本量明显偏少并行地做类别均衡采样。20 类蝴蝶的野外采集数据常有长尾分布少数类只有几十张多数类上百张。WeightedRandomSampler按样本权重采样变相提高小样本类别的出现频率from torch.utils.data.sampler import WeightedRandomSampler import torch class_counts torch.bincount(torch.tensor(train_dataset.targets)) weights 1.0 / class_counts.float() sample_weights weights[train_dataset.targets] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_dataset, batch_size32, samplersampler, num_workers4)每个样本的权重设为1 / class_count[label]样本量少的类权重更高。replacementTrue表示同一张图在一个 epoch 内可能被多次采样总数仍为num_samples。使用 sampler 后 DataLoader 的shuffle必须保持默认 False否则和 sampler 冲突报错。训练完成后把 best_model.pth 配合 2.3 节生成的 class_names.json 一起保存推理时只需加载这两个文件不再需要原始 zip 路径。本文还有配套的精品资源点击获取