简介:蝴蝶分类数据集20类.zip是一份面向机器学习与图像识别场景的标注图像资源,其中包含二十个不同蝴蝶物种的样本图片,可供研究者训练分类模型、验证物种识别算法,也可用于生物多样性与物种亲缘关系的对比研究。压缩包共收录1870个文件,主要素材是1866张JPG格式的蝴蝶图像,这些图像按类别存放在独立子目录中,便于批量读取与多角度观察;此外还配有2个TXT清单,分别记录全部物种名称及其所属属名,方便标签核对与分类学梳理;另有1个JSON元数据字典,用于存储图片路径、物种归属、描述性字段等结构化信息,便于程序化解析。整份资源体积约60.96MB,规模紧凑,下载后可直接开展图像增强、标准化等预处理工作。目前已有119人学习下载,数据集标注完整、组织清晰,既适合作为卷积神经网络入门训练的基准数据,也能为昆虫分布、进化关系及物种保护等科研问题提供基础数据支撑,实用价值突出。
1. 蝴蝶分类数据集20类.zip:一个解压就能开始练手的小型分类数据包
如果你的工作台或下载目录里恰好放着这个叫“蝴蝶分类数据集20类.zip”的压缩包,大概率是准备做图像分类的入门练习或课程设计。它的大小通常在几十到几百MB,解压后是20个按蝴蝶类别命名的文件夹,每类图片少则几十张、多则几百张。这个体量恰好能跑通一整套图像分类流程:解压、数据体检、训练集/验证集划分、迁移学习训练、分类评估,一个环节都不缺。它最适合两类人:一是想快速验证baseline、不想一上来就啃几十GB大模型库的初学者,二是需要做一个可复现对比实验的进阶学习者。它的核心价值在于,让你在半天之内完整经历从“拿到zip”到“训练出可用的分类器”的全过程,数据规模适中、类别粒度真实。
2. zip解压与数据体检:先搞清楚20类到底装了什么再训练
拿到压缩包后的第一件事不是写模型,而是把zip解压干净,弄清楚里面到底是什么布局。这个步骤决定了你后续所有处理路径是否顺畅,也是新手最容易跳过的坑。先看目录,再体检,再判断分布,三步走完再碰训练代码。
2.1 解压zip与目录结构:先处理“套娃”和隐藏的二级压缩包
最常见的操作是在Linux或macOS下用unzip解压,Windows下则用解压软件,但命令行的方式更可控、更适合脚本化:
mkdir -p butterfly_data unzip 蝴蝶分类数据集20类.zip -d butterfly_data cd butterfly_data find . -maxdepth 3 -type d | sort | head -50参数说明:-d指定解压目标目录,避免zip内容直接散落在当前目录;find加maxdepth 3只列出三层以内的目录,避免把类里几百个图片路径也刷到屏幕上。执行后你就能一眼看清根目录下到底有多少个文件夹、每个文件夹叫什么名字。
解压完成后要重点检查两种结构问题。第一种是套娃:外层zip解压出来里面还套着一个zip,或者再包一层同名目录,这种情况非常常见。比如“蝴蝶分类数据集20类.zip”解压后,里面可能是一个叫“butterfly”的文件夹,再往下一层才是20个类别,那么你在组织训练目录时就必须先把这个中间层剥掉。第二种是目录层级不统一:有的类直接平铺在根目录下,有的类下面又按科属分了两层。如果你要做的是“20类平级分类”,遇到层级结构就必须把所有物种目录提升到同一层,否则后面用到ImageFolder时,它会自动把第一层目录当作类别,结果数出来可能只有10类或30类,而不是预期的20类。
另外值得确认的是压缩包内是否附带README或labels.txt。这类文件通常记录了类别全名、数据来源或标注口径,但由于它不是图片,很容易被忽略。我见过不止一个数据集解压后实际内容比zip名字多出或缺少一类,这种偏差会直接影响后续分类评估的可信度,所以目录层面的核对是第一步。
2.2 数据体检脚本:图片数量、尺寸、通道与损坏检测
目录结构确认后,用体检脚本把每个类的底数摸清楚。这一步能避免训练过程在中途的某次迭代中突然报错,也能提前发现类别数量上的隐患。核心脚本用PIL和os做一次全量扫描:
import os from PIL import Image root = "butterfly_data" cls_dirs = sorted([d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d))]) print(f"number of classes: {len(cls_dirs)}") for cls in cls_dirs: cls_path = os.path.join(root, cls) files = os.listdir(cls_path) valid, bad, n = 0, 0, 0 size_counter = {} typical_size = None for f in files: if not f.lower().endswith((".jpg", ".jpeg", ".png", ".bmp")): continue p = os.path.join(cls_path, f) try: with Image.open(p) as im: im.verify() size_counter[im.size] = size_counter.get(im.size, 0) + 1 valid += 1 typical_size = im.size except Exception: bad += 1 n += 1 print(f"{cls}: files={n}, valid={valid}, bad={bad}, " f"distinct_sizes={len(size_counter)}, typical={typical_size}")逻辑说明:im.verify()只校验文件头是否完整,不会真正解码整张图,所以速度快、适合全量扫描。扫描后每类打印五个数值:文件总数、能正常读取数、损坏数、尺寸种类数、最后一张图的尺寸。如果bad不为0,说明图有问题或格式伪装;如果distinct_sizes很大,说明这个数据集的图片没有被统一缩放,训练时需要在transforms里统一Resize。
这个环节最容易被跳过,但以我的经验,数据集里图片损坏的比例完全没有规律,可能一个类只有一两张坏图,另一个类却藏着十几个扩展名为jpg、实际已截断的文件。如果不提前清理掉,DataLoader很可能在第3个epoch或第7个epoch时突然抛UnidentifiedImageError,而且何时触发与数据顺序相关,排查起来非常消耗时间。体检脚本跑一次只要几分钟,能省下后面几小时的排障时间。
2.3 类别分布初判:哪些类会拖后腿,一眼就能看出来
体检脚本的输出就是一张类别数量表,接着就可以做第一轮分布判断。一个理想的“20类”数据集,每类最好有100到500张图片。但如果某些类只有二三十张,而另一些类有近千张,这就需要为后续的数据划分和分类评估提前做准备。
蝴蝶分类数据集常见的来源包括野外拍摄和馆藏标本扫描,两种来源的图片风格差异很大:野外拍摄背景复杂、蝴蝶姿态多变、光线不匀;标本扫描则背景干净、角度固定、光照均匀。如果zip里两种来源的图片都混在一起,训练出的模型很容易学会“绿色背景就是某类”“白底图片就是某类”这种伪规律。体检阶段就应该随机抽几类图片看看背景差异,如果发现出现两种明显的背景风格,一个稳妥做法是训练前先做一次来源层面的分组,在训练集和验证集中同时保留两种风格,避免验证集只在单一风格上评估。
对于数量特别少的类,我会在体检表上单独标注,并在后续训练脚本里为它们增加权重。一个常见的做法是把类别权重写进一个字典:
class_weights = { "某类英文代号": 1.5, "另一个少数类代号": 1.2, # 数值越大,训练时该类loss的放大倍数越高 }这里要说明,权重只是在样本数量不足时的补救措施,并不能凭空创造数据。如果一个类有效图片不到40张,那么无论权重怎么调,这个类的泛化能力都有限。比较务实的底线是保证每个类至少40张可用图,低于这个数优先考虑补充数据,而不是急着训练。
3. 把原始目录整理成训练可用格式:训练集/验证集划分与标签映射
zip解压后的目录结构再规整,也不能直接作为PyTorch训练输入。中间需要做一次数据整理:按比例拆出训练集和验证集,把目录名映射成稳定的语义标签,并保存一份可复查的映射文件。这一节做的事情虽然琐碎,但直接影响训练代码的复杂度和后续实验的可复现性。
3.1 为什么标准目录结构比自定义Dataset更适合起步
torchvision提供了一个叫ImageFolder的数据集类,它规定根目录下的第一层子文件夹就是类别名,每一类图片分别放在各自的子文件夹里,label由文件夹名自动生成。对于一个目录名已经是类别名的数据集,这是最合适的数据装载方式。你自己写一个Dataset类当然也可以,但问题在于它把“路径到标签”的映射逻辑写死在代码里,当类别数或目录结构变动时,需要改代码并重新匹配字符串,排错成本高。
ImageFolder的最大优势是把映射关系打开在属性里:train_ds.classes直接给出类别列表,train_ds.class_to_idx给出从类名到整数的字典。训练时只要打印这两项,就能立刻确认20类是不是真的是20类、顺序是否符合预期。这对训练流程的稳定性和后续分类评估都有好处,因此我一般会先整理目录,让数据本身贴合ImageFolder的约定。
3.2 数据划分脚本:按比例拆train/val,类标签自动生成
划分脚本要做的事情很简单:在每个类别内随机抽样,按比例拆成train和val两份。但有几个细节不能省:固定随机种子保证实验可复现;先shuffle再切割,避免直接取前缀。很多数据集的图片列表本身就按采集时间排序,如果直接取前80%,验证集和训练集的采样来源就不够独立。
import os, random, shutil random.seed(42) root = "butterfly_data" out_root = "butterfly_split" split_ratio = 0.8 for cls in sorted(os.listdir(root)): src_dir = os.path.join(root, cls) files = [f for f in os.listdir(src_dir) if f.lower().endswith((".jpg", ".jpeg", ".png", ".bmp"))] random.shuffle(files) n_train = int(len(files) * split_ratio) for phase, subset in [("train", files[:n_train]), ("val", files[n_train:])]: dst_dir = os.path.join(out_root, phase, cls) os.makedirs(dst_dir, exist_ok=True) for f in subset: src = os.path.join(src_dir, f) dst = os.path.join(dst_dir, f) shutil.copyfile(src, dst) print(f"{cls}: train={n_train}, val={len(files) - n_train}")参数说明:split_ratio=0.8是训练集占比,对中小规模数据集,0.8/0.2的划分比较常用;如果你担心验证集太小导致评估噪声大,可以改为0.85/0.15。脚本中copyfile是复制而非移动,这样原始解压目录被完整保留,之后做交叉验证或重新划分时不需要再次解压。按类别内切分还天然保持了分层比例,不需要引入额外的分层采样库。
一个容易被忽略的问题:不要把训练集和验证集切分后再做整目录shuffle,那样会导致同一张图片跨集合出现,造成数据泄漏。按类内顺序切分并保留原始目录,是避免重复图片混入的最简单做法。
3.3 标签映射落地:中文目录名转英文代号,写一份class_to_idx.json
目录名如果是中文或带空格,在Linux与Windows之间搬运时偶尔会遇到编码和路径解析问题。稳妥的做法是准备一份类别映射表,把原始目录名转换为语义清晰的英文代号,同时生成class_to_idx.json,让训练、评估和推理阶段都读取这份JSON,而不是在代码里手写标签顺序。
import json with open("raw_cls_names.txt", "r", encoding="utf-8") as f: raw_names = [line.strip() for line in f if line.strip()] mapping = { "某蝴蝶原始名": "Butterfly_01", # 在这里维护你手工整理的同义映射 } class_to_idx = {} for i, raw in enumerate(raw_names): code = mapping.get(raw, f"butterfly_{i:02d}") class_to_idx[code] = i with open("class_to_idx.json", "w", encoding="utf-8") as f: json.dump(class_to_idx, f, ensure_ascii=False, indent=2) print(class_to_idx)这段代码的核心价值是固化“目录名到整数标签”的关系。后续用ImageFolder或自定义Dataset时,都从这个JSON读取映射,避免多次运行数据管线时标签顺序不一致,导致模型权重和标签语义错位。分类评估打印混淆矩阵时,也需要用这份JSON把整数索引还原成可读的类名。我在实际项目中见过太多次“训练时用0到19的索引当标签,评估时忘了写映射,最后只能对着混淆矩阵猜第7类是哪个蝶”的翻车经历,所以花两分钟做映射是值得的。
4. 用迁移学习训练20类蝴蝶分类器:ImageNet预训练权重是省时间的关键
目录和标签都准备好之后,就进入模型训练环节。蝴蝶分类属于典型的细粒度图像分类,类间差异往往只体现在翅膀花纹和色斑分布上,这对模型的特征提取能力提出了较高要求。在这个数据规模下,迁移学习是性价比最高的选择。
4.1 选型理由:20类蝴蝶为什么天然适合迁移学习
细粒度分类的一个经典类比是flower102花卉按类型分类:类别总数不算多,但类间外观极其相似。蝴蝶和花卉、鸟类都属于同一类问题,区别在于蝴蝶的类间差异更细微,很多品种只靠翅膀背面花纹来区分。如果从头随机初始化一个ResNet来训练,以每类几十到几百张的样本量,模型很难学出足够有判别力的层次化特征,大概率会过拟合,训练集表现很好、验证集却一塌糊涂。
更务实的做法是直接利用在ImageNet-1K上预训练过的权重。这里要澄清一个常见误区:很多人以为“先用ImageNet-1K数据集下载并预训练模型”才能继续微调,其实不需要。torchvision中的weights参数可以直接加载官方发布的预训练权重,你不需要下载原始ImageNet图片,只需要联网让torchvision自动下载权重文件即可。以预训练权重为起点,模型已经具备识别边缘、纹理、色块等通用视觉特征的能力,你只需要替换最后的分类头并微调少数层的参数,就能在几百张图的小数据集上获得可用的分类器。
4.2 数据加载与增强:transforms配置与关键参数
数据加载的核心是transforms配置。由于蝴蝶数据集图片尺寸不统一、背景复杂度高,训练变换要兼顾尺度归一和过拟合抑制。基本配置是:先将图缩放到256,再随机裁剪出224的区域,配合随机水平翻转与颜色抖动。颜色抖动幅度要控制得当,我一般给0.2到0.3的亮度/对比度扰动,因为蝴蝶翅膀的色泽本身就是重要判别特征,扰动过大会改变真实外观,扰动过小又起不到增强效果。
from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p=0.5), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.02), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) train_ds = datasets.ImageFolder("butterfly_split/train", transform=train_tf) val_ds = datasets.ImageFolder("butterfly_split/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4)参数说明:Resize到256再RandomCrop到224,是为了让同一张图在训练时产生多个局部视角,提升模型对位移和尺度变化的鲁棒性。验证集不做随机裁剪,只用Resize到224,确保每次评估结果可复现。Normalize的三组数值必须与ImageNet预训练权重保持一致,不能随意修改,否则输入分布与预训练分布不匹配,微调效果会明显下降。num_workers=4在主流台式机上够用,如果CPU核心数多可以提高到8,但在Windows下需要注意把数据加载代码放在if __name__ == "__main__"保护块中,避免多进程重复执行脚本。
4.3 训练循环:resnet18微调的核心代码与超参
用ResNet18作为baseline是这类中小型图像数据集的稳妥选择。ResNet34或ResNet50当然也能用,但细粒度分类任务里,小模型配合合理的正则化往往比大模型更稳定,推理速度也更快。替换分类头时,只需要把最后的fc层改为输出20维的线性层:
import torch import torchvision model = torchvision.models.resnet18( weights=torchvision.models.ResNet18_Weights.IMAGENET1K_V1 ) model.fc = torch.nn.Linear(model.fc.in_features, 20)超参设置上,因为整个网络初始化自预训练权重,全量微调用Adam时,我一般从1e-4学习率起步,而不是常见的3e-4,这个量级对预训练权重的扰动更小,收敛也更稳定。损失函数用CrossEntropyLoss,但如果某些类样本明显偏少,可以传入类权重向量。优化器配合余弦退火调度,训练20到30个epoch足以看到清晰的收敛趋势。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) criterion = torch.nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max=25, eta_min=1e-6) for epoch in range(25): model.train() train_loss = 0.0 for x, y in train_loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() loss = criterion(model(x), y) loss.backward() optimizer.step() train_loss += loss.item() * x.size(0) scheduler.step() model.eval() correct = total = 0 with torch.no_grad(): for x, y in val_loader: x, y = x.to(device), y.to(device) pred = model(x).argmax(dim=1) correct += (pred == y).sum().item() total += y.size(0) print(f"epoch {epoch:02d} | loss={train_loss/len(train_ds):.4f} " f"| val_acc={correct/total:.4f}")参数说明:CosineAnnealingLR的T_max=25要和总epoch数对齐,它表示学习率从初始值下降到eta_min的周期长度;最后几个epoch学习率已经很小,val_acc出现小幅波动是正常的,不代表模型退化。这里我没有写混合精度,因为batch_size和数据集规模都不大,FP32全精度完全能跑,硬加AMP反而会引入设备兼容性上的麻烦。
如果希望更精细地微调,可以冻结前几层卷积,只微调最后几个残差块和分类头。这种做法收敛稍慢,但对预训练特征的破坏更小,适合想要更保守的迁移策略时使用。蝴蝶图像与自然图像的分布差异不大,全量微调优先级更高,如果时间充裕可以两者都试。
4.4 分类评估:除了准确率还要看per-class recall和混淆矩阵
训练结束时,不能只盯着val_acc这一个指标。20类蝴蝶数据集里,整体准确率98%和所有类别平均准确率98%含义完全不同:如果有两个近缘种被系统性地互相混淆,全局准确率可能只掉两三个点,但这两类在真实场景中的表现会非常差。所以分类评估要同时覆盖整体指标和细粒度错误。
from sklearn.metrics import classification_report, confusion_matrix import numpy as np model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for x, y in val_loader: x, y = x.to(device), y.to(device) pred = model(x).argmax(dim=1).cpu().numpy() all_preds.extend(pred) all_labels.extend(y.cpu().numpy()) target_names = [cls for cls in sorted(os.listdir("butterfly_split/train"))] print(classification_report(all_labels, all_preds, target_names=target_names)) cm = confusion_matrix(all_labels, all_preds) np.save("confusion_matrix.npy", cm)这段代码会把每个类的precision、recall、f1-score和support打印出来,同时把混淆矩阵保存为npy文件,方便后续加载绘图。判断一个类是否健康的关键指标不是precision,而是recall是否低于整体平均水平10个百分点以上。如果某个类recall明显偏低,说明它经常被识别成其他类别,需要重点分析类和类之间的混淆模式,这个分析是最后一章要讲的内容。
5. 蝴蝶数据集避坑笔记:四条真实的踩坑记录
数据类项目最容易出问题的环节往往不在模型结构,而在数据本身的细节。这一章记录我处理这类分类数据集时踩过的四个真实问题,按“现象 → 原因 → 解决”展开,希望能帮你少走弯路。
5.1 图片损坏或格式混杂:训练中途报错、loss变成NaN
现象:训练到第几个epoch时,DataLoader突然抛PIL.UnidentifiedImageError: cannot identify image file 'xxx.jpg',或者loss在某个batch变成NaN后一路不恢复,之前的训练成果全部作废。
原因:这类数据集多由人工采集或半自动下载整理而来,图片并未做完整体校验。常见情况包括jpg扩展名但文件已截断、PNG带透明通道被误改后缀、扫描标本图片夹杂了损坏的ICC色彩配置文件等。这些文件在体检阶段不显眼,但会在数据加载时随机触发异常。
解决:在第二章的体检阶段就把损坏图片找出来。我习惯维护两份清单,一份corrupted.txt记录所有损坏文件的路径,一份removed.txt记录已剔除的文件,而不是直接删掉。这样处理的好处是,如果某类图片本来就很少,还能根据清单判断是数据源缺失还是图片质量问题。若某个类坏图比例超过10%,优先考虑从其他渠道补充该类的有效数据,而不是简单删掉了事。
5.2 类别不均衡:少数类被多数类“吃掉”
现象:训练曲线很漂亮,验证集整体准确率接近98%,但按类展开看,某两三个类的recall只有0.4甚至更低,其余类都接近1.0。
原因:20类的数量分布并不均匀,有的类上百张,有的类只有几十张。模型在多数类上积累的梯度更多,分类边界会偏向样本量大的类,少数类被挤压到特征空间边缘,泛化能力自然下降。
解决:第一种做法是用WeightedRandomSampler按类别样本数的倒数调整采样权重,让少数类在同一个epoch中被采样多次;第二种做法是在CrossEntropyLoss中传入按样本数计算出的类别权重;第三种做法是对少数类单独做更重度的数据增强,比如旋转、透视变换。三个办法叠加时要注意幅度,不能对少数类做过于夸张的变换,否则模型学到了异常形变当作正常样式,验证集指标反而会下降。最保守的起点是只加类权重,观察少数类recall是否回升,再决定是否叠加其他手段。
5.3 背景过拟合:模型靠叶子认蝴蝶而不是靠翅膀
现象:训练集和验证集来自同一个数据源时准确率很高,但换一批新背景图片,比如从标本照换成野外逆光照,准确率下降明显,甚至跌破60%。
原因:蝴蝶分类数据集中图片的背景与类别之间常常存在强相关,例如某类蝴蝶总出现在黄色花丛中,另一类标本总贴在白底卡纸上。卷积网络会优先学到最容易区分的局部特征,当背景可作为分类线索时,模型会偷懒,把背景和类别绑定起来,而不是真正聚焦在翅膀结构上。
解决:在训练transforms中加入RandomErasing或Cutout,随机遮住图像的一部分区域,强制模型从多个区域提取特征。另一种有效做法是,在训练集里混入不同背景风格的数据,哪怕只是9:1的比例,也能显著降低模型对单一背景的依赖。我通常还会在训练后准备一批“自然场景负样本”做验证,如果真实场景准确率比训练时的验证准确率低超过5个百分点,就优先怀疑背景过拟合,而不是数据标注问题。
5.4 zip文件名中文乱码与路径分隔符问题
现象:在Windows下解压zip时文件名显示正常,但把同一个zip拷到Linux服务器解压后,目录名变成乱码或出现?;更隐蔽的情况是Windows下训练脚本报错,找不到形如butterfly_data/某蝶种/xxx.jpg的路径。
原因:zip在中文Windows环境下打包时,文件名编码通常为GBK或CP936,而Linux的unzip默认按UTF-8解码,编码不匹配就会产生乱码。另一个常见原因是最初打包时使用了反斜杠作为路径分隔符,Windows能识别但Linux不行。
解决:Linux下解压时带上编码参数:
unzip -O gbk 蝴蝶分类数据集20类.zip -d butterfly_data如果当前系统的unzip版本不支持-O参数,可以先解压再批量重命名,或者直接把所有乱码目录改成英文代号,顺便把第三章的class_to_idx.json映射表建好。还有一个从源头避免问题的方式:在Windows下用7-Zip把这些数据重新打包为英文文件名再分发。路径分隔符问题则建议统一使用os.path.join或pathlib来处理,不要手拼字符串路径。
6. 从“能分类”到“分得准”:混淆矩阵、测试时增强与模型存根
训练完成只是第一步,让模型在真实场景中稳定可靠才是目的。这一章分享三个我每次做细粒度分类实验都会用到的技巧。
6.1 训练完先画混淆矩阵,别急着部署
我每次训练完第一件事不是看acc,而是把混淆矩阵加载出来逐行看,确认哪些类互相打架。对于蝴蝶这种细粒度分类,类间混淆集中在翅膀花纹相似的品种上非常常见,这类问题属于“数据本身区分度不足”,不是单纯调参能解决的。常做的处理是找到这两个品种的图像差异点,分析模型到底在什么背景下出错。如果训练集里甲种蝴蝶频繁出现在乙种常见的花朵背景中,模型学到背景关联后就会把甲误判为乙,这时真正要调整的是训练集分布,而不是增加网络深度。如果你想沿这个方向扩展成目标检测任务,可以参照“用yolov8训练自己的数据集”的流程重新制作检测标注,但注意分类用途的文件夹结构不能直接当作检测标注来源。
6.2 测试时增强:推理时也做随机裁剪,白捡零点几个点
测试时增强(TTA)是一个性价比很高的推理技巧。验证或推理时,对同一张图做10次随机裁剪,把10次softmax结果平均后再取最大值,这样可以显著降低单次随机裁剪带来的方差,通常能提升0.3到1个百分点的准确率。代码改动很小:
tta_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) probs = [] with torch.no_grad(): for _ in range(10): x = tta_transform(img).unsqueeze(0).to(device) probs.append(torch.softmax(model(x), dim=1)) probs = torch.mean(torch.cat(probs, dim=0), dim=0) pred_idx = probs.argmax().item()这里要说明的是,TTA提升的是单图推理的稳定性而不是模型的真实泛化能力。如果模型本身已经严重过拟合,TTA只能把准确率从0.92抬到0.93,解决不了根本问题。更有效的做法是先把验证集上的混淆矩阵厘清,再考虑要不要用TTA。
6.3 一个数据管理习惯:每次实验都存一份数据hash
最后分享一个让我省下很多对比时间的小习惯:把数据集目录连同划分方式一起做SHA256校验并记录下来。20类蝴蝶数据集这种规模的中小数据,常被人反复拷贝和重新整理。别人发来一个“补数据”的zip,你直接合并进原目录后,某类图片数量可能翻倍,另一类则没有变化,训练出的模型与之前的对比就失去了意义。每次实验结束后,把butterfly_split目录的hash、划分脚本的版本、训练命令一起写入实验log,当模型指标异常时可以快速定位是数据变了还是权重变了。这一条不涉及任何模型技巧,却能规避大量重复性排障工作。
这次在蝴蝶分类数据集上走完整个流程后,我最深的体会是:数据体检和标签映射消耗的时间,往往比训练本身还要长。模型能力的上限由数据质量决定,迁移学习只是帮你更接近这个上限而已。希望这篇笔记能帮你把前半段路径走顺,少踩几个无谓的坑。
本文还有配套的精品资源,点击获取