基于ResNet的2D图像多分类实战:从数据划分到混淆矩阵的避坑指南
简介这份资源面向希望入门深度学习图像分类的开发者与学习者提供基于ResNet的2D图像多分类任务完整实现。内容围绕残差网络的核心机制展开涵盖残差块结构、短路连接设计以及深层网络训练中梯度消失问题的解决思路同时涉及数据预处理、训练集与验证集划分、优化器与损失函数选择、学习率调度、可视化分析及预测后处理等关键环节帮助读者建立从数据处理到模型评估的完整认知。资源包共20个文件以12个Python脚本为主体辅以6个编译缓存文件和2张png图片压缩包约382KB目录中可看到模型定义、数据增强、训练主流程、评估与可视化等模块划分结构清晰便于按功能查阅。目前已有215人学习下载适合作为图像分类任务的练手项目也可为更复杂的图像识别场景打下实践基础。1. 从一次「准确率 92% 却没法上线」的翻车说起手里有一批 2D 图像可能是工业质检的零件照片、医疗影像切片、商品主图也可能是遥感小图类别数从 3 类到 30 类不等每类样本几百到几千张。任务描述起来很朴素基于 ResNet 的 2D 图像简单多分类。但真正动手的人很快会发现「简单」两个字是陷阱——数据集划分稍有不慎就泄漏预训练模型加载方式不对就白训学习率没配好 loss 直接躺平最后混淆矩阵一拉某两个类互相吞得干干净净。这篇笔记面向的是想把这套流程真正跑通、并且能复现出可解释结果的一线从业者。我会按「数据怎么组织 → ResNet 怎么选和改 → 训练循环怎么写 → 指标怎么读 → 坑怎么排」的顺序把基于 ResNet 的 2D 图像多分类任务从零到可交付讲一遍。不依赖任何特定框架版本PyTorch 生态为主代码可以直接抄去改。新手能照着一步步跑熟手能直接跳到参数边界和排查章节。2. 数据管线与 ResNet 预训练模型的选型逻辑2.1 2D 图像多分类的数据组织目录结构与划分比例绝大多数 2D 图像多分类项目第一步不是写模型而是把文件夹摆对。常见做法是ImageFolder兼容的三段式结构dataset/ ├── train/ │ ├── class_a/ │ ├── class_b/ │ └── class_c/ ├── val/ │ ├── class_a/ │ ├── class_b/ │ └── class_c/ └── test/ ├── class_a/ ├── class_b/ └── class_c/划分比例上样本量在千级时我一般用 7:1.5:1.5样本量过万可以用 8:1:1。这里有个血泪经验必须先按类别分层抽样再落盘否则小类别可能整个 val 集里一张都没有训练时 val loss 看着降实际是在过拟合大类别。import os, shutil, random from pathlib import Path from collections import defaultdict def split_dataset(src_dir, dst_dir, ratios(0.7, 0.15, 0.15), seed42): random.seed(seed) src Path(src_dir) classes [d.name for d in src.iterdir() if d.is_dir()] for cls in classes: imgs list((src / cls).glob(*.*)) random.shuffle(imgs) n len(imgs) n_train int(n * ratios[0]) n_val int(n * ratios[1]) splits { train: imgs[:n_train], val: imgs[n_train:n_train n_val], test: imgs[n_train n_val:], } for split, files in splits.items(): out Path(dst_dir) / split / cls out.mkdir(parentsTrue, exist_okTrue) for f in files: shutil.copy2(f, out / f.name) split_dataset(raw_images, dataset)这段脚本做三件事遍历每个类别目录、按固定随机种子打乱、按比例切分并复制到目标结构。seed42是为了让划分可复现团队协作时别人拿到同样的原始数据能切出完全一致的 train/val/test。ratios三个值分别对应训练、验证、测试验证集用于调参和早停测试集只在最后评估一次中途反复看测试集等于变相泄漏。提示如果原始数据里存在同一物体多角度拍摄的图片划分时要按「物体 ID」分组切分而不是按图片随机切分否则同一物体的不同角度会同时出现在 train 和 val指标虚高。2.2 ResNet 预训练模型怎么选18/34/50 的边界ResNet 系列里2D 图像多分类最常用的是 resnet18、resnet34、resnet50。选型不是越大越好要看数据量和类别难度模型参数量适合场景单卡 224×224 推理延迟参考resnet18~11M数据 5k类别 10纹理差异明显最低resnet34~21M数据 5k~20k类别 10~50中等resnet50~25M数据 20k类别细粒度、类间差异小较高数据量在几千张时resnet50 的瓶颈层容易过拟合反而不如 resnet18 稳。我一般先用 resnet18 跑通全流程拿到 baseline再换 resnet34/50 对比如果提升不到 2 个点就回到小模型加数据增强。加载预训练权重时torchvision的接口已经统一import torch import torch.nn as nn from torchvision import models def build_resnet(num_classes, archresnet18, pretrainedTrue): weights IMAGENET1K_V1 if pretrained else None model getattr(models, arch)(weightsweights) # 替换最后的全连接层输出维度改为类别数 in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) return model model build_resnet(num_classes10, archresnet18, pretrainedTrue)关键点是model.fc的替换ResNet 在 ImageNet 上是 1000 类fc.in_features是 512resnet18/34或 2048resnet50换成自己的类别数即可。pretrainedTrue时权重来自 ImageNet对自然图像迁移效果最好如果是医学、遥感这类和 ImageNet 分布差异大的 2D 图像预训练仍有帮助但建议冻结前几个 stage 先训 fc再解冻全网络微调否则大梯度会把预训练特征冲烂。2.3 数据增强与归一化别让预处理成为精度天花板2D 图像多分类里增强策略直接决定泛化上限。训练集用随机裁剪、水平翻转、颜色抖动验证/测试集只做 resize 中心裁剪 归一化。归一化参数用 ImageNet 的均值方差是常规操作from torchvision import transforms train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.2, 0.2, 0.2), 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]), ])RandomResizedCrop的scale(0.7, 1.0)控制裁剪区域占原图比例太小会丢关键部位太大等于没增强。ColorJitter三个参数分别是亮度、对比度、饱和度扰动幅度工业质检场景要慎用因为颜色本身可能是判别特征。归一化的 mean/std 必须和预训练权重匹配自己算一套反而会让预训练特征失效。3. 训练循环、学习率策略与混淆矩阵落地3.1 训练循环骨架从 DataLoader 到反向传播训练循环是整套流程的心脏写清楚每个环节才能定位问题from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder import torch.optim as optim train_ds ImageFolder(dataset/train, transformtrain_tf) val_ds ImageFolder(dataset/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size64, shuffleFalse, num_workers4, pin_memoryTrue) device torch.device(cuda if torch.cuda.is_available() else cpu) model build_resnet(len(train_ds.classes)).to(device) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() logits model(imgs) loss criterion(logits, labels) loss.backward() optimizer.step() scheduler.step() # 验证阶段见下节batch_size32是 224×224 输入在 8G 显存下的稳妥值显存够可以上 64。AdamW比 SGD 更容易起步lr3e-4是微调预训练模型的常用起点weight_decay1e-4抑制过拟合。CosineAnnealingLR让学习率按余弦曲线衰减T_max设成总 epoch 数训练后期自动收敛。pin_memoryTrue配合 GPU 训练能减少数据搬运开销。3.2 学习率与 batch size 的联动别让 loss 一开始就躺平学习率和 batch size 是联动的。经验规则是lr随batch_size线性放大batch 从 32 到 64lr 可以从 3e-4 提到 6e-4。但微调预训练模型时lr 超过 1e-3 很容易让 loss 在前几个 step 直接飙到 nan因为预训练权重的尺度经不起大梯度。如果发现 loss 前 100 step 不降反升先查三件事lr 是不是太大、归一化是不是和预训练不匹配、标签是不是有越界或错位。我一般会先用一个极小 lr1e-5跑 50 step确认 loss 能稳定下降再逐步放大到目标值。这个「先探路再加速」的习惯帮我省过很多次重训。3.3 混淆矩阵与分类报告python 多分类混淆矩阵代码训练完只看 accuracy 是不够的2D 图像多分类里类别不平衡很常见必须看混淆矩阵from sklearn.metrics import confusion_matrix, classification_report import numpy as np def evaluate(model, loader, device, class_names): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in loader: imgs imgs.to(device) logits model(imgs) preds logits.argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(classification_report(all_labels, all_preds, target_namesclass_names, digits4)) return cm cm evaluate(model, val_loader, device, train_ds.classes)confusion_matrix的行是真实标签、列是预测标签对角线是正确数。classification_report给出每个类的 precision、recall、f1。重点看两类recall 特别低的类漏检严重和 precision 特别低的类误检严重。如果某两类在矩阵里互相大量误判说明它们的特征在 ResNet 高层被混在一起要么加数据要么考虑引入更强的特征表达。注意混淆矩阵的类别顺序默认按标签数值排序画图或汇报时务必对齐class_names否则会把结论讲反。4. 避坑与排查2D 图像多分类最常见的 5 个翻车现场4.1 现象训练准确率 99%验证准确率 60%原因数据泄漏或增强过强。常见的是划分时同一物体的多张图分散到 train 和 val或者验证集误用了训练增强随机裁剪、翻转。解决按物体 ID 分组划分验证集只用确定性变换重新跑一遍对比。4.2 现象loss 一直是 nan 或前几个 step 爆炸原因学习率过大、输入未归一化、标签越界。解决把 lr 降到 1e-5 试跑确认ToTensor和Normalize都在 pipeline 里检查标签最大值是否小于类别数。三者逐一排除通常 10 分钟内能定位。4.3 现象某个类别 recall 长期为 0原因该类别样本太少或被其他类别的特征淹没。解决先确认该类在 train 里有足够样本至少几十张再考虑加类别权重CrossEntropyLoss(weight...)或对该类做重采样。如果样本实在少先做二分类验证该类是否可分再决定要不要合并类别。4.4 现象换了 resnet50 反而不如 resnet18原因数据量不够大模型过拟合或学习率没随模型深度调整。解决小数据优先小模型换大模型时把 lr 调小一档并加更强的数据增强或 dropout。别迷信「模型越大越好」2D 图像多分类里数据质量往往比模型容量更决定上限。4.5 现象推理时单张图预测结果和验证集不一致原因推理时的预处理和验证集不一致比如忘了 resize 到 256 再 center crop或归一化参数写错。解决把验证集的val_tf抽成一个函数训练和推理共用同一份杜绝两处各写一套。5. 进阶技巧用特征可视化验证 ResNet 到底学到了什么跑通流程只是及格线真正让方案可信的是能解释模型为什么这么判。我常用的手段是取 ResNet 倒数第二层的特征全局平均池化前的 512 维向量做 t-SNE 降维后按类别着色。如果同类聚成一团、异类分开说明特征空间是健康的如果某两类完全重叠混淆矩阵里的误判就有了根因。import torch.nn as nn from sklearn.manifold import TSNE import matplotlib.pyplot as plt # 去掉 fc 层保留特征 feat_model nn.Sequential(*list(model.children())[:-1]).to(device) feat_model.eval() feats, labels_all [], [] with torch.no_grad(): for imgs, labels in val_loader: out feat_model(imgs.to(device)).squeeze(-1).squeeze(-1) feats.append(out.cpu().numpy()) labels_all.extend(labels.numpy()) feats np.concatenate(feats, axis0) emb TSNE(n_components2, perplexity30, random_state42).fit_transform(feats) plt.figure(figsize(8, 6)) for cls in np.unique(labels_all): idx np.array(labels_all) cls plt.scatter(emb[idx, 0], emb[idx, 1], s8, labelstr(cls)) plt.legend() plt.savefig(tsne.png, dpi150)perplexity30是 t-SNE 的常用值样本少可以降到 10。random_state42保证每次图一致方便对比不同模型。看图的诀窍是先看整体是否分块再看边界处哪些类贴在一起那些就是下一步要补数据或加特征的方向。另一个技巧是冻结 backbone 只训分类头作为对照实验。如果冻结后指标和全量微调差不多说明你的数据分布和 ImageNet 接近预训练特征已经够用如果差很多说明领域差异大全量微调是必要的。这个对照能帮你在「要不要花算力微调」上做决策而不是凭感觉。我自己踩过最深的一个坑是早期图省事把验证集也做了随机增强结果 val 指标忽高忽低调了两周参数才发现是评估管线的问题。从那以后我养成了一个习惯任何一次指标异常先怀疑数据管线再怀疑模型。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →