☰
小样本昆虫图像分类实战:迁移学习与数据增强提升217张图片的识别率
2026/10/10 18:20:44 网站建设 项目流程

简介:面向深度学习与图像识别的初学者,以及需要快速搭建目标检测实验的开发者,这份昆虫识别数据集提供了轻量且完整的数据基础,能够省去自行采集图片和手工标注的环节,尤其适合刚接触YOLO、SSD等目标检测框架的学习者。数据集涵盖 acuminatus、armandi、boerner、coleoptera、leconte、linnaeus 共6类昆虫,包内共438个文件,包括217张JPG图片、217个XML标注文件及4个TXT说明文件,压缩包整体约16.61MB,体积小巧,便于下载、存储和快速部署。每张图片都有对应的XML标注,记录目标类别和边界框坐标,可直接用于目标检测、昆虫分类等模型训练;TXT文件可供查看类别清单或数据划分方式。数据已按7:2:1划分为训练集、验证集和测试集,免去手动切分的步骤,从而可以快速迭代模型参数、检验算法效果。目前已有1703人浏览学习,适合作为小规模入门数据集,用来跑通检测流程、开展课程实验或进行迁移学习热身,也可以用于练习数据格式转换和标注可视化检查。

1. 217 张昆虫图片能做什么:这不是一个“玩具数据集”

拿到「昆虫识别数据集.zip」这类小包,多数人的第一反应是:才 217 张图,能训出什么?我的判断正好相反——小数据集恰恰是跑通深度学习图像识别全流程的最快路径。这个包包含 6 种昆虫,分别是 acuminatus、armandi、boerner、coleoptera、leconte、linnaeus,有图片有标注,还给出了 7:2:1 的训练/验证/测试划分建议。适合刚学 PyTorch、想做一个完整分类项目、或者需要反复调参验证效果的从业者。别拿它和几百万张的大数据集比,拿来练迁移学习、数据增强、模型评估这一套,它比大包效率高得多,也能把踩坑提前暴露出来。

2. 拆开 zip 先做体检:目录结构、标注格式和 7:2:1 切分

2.1 先看目录:别急着训练

很多人下载数据集后第一件事就是写训练脚本,结果训练到一半发现图片损坏、标签缺失、目录混乱,白跑几小时。我现在的习惯是:任何数据集 zip 解压后,先做一次体检,搞清楚三件事——图片在哪、标注在哪、类名怎么映射。

unzip 昆虫识别数据集.zip -d insect_data cd insect_data find . -maxdepth 2 -type f | head -20

解压时如果终端不识别中文文件名,先把 zip 重命名为insect_data.zip再解压,省得后面所有路径都带中文。find的-maxdepth 2只扫两层目录,避免把大目录树全部打出来;head -20是只看前 20 个文件,确认结构后再说。

接着用 Python 做一个更完整的盘点:

from pathlib import Path data_dir = Path("insect_data") imgs = sorted(data_dir.rglob("*.jpg")) anns = sorted(data_dir.rglob("*.xml")) + sorted(data_dir.rglob("*.txt")) + sorted(data_dir.rglob("*.json")) print("jpg 数量:", len(imgs)) print("候选标注数量:", len(anns)) print("图片样例:", [p.name for p in imgs[:5]])

rglob是递归查找,不管图片嵌在第几层目录都能找出来。.jpg只筛选图片文件,XML、TXT、JSON 都先列出来,因为标注格式在摘要里没写死,拿到手才有真相。这步能快速看出图片和标注是否一一对应,尤其要警惕图片 217 张、标注却只有 150 张的情况。

2.2 标注格式怎么读:VOC、YOLO、CSV 的三种情况

这个数据集的摘要只写了“包含图片和标注”,没写标注格式。我把常见情况分成三类,分别给对应的解析方式。

如果看到.xml,大概率是 VOC 格式,用 ElementTree 解析:

import xml.etree.ElementTree as ET def parse_voc(xml_path): tree = ET.parse(xml_path) root = tree.getroot() names = [] for obj in root.iter("object"): name = obj.find("name").text bbox = obj.find("bndbox") names.append({ "name": name, "xmin": int(bbox.find("xmin").text), "ymin": int(bbox.find("ymin").text), "xmax": int(bbox.find("xmax").text), "ymax": int(bbox.find("ymax").text), }) return names

VOC 里每个 object 节点对应一个目标,name是类别,bndbox是坐标。这种格式适合转到目标检测,直接把 XML 转成 YOLO 格式后丢给 ultralytics 训练。如果看到.txt,大概率是 YOLO 的归一化坐标格式,每行是class x_center y_center width height,我一般会写一个解析函数确认坐标范围。

def parse_yolo_txt(txt_path): lines = txt_path.read_text(encoding="utf-8").strip().splitlines() return [line.split() for line in lines if line.strip()]

