ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

蝴蝶分类数据集实战:从解压清洗到模型训练避坑指南

蝴蝶分类数据集实战:从解压清洗到模型训练避坑指南 简介蝴蝶分类数据集20类.zip 是一份面向机器学习、图像识别与生物多样性研究的图像分类数据集主要服务于需要训练蝴蝶种类识别模型的算法工程师、科研人员及计算机视觉方向的学生。压缩包内共1870个文件以1866张蝴蝶JPG图像为主体另含物种清单、属名对照两个文本文件及一个JSON字典文件整体大小约61MBZip格式便于直接解压使用。数据集按20类蝴蝶物种组织样本图像覆盖不同个体的多角度状态标注信息分层提供JSON文件记录每张图像的路径与描述species.txt给出20个物种名称genus.txt提供对应属名便于使用者快速构建分类标签并分析类间亲缘关系。这种结构既适合卷积神经网络等深度学习模型的训练与验证也可用于物种分布、进化关系等生态学对比研究。目前已有119人学习下载图像样本清晰、层级标注完整可直接用于算法实验或作为生物信息教学示例。1. 拿到蝴蝶分类数据集20类.zip先别急着解压训练下载好的压缩包叫蝴蝶分类数据集20类.zip。很多人的第一反应是解压、打开目录、直接开训结果经常是第一个 epoch 就崩在读图或者验证集虚高、一到真实场景就翻车。这个包装的就是20类蝴蝶图像任务上属于细粒度图像分类典型用途是练迁移学习、调数据增强、做类别不平衡实验也可以转成检测格式给目标检测用。适合刚入图像分类的开发者也适合需要一份干净数据做模型选型的团队。这篇按我自己的习惯顺序讲先验包再清洗然后划分、训练最后说验证和避坑。每一步给出直接能跑的代码和参数新手按着走熟手可以拿避坑部分对照一下自己的流程。2. 解压后的第一件事目录结构、类别统计与坏图清洗拿到 zip 后先不要急着开训。分类数据集的质量决定后面所有实验的有效性这个阶段花十分钟比训练时 debug 十小时划算。下面几步分别是验包、盘点、清洗按顺序做一遍后面会少很多玄学问题。2.1 先验包再解压zip 完整性检查与嵌套压缩包解压前先用系统自带的命令验一遍完整性。很多人跳过这步结果解到一半报unexpected end of file才回去重新下载浪费一次时间。unzip -l butterfly_dataset_20classes.zip | head -40 unzip -t butterfly_dataset_20classes.zip-l参数只列出包内文件清单不实际解压用head -40看前40行就能大致判断目录结构是“顶层目录 20个类别子目录”还是“一堆散图 csv”。也能提前发现里面有没有再套一层 zip——有些数据集在传播时被二次压缩过直接解压会让后续路径多一层目录读取时莫名其妙找不到文件。-t参数逐个条目校验 CRC32输出No errors detected才算完整如果报错说明文件在下载或复制阶段丢了字节重新下载比重修更省时间。Windows 下没有系统自带 unzip用 PowerShell 的Expand-Archive之前先对比来源文件的哈希值不然解到一半报错很难定位。完整性问题之外还要检查包内文件名是否“干净”。用 python 的 zipfile 模块看几个关键点import zipfile from pathlib import Path zpath Path(butterfly_dataset_20classes.zip) with zipfile.ZipFile(zpath) as zf: names zf.namelist() print(总条目数:, len(names)) nested [n for n in names if n.lower().endswith(.zip)] bad_names [n for n in names if not n.isascii()] if nested: print(发现嵌套zip:, nested) if bad_names: print(含非ASCII文件名样本:, bad_names[:5])namelist()返回包内相对路径列表nested用来捕捉里面套了压缩包的情况存在嵌套就决定要不要做二层解压。bad_names检查的是非 ASCII 字符——不少生态类数据集的目录名是中文后续在 Linux 服务器、Docker 容器或跨平台脚本里会有各种莫名其妙的问题这里先做个预警。zipfile 能解开 CRC 正常但文件名编码有问题的包这种包后面清洗时最容易踩坑所以第一次解压后就做路径标准化能省下后续大量调试时间。2.2 目录结构自动盘点类别数、样本数与尺寸分布解压完成后第一件事是统计目录结构。常见的数据集布局有两种一种是友好的data_root/类别名/*.jpg直接按目录读另一种是图片散放、类别写在 csv 里。下面以第一种为例这也是最推荐整理成的形态。from pathlib import Path from collections import Counter data_root Path(data/butterfly) class_dirs sorted([p for p in data_root.iterdir() if p.is_dir()]) print(识别到类别数:, len(class_dirs)) counts {} for class_dir in class_dirs: imgs list(class_dir.glob(*.jpg)) list(class_dir.glob(*.png)) counts[class_dir.name] len(imgs) print(每类数量:, dict(sorted(counts.items(), keylambda x: x[1])))每个子目录当成一个类别目录名就是标签。标题写的是20类统计出来就应当是20个目录如果多一个或者少一个先检查是不是有隐藏目录比如 macOS 的__MACOSX或者解压时漏了文件不要硬着头皮往下训。glob 这里只匹配了 jpg 和 png实际包里有其他扩展名时先用 2.1 里的namelist结果确认一下再补。光看类别数不够还要看每类数量。20类数据集的样本量分布往往很不均匀有的类上百张有的类可能只有十几张。少于20张的类别在后续训练里基本是炮灰要么靠增强要么合并这个统计结果会直接影响第4章的类别权重设置。另外建议顺带统计图片尺寸分布from PIL import Image import statistics widths, heights [], [] for img_path in list(data_root.glob(*/*.jpg))[:300]: with Image.open(img_path) as im: w, h im.size widths.append(w) heights.append(h) print(width median:, statistics.median(widths)) print(height median:, statistics.median(heights))这里取前300张估算就够了不一定要全量扫一遍中位数比平均值抗离群。如果图片宽高比差异很大比如有横构图有竖构图后边直接用正方形 Resize 会把长翅膀的蝴蝶压变形这时候就要考虑Resize(256)再加裁剪的方案而不是硬拉成正方形。把统计结果存成一份data_stats.json训练效果不好时回头翻这个文件往往能快速定位是不是数据形态带来的问题。提示类别名如果是中文或带空格后续所有路径拼接都很容易出问题。建议清洗阶段顺手把目录名规范化成class_00到class_19再单独存一份class_names.json保留原始中文名不要直接改原包。2.3 坏图与伪装扩展名清洗下载来的图片经常是“看起来是jpg实际是webp”或者某几张图片已经损坏但文件大小不为0。这类脏数据数量不多却能让训练中途崩掉。清洗脚本一次性处理干净from PIL import Image from pathlib import Path import shutil quarantine Path(quarantine) quarantine.mkdir(exist_okTrue) image_exts {.jpg, .jpeg, .png, .bmp, .webp} good bad 0 for img_path in data_root.rglob(*): if not img_path.is_file() or img_path.suffix.lower() not in image_exts: continue try: with Image.open(img_path) as im: im.verify() with Image.open(img_path) as im: im.convert(RGB) if img_path.suffix.lower() not in {.jpg, .jpeg}: new_path img_path.with_suffix(.jpg) with Image.open(img_path) as im: im.convert(RGB).save(new_path, quality95) img_path.unlink() good 1 except Exception: bad 1 dest quarantine / img_path.parent.name dest.mkdir(parentsTrue, exist_okTrue) shutil.move(str(img_path), str(dest / img_path.name)) print(fgood{good}, bad{bad})脚本核心是“先验后转坏了移走不删除”。im.verify()只读文件头和数据块速度很快但 verify 之后必须重新open一次因为 verify 会把文件对象置为不可读状态。convert(RGB)统一三通道避免灰度图和带透明通道的 PNG 在进 DataLoader 时通道数不匹配。损坏的图不删除移动到quarantine目录隔离后续确认误杀还能找回。伪装扩展名的处理也在这里webp 或 png 后缀的图被重新编码为 jpgquality95足够不必追求100。清洗后如果发现原本40张的类别只剩10张说明原始包质量比预期差先去补数据再训练而不是靠模型硬扛。到这一步处理后的目录结构就稳定了data/butterfly/class_*/*.jpg配上class_names.json后面给分类训练直接喂或者参照 YOLOv8 训练自己的数据集的流程转成检测格式都能少绕弯。3. 20类蝴蝶分类用 PyTorch 搭一个能跑的基线清洗完就可以进入正题。分类任务的第一步不是选模型而是把数据划分、读取、增强这些训练链路细节定下来。这些细节决定实验能不能复现比换一个更大的模型影响更明显。3.1 数据划分按类别分层抽样留一份“后悔药”很多公开数据集的图片是按拍摄时间或目录顺序排的直接按比例切分会把同一只蝴蝶的连拍照片同时分进训练集和验证集验证准确率虚高。正确做法是按类别做分层抽样from sklearn.model_selection import train_test_split images, labels [], [] for idx, name in enumerate(class_names): for img_path in sorted((data_root / name).glob(*.jpg)): images.append(str(img_path)) labels.append(idx) X_train, X_tmp, y_train, y_tmp train_test_split( images, labels, test_size0.3, stratifylabels, random_state42, ) X_val, X_test, y_val, y_test train_test_split( X_tmp, y_tmp, test_size0.5, stratifyy_tmp, random_state42, )代码里stratifylabels让切分后的集合中20类的比例与全集一致避免某个类别全跑进测试集。test_size0.3先切出30%再对这部分按1:1切验证和测试等价于70/15/15的比例。random_state42固定随机种子保证每次重跑结果一致。测试集只在最终验证时碰一次反复调参只能用验证集这是给自己留的“后悔药”。这里有个更严谨的细节分层抽样只能保证类别比例一致不能保证“拍摄来源隔离”。如果包内图片来自同一物种的连拍序列连拍帧之间高度相似分到训练和验证后验证集依然虚高。理想做法是按“拍摄批次/个体”维度分组切分但多数数据集没有这个元数据退而求其次的做法是按文件名哈希分桶再切至少把连续编号的照片打散。3.2 Dataset 与数据增强先把输入管干净图像分类的 Dataset 本质就三件事读图、转 RGB、做变换。用 PyTorch 实现from torch.utils.data import Dataset from PIL import Image class ButterflyDataset(Dataset): def __init__(self, paths, labels, transformNone): self.paths paths self.labels labels self.transform transform def __len__(self): return len(self.paths) def __getitem__(self, idx): with Image.open(self.paths[idx]).convert(RGB) as im: img self.transform(im) return img, self.labels[idx]用with Image.open而不是cv2.imread是因为彩色图片里有少量16位PNG或CMYK的JPEGOpenCV 对某些编码会读成 NonePIL 则很少出问题。convert(RGB)把灰度图和 RGBA 图统一成三通道后面模型输入通道数才不会报错。增强策略是基线模型最值得花时间的地方。蝴蝶分类属于细粒度识别增强参数的坑和非细粒度任务不一样from torchvision import transforms train_transforms transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomAffine(degrees15, translate(0.1, 0.1)), transforms.RandomHorizontalFlip(p0.5), 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_transforms 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]), ])几个参数要说明Resize(256)后接RandomResizedCrop(224)先等比缩放到短边256再随机裁比直接Resize((224, 224))少一些形变损失。scale(0.7, 1.0)控制裁剪面积占原图的70%到100%这个范围对蝴蝶这种主体较大的图合适如果后续发现模型只认蝴蝶头胸、对翅膀不敏感可以把下限降到0.5逼它看更多局部。degrees15而不是90因为蝴蝶左右翅纹方向是有生物学意义的90度旋转会制造很多现实中不存在的样本水平翻转会让翅纹左右颠倒但对大多数蝶种不是致命问题p0.5可以接受。ColorJitter三项都开到0.2不要更高——蝴蝶颜色是强分类特征过度颜色增强会把类别差异磨掉。Normalize先沿用 ImageNet 的均值和标准差这套参数即使模型不是在 ImageNet 上预训练的也能稳定训练。20类小样本自己统计的 mean/std 很容易过拟合收益不大。3.3 模型选型与训练循环ResNet18 起步够用为先20类小规模数据用 ResNet18 起步是最稳的。显存占用小迁移学习收敛快几十分钟能出一版基线如果后来发现欠拟合再往 ResNet50 或 EfficientNet 上换。不要一上来就上 ViT蝴蝶数据集规模通常撑不起大模型的训练。import torch import torch.nn as nn from torchvision import models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 20) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) criterion nn.CrossEntropyLoss() scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) scaler torch.cuda.amp.GradScaler()weightsResNet18_Weights.IMAGENET1K_V1是新版 torchvision 的推荐写法老代码里的pretrainedTrue会被警告。加载的是在 ImageNet 上预训练过的权重迁移到蝴蝶上用远少于从头训练的数据就能收敛这也是这种小数据集任务能跑起来的根本原因。fc层输出改成20。优化器选 AdamWlr1e-4是迁移学习微调常见起点从头训练才用更高学习率。weight_decay1e-4是温和的正则太大容易欠拟合。余弦退火T_max30表示30个 epoch 内学习率从1e-4平滑降到接近0比 StepLR 更适合细粒度分类的收敛曲线。训练循环直接给一版能跑的best_acc 0.0 for epoch in range(30): model.train() total_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() with torch.cuda.amp.autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() total_loss loss.item() * images.size(0) scheduler.step() model.eval() correct total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) preds outputs.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) val_acc correct / total if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_butterfly.pth) print(fepoch{epoch1}, loss{total_loss/len(train_loader.dataset):.4f}, val_acc{val_acc:.4f})保存的是state_dict而不是整个模型后面加载时只要拿到相同结构就能恢复文件也更小。每次验证集准确率上升才覆盖一次权重避免最后几个 epoch 过拟合时把好权重冲掉。scaler.scale(loss).backward()、scaler.step(optimizer)、scaler.update()三个步骤缺一不可混精度省显存但不能忽略更新。如果显存不够把 batch_size 从32降到16学习率同步降到5e-5。纯 CPU 环境下把混精度的三行去掉直接loss.backward()和optimizer.step()就行。注意Windows 上跑 DataLoader 时 num_workers 设成 0 或 2 就够设大了容易在数据加载阶段卡死Linux 上可以设 4 到 8。4. 蝴蝶分类数据集常用避坑5条血泪经验训练跑通只是开始数据集本身和训练链路里有几个坑几乎每次做细粒度分类都会撞上。下面每条都是实际踩过的情况按“现象 → 原因 → 解决”梳理。4.1 cv2.imread 返回 None训练到一半崩了现象训练循环跑到某个 batch 突然抛 TypeError说 NoneType 不是可下标对象定位到cv2.imread返回了 None。原因zip 解压时 CRC 校验通过不代表每个文件内部没有截断或编码异常另外部分图片是16位PNG、CMYK JPEG 或非法位深OpenCV 读不了就静默返回 NonePIL 却能打开。解决所有读图统一走 PIL 的Image.open加convert(RGB)训练前先跑第2章的清洗脚本把坏图隔离。顺序很重要先清洗再训练不要边跑边祈祷。如果已经跑到中间崩了别重头再开先用清洗脚本扫一遍现有目录把坏图移走再从断点继续训练即可。4.2 路径带中文或空格DataLoader 状态错乱现象Windows 上正常代码传到 Linux 服务器或 Docker 容器里训练DataLoader 频繁报 FileNotFoundError或者某些路径拼接后多了一个转义符。原因原数据集的类别名可能带中文或全角字符Windows 的本地编码能容忍Linux 的 UTF-8 严格模式就出问题路径带空格时如果在预处理里用了字符串拼接而不是 pathlib很容易产生残缺路径。解决拿到包先做路径标准化目录名全部改成class_00到class_19原始中文名存进class_names.json。整套代码里只用 pathlib 的 Path 操作不手写os.path.join和字符串加花括号。第2章的提示里就说过这个真正踩过坑的人会无条件照做。4.3 验证集虚高换一批照片直接翻车现象val_acc 到95%以上模型看着挺强换一批没见过的同一物种实拍照片测试准确率掉到60%左右。原因数据划分时把同一只蝴蝶的连拍帧、同一场景的不同角度照片同时分进了训练集和验证集验证集和训练集过于接近模型相当于在开卷考试。解决划分的粒度要从“图片”提升到“拍摄批次/个体”。如果数据集的目录里能看出拍摄时间或地点按这个维度聚合后切分看不出来时按文件名哈希分桶并固定 random_state。最实在的办法是划分后人工抽查几个验证集样本和训练集中对应类别的相似度觉得太像就重新切。4.4 模型学的是背景而不是蝴蝶现象训练 loss 降得很快但把验证图片里的蝴蝶单独抠出来贴到白背景上测试准确率暴跌或者可视化注意力热力图模型盯的是叶片和花朵对蝶翅区域几乎没有高响应。原因自然拍摄的蝴蝶数据集里蝴蝶和寄主植物强相关某些蝶只出现在特定植物上模型学到了“有这个背景就是这一类”而没真正学到蝶翅特征。解决数据增强里的RandomResizedCrop调低scale下限强迫模型看局部ColorJitter 保持0.2不要抹掉真实颜色信号。更关键的是做一次去背景验证——用简单的阈值分割或现成分割模型把主体抠出来贴到统一背景上对比分类准确率。如果准确率掉得厉害说明结果是背景识别的假象业务上不能直接上线。4.5 样本不均少数类被多数类压着打现象整体准确率还行看每类准确率时发现有三类只有40%左右对应的样本数都不到30张。原因分层抽样只保证比例一致不改变“总量少”的事实。多数类的梯度信号把少数类稀释少数类的特征在交叉熵里贡献太小。解决训练时给 CrossEntropyLoss 传一个按类别样本量反比的权重counts torch.tensor([per_class_count[i] for i in range(20)], dtypetorch.float) weights 1.0 / counts weights weights / weights.sum() * 20 criterion nn.CrossEntropyLoss(weightweights.to(device))先取倒数再归一化到均值1避免权重过大导致 loss 抖动。但类别权重只是缓解不是解药如果某类样本数实在过少先考虑数据增强做样本扩充或者把相近类合并最后再上 loss 权重。5. 进阶验证别只盯准确率用混淆矩阵和伪装测试看清模型学到什么到这一步模型已经有85%以上验证准确率了。下一步不是急着换更大的模型而是先搞清楚它错在哪。混淆矩阵是最直接的切入方式。在验证集循环里把每批的preds和labels收集到两个列表拼成数组后喂给 sklearnfrom sklearn.metrics import confusion_matrix, classification_report cm confusion_matrix(all_labels, all_preds) print(classification_report(all_labels, all_preds, target_namesclass_names, digits3)) for i in range(20): for j in range(20): if i ! j and cm[i][j] 3: print(f{class_names[i]} - {class_names[j]}: {cm[i][j]} 张被误判)对角线是每类准确率非对角元素高说明那两个类视觉上太接近。蝴蝶这种细粒度数据最常见的误判发生在同科不同种之间比如蛱蝶科里翅纹相似的近缘种。分类报告里的macro avg比整体准确率更值得看它不受多数类影响直接反映模型对20类是否平均。看清误判后再做一次“伪装测试”把验证集里的蝴蝶主体抠出来贴到和原图完全不同的背景上比如白底或城市街景看准确率掉多少。这一步专门对付 4.4 说的背景依赖问题。如果掉得厉害就别急着上生产环境先补多样化背景的数据。我现在拿到任何 zip 数据集第一件事永远是验包和画类别分布图不再急着解压就开训这个习惯帮我少翻了好几次车。一个 state_dict 加一份划分记录 json 一起保存也是给未来的自己留的后悔药。希望这篇能帮到你。本文还有配套的精品资源点击获取
返回列表