20类蝴蝶分类数据集实战:基于ResNet迁移学习的完整流程 简介面向机器学习、图像识别与生物多样性研究场景这份《蝴蝶分类数据集20类》压缩包提供了20个蝴蝶类别的图像样本及配套标注信息可用于训练分类模型或开展物种比对分析。包内共1870个文件以1866张jpg图像为主体另含2个txt文本与1个json字典——其中文本文件对应物种与属名清单json则记录图片路径、物种等元数据方便直接加载为训练标签。整体压缩包约60.96MB文件结构简洁适合入门级图像分类项目快速上手。目前已有118人学习/下载。借助这份数据读者既能搭建CNN等深度学习模型完成蝴蝶种类自动识别也能结合属名与物种信息观察类群间的亲缘关系为昆虫生态或保护研究提供基础素材。1. 蝴蝶分类数据集20类先别急着解压这份资源到底能干什么“蝴蝶分类数据集20类.zip”这个名字看着人畜无害实际上下载下来解压只是第一步。它里面是20个蝴蝶物种的图片集合配套一个 JSON 字典、物种名清单和属名清单。对做图像分类的人来说这是一套可以完整跑通“数据准备 → 预处理 → 迁移学习 → 评估推理”全流程的真实素材不是那种随手生成的玩具集。适合刚接触分类任务的初学者拿来找手感也适合想验证 ResNet 等预训练模型在细粒度图像上表现的老手。但这份数据有它自己的脾气文件命名不规整、JSON 字段需要确认、部分类别图片数量可能失衡。我建议先花十分钟摸清结构再动手训练。2. 压缩包内部结构dict.json、species.txt、genus.txt 与图片目录怎么配合拿到压缩包第一步不是双击解压而是先把它当成一个黑匣子拆开看。ZIP 里的核心文件是四个部分Butterfly20_dict.json、species.txt、genus.txt和放着 JPEG 图片的Butterfly20目录。前三个是元数据最后一个才是真正喂给模型的图像数据。这四者之间的关系决定了你后面写 Dataset 时是按文件夹读还是按 JSON 的字段索引所以值得逐一把它们确认清楚。2.1 先打印 dict.json 的键不要猜字段名JSON 是最容易“想当然”出问题的文件。不同人导出 JSON 的习惯不一样有人用image_path有人用img有人把标签写成字符串有人写成数字索引。与其对着别人的代码猜不如先写一段脚本把结构打出来。import json with open(Butterfly20_dict.json, r, encodingutf-8) as f: data json.load(f) # 先看最外层的类型 print(type(data)) # 可能是 dict也可能是 list if isinstance(data, dict): keys list(data.keys()) print(第一层键数量:, len(keys)) print(前5个键:, keys[:5]) # 取一个样本打印完整内容 sample_key keys[0] print(样本值:, data[sample_key])这段代码的逻辑很简单先判断 JSON 最外层是字典还是列表再打印键名和一条样本。常见做法是先跑这一步确认字段名再写下游代码。这里要关注三个点路径是绝对路径还是相对路径标签是物种字符串还是数字编号目录名和 JSON 里的键是否对得上。这三个点只要有一个不一致后面训练时就会出现 FileNotFoundError 或者标签错位而且这种错位通常很隐蔽loss 还在降但验证集准确率永远上不去。2.2 species.txt 与 genus.txt类别名称与生物学层级species.txt和genus.txt是纯文本标签文件每一行对应一个类别。常见做法是 20 个物种名按固定顺序排列顺序与训练标签的索引一致。genus.txt是属名用于理解物种之间的亲缘关系。两个文件的行号顺序非常关键。如果 species.txt 的第 0 行是某种蝴蝶那么数据集中所有该物种的图片标签都应该映射到 0。可以用一段简单的脚本做一致性校验。with open(species.txt, r, encodingutf-8) as f: species_list [line.strip() for line in f.readlines() if line.strip()] with open(genus.txt, r, encodingutf-8) as f: genus_list [line.strip() for line in f.readlines() if line.strip()] print(物种数量:, len(species_list)) print(属名数量:, len(genus_list)) # 检查两个文件的物种-属对应关系 for i in range(min(len(species_list), len(genus_list))): print(f索引 {i}: {genus_list[i]} {species_list[i]})这个校验的意义在于当你把标签转成 one-hot 或者交叉熵的类别索引时必须确保 species_list 的行号就是标签编号。如果中间混了一行空行或者重复项前面的物种全都会跟着错位。我一般会在这里顺便检查是否有重复物种名因为重复就意味着两个类别的图片混在同一个标签下对细粒度分类来说几乎是致命的。2.3 图片目录的命名规律与文件分布从压缩包的正文可以看到图片文件名是纯数字加.jpg后缀比如077.jpg、050.jpg而且不同类别的子目录里可能出现同名文件。这说明文件名本身不携带类别信息类别只能通过文件所在的目录或者 JSON 中的映射关系来确定。from pathlib import Path img_root Path(Butterfly20) dir_counts {} for sub_dir in sorted(img_root.iterdir()): if sub_dir.is_dir(): jpgs list(sub_dir.glob(*.jpg)) list(sub_dir.glob(*.jpeg)) dir_counts[sub_dir.name] len(jpgs) print(f{sub_dir.name}: {len(jpgs)} 张)运行这段脚本能得到每个子目录的图片数量分布。观察这个输出有两个目的第一确认数据整体规模判断 20 类平均每类多少张第二排查有没有目录图片特别少。如果某类只有个位数图片那这个类别基本不可能单独训出可靠的分类器必须靠后面第 5 章说到的数据增强或者类别加权来处理。2.4 识别隐藏的干扰文件压缩包里有.DS_Store文件这是 macOS 自动生成的系统文件。它本身不是图片但如果你在遍历目录时用iterdir()不加过滤它就会混进图片路径列表后续用 PIL 或 OpenCV 读取时大概率报错或者返回空对象。这种情况在跨系统解压时很常见处理方式是在遍历时显式过滤隐藏文件和后缀不在白名单里的文件。不要指望每个打包者都清理干净自己代码里做一次过滤才是可靠的做法。3. 数据预处理与增强给 20 类蝴蝶准备干净的训练输入图片数据拿来就训是不可取的。蝴蝶图片的尺寸、光照、角度差异很大如果不做统一预处理模型在验证集上的表现会忽高忽低这就成了玄学调参。这一章的目的就是把这些不稳定性扼杀在进入网络之前。3.1 统一尺寸与标准化迁移学习的前提条件如果你打算用 PyTorch 里现成的 ResNet 等预训练模型输入尺寸通常是 224×224标准化时用的是 ImageNet 数据集的均值和方差。这是预训练权重的基本要求因为模型在 ImageNet 上训练时的输入分布就是这样输入不一致会导致权重无法正常发挥作用。from torchvision import transforms train_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]) ])这段代码的逻辑是先把图片短边缩放到 256再从中心裁剪出 224×224 的区域。缩放和裁剪能避免直接把不同宽高比的图片强行塞进网络导致形变裁剪后的尺寸刚好匹配 ResNet 的输入要求。ToTensor会把 PIL 图片变成张量并归一化到 [0,1]Normalize再按 ImageNet 的均值和方差做标准化这样输入分布就和预训练阶段基本一致了。3.2 分层划分训练集、验证集与测试集分类数据集的划分不能简单用random.shuffle因为蝴蝶数据在采集时可能是按批次来的某些批次的背景、光照高度相似随机切分会导致验证集和训练集之间存在信息泄漏。正确做法是按类别做分层采样。import random from collections import defaultdict samples [] # 每个元素是 (图片路径, 标签索引) # 假设你已经从 JSON 或目录结构构建了 samples 列表 by_label defaultdict(list) for path, label in samples: by_label[label].append((path, label)) train_set, val_set, test_set [], [], [] for label, items in by_label.items(): random.shuffle(items) n len(items) train_set.extend(items[:int(n * 0.8)]) val_set.extend(items[int(n * 0.8):int(n * 0.9)]) test_set.extend(items[int(n * 0.9):])这段代码先把相同标签的样本归拢再在每个类别内部按 8:1:1 的比例切分。这样做保证了每个类别在三个集合中的占比一致验证集和测试集不会出现“某个类别完全没有样本”的情况。比例参数可以根据数据规模调整如果总数很少可以改成 7:2:1 或者直接用交叉验证。3.3 数据增强别让模型只会认同一张照片蝴蝶数据集的图片数量通常算不上充裕每类几十到几百张算是正常水平。数据增强是扩充样本最直接的手段。对蝴蝶这类目标随机水平翻转、随机裁剪、色彩抖动是效果最明显的三类增强蝴蝶的翅膀颜色和姿态对光照变化很敏感。train_transforms transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.4, contrast0.4, saturation0.4, hue0.1), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])RandomResizedCrop会在指定比例范围内随机裁剪模拟不同距离和拍摄角度下的目标尺度变化ColorJitter的四个参数分别控制亮度、对比度、饱和度和色相的变化幅度。这个增强组合在训练阶段使用验证和测试仍用上一节的基础变换保证评估时的稳定性。有一点要提醒如果后面想把这个数据集转成 YOLO 等目标检测格式训练集上的随机裁剪类增强就不能直接套用因为目标框坐标需要跟随裁剪区域同步变换否则标注框和实际目标位置错位训练出来的检测模型基本没法用。3.4 检查增强后的样本批次增强配置并不是越多越好。我见过有人把旋转和裁剪范围调得过大导致蝴蝶主体被切掉一半模型学到的全是残破样本。训练前可以单独跑一遍增强后的 DataLoader把每个 batch 里的几张图存下来肉眼检查一遍确认目标主体还完整、没有被增强到面目全非的程度再开始训练。4. 用 PyTorch 训练 20 类蝴蝶分类模型ResNet50 迁移学习全流程预处理做完就到了核心环节。这一章给出一个可以直接跑的迁移学习流程用 ResNet50 在 ImageNet 上的预训练权重做初始化替换最后一层全连接输出为 20 类。迁移学习在这个场景下是明确首选蝴蝶图像和通用物体特征有大量可共享的低层纹理信息从头训练一个 CNN 在这个数据规模下基本没法收敛到可用水平。4.1 自定义 Dataset 与 DataLoaderPyTorch 里最省事的读图方式是ImageFolder前提是目录结构是“类名/图片”的层级组织。如果这份数据的目录组织不是标准层级结构就需要自己写 Dataset。from torch.utils.data import Dataset from PIL import Image class ButterflyDataset(Dataset): def __init__(self, samples, transformNone): self.samples samples # [(路径, 标签), ...] self.transform transform def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] image Image.open(path).convert(RGB) if self.transform: image self.transform(image) return image, labelconvert(RGB)这步是关键。有些图片是带透明通道的 PNG 或者灰度图如果不统一转成三通道 RGB后面进网络时张量形状对不上会直接报错。初始化 DataLoader 时batch_size在有 8GB 显存的情况下可以设 32稍小一点的显卡就设 16num_workers建议设成 4 到 8数据加载会明显变快。4.2 迁移学习替换全连接层与参数冻结策略ResNet50 的最后一层全连接是 2048 维到 1000 类我们需要把它换成 2048 到 20。参数冻结有两种选择第一种是冻结 backbone 的全部参数只训练最后一层适用于数据量很少的情况第二种是先训最后一层再解冻 backbone 低学习率微调效果更好但时间翻倍。import torchvision.models as models import torch.nn as nn model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) num_features model.fc.in_features model.fc nn.Linear(num_features, 20) # 先冻结 backbone 参数 for param in model.parameters(): param.requires_grad False # 只让最后一层可更新 for param in model.fc.parameters(): param.requires_grad True第一阶段的逻辑是冻结所有 backbone 参数只训练新加的 fc 层。这样训练速度快且不容易在小数据集上过拟合。训练若干轮后如果想进一步提升精度可以解冻后面几层继续微调但学习率要降一个数量级。4.3 优化器、学习率与训练循环优化器选 Adam初始学习率 1e-4这个组合在大多数分类任务上不需要过多调整就能稳定收敛。学习率调度用 StepLR每 10 个 epoch 衰减为原来的 1/10。30 个 epoch 对这个小数据集够用了。import torch from torch import nn, optim device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-4) scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.1) best_val_acc 0.0 for epoch in range(30): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) scheduler.step() # 验证 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc correct / total print(fEpoch {epoch1}: loss{running_loss/len(train_loader.dataset):.4f}, val_acc{val_acc:.4f}) if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_butterfly_model.pth)这段循环做了四件事前向计算、反向传播更新参数、每个 epoch 结束后跑一次验证集、只在验证集准确率提升时保存模型。注意scheduler.step()必须放在每个 epoch 结束后而不是每个 batch 结束后。保存 best 模型而不是最后一个 epoch 的模型是因为小数据集上后期通常已经开始过拟合验证集最好的那个状态才是泛化能力最强的。5. 避坑与常见问题解压、路径、类别不平衡与训练异常这一章是血泪经验的浓缩。前几章的流程看起来顺理成章但真正跑起来的坑全在细节里。以下四条是拿到这个数据集后最常踩的问题我把现象、原因和解决办法一起列出来。5.1 解压后混入 .DS_Store 导致图片读取报错现象Image.open()偶尔打不开文件或者报cannot identify image file程序在某个 batch 随机崩溃。原因压缩包里混入了 macOS 的.DS_Store系统文件遍历目录时它也被当成图片读入。解决在构建samples列表时用后缀名白名单过滤不要用os.listdir后直接全收。valid_suffixes {.jpg, .jpeg, .png} samples [] for path in all_files: if Path(path).suffix.lower() in valid_suffixes: samples.append((path, label))这段过滤代码放在数据加载前可以在根源上把非图片文件排除掉。5.2 JSON 键名与路径分隔符不一致现象在 Windows 上生成的 JSON 里存的是Butterfly20\\类别A\\001.jpg这种反斜杠路径拿到 Linux 服务器上训练时全部找不到文件。原因JSON 里的路径分隔符是平台相关的。解决读取路径后统一替换分隔符再检查文件是否存在。import os path data[sample_key] path path.replace(\\, os.sep)这个替换操作放在读取 JSON 之后第一时间做避免后续所有依赖路径的环节全盘出问题。5.3 类别不平衡部分蝴蝶图片明显偏少现象训练集总准确率很高但少数几个类别在验证集上准确率是 0。原因这些类别的图片数量太少模型在训练时几乎没见过几回。解决用WeightedRandomSampler对少数类做上采样让每个 epoch 中少数类被抽到的次数更多。from torch.utils.data import WeightedRandomSampler label_counts defaultdict(int) for _, label in train_samples: label_counts[label] 1 weights [1.0 / label_counts[label] for _, label in train_samples] sampler WeightedRandomSampler(weights, num_sampleslen(train_samples), replacementTrue) train_loader DataLoader(train_dataset, batch_size32, samplersampler)这里1.0 / label_counts[label]的含义是图片越少的类别单张被采样的权重越高。采样器会按权重做有放回抽样变相凑齐了每个类别的训练样本数。5.4 尺寸与输入格式不匹配现象训练过程中报size mismatch或Expected 3 input channels, got 4。原因一部分图片是 RGBA 四通道或者长宽比极端导致 Resize 后仍不符合网络输入。解决Image.open(path).convert(RGB)统一通道数同时在 transform 里用固定尺寸的Resize加CenterCrop别用transforms.ToTensor()直接收原图尺寸。6. 模型验证与新图推理测试集指标、单图预测脚本与模型导出最后一章是收尾的动作模型训练完不算完你得知道它在没见过的数据上表现怎么样并且能把训练好的权重真正拿去做单张图片的预测。这决定了这个数据集能不能帮你打通一个完整的实用流程。6.1 测试集评估与混淆矩阵验证集只用来做训练时的参考真正衡量模型水平的是测试集。加载之前保存的 best 模型在测试集上跑一遍用混淆矩阵看哪些类别容易被混淆。from sklearn.metrics import confusion_matrix, classification_report model.load_state_dict(torch.load(best_butterfly_model.pth)) model.eval() all_preds [] all_labels [] with torch.no_grad(): for images, labels in test_loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) cm confusion_matrix(all_labels, all_preds) print(classification_report(all_labels, all_preds, target_namesspecies_list))classification_report会按类别输出 precision、recall、f1-score。重点看哪些类别的 recall 偏低recall 低说明这个类别的蝴蝶经常被认成别的类。6.2 单张图片推理脚本训练好的模型最终要拿来识别新图片。写一个独立的推理脚本输入任意一张蝴蝶图片输出 Top-3 的预测类别和置信度。def predict_single_image(model, image_path, transform, species_list, top_k3): image Image.open(image_path).convert(RGB) tensor transform(image).unsqueeze(0).to(device) model.eval() with torch.no_grad(): output model(tensor) probs torch.softmax(output, dim1)[0] top_probs, top_indices torch.topk(probs, top_k) results [] for prob, idx in zip(top_probs.cpu().numpy(), top_indices.cpu().numpy()): results.append((species_list[idx], prob)) return results注意unsqueeze(0)这一步是为了把单张图片变成形状为[1, 3, 224, 224]的 batch否则网络的前向传播会报维度错误。softmax把输出转成概率分布topk取出概率最高的前 k 个结果。6.3 导出 ONNX 方便部署训练在 PyTorch 里完成但部署环境不一定有 PyTorch。导出成 ONNX 格式是常见的做法后续可以转成不同平台的推理格式。dummy_input torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy_input, butterfly_model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, opset_version11 )dynamic_axes声明了 batch 维度是可变的这样导出后的模型既能单张推理也能同时处理多张图片。opset 版本用 11兼容性比较好大部分推理框架都支持。从那以后我每次拿到新的分类数据集都会强制自己先做一遍全量文件名扫描再统计每类图片数量最后才写 Dataset 和训练脚本。这个习惯帮我省掉了无数次跑到一半报错的返工。这份蝴蝶数据集不大但该踩的坑一个不少按这套流程走一遍你后续遇到其他分类数据就有底了。希望帮到你。本文还有配套的精品资源点击获取