简介:这份数据集面向计算机视觉初学者、科研人员及竞赛选手,提供11种常见动物的已标注图像约7000张,涵盖狗、牛、羊、老虎、猪等类别,数据已经过预处理,可直接作为分类网络输入,省去手动清洗与统一尺寸的步骤。压缩包内共2000个文件,以1998张jpg图片为主体,另附1个python脚本与1个json文件,整体大小172.83MB,轻量易下载。json内保存了类别与索引的对应关系,方便加载时映射;show脚本可一键可视化样本,便于检查数据质量;训练集与测试集已按类别分开存放,天然适合划分训练集和验证集。目前已有117人学习下载,适合用于图像分类模型训练、不同网络结构对比、消融实验、课程设计乃至迁移学习基线测试。资源目录结构直观,同一类别图片集中放置,既利于批量读取,也便于快速核对标注,对需要快速启动分类项目的开发者效率提升明显。
1. 7000张已标注数据,为什么第一步不是选模型而是先拆数据
拿到这份“11种常见动物图像分类数据集【已标注,约7000张数据】”,我见过太多人第一反应是直接丢进某个图像分类模型里从零训练,从ResNet跑到ViT,最后在验证集上拿到一个不上不下的准确率,然后开始怀疑模型不行。实际上7000张图分到11个类别,每类平均只有600多张,这个规模对深度学习来说属于典型的小样本场景,从零训练一个深层CNN几乎必然过拟合。这份数据集真正适合的场景是:第一次完整跑通图像分类全流程、做预训练模型微调的基线实验、或者在课上带学生走一遍数据清洗到模型部署的闭环。它的价值不在“大”,而在“已标注、可直接用”,省掉最耗时的标注环节,把精力集中在数据组织、训练策略和调参上,这也是我拿到任何一份数据集后第一个要说的事——先拆数据,再谈模型。
2. 动手前先拆数据:目录结构、标签文件与train/val/test划分
2.1 先认识这份已标注数据的两种常见组织方式
“已标注”这三个字在不同数据集里含义差别很大。常见做法有两种:一种是目录名即标签,数据目录下按类别建子文件夹,每类放对应图片,比如dataset/cat/001.jpg,PyTorch 的torchvision.datasets.ImageFolder直接就能读;另一种是图片放在一个大目录里,旁边挂一个 CSV 或 JSON 标注文件,文件里写文件名,类别编号的映射关系。这份数据集核心字段是“11种常见动物、已标注、约7000张”,最可能的是第一种目录即标签的方式,因为对图像分类来说这是成本最低的标注形式,人工把图片拖进对应文件夹就算标完了。
拿到数据后先别急着写训练代码,在终端里看一眼目录结构:
# 查看数据集根目录下的结构,限制两层深度 tree -L 2 dataset/ # 统计每个类别目录下的图片数量 for d in dataset/*/; do echo -n "$d: " ls "$d" | wc -l donetree -L 2只展开两层,避免图片文件把终端刷爆;第二段循环统计每个类别的图片数量,这一步能快速发现类别不均衡问题。我在实际项目里会把这个统计结果记下来,因为后面划分数据集、选损失函数、判断是否要做类别加权都要用它。
还有一种情况是下载下来只有一个压缩包,解压后全是.jpg,这时候就要找标注文件。先用ls看看根目录下有没有csv、json、txt后缀的文件,再用head -n 5看格式。如果只有图片没有标注,别自己硬猜标签,回头找发布页的说明,这是最容易踩坑的第一步。
2.2 用脚本统计类别分布与样本量,先过一遍数据质量
目录结构看完后,我会写一个几十行的 Python 脚本把类别分布、图片尺寸、损坏文件一次性查清楚。这一步的价值在于后面所有决策都建立在这些数字上:如果某个类只有200张而另一个类有1000张,那训练时的采样策略和评估指标都要跟着改;如果图片尺寸千奇百怪,加载管线里就要考虑是否统一缩放。
import os from collections import Counter from PIL import Image data_root = "dataset" class_counts = Counter() size_set = set() broken = [] for class_name in sorted(os.listdir(data_root)): class_dir = os.path.join(data_root, class_name) if not os.path.isdir(class_dir): continue for fname in os.listdir(class_dir): fpath = os.path.join(class_dir, fname) try: with Image.open(fpath) as img: size_set.add(img.size) # (宽, 高) class_counts[class_name] += 1 except Exception: broken.append(fpath) print("类别分布:", dict(class_counts)) print("图片尺寸种类:", size_set) print("损坏文件数:", len(broken), broken[:5])这段代码遍历每个类别目录,用 Pillow 打开图片读取实际尺寸并计数。Camera打开图片时如果文件损坏会抛异常,统一记到broken列表里。注意PIL.Image.open默认是惰性加载,只读文件头不读完整像素,所以这个脚本跑得很快,7000张图几秒钟就能扫完。图片尺寸这一步很容易被忽略,我遇到过一个数据集里混着224x224和1024x768两种分辨率,如果不统一尺寸,DataLoader 会直接报错。
顺手把所有图片转成 RGB 三通道也是个好习惯,后面用 ImageNet 预训练权重时输入必须是三通道,灰度图会踩坑。不用担心这步耗时,批处理很快。
2.3 按类别分层划分数据集:三个必设参数与一段可复用脚本
数据集划分是图像分类里最不该偷懒的环节。随意按顺序切,或者用默认的随机切,都可能让某个类别在训练集和验证集里的比例失衡。正确做法是按类别分层抽样,保证每个类别在三个子集里的占比和原始数据一致。我一般按 7:1.5:1.5 划分训练、验证、测试,对7000张这个量级来说,测试集留1000张左右足够评估最终效果。
import random from collections import defaultdict random.seed(42) # 固定种子,保证可复现 ratio_train, ratio_val = 0.7, 0.15 splits = {"train": [], "val": [], "test": []} for class_name, files in class_files.items(): random.shuffle(files) n_train = int(len(files) * ratio_train) n_val = int(len(files) * ratio_val) for f in files[:n_train]: splits["train"].append((f, class_name)) for f in files[n_train:n_train + n_val]: splits["val"].append((f, class_name)) for f in files[n_train + n_val:]: splits["test"].append((f, class_name))三个关键参数:random.seed(42)固定随机种子,让划分结果可复现,换台机器跑也是同样的分布;ratio_train和ratio_val控制比例,测试集比例就是1 - ratio_train - ratio_val。class_files是一个defaultdict(list),key 是类别名,value 是该类别下所有文件路径,在上一节的统计脚本里顺手就能建好。
分层划分的实现思路是先把每个类别的文件单独打乱,再按比例切三段。这样即使某个类别只有200张图,也能保证它在三个子集里的比例一致,不会出现验证集里某类只有三五张的尴尬。划分结果建议写成一个 CSV 存下来,后面每次训练都用同一份划分文件,不同实验之间才有可比性,这也是复现实验最基础的一步。
提示:如果你下载的数据集里已经自带了 train/val 目录,就别再自己划一遍了,直接用自带的划分,最多用脚本确认一下各类别比例是否合理。
3. 把已标注图片接进PyTorch:写一个能落地复现的加载管线
3.1 为什么用ImageFolder而不是手写Dataset——读目录即得标签
对于目录结构刚好是“每类一个文件夹”的数据集,torchvision.datasets.ImageFolder是最省事的加载方案。它会自动扫描根目录下的子文件夹,把文件夹名作为类别标签,按字母序生成类别索引,并把图片路径和索引对应起来。这比手写一个 Dataset 类少掉一大堆样板代码,而且出错概率低——手写 Dataset 最常见的翻车点就是标签名和文件名没对齐,ImageFolder 把这个过程变成了自动化操作。
from torchvision import datasets, transforms transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), ]) dataset = datasets.ImageFolder(root="dataset", transform=transform) print("类别映射:", dataset.class_to_idx) # {'cat': 0, 'dog': 1, ...} print("样本数:", len(dataset)) # 约7000 img, label = dataset[0] print("单张样本形状:", img.shape, "标签:", label)ImageFolder的构造参数里root指向数据集根目录,class_to_idx会按文件夹名的字母序生成映射字典。注意类别索引是自动按字母序排的,不是按视觉相似度或者文件夹创建顺序。后面如果用预训练模型做迁移学习,类别索引只需要在最后一层全连接层映射到11个输出即可,不需要手动维护标签文件。
这段代码跑通后,说明数据能被 PyTorch 正常读取,数据管线的最核心部分已经打通。如果这一步报错,先检查是不是有文件夹是空的、有没有图片格式损坏、有没有隐藏文件混在目录里——这些是 ImageFolder 最常见的三个雷。
3.2 加载管线的三个关键参数:归一化均值、图像尺寸、batch size
图像分类的加载管线里,有三个参数直接决定训练能否收敛,必须手动设清楚。第一个是归一化的 mean 和 std,使用 ImageNet 预训练权重时必须用 ImageNet 的统计值mean=[0.485, 0.456, 0.406]、std=[0.229, 0.224, 0.225],否则预训练权重等于白加载;第二个是图像尺寸,ResNet 系列要求输入 224x224,输入尺寸不一致会导致网络结构计算出错;第三个是 batch size,对7000张这个规模,32 到 64 是合理的起点,太小梯度不稳定,太大会把显存撑爆。
from torch.utils.data import DataLoader from torchvision import transforms, datasets data_transforms = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) full_dataset = datasets.ImageFolder(root="dataset/train", transform=data_transforms) train_loader = DataLoader(full_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) print("每个 batch 的形状:", next(iter(train_loader))[0].shape)transforms.Normalize必须在ToTensor之后调用,因为它接收的是张量而不是 PIL 图片,这个顺序写反了会报类型错误。num_workers=4表示用4个进程做数据加载,训练时 CPU 提前准备下一批图片,避免 GPU 空等;pin_memory=True可以把数据放到锁页内存,加快 CPU 到 GPU 的拷贝速度。这两个参数不影响精度,但能把训练速度提升不少,小数据集上不明显,但养成习惯没坏处。
实际训练时 train 和 val 的 transform 通常不一样,train 需要随机翻转、随机裁剪做数据增强,val 只需要把图片缩放到固定尺寸再归一化。这里先不把增强加进来,第5章的过拟合部分再展开讲原因。
3.3 先写一个冒烟脚本:不训练,先验证数据能不能正确加载
数据管线的坑通常不在写的时候,而在跑起来之后。最常见的现象是:训练脚本启动了,GPU 占用率上去了,结果跑了几个 epoch 之后报维度错误或者标签越界。与其等到训练中途翻车,不如先写一个几行代码的冒烟脚本,把数据加载、模型前向传播、损失函数计算整条链路走一遍,确认所有变量维度匹配再开始正式训练。
import torch from torchvision import models model = models.resnet18(pretrained=False) model.fc = torch.nn.Linear(model.fc.in_features, 11) # 取一个 batch 的真实数据做前向传播 images, labels = next(iter(train_loader)) outputs = model(images) print("输出形状:", outputs.shape) # 期望 [32, 11] loss_fn = torch.nn.CrossEntropyLoss() loss = loss_fn(outputs, labels) print("损失值:", loss.item())冒烟脚本的意义在于只验证链路不训练,确认模型能接收 DataLoader 输出的 batch、最后一层全连接层的输出维度是类别数11、CrossEntropyLoss 能正常算损失。如果这里输出 shape 不是[32, 11],多半是torchvision模型库里fc层的名字记错了——ResNet 系列最后一层叫fc,但 VGG 和 DenseNet 的最后一层有别的名字,写错了 PyTorch 会在赋值时直接抛错,这在换模型结构时是高频翻车点。pretrained先设成False,冒烟只验证维度是否正确,不在乎权重质量。
4. 用ResNet迁移学习跑通第一个分类baseline:参数怎么设、日志看什么
4.1 7000张数据为什么必须用预训练权重做微调
从零训练一个 ResNet18 在 ImageNet 上需要128万张图训几十个 epoch,而这份数据集只有7000张、11个类别。从零初始化卷积核,相当于让模型在600张图里重新学“猫长什么样”,结果必然是欠拟合或者过拟合。迁移学习的思路是:把在 ImageNet 上训好的权重加载进来,低层卷积的边、角、纹理特征直接复用,只需要微调高层语义特征和最后的分类层。对7000张图这个规模,这是唯一靠谱的路线,别在从零训练上浪费时间。
import torch from torchvision import models # 使用 ImageNet 预训练权重 model = models.resnet18(pretrained=True) # 替换最后一层全连接,输出11类 num_features = model.fc.in_features model.fc = torch.nn.Linear(num_features, 11)pretrained=True会下载 ImageNet 权重文件到本地缓存目录,默认是~/.cache/torch/checkpoints/。如果网络环境受限,也可以手动下载权重文件放到指定路径,用torch.load加载后model.load_state_dict赋进去。替换fc层时用model.fc.in_features读取原始输入维度,这是标准写法,不会写死数值。其他地方都不动,只改最后一层,因为前几层学到的底层特征对动物图像依然有效。
4.2 微调的超参数推荐:学习率、冻结层数、epoch与轮次
微调的核心矛盾是:学习率太大容易把预训练权重冲坏,太小又训不动新加的fc层。我一般按下面这个表设初始值,然后根据验证集表现调整。
| 策略 | 冻结层 | 优化器 | 学习率 | 适用场景 |
|---|---|---|---|---|
| 只训分类层 | 全部冻结 | Adam | 1e-3 | 数据量极小或只是想快速跑通 |
| 全量微调 | 不冻结 | Adam | 1e-4 | 数据分布与 ImageNet 差异大 |
| 冻结前几层,微调后面 | 冻结前5层 | SGD | 1e-3 | 数据量中等,标准做法 |
对这个11类、7000张的数据集,我建议直接用全量微调,学习率1e-4,优化器用 Adam 或者带动量的 SGD。冻结太多层在这个规模上反而限制模型对动物图像特有特征的表达能力。epoch 设20到30,训练过程里每轮都做一次验证,保存验证准确率最高的权重,这就是最朴素的 early stopping 模型选择策略。
这里有个血泪经验:全量微调不是把整个网络当随机初始化来训。学习率一旦超过3e-4,预训练权重会被快速破坏,验证集曲线会出现典型的“训练损失下降、验证损失先降后涨”的不稳定波形。如果看到这种情况,第一反应不是加正则化,而是把学习率降一个量级。
4.3 训练脚本与验证指标:准确率之外还要看混淆矩阵
下面是微调训练的完整脚本,数据加载部分沿用第3章的代码,直接从这里接起来跑即可。验证阶段除了算准确率,还会把预测结果和真实标签存下来,方便后面画混淆矩阵分析哪些类别容易互相搞混。
import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms, models # ---------- 数据加载 ---------- data_path = "dataset" train_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) val_transform = 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(data_path + "/train", train_transform) val_ds = datasets.ImageFolder(data_path + "/val", val_transform) train_loader = torch.utils.data.DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = torch.utils.data.DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4) # ---------- 模型与优化器 ---------- device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = models.resnet18(pretrained=True) model.fc = nn.Linear(model.fc.in_features, 11) model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) # ---------- 训练循环 ---------- best_acc = 0.0 for epoch in range(20): 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) # 每轮验证 model.eval() correct, total = 0, 0 all_preds, all_labels = [], [] with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) all_preds.extend(preds.cpu().tolist()) all_labels.extend(labels.cpu().tolist()) acc = correct / total print(f"Epoch {epoch+1:02d} | Loss {running_loss/len(train_ds):.4f} | Val Acc {acc:.4f}") if acc > best_acc: best_acc = acc torch.save(model.state_dict(), "best_animal_model.pth") scheduler.step()训练循环有三个细节值得说明。optimizer.zero_grad()必须在每步反向传播之前清空梯度,否则梯度会跨 batch 累加,导致训练发散;scheduler.step()是学习率调度器,每10个 epoch 衰减到原来的0.1,前10轮用较大学习率快速收敛,后10轮用较小的学习率精调;torch.no_grad()包裹验证过程,告诉 PyTorch 不需要计算梯度,省显存也省时间。
验证指标只看准确率不够,还要把all_preds和all_labels存下来。用 sklearn 的confusion_matrix画个混淆矩阵,能一眼看出哪两个类别互混严重。我之前在一个动物分类项目里发现“狐狸”和“狗”的混淆率接近30%,模型整体准确率看着有85%,但狐狸这一类单独拿出来准确率只有60%,这种问题只能靠混淆矩阵暴露,整体准确率会把它藏得干干净净。
5. 避坑排查:7类在动物分类数据上最容易翻车的问题
5.1 类别不均衡:现象是“准确率高但某类全错”
现象:训练结束后整体准确率在85%以上,但逐个类别检查召回率,发现某两类几乎全错。比如“羊”的召回率只有20%,大部分羊被预测成了“牛”。原因:下载的解压包里类别分布就不均衡,羊只有100来张,牛有800张,模型在训练中天然偏向样本多的类别。解决:先统计分布确认问题,再用分层采样保证每个 batch 里各类别比例均匀。WeightedRandomSampler是根据样本权重做不放回采样的工具,权重和样本量成反比,样本少的类别单张权重更高。
from torch.utils.data import WeightedRandomSampler # 按类别样本量反比设置权重 class_weights = [sum(class_counts.values()) / class_counts[cls] for cls in class_counts] sample_weights = [class_weights[label] for _, label in dataset.samples] sampler = WeightedRandomSampler(sample_weights, num_samples=len(dataset), replacement=True) train_loader = DataLoader(dataset, batch_size=32, sampler=sampler)replacement=True表示抽样时允许同一张图重复出现,这样样本少的类别在不均衡的情况下每个 epoch 也能被充分采到。代价是训练速度变慢,因为 DataLoader 的随机采样索引不再按顺序读文件,但这点损耗可以接受。加了这个采样器后整体准确率可能略降,但小类别的召回率会明显上升。
5.2 标签错标与背景干扰:噪声数据比你想的更多
现象:训练集损失稳定下降,验证集准确率卡在某个值上不去,把误判的样本打印出来发现有些图本身标注就错了。比如一张明显是豹子的图被标成了“猫”。原因:标注是人工完成的,“已标注”不代表“全对”,特别是动物图像里相似物种、幼年个体、遮挡严重的图,人工标错率能到3%~5%。7000张图里大概有200到300张噪声标签。解决:把训练集中模型预测置信度低的样本全部导出,人工过一遍。低置信度往往意味着标签可疑或图像本身模糊。
# 训练结束后,统计各样本置信度并导出低置信度样本 model.eval() low_conf_samples = [] with torch.no_grad(): for images, labels in train_loader: images = images.to(device) outputs = torch.softmax(model(images), dim=1) conf, preds = outputs.max(dim=1) for img, label, pred, c in zip(images, labels, preds, conf): if c.item() < 0.55: # 置信度阈值,根据实验调整 low_conf_samples.append((label.item(), pred.item(), c.item()))置信度阈值设多少取决于训练收敛情况,我通常设在0.5到0.6之间,太低了筛不出噪声,太高了会把正常难分样本也挑出来。把这些样本整理成一张网格图,每张图下面标注“真实标签-预测标签-置信度”,肉眼扫一遍。这个习惯能让你对手里的数据质量有非常具体的认知,比任何数据增强技巧都值钱。
5.3 过拟合:train acc 99%而val崩掉的三个信号
现象:训练集准确率冲到99%,验证集准确率只有75%左右,gap 越来越大。原因很简单,7000张图太少,模型把训练集的细节连带噪声一起背下来了,也跟微调时没有加数据增强有关。解决:先从数据增强开始,加随机水平翻转、随机旋转、随机裁剪;再往模型里加 Dropout 或者使用权重衰减。数据增强是最直接的后悔药,对图像分类任务来说几乎永远有效。
train_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(degrees=15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ])注意RandomResizedCrop的scale参数含义是裁剪面积占原图面积的比例,设成(0.7, 1.0)表示随机裁剪原图70%到100%的区域再缩放到224,相当于模拟了不同距离下的观察视角。ColorJitter的三个参数分别控制亮度、对比度、饱和度扰动范围,这是模拟光照变化。数据增强不是越猛越好,增强过猛会把图像破坏到模型认不出来,验证集准确率反而下降。从温和的增强组合开始,逐步加大幅度,观察验证集变化曲线,这是另一个容易走偏的地方。
5.4 类别相似度高:豹子vs花猫、狐狸vs狗这类混淆怎么处理
现象:混淆矩阵里两个类别互混严重,比如“豹子”被预测成“花猫”的比例接近25%。原因:这两个类别在纹理、颜色、体态上本身就高度相似,数据量又不大,模型没有学到区分性的特征。解决:先检查标注是否严格,豹子的斑点纹路和猫的条纹在视觉上可以区分,但如果训练图里大量混入低分辨率、遮挡严重的图,这个区分度会被噪声淹没。然后考虑把相似类别合并,或者增加相似类别的训练样本。
我之前遇到过一个项目,客户要求区分“猎豹”和“花豹”,两者都是猫科、都有斑点,7000张里这两类加起来只有不到300张。合并成一个“豹类”之后整体准确率提升了6个百分点,客户也能接受。这是一个务实的取舍:数据集规模撑不起细粒度区分时,粗粒度分类是更稳定的选择。反过来如果明确要求细分,就补样本、补标注,而不是继续调模型。
5.5 训练结果不可复现:随机种子与PyTorch的“玄学”
现象:同一份代码,同一个数据集,跑两次得到的验证集准确率差2~3个百分点。原因:PyTorch 里数据加载的 shuffle、模型权重初始化、数据增强的随机裁剪都用了随机数,不固定种子时每次训练路径完全不同。这对调参是致命的,因为你无法判断准确率变化是改动引起的还是纯随机波动。解决:在训练脚本开头固定所有随机源。
import random, numpy as np, torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = Falsecudnn.deterministic = True让 cuDNN 使用确定性卷积算法,代价是速度略慢;cudnn.benchmark = False禁止 cuDNN 自动选择最优算法。这两个开关一起设,才能保证同一份代码在相同硬件上跑出完全相同的结果。如果换了 GPU 型号,结果仍然可能有细微差异,因为不同 GPU 上浮点运算顺序不完全一致,这个属于 PyTorch 的玄学范畴,建议在项目文档里记录训练时的 GPU 型号。
6. 在baseline之上再进一步:用数据增强与模型选型把准确率推高3到5个点
跑通第一个 baseline 并解决掉第5章那些坑之后,模型验证集准确率应该稳定在80%以上。这时要做的是控制变量的消融实验,而不是盲目换模型或者继续堆 epoch。我只改一个变量,其他全部不动:先加数据增强,再换模型结构,最后调学习率。每次改动的准确率变化都记录在同一张表里,标注好配置和当时的 seed。
从 ResNet18 换到 ResNet50 是常见的第一步尝试,参数量大了但仍然在7000张数据的承受范围内。也可以试试 EfficientNet-B0,它用更少的参数达到相近的精度,但要注意不同 torchvision 版本里 EfficientNet 的权重加载方式和 ResNet 略有差别。至于 transformer 图像分类方向,ViT 在这类小数据集上直接微调表现通常不如 CNN,但用 DeiT 这类带知识蒸馏的变体有机会跟 ResNet 打平或略高,有兴趣可以当进阶实验跑一下,不用一上来就放弃。
最后一个习惯值得坚持:把测试集里模型预测出错的样本单独导出成一张网格图,下面标注真实标签和预测标签,肉眼看一遍。区分这些错误是标注错、遮挡严重、还是模型理解不了,比再调三个超参数都管用。我经手的每个图像分类项目几乎都在这个环节发现了训练集里懒得清理却必须清理的问题。这是我每次跑完 baseline 后的第一个动作,也建议你保留这个步骤,它让后续调参有了明确方向,希望帮到你。
本文还有配套的精品资源,点击获取