动物图片数据集JPG实战:10类28K图像从清洗到分类器训练
2026/9/24 18:10:51 网站建设 项目流程

简介:这是一份面向计算机视觉初学者与模型训练爱好者的动物图片数据集,覆盖狗、猫、马、蜘蛛、蝴蝶、鸡、羊、牛、松鼠、大象共10个类别,适合用于图像分类、迁移学习及数据增强等实验场景。资源包共约2000个文件,以jpeg与jpg图像为主,另有少量png图片及1个py脚本,压缩包整体约586MB,主目录按类别分文件夹存放,每类图像数量在2K至5K之间,便于直接按标签读取与划分训练集、验证集。目前已有790人学习下载,可作为课程作业、练手项目或算法对比的现成素材。读者可借此快速搭建多分类基线模型,验证卷积网络、预训练微调等方案效果,并利用脚本辅助完成数据整理与预处理,省去自行爬取与清洗图像的繁琐环节。

1. 动物图片数据集 JPG(10类28K+图像):从拿到压缩包到跑通第一个分类器

你从某个渠道拿到一个名为「动物图片数据集 JPG(10类28K+图像)」的压缩包,解压后是一堆按类别分好的文件夹,每个文件夹里塞满 JPG。这时候真正的问题才刚开始:这 28K 张图到底能不能直接喂给模型?类别是否均衡?图片尺寸是否统一?有没有损坏文件、重复图、灰度图混在 RGB 里?我见过太多人拿到数据集直接ImageFolder一把梭,训练到一半 loss 不降,回头查才发现某个类里混进了几十张标注错误的图。这个数据集的价值不在于「图多」,而在于它是一个体量适中、类别清晰、适合做迁移学习与数据管线验证的起点——10 类、28K 量级,单卡几十分钟能跑完一个 epoch,既不会小到过拟合看不出问题,也不会大到调一次参等半天。它适合三类人:刚入门想完整走一遍「数据清洗→划分→训练→评估」的人;想验证自己那套增强策略、采样策略是否有效的人;以及需要一个小规模基准来快速对比不同 backbone 的人。下面我按自己实际处理的顺序,把这条链路拆开讲。

2. 先摸清家底:10 类 28K 图像的目录结构与统计口径

拿到任何图像数据集,第一件事不是写模型,而是写统计脚本。很多人跳过这步,后面所有「玄学」问题都源于此。你需要知道每个类的样本数、图片宽高分布、通道模式、文件完整性。这些数字决定了你后面要不要做重采样、要不要统一 resize、要不要剔除异常样本。

2.1 用 Python 扫一遍目录,输出类别分布与尺寸直方图

假设解压后的根目录叫animals10,下面直接是类别文件夹。下面这段脚本会遍历所有 JPG,统计每类数量、尺寸分布、通道数,并标记无法读取的文件。

