简介这份数据集面向计算机视觉初学者、科研人员及竞赛选手提供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.jpgPyTorch 的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(rootdataset, transformtransform) 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(rootdataset/train, transformdata_transforms) train_loader DataLoader(full_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) print(每个 batch 的形状:, next(iter(train_loader))[0].shape)transforms.Normalize必须在ToTensor之后调用因为它接收的是张量而不是 PIL 图片这个顺序写反了会报类型错误。num_workers4表示用4个进程做数据加载训练时 CPU 提前准备下一批图片避免 GPU 空等pin_memoryTrue可以把数据放到锁页内存加快 CPU 到 GPU 的拷贝速度。这两个参数不影响精度但能把训练速度提升不少小数据集上不明显但养成习惯没坏处。实际训练时 train 和 val 的 transform 通常不一样train 需要随机翻转、随机裁剪做数据增强val 只需要把图片缩放到固定尺寸再归一化。这里先不把增强加进来第5章的过拟合部分再展开讲原因。3.3 先写一个冒烟脚本不训练先验证数据能不能正确加载数据管线的坑通常不在写的时候而在跑起来之后。最常见的现象是训练脚本启动了GPU 占用率上去了结果跑了几个 epoch 之后报维度错误或者标签越界。与其等到训练中途翻车不如先写一个几行代码的冒烟脚本把数据加载、模型前向传播、损失函数计算整条链路走一遍确认所有变量维度匹配再开始正式训练。import torch from torchvision import models model models.resnet18(pretrainedFalse) 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(pretrainedTrue) # 替换最后一层全连接输出11类 num_features model.fc.in_features model.fc torch.nn.Linear(num_features, 11)pretrainedTrue会下载 ImageNet 权重文件到本地缓存目录默认是~/.cache/torch/checkpoints/。如果网络环境受限也可以手动下载权重文件放到指定路径用torch.load加载后model.load_state_dict赋进去。替换fc层时用model.fc.in_features读取原始输入维度这是标准写法不会写死数值。其他地方都不动只改最后一层因为前几层学到的底层特征对动物图像依然有效。4.2 微调的超参数推荐学习率、冻结层数、epoch与轮次微调的核心矛盾是学习率太大容易把预训练权重冲坏太小又训不动新加的fc层。我一般按下面这个表设初始值然后根据验证集表现调整。策略冻结层优化器学习率适用场景只训分类层全部冻结Adam1e-3数据量极小或只是想快速跑通全量微调不冻结Adam1e-4数据分布与 ImageNet 差异大冻结前几层微调后面冻结前5层SGD1e-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_size32, shuffleTrue, num_workers4) val_loader torch.utils.data.DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) # ---------- 模型与优化器 ---------- device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(pretrainedTrue) model.fc nn.Linear(model.fc.in_features, 11) 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_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(dim1) 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(fEpoch {epoch1: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_sampleslen(dataset), replacementTrue) train_loader DataLoader(dataset, batch_size32, samplersampler)replacementTrue表示抽样时允许同一张图重复出现这样样本少的类别在不均衡的情况下每个 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), dim1) conf, preds outputs.max(dim1) 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(degrees15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.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(seed42): 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 后的第一个动作也建议你保留这个步骤它让后续调参有了明确方向希望帮到你。本文还有配套的精品资源点击获取