如果看到.json,可能是 COCO 格式,用json.load()读,重点关注categories和annotations两个字段。无论哪种格式,我建议先随机抽三张图,把标注可视化一下,确认不是“文件名对上了但框和昆虫完全无关”。

2.3 按 7:2:1 切分:先切分,再做增强

摘要明确说了训练集:验证集:测试集 = 7:2:1,这个比例在小数据集上是合理的。217 张图,训练集约 152 张,验证集约 43 张,测试集约 22 张。切分最忌讳的是直接按目录顺序切,因为图片可能是按采集时间排列的,顺序切会把同类昆虫全切到训练集里。

import random import shutil from pathlib import Path random.seed(42) imgs = sorted(Path("insect_data").rglob("*.jpg")) random.shuffle(imgs) n = len(imgs) train_imgs = imgs[:int(n * 0.7)] val_imgs = imgs[int(n * 0.7):int(n * 0.9)] test_imgs = imgs[int(n * 0.9):] print(f"train={len(train_imgs)}, val={len(val_imgs)}, test={len(test_imgs)}")

random.seed(42)固定随机种子,保证每次切分结果一致;shuffle打乱顺序,避免同类图片扎堆。这个脚本是最基础的随机切分,只解决了“顺序”问题,没解决“类别均衡”问题。如果某个类只有 10 张图,随机切有可能把这类全部切进训练集。更稳的方式是先按类别分组,再从每个类里按 7:2:1 抽。

from collections import defaultdict class_to_imgs = defaultdict(list) for img in imgs: label = img.parent.name class_to_imgs[label].append(img) train_imgs, val_imgs, test_imgs = [], [], [] for label, label_imgs in class_to_imgs.items(): random.shuffle(label_imgs) k = len(label_imgs) train_imgs += label_imgs[:int(k * 0.7)] val_imgs += label_imgs[int(k * 0.7):int(k * 0.9)] test_imgs += label_imgs[int(k * 0.9):]

这里默认图片的父目录名就是类别名。如果解压后图片是平铺的,就要先从标注文件里拿到类别名,再按类别归组。分层切分的好处是每个类在三个集合中都存在,模型评估某个冷门类时不会因为测试集里压根没有它而直接报零。

3. 用 PyTorch 跑通昆虫分类:迁移学习、数据增强和训练参数

3.1 为什么选迁移学习:217 张图不够从零训一个 CNN

六类昆虫,152 张训练图,平均每类才 25 张。从零训练一个 ResNet 这样的网络,参数百万级,喂这么点数据基本只能记住训练集,验证集一测就露馅。所以我在这种场景下从不自己设计卷积网络,直接用 ImageNet 预训练权重,把最后一层全连接替换成 6 分类输出,前面卷积层全部冻结,只训练分类头。

import torch import torch.nn as nn from torchvision import models model = models.resnet18(pretrained=True) num_classes = 6 model.fc = nn.Linear(model.fc.in_features, num_classes) for name, param in model.named_parameters(): if name.startswith("fc"): param.requires_grad = True else: param.requires_grad = False

resnet18在小型数据集上比 resnet50 更合适,因为 resnet50 参数量大,冻结后只训练最后一层问题不大,但一旦解冻更多层,过拟合速度会非常快。name.startswith("fc")只保留最后的全连接层可训练,其余参数不更新,这样真正参与训练的参数量很小,几分钟就能跑完一个 epoch。

提示:如果图片是灰度图,而预训练模型期望输入是三通道 RGB,加载时要先img.convert("RGB"),否则后面Normalize的时候维度对不上,最常见的报错就是size mismatch。

3.2 数据加载与增强:让 152 张图“长”出三倍样本

数据增强是小数据集最后的救命稻草。我的经验是:翻转加旋转加颜色扰动,组合起来效果比单用一种好得多。PyTorch 里用torchvision.transforms直接组合。

from torchvision import transforms train_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomAffine(degrees=10, translate=(0.05, 0.05)), transforms.ColorJitter(brightness=0.3, contrast=0.3), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

Resize统一到 224×224 是为了匹配 ImageNet 预训练模型的输入;RandomHorizontalFlip对昆虫图片有效,因为昆虫姿态不依赖左右方向;RandomAffine的degrees=10控制最大旋转角度,旋转太大会把昆虫形状破坏;ColorJitter的brightness和contrast模拟光照变化。验证集和测试集不能做随机增强,只用Resize和Normalize,否则每次评估结果都不一样,没法对比实验。

Normalize的三个均值和三个标准差是 ImageNet 的标准值,使用预训练模型时不能乱改。改成别的数值,轻则训练不收敛,重则验证集 acc 永远在 20% 左右徘徊,这时候先回头检查 Normalize 参数是否和预训练权重匹配。