import os from collections import defaultdict, Counter from PIL import Image import numpy as np root = "animals10" exts = (".jpg", ".jpeg", ".JPG", ".JPEG") class_count = Counter() size_counter = Counter() mode_counter = Counter() bad_files = [] widths, heights = [], [] for cls in sorted(os.listdir(root)): cls_dir = os.path.join(root, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if not fname.endswith(exts): continue fpath = os.path.join(cls_dir, fname) try: with Image.open(fpath) as im: w, h = im.size mode = im.mode im.verify() # 检测截断文件 except Exception as e: bad_files.append((fpath, str(e))) continue class_count[cls] += 1 size_counter[(w, h)] += 1 mode_counter[mode] += 1 widths.append(w) heights.append(h) print("类别分布:", dict(class_count)) print("通道模式:", dict(mode_counter)) print("宽 min/median/max:", np.min(widths), np.median(widths), np.max(widths)) print("高 min/median/max:", np.min(heights), np.median(heights), np.max(heights)) print("最常见尺寸 top5:", size_counter.most_common(5)) print("损坏文件数:", len(bad_files)) for p, e in bad_files[:10]: print(" ", p, e)

逻辑说明:im.verify()是关键,它不真正解码像素,只检查文件头与数据完整性,速度快,能在训练前把截断的 JPG 揪出来。size_counter(w,h)元组做键,能直接看出尺寸是否统一。参数上,exts我特意把大小写都列上,因为很多数据集从不同来源拼凑,扩展名大小写混乱是常态。

跑完后你大概率会看到两种情况之一:要么尺寸高度统一(比如都是 224×224 附近),说明已经过预处理;要么宽高差异很大,那 resize 策略就得认真设计。类别分布如果最大类与最小类差距超过 2 倍,后面训练时就要考虑带权采样或类平衡增强。

2.2 判断是否需要清洗:重复图、灰度图、极小图三类问题

统计结果里如果mode_counter出现LRGBA,说明混入了灰度图或带透明通道的图。灰度图直接转 RGB 即可,但透明通道要小心——直接convert("RGB")会把透明区域填成黑色,如果原图主体边缘依赖透明,可能引入伪影。常见做法是先合成到白底再转 RGB。

极小图(比如宽或高小于 64)是另一个坑。它们 resize 到 224 后会严重模糊,相当于给模型喂噪声。我一般会设一个阈值,把短边小于 64 的图单独列出来,人工抽看或直接剔除。

重复图检测用感知哈希(pHash)比 MD5 更实用,因为同一张图可能被存成不同质量。下面这段用imagehash库快速找近似重复:

import imagehash from PIL import Image import os hashes = {} dups = [] for cls in os.listdir(root): cls_dir = os.path.join(root, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): fpath = os.path.join(cls_dir, fname) try: with Image.open(fpath) as im: h = imagehash.phash(im.convert("RGB")) except Exception: continue if h in hashes: dups.append((fpath, hashes[h])) else: hashes[h] = fpath print("近似重复对数量:", len(dups)) for a, b in dups[:10]: print(a, "<->", b)

参数说明:phash的默认 hash 尺寸是 8,对轻微缩放、压缩不敏感。如果重复对很多,说明数据集里存在同一张图的多个副本,训练时必须去重,否则验证集可能泄漏训练集内容,指标虚高。

3. 划分训练/验证/测试集:别让同一张图跨集出现

数据划分看似简单,但图像数据集最容易翻车的地方就在这里。如果你先随机划分再去做增强,或者划分时没考虑重复图,验证集指标会好得离谱,上线就崩。正确顺序是:先去重,再划分,最后各自做增强。

3.1 按类别分层抽样的划分脚本与固定随机种子

分层抽样保证每个类在训练/验证/测试中的比例一致,避免某个类在验证集里一张都没有。下面脚本按 7:1.5:1.5 划分,并固定种子保证可复现。

import os, shutil, random from collections import defaultdict random.seed(42) src_root = "animals10_clean" # 去重清洗后的目录 dst_root = "animals10_split" split_ratio = {"train": 0.7, "val": 0.15, "test": 0.15} for cls in os.listdir(src_root): cls_dir = os.path.join(src_root, cls) if not os.path.isdir(cls_dir): continue files = [f for f in os.listdir(cls_dir) if f.lower().endswith((".jpg", ".jpeg"))] random.shuffle(files) n = len(files) n_train = int(n * split_ratio["train"]) n_val = int(n * split_ratio["val"]) splits = { "train": files[:n_train], "val": files[n_train:n_train + n_val], "test": files[n_train + n_val:] } for split, flist in splits.items(): out_dir = os.path.join(dst_root, split, cls) os.makedirs(out_dir, exist_ok=True) for f in flist: shutil.copy(os.path.join(cls_dir, f), os.path.join(out_dir, f)) print(cls, "total", n, "train", len(splits["train"]), "val", len(splits["val"]), "test", len(splits["test"]))

逻辑说明:random.seed(42)是后悔药,没有它你第二次划分结果不同,实验无法对比。按类循环保证分层。shutil.copy保留原文件,避免后续增强污染原始数据。参数上,7:1.5:1.5 适合 28K 量级——训练集约 19.6K,验证和测试各约 4.2K,足够评估。

3.2 用 ImageFolder 和 DataLoader 验证划分结果

划分完别急着训练,先用ImageFolder加载一遍,确认每个 split 的类别数和样本数符合预期,同时检查是否有空文件夹。

from torchvision import datasets, transforms from torch.utils.data import DataLoader tf = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), ]) for split in ["train", "val", "test"]: ds = datasets.ImageFolder(os.path.join(dst_root, split), transform=tf) dl = DataLoader(ds, batch_size=32, shuffle=False, num_workers=4) x, y = next(iter(dl)) print(split, "classes", len(ds.classes), "samples", len(ds), "batch", x.shape, y.shape)

