多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

动物图片数据集JPG实战:10类28K图像从清洗到分类器训练

动物图片数据集JPG实战:10类28K图像从清洗到分类器训练 简介这是一份面向计算机视觉初学者与模型训练爱好者的动物图片数据集覆盖狗、猫、马、蜘蛛、蝴蝶、鸡、羊、牛、松鼠、大象共10个类别适合用于图像分类、迁移学习及数据增强等实验场景。资源包共约2000个文件以jpeg与jpg图像为主另有少量png图片及1个py脚本压缩包整体约586MB主目录按类别分文件夹存放每类图像数量在2K至5K之间便于直接按标签读取与划分训练集、验证集。目前已有790人学习下载可作为课程作业、练手项目或算法对比的现成素材。读者可借此快速搭建多分类基线模型验证卷积网络、预训练微调等方案效果并利用脚本辅助完成数据整理与预处理省去自行爬取与清洗图像的繁琐环节。1. 动物图片数据集 JPG10类28K图像从拿到压缩包到跑通第一个分类器你从某个渠道拿到一个名为「动物图片数据集 JPG10类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出现L或RGBA说明混入了灰度图或带透明通道的图。灰度图直接转 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_okTrue) 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), transformtf) dl DataLoader(ds, batch_size32, shuffleFalse, num_workers4) 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_workers4根据你机器 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(p0.5), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3, hue0.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% 区域模拟遮挡和不同距离。hue0.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), transformtrain_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( weightssample_weights, num_sampleslen(sample_weights), replacementTrue ) train_loader DataLoader(train_ds, batch_size32, samplersampler, num_workers4)逻辑说明class_weights是类别频率的倒数样本权重取其所属类的权重。replacementTrue表示有放回采样少数类会被重复抽到。num_samples设成训练集大小保证一个 epoch 见到的样本数与原来一致。注意用了 sampler 就不能再设shuffleTrue两者冲突。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(weightsmodels.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(), lr1e-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)参数说明weightsIMAGENET1K_V1加载预训练权重别用pretrainedTrue旧写法。冻结所有参数只训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_memoryTrue。现象五换了 backbone 后精度反而下降。原因可能是新 backbone 的输入归一化参数不同或者学习率没重新调。解决确认每个 backbone 对应的预处理配置微调时先用小学习率 warmup 几个 epoch。注意这五条里重复图泄漏和标签错位是最隐蔽的建议在正式训练前花十分钟跑一遍检查脚本比训到一半再回头查省事得多。6. 把 28K 图像用出更高价值渐进式分辨率训练与错误分析baseline 跑通后如果你想让这个数据集发挥更大作用我推荐两个进阶方向渐进式分辨率训练和系统化错误分析。前者能在不换模型的前提下涨点后者能告诉你下一步该补什么数据。渐进式分辨率训练的思路是先用小分辨率如 128快速训几个 epoch让模型学到粗粒度特征再逐步升到 224 甚至 288 微调。这样做的好处是前期计算量小、收敛快后期高分辨率精修细节。实现上只需在训练循环里按 epoch 切换 transformdef 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_size64, samplersampler, num_workers4) for epoch in range(epochs): # 正常训练循环 pass参数说明scale保持不变分辨率提升时裁剪区域绝对像素变大模型能看到更多细节。学习率在切换分辨率时建议降一半避免破坏已学特征。这个策略在 28K 量级上通常能比固定 224 高 1 到 2 个百分点。错误分析则是把验证集上预测错误的样本全部导出按「真实类 → 预测类」分组看混淆矩阵里哪些类对最容易混。动物数据集里猫和狗、狼和狐狸这类视觉相似的类往往是重灾区。把错分样本抽出来人眼看你会发现问题可能出在背景主导比如所有「马」的图都在草地上模型学的是草地、标注错误、或者图像质量太差。针对性地补数据或做背景增强比盲目加 epoch 有效得多。我自己的习惯是每跑完一个模型必做一次错误分析把 top-50 高置信度错分样本存到一个文件夹里下次调参前先翻一遍。这个习惯帮我省下了大量无效实验时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表