3.3 训练脚本和参数建议:batch size、学习率和早停

数据切好、增强配好,就可以写训练循环了。我一般会把训练和验证拆成两步,训练时每个 epoch 结束后立刻在验证集上跑一次。

from torch.utils.data import DataLoader from torchvision import datasets train_ds = datasets.ImageFolder("insect_split/train", transform=train_tf) val_ds = datasets.ImageFolder("insect_split/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=16, shuffle=True, num_workers=2) val_loader = DataLoader(val_ds, batch_size=16, shuffle=False, num_workers=2) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1) for epoch in range(15): model.train() for imgs, labels in train_loader: out = model(imgs) loss = criterion(out, labels) optimizer.zero_grad() loss.backward() optimizer.step() model.eval() total, correct = 0, 0 with torch.no_grad(): for imgs, labels in val_loader: out = model(imgs) pred = out.argmax(dim=1) correct += (pred == labels).sum().item() total += labels.size(0) print(f"epoch {epoch + 1}, val acc: {correct / total:.4f}") scheduler.step()

batch_size=16在 224×224 的图上每 batch 占内存不算大,CPU 也能跑;如果显存吃紧就换成 8。lr=1e-3是 Adam 配合迁移学习的常用起点,太高会震荡,太低收敛太慢。StepLR每 5 个 epoch 把学习率乘 0.1,让后期训练更稳定。num_workers=2是 Linux/Mac 的常用设置,Windows 上如果报多线程错误,直接改成num_workers=0。

这个循环只训练最后一层,所以 15 个 epoch 完全够。千万不要上来就batch_size=64加num_workers=8,对小数据集反而容易把错误信号放大,loss 曲线跟心电图一样。

4. 小数据集避坑指南:5 个让我重跑实验的常见问题

4.1 训练集 acc 99%,验证集 acc 只有 60%

现象是最经典的自欺欺人:训练 loss 一路降到 0.1 以下,训练集 acc 到了 99%,但验证集 acc 卡在 60%。原因很简单,152 张训练图对 6 分类来说样本量太少,模型直接把图像背景和噪声一起记住了,迁移学习也没救回来。

解决方法是分三路下手:一是冻结更多层,只训练最后一个全连接头;二是加强数据增强,把RandomAffine的degrees加到 15,或者加RandomErasing让模型不能只看局部纹理;三是在全连接层里加nn.Dropout(p=0.3),强制模型不依赖单个神经元的输出。从那以后我养成了习惯,训练时每隔几个 epoch 就瞟一眼训练 acc 和验证 acc 的差值,差值超过 25 个点立刻停,不等着把 15 个 epoch 跑完。

4.2 PIL 报 “cannot identify image file”

现象是训练到一半,DataLoader 突然抛PIL.UnidentifiedImageError: cannot identify image file,断点续训都麻烦。原因是数据集中有些图片扩展名是.jpg,但文件本身是坏的、空的,或者后缀被改过,Pillow 打开后识别不出真实格式。

解决方法是训练前先做一轮图片体检,把不能正常打开的图片挑出来:

from PIL import Image from pathlib import Path def filter_bad_images(paths): good_paths = [] for p in paths: try: with Image.open(p) as im: im.verify() good_paths.append(p) except Exception as e: print("bad image:", p, e) return good_paths imgs = filter_bad_images(sorted(Path("insect_data").rglob("*.jpg"))) print("保留图片数:", len(imgs))

Image.open默认是惰性加载,不真正读全部数据,必须跟一个verify()才能把文件完整性校验跑完。挑出来的坏图片不要直接删,先移到broken_images/目录,确认是损坏还是偶尔个例。如果坏图只有三张,移除后重新切分即可;如果坏图超过 10%,要考虑 zip 包是不是没下载完整,重下一次再验。

4.3 标签和图片对不上,模型学了个寂寞

现象是训练正常,验证 acc 也不低,但看分类报告发现某个类的 precision 是 0。原因是切分图片列表和标注列表没有按同一个文件名规则配对,比如图片叫0037.jpg,标注叫0037.xml,但切分时一个用了带路径的完整名,另一个用了文件名,排序一乱全错位。

我现在的做法是:不管图片和标注放在哪个目录,统一用Path.stem作为关联键,再写一个检查脚本,确认每个图片都有对应标注、每个标注都有对应图片。

from pathlib import Path img_stems = {p.stem: p for p in Path("insect_data").rglob("*.jpg")} ann_stems = {p.stem: p for p in Path("insect_data").rglob("*.xml")} img_only = set(img_stems) - set(ann_stems) ann_only = set(ann_stems) - set(img_stems) print("只有图片没标注:", img_only) print("只有标注没图片:", ann_only)

Path.stem返回去掉后缀的文件名,0037.jpg和0037.xml会得到同一个0037,是最安全的配对方式。不要手动去字符串切片拼接,文件名里有没有下划线、减号,不同系统排序规则还不一样。检查通过后再进切分流程,能省掉后面一整轮踩坑。

4.4 总 acc 还不错,但某个类全错

现象是验证集整体 acc 75%,但armandi这个类的 recall 是 0,因为它总共只有 15 张图,被随机切分切走了 12 张训练、3 张验证,测试集里只剩 1 张。原因就是类别不平衡加随机切分。

解决方法是先用分层切分替换随机切分,再看具体类别分布。如果某个类实在太少,我给训练集加WeightedRandomSampler,让少样本类被抽到的概率更高。

from torch.utils.data import WeightedRandomSampler labels = [label for _, label in train_ds.samples] weights = [1.0 / labels.count(l) for l in labels] sampler = WeightedRandomSampler(weights, num_samples=len(labels), replacement=True) train_loader = DataLoader(train_ds, batch_size=16, sampler=sampler)

weights越大,采样到该样本的概率越高;1.0 / labels.count(l)是反比权重,样本越多的类别权重越小,样本越少的类别权重越大。num_samples设置成训练集总长度,保证每个 epoch 采样数量不变。注意用sampler后不能再传shuffle=True,这是常见的翻车点,Dataloader 会直接报冲突错误。

4.5 Windows 下中文路径乱码,模型加载失败

现象是换到 Windows 后,解压出的中文目录名显示成乱码,ImageFolder找不到类目录,直接抛FileNotFoundError。原因是 zip 包里的中文文件名编码在不同系统间不一致,Windows 默认用 GBK,Linux 用 UTF-8,解压工具处理不好就会乱。

解决方法是解压后第一步就把所有目录和文件名改成 ASCII 命名,不要抱侥幸心理。批量重命名可以用pathlib在 Python 里完成,把中文目录甲虫/改成beetle/,图片保持原名不变。另外读取标注文件时,所有open()操作都显式加encoding="utf-8",不要依赖系统默认编码,否则同样的代码在 Windows 和 Linux 上跑出完全不同的结果。

5. 验证模型不是看 val_acc:混淆矩阵和置信度阈值的实用操作

217 张图按 7:2:1 切,测试集只有 21 张左右,错一张图 acc 就掉将近 5 个点。只看验证集 acc,根本分不清模型是“全部类都学得均衡”还是“运气好把常见的几个类猜对了”。我每次训完的最后一步,永远是输出混淆矩阵和分类报告。

from sklearn.metrics import confusion_matrix, classification_report model.eval() y_true, y_pred = [], [] with torch.no_grad(): for imgs, labels in val_loader: out = model(imgs) conf, pred = torch.softmax(out, dim=1).max(dim=1) y_true += labels.tolist() y_pred += pred.tolist() print(confusion_matrix(y_true, y_pred)) print(classification_report(y_true, y_pred, target_names=train_ds.classes))

torch.softmax把 logits 转成概率分布,max(dim=1)同时拿到最大概率和对应类别。混淆矩阵对角线是对的部分,非对角线是具体哪个类被认成了哪个类,比单个 acc 数字有用得多。如果coleoptera经常被认成leconte,说明这两个类的训练样本在视觉上太像,需要回去看原始图片和标注,是标注错了还是本来就需要更细的特征。

分类报告里的 macro 平均和 weighted 平均也值得看。六类样本不均衡的情况下,weighted precision 会被样本多的类拉高,macro 平均则只看每一类的平均表现。我一般以 macro 为主,因为这个小数据集的目标是“每个类都要认对”,不是“把总量刷高”。

数据量小还带来另一个问题:模型对低置信度样本的判断不可靠。我给预测加一道置信度门槛,低于阈值的样本不采用,宁可标成“不确定”。

with torch.no_grad(): for imgs, labels in val_loader: probs = torch.softmax(model(imgs), dim=1) conf, pred = probs.max(dim=1) mask = conf >= 0.75 print(f"高置信度样本: {mask.sum().item()} / {labels.size(0)}")

阈值设多少没有标准答案,我会在验证集上扫一遍 0.5、0.6、0.75、0.9,看高置信度样本被接受后的 acc 能到多少。小数据集上这个手段特别有用,因为有些类本身外观模糊,让模型硬猜最终只会带歪整个评估结果。

从那以后,我每次跑完一组实验都强制走一遍“confusion matrix + classification report + 置信度阈值扫描”这个流程,再去看 val_acc。这包昆虫识别数据集虽小,但正好能练出这套评估习惯。如果你也想把这个 zip 包拿来复现一遍,解压后按第二章的体检流程走,再用第三章的迁移学习脚本训练,最后按第五章做模型评估,基本不会卡在奇怪的地方。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询