参数说明:Resize(256)CenterCrop(224)是验证/测试阶段的标准做法,先缩到短边 256 再中心裁剪,保留主体。训练阶段则应该用RandomResizedCrop(224)做增强。num_workers=4根据你机器 CPU 核数调整,太少会拖慢数据加载,太多会争抢资源。

如果某个 split 的len(ds.classes)不是 10,说明有类别文件夹为空或命名不一致,回去检查目录。

4. 训练管线搭建:从增强策略到类别不平衡处理

数据准备好了,接下来是训练管线。这个数据集 10 类 28K,单卡完全能跑,但增强策略和采样策略直接决定最终精度。我一般先用一个轻量 backbone 跑通,再换大的。

4.1 训练增强与验证增强的差异配置

训练增强要「狠」,验证增强要「稳」。下面是我常用的配置,针对动物图片,颜色抖动和随机裁剪对纹理、姿态变化有帮助。

from torchvision import transforms train_tf = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.6, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.05), transforms.RandomRotation(15), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) val_tf = 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]), ])

参数说明:scale=(0.6, 1.0)允许裁到原图 60% 区域,模拟遮挡和不同距离。hue=0.05要小,动物毛色是重要特征,色相大幅偏移会破坏语义。RandomRotation(15)对动物姿态合理,但别超过 30 度,否则背景出现黑边。Normalize 的均值方差是 ImageNet 统计值,用预训练权重时必须一致。

4.2 类别不平衡时的 WeightedRandomSampler 用法

如果第 2 章统计发现最大类是最小类的 2 倍以上,直接训练会让模型偏向多数类。两种解法:带权采样或类权重损失。带权采样更常用,因为它让每个 batch 内类别更均衡。

import numpy as np from torch.utils.data import WeightedRandomSampler from torchvision import datasets train_ds = datasets.ImageFolder(os.path.join(dst_root, "train"), transform=train_tf) targets = [s[1] for s in train_ds.samples] class_counts = np.bincount(targets) class_weights = 1.0 / class_counts sample_weights = [class_weights[t] for t in targets] sampler = WeightedRandomSampler( weights=sample_weights, num_samples=len(sample_weights), replacement=True ) train_loader = DataLoader(train_ds, batch_size=32, sampler=sampler, num_workers=4)

逻辑说明:class_weights是类别频率的倒数,样本权重取其所属类的权重。replacement=True表示有放回采样,少数类会被重复抽到。num_samples设成训练集大小,保证一个 epoch 见到的样本数与原来一致。注意:用了 sampler 就不能再设shuffle=True,两者冲突。

4.3 用预训练 ResNet 跑通第一个 baseline

管线搭好后,先用 ResNet18 或 ResNet50 跑一个 baseline。冻结 backbone 先训分类头,再解冻微调,是省时且稳的做法。

import torch import torch.nn as nn from torchvision import models device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) for p in model.parameters(): p.requires_grad = False model.fc = nn.Linear(model.fc.in_features, 10) model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-3) for epoch in range(5): model.train() 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() print("epoch", epoch, "done")

