ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

动物图像分类实战:PyTorch数据预处理与ResNet训练避坑指南

动物图像分类实战:PyTorch数据预处理与ResNet训练避坑指南 简介一套已标注的11种常见动物图像分类数据集共约7000张图片覆盖狗、牛、羊、老虎、猪等类别面向深度学习初学者、算法工程师及课程设计开发者可用于图像分类网络训练、算法效果验证与教学演示减少数据收集和标注成本。数据已完成预处理并划分出训练集与测试集按类别目录存放可作为分类网络的直接输入附带的Python可视化脚本便于快速浏览各类别样本JSON文件则提供类别名称与标签映射简化数据加载流程。压缩包内共2000个文件以1998张jpg图像为主另有1个Python脚本和1个JSON说明文件整体大小约172.83MB目录层次清晰解压后即可投入实验。目前已有117人学习下载适合需要现成标准数据集开展模型训练、精度对比或项目实践的人群。1. 11种常见动物图像分类数据集先验证同分布再谈模型精度做动物识别最怕的不是模型不收敛而是训完才发现训练集和验证集根本不是同一个分布。最近我拆了一份11类常见动物图像分类数据集约7000张已标注图片覆盖狗、牛、羊、老虎、猪等常见类别train/test 划分和可视化脚本都齐了。放进训练管线之前我的第一件事不是急着搭网络而是把图片、json标签和目录结构三样对齐。多数开箱即用的数据集坑都藏在标签顺序和类别不均衡里。这份资源适合两类人急着交实验结果的算法工程师和不想被数据预处理劝退的图像分类新手。2. 拆包看货目录结构、json标签与7000张图片的真相2.1 先看目录树train/test怎么分、类名怎么对上打开压缩包第一件事我一般不看 README先看目录树。大多数图像分类数据集的结构逃不出两种一种是 train/、test/ 下直接堆图片标签全靠 json 或 csv 对另一种是 train/、test/ 下再按类建子目录torchvision 的 ImageFolder 直接能读。这份数据集属于后者训练集和测试集里各类图片分别放在各自的目录中。所以第一眼要确认的是每个类子目录的命名到底是英文类名还是 0~10 的编号。在终端里执行tree -L 2 ./如果机器上没有 tree用 find 也能看find . -maxdepth 2 -type d | sort输出出来后重点看两个东西。第一train 下是不是正好有 11 个子目录test 下是否也是同样的 11 个第二子目录命名和 json 里的类别描述是否一致。这份数据集的图片文件名是 00000016.jpg、00000226.jpg 这种六位数字连续编号你没法从文件名判断类别所以目录名就是第一层标签信息来源这个必须一开始就核对清楚。2.2 读json标签cls_names与目录顺序的对齐问题接下来开 json。资源描述里写了“具体查看 json 文件”实际包内的 json 文件名可能是 meta.json、labels.json 或 class_indices.json。我下面以 meta.json 举例不管它叫什么读法都一样import json with open(meta.json, r, encodingutf-8) as f: meta json.load(f) print(json keys:, list(meta.keys())) print(classes:, meta.get(cls_names))常见的结构是 cls_names 存类别名列表train/test 部分是一组 image 和 label 的记录。拿到后我第一眼确认三件事cls_names 是不是正好 11 个label 的取值是否落在 0~10以及 label 下标和 cls_names 的顺序是不是一一对应。这里有个关键点如果 train 目录下用编号建子目录而 cls_names 是按语义顺序排列的那编号子目录和 cls_names 下标很可能不是同一套顺序。比如编号 0 的子目录可能对应 cls_names 里的第 3 项。这种错位在训练里不会让 loss 变大但会让推理结果完全对不上类名属于最隐蔽的坑。所以 2.1 和 2.2 这两步不能省。2.3 运行show脚本先看数据再谈训练json 看完建议马上跑一遍资源自带的 show 脚本。它的作用通常是把每个类别随机抽几张图拼成一张网格图存下来让你一眼看到数据长什么样。如果包内入口叫 show.py运行方式一般是python show.py --data_dir ./ --num_per_class 5具体参数以脚本里 argparse 为准有的脚本叫 --data-root、--num-images。跑完后会生成类似 preview.png 的图片。打开它看三个点一是图像清晰度和尺寸是否统一二是类别之间是否存在背景强相关比如羊都在草地上、老虎都在笼子里三是同类图片的多样性够不够。这三个观察结论直接决定后面要不要加数据增强、要不要做类别均衡不是可有可无的仪式。提示如果 show 脚本是用 matplotlib 写的在无显示环境的服务器上运行会报 no display 错误先执行export MPLBACKENDAgg再跑。2.4 统计类别分布“约7000张”到底怎么分配图片数量和类别分布直接决定训练策略我一般先写一个统计脚本。常见做法是遍历 train 和 test 目录按类累计文件数再和 json 记录对一遍import os from collections import Counter def count_images(root): counter Counter() for cls_name in os.listdir(root): cls_path os.path.join(root, cls_name) if os.path.isdir(cls_path): counter[cls_name] len(os.listdir(cls_path)) return counter for split in [train, test]: print(split, count_images(os.path.join(./, split)))这段逻辑是按子目录名统计图片数量输出形如train Counter({dog: 720, cow: 540, ...})。拿到分布后关注两个数字总张数是否真的在 7000 左右最多和最少类的比值是否超过 2。多数类 800 张、少数类 300 张这种分布直接用 CrossEntropyLoss 也能训但少数类的召回通常会偏低后续第 5 章会专门讲处理办法。统计完顺手把结果和 json 里的标签数量对一遍数量不一致说明目录和 json 版本可能没对上这种包实际用之前最好先跑通一个小实验验证。3. 训练准备把 JPG 目录变成 ImageFolder 和 DataLoader3.1 用 torchvision.datasets.ImageFolder 读取数据确认 train 目录下是按类分子目录之后就不需要手写 Dataset 了。torchvision 的 ImageFolder 默认把每个子目录当成一个类别自动生成 class_to_idx 映射这对图像分类任务来说是最省事的入口。from torchvision import datasets, transforms data_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) dataset datasets.ImageFolder(rootdataset/train, transformdata_transform) print(dataset.classes, len(dataset))这段代码的逻辑是ImageFolder 扫描 root 下的子目录每个子目录对应一个类dataset 的长度等于全部图片数classes 是按字母序排好的类别名。需要注意ImageFolder 的 classes 顺序是字母序不是 json 里的顺序所以后续读取标签时要手动对齐。Resize 到 224x224 是因为后面要接的 resnet18 默认输入就是这个尺寸Normalize 用的是 ImageNet 统计量动物图像也同样适用不需要额外重新统计。3.2 在train内部再拆出val别把test当调参集资源虽然已经划分了训练集和测试集但训练时不能拿 test 来调参。标准做法是从 train 里再拆一部分当验证集test 留到最后一刻再碰。from torch.utils.data import random_split train_size int(len(dataset) * 0.8) val_size len(dataset) - train_size train_set, val_set random_split(dataset, [train_size, val_size])random_split 按比例切分训练集保留 80%剩余 20% 当验证集。这里有个细节random_split 默认不设随机种子每次运行划分都不一样。想让实验可复现先调用torch.manual_seed(42)再切。验证集的作用是观察每个 epoch 的泛化表现决定要不要早停、要不要调学习率test 只在最终评估时用一次。3.3 DataLoader参数batch_size、num_workers、pin_memory怎么设数据和验证集都准备好了接下来把三个数据集包装成 DataLoader。这一步看似简单参数设置不对也会影响训练速度和显存占用。from torch.utils.data import DataLoader train_loader DataLoader(train_set, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_set, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)逻辑说明shuffle 只在训练集开启打乱样本顺序避免模型学到固定排列验证集不需要 shuffle。num_workers 是读取数据的子进程数不是越大越好通常设为 CPU 核心数的一半机器核心少的可以先从 2 开始试。pin_memory 在单机单卡、用 GPU 训练时能减少传输时间CPU 训练开不开影响不大。4. 跑一个分类基线resnet18从头训 vs 迁移学习4.1 用resnet18从零训练的最简脚本这份数据集规模在 7000 张左右先用 resnet18 跑一个最简基线比直接上 resnet50 更合理。resnet18 参数量小不容易在几千张图上过拟合训练一轮也快适合先确认数据链路是否通畅。import torch from torchvision import models model models.resnet18(num_classes11) criterion torch.nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) for epoch in range(30): 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() print(fepoch {epoch 1} loss {running_loss / len(train_loader):.4f})这个训练循环做的事情是每轮遍历训练集把图片和标签搬到设备上前向得到预测计算交叉熵损失反向传播再让优化器更新参数。num_classes 必须改成 11对应数据集的类别数。Adam 的初始学习率用 1e-3 是常见做法验证集和 loss 下降曲线需要每轮打印出来。注意在验证阶段要切换model.eval()并用torch.no_grad()关闭梯度否则推理时会多占显存。4.2 迁移学习替换fc层并降低学习率对于只有约 7000 张图片的数据集从零训练 resnet18 能跑到一个可用的精度但想更快收敛实战里更推荐用 ImageNet 预训练权重做迁移学习。做法是把最后一层全连接替换成 11 分类。model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc torch.nn.Linear(model.fc.in_features, 11)这里的关键是weights参数新版 torchvision 推荐用这个写法老代码里的pretrainedTrue已经过时。替换 fc 层后前面的卷积层保留了 ImageNet 上学到的通用特征比如纹理、边缘、形状后面新加的全连接层负责把这 11 类的特征映射关系重新学出来。对应的学习率要调低一般从 1e-4 起步因为预训练权重已经比较好了学习率太大会把这些特征破坏掉。4.3 训练超参先定基线再谈调优超参不是越多越好对于这种中小型数据集我一般先把下面这组参数固定跑通一条完整链路后再动。参数从零训练迁移学习依据lr1e-31e-4预训练权重需要更小步长微调batch_size16~32328GB显存通常能承受32epoch30~5010~20观察验证集早停weight_decay1e-41e-4抑制过拟合输入尺寸224224resnet18默认输入这里有一个常见误判觉得用了预训练模型就不需要调参结果把 lr 保持 1e-3第一个 epoch 验证精度反而掉得厉害。原因是预训练特征已经收敛梯度更新幅度太大会把良好初始化的权重破坏。更稳妥的思路是先按这个基线跑如果 val loss 在 10 个 epoch 内不再下降再考虑把全连接层换成更大的隐藏层或者换 ViT 这类 transformer 结构来做图像分类对比。换 ViT 时只需要把 Resize 改成 384x384并使用对应的 Normalize 均值方差ImageFolder 的输出可以直接接进去。5. 避坑清单动物分类训练里的五次翻车记录5.1 标签错位验证loss很低推理结果却全乱现象训练过程看起来一切正常验证集 loss 降到很低但拿一张图片做推理时预测的类名和图片内容完全对不上。原因ImageFolder 按子目录字母序建类别索引而 json 里的 cls_names 是按作者自定义顺序编号的。两者下标不一致模型训练的 label 和 json 里写的类名是两套映射验证集 accuracy 也可能被这种系统性错位带偏。解决训练脚本里先打印dataset.classes和meta[cls_names]人工比对两边顺序。不一致时就以 json 为准把目录名和 json 类名做一次显式映射class_to_idx {cls_name: idx for idx, cls_name in enumerate(meta[cls_names])} dataset.class_to_idx class_to_idx此后所有标签读取都走这份映射不再依赖 ImageFolder 的默认字母序。5.2 类别不均衡少数类召回率明显偏低现象训练完成后看分类报告多数类 accuracy 能到 0.95少数类只有 0.7 左右尤其是样本少的类别预测结果经常被并到相近的多数类里。原因约 7000 张图片分到 11 类不是平均每类 636 张有的类图片多有的类图片少。CrossEntropyLoss 对多数类梯度贡献更大模型自然偏向学多数类。解决统计完类别分布后我一般会给 Loss 加类别权重或者用 WeightedRandomSampler 重采样。from torch.utils.data import WeightedRandomSampler class_counts [723, 510, 680, 415, 900, 610, 720, 380, 550, 660, 345] weights [1.0 / c for c in class_counts] sample_weights [weights[label] for _, label in dataset.samples] sampler WeightedRandomSampler(sample_weights, num_sampleslen(dataset), replacementTrue)逻辑是给样本较少的类更大的采样概率让每个 batch 里少数类出现的比例提高。replacementTrue 表示有放回采样少数类图片会在一个 epoch 里被多次抽到。5.3 图像损坏训练中途报解码错误现象训练跑了一两百个 batch突然报Image decoding error或某张图读出来是 None程序直接中断。原因从网络整理来的数据集里总会有零字节文件、截断的 jpg 或者伪后缀图片这类文件在预处理时能被 PIL 打开但解码到一半就失败。解决训练前先全量扫描一遍把损坏文件找出来。from PIL import Image import os for root, _, files in os.walk(dataset): for name in files: path os.path.join(root, name) try: Image.open(path).verify() except Exception: print(damaged:, path)verify()只检查文件头和解码完整性不会整图载入内存速度很快。扫描出来的文件建议直接移到 backup 目录而不是删除避免后续想恢复时没后悔药。5.4 背景偏置模型靠草地判断牛羊而不是靠形态现象在原始测试集上 accuracy 不错但换一批背景差异大的图片精度立刻掉下来。可视化样本时发现羊几乎都在草地牛也在草地老虎都在园区或笼子里。原因自然场景图像分类普遍存在背景泄漏。模型可能学会了用绿色像素判断牛羊而不是用角、体型这些真实特征。这个坑在训练曲线上看不出来必须靠可视化发现。解决用 show 脚本随机抽样确认每个类别的背景多样性。如果背景偏置严重可以在数据增强里加入随机裁剪、随机擦除或者对图像做局部遮挡强迫模型关注动物本体。训练集和测试集的背景分布不一致时单靠增强不一定能完全解决至少要意识到这个上限存在。5.5 显存与内存翻车全量加载图片直接OOM现象代码写成先遍历所有图片把所有图像转为 Tensor 存到 list再送进 DataLoader。运行到一半内存爆炸或者 GPU 显存报 out of memory。原因7000 张 224x224 的 RGB 图一张大约 150KB全量转成 Tensor 后体积会膨胀好几倍内存容易扛不住batch_size 设 128 也可能直接吃满显存。解决路径和标签交给 Dataset图片在__getitem__里按需读取。from torch.utils.data import Dataset from PIL import Image class AnimalDataset(Dataset): def __init__(self, image_paths, labels, transformNone): self.image_paths image_paths self.labels labels self.transform transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img Image.open(self.image_paths[idx]).convert(RGB) if self.transform: img self.transform(img) return img, self.labels[idx]这样内存里始终只保留一个 batch 的 Tensor。batch_size 保守一点设 328GB 显存跑 resnet18 基本没压力。如果还想加大有效 batch可以用梯度累积而不是直接调大 batch_size。6. 进阶验证、导出与单图推理让模型真正落地6.1 用混淆矩阵找出易混淆类训练跑完先别急着看 accuracy我一般先出混淆矩阵。import numpy as np from sklearn.metrics import confusion_matrix all_preds, all_labels [], [] model.eval() with torch.no_grad(): for images, labels in val_loader: images images.to(device) preds model(images).argmax(dim1) all_preds.extend(preds.cpu().tolist()) all_labels.extend(labels.tolist()) cm confusion_matrix(all_labels, all_preds) print(cm)混淆矩阵能直接暴露模型把哪两类混在一起。比如羊和牛、老虎和狗如果这两类互相串说明视觉特征接近需要针对这类组合增加样本或增强策略而不是盲目堆 epoch。6.2 导出TorchScript或ONNX模型验证没问题后把训练好的权重固化成部署格式。TorchScript 适合 C 和移动端场景做法是加载权重后直接 script 保存model.load_state_dict(torch.load(best_model.pt, map_locationcpu)) model.eval() scripted torch.jit.script(model) scripted.save(animal_classifier.pt)如果目标是移动端导出 ONNX 再转 NCNN 也是常见路线。这里要注意model.eval()必须写否则 Dropout 和 BatchNorm 在导出后行为不一致推理结果会和训练时对不上。6.3 单图推理先自测再交付模型部署前拿一张训练集之外的图跑单图推理确认输出类名和 json 映射对应。我习惯把类别映射固化到推理脚本里避免每次都要读 json 再反转idx_to_name {idx: name for name, idx in class_to_idx.items()} with torch.no_grad(): tensor data_transform(Image.open(test_img.jpg).convert(RGB)).unsqueeze(0) pred model(tensor).argmax(dim1).item() print(idx_to_name[pred])unsqueeze(0)是在 batch 维上加一维因为模型要求输入是四维张量。这步自测虽然简单却能挡住标签错位、预处理不一致这两类最常见的交付事故。那次因为标签顺序没对齐白训了整个周末后我每次拿到新数据集都强制把“图片路径—json标签—目录顺序”三对照走一遍再进训练循环。希望这个习惯帮到你。本文还有配套的精品资源点击获取
返回列表