参数说明:weights=IMAGENET1K_V1加载预训练权重,别用pretrained=True旧写法。冻结所有参数只训fc,学习率可以设大一点 1e-3。跑 5 个 epoch 后解冻全部,学习率降到 1e-4 再微调。如果显存够,batch size 可以上 64 或 128,配合学习率线性缩放。

5. 避坑与排查:处理这个数据集时最常见的 5 个翻车现场

这一章是我自己踩过的坑,按「现象 → 原因 → 解决」写,你遇到时可以直接对照。

现象一:训练 loss 震荡不降,验证精度卡在 10% 左右。原因通常是标签与文件夹名不对应,或者ImageFolder读到的类别顺序和你以为的不一致。解决:打印train_ds.class_to_idx,确认映射关系;再抽几张图连同标签可视化,人眼核对。

现象二:验证精度异常高,测试集却崩了。原因是重复图跨集泄漏,或者同一张图的增强版本同时出现在训练和验证。解决:划分前做 pHash 去重,划分后再用哈希交集检查三个 split 之间是否有重复。

现象三:训练时突然报OSError: image file is truncated原因是部分 JPG 下载不完整。解决:第 2 章的im.verify()能提前发现;如果已经开训,在 Dataset 的__getitem__里加 try/except 跳过坏图,并记录日志。

现象四:GPU 利用率低,训练速度慢。原因多半是num_workers设太小或磁盘 IO 瓶颈。解决:num_workers设为 CPU 核数的 2/3;如果图片存在机械硬盘,先拷到 SSD;开启pin_memory=True

现象五:换了 backbone 后精度反而下降。原因可能是新 backbone 的输入归一化参数不同,或者学习率没重新调。解决:确认每个 backbone 对应的预处理配置;微调时先用小学习率 warmup 几个 epoch。

注意:这五条里,重复图泄漏和标签错位是最隐蔽的,建议在正式训练前花十分钟跑一遍检查脚本,比训到一半再回头查省事得多。

6. 把 28K 图像用出更高价值:渐进式分辨率训练与错误分析

baseline 跑通后,如果你想让这个数据集发挥更大作用,我推荐两个进阶方向:渐进式分辨率训练和系统化错误分析。前者能在不换模型的前提下涨点,后者能告诉你下一步该补什么数据。

渐进式分辨率训练的思路是:先用小分辨率(如 128)快速训几个 epoch,让模型学到粗粒度特征,再逐步升到 224 甚至 288 微调。这样做的好处是前期计算量小、收敛快,后期高分辨率精修细节。实现上只需在训练循环里按 epoch 切换 transform:

def build_train_tf(size): return transforms.Compose([ transforms.RandomResizedCrop(size, scale=(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.3, 0.3, 0.3, 0.05), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) schedule = [(128, 3), (160, 3), (224, 5)] # (分辨率, epoch 数) for size, epochs in schedule: train_ds.transform = build_train_tf(size) train_loader = DataLoader(train_ds, batch_size=64, sampler=sampler, num_workers=4) for epoch in range(epochs): # 正常训练循环 pass

参数说明:scale保持不变,分辨率提升时裁剪区域绝对像素变大,模型能看到更多细节。学习率在切换分辨率时建议降一半,避免破坏已学特征。这个策略在 28K 量级上通常能比固定 224 高 1 到 2 个百分点。

错误分析则是把验证集上预测错误的样本全部导出,按「真实类 → 预测类」分组,看混淆矩阵里哪些类对最容易混。动物数据集里,猫和狗、狼和狐狸这类视觉相似的类往往是重灾区。把错分样本抽出来人眼看,你会发现问题可能出在:背景主导(比如所有「马」的图都在草地上,模型学的是草地)、标注错误、或者图像质量太差。针对性地补数据或做背景增强,比盲目加 epoch 有效得多。

我自己的习惯是每跑完一个模型,必做一次错误分析,把 top-50 高置信度错分样本存到一个文件夹里,下次调参前先翻一遍。这个习惯帮我省下了大量无效实验时间。希望帮到你。

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

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

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

立即咨询