简介:这份资源面向希望入门深度学习图像分类的开发者与学习者,提供基于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), seed=42): 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(parents=True, exist_ok=True) for f in files: shutil.copy2(f, out / f.name) split_dataset("raw_images", "dataset")这段脚本做三件事:遍历每个类别目录、按固定随机种子打乱、按比例切分并复制到目标结构。seed=42是为了让划分可复现,团队协作时别人拿到同样的原始数据能切出完全一致的 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, arch="resnet18", pretrained=True): weights = "IMAGENET1K_V1" if pretrained else None model = getattr(models, arch)(weights=weights) # 替换最后的全连接层,输出维度改为类别数 in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) return model model = build_resnet(num_classes=10, arch="resnet18", pretrained=True)关键点是model.fc的替换:ResNet 在 ImageNet 上是 1000 类,fc.in_features是 512(resnet18/34)或 2048(resnet50),换成自己的类别数即可。pretrained=True时权重来自 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", transform=train_tf) val_ds = ImageFolder("dataset/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_ds, batch_size=64, shuffle=False, num_workers=4, pin_memory=True) 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(), lr=3e-4, weight_decay=1e-4) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) 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_size=32是 224×224 输入在 8G 显存下的稳妥值,显存够可以上 64。AdamW比 SGD 更容易起步,lr=3e-4是微调预训练模型的常用起点,weight_decay=1e-4抑制过拟合。CosineAnnealingLR让学习率按余弦曲线衰减,T_max设成总 epoch 数,训练后期自动收敛。pin_memory=True配合 GPU 训练能减少数据搬运开销。
3.2 学习率与 batch size 的联动:别让 loss 一开始就躺平
学习率和 batch size 是联动的。经验规则是lr随batch_size线性放大:batch 从 32 到 64,lr 可以从 3e-4 提到 6e-4。但微调预训练模型时,lr 超过 1e-3 很容易让 loss 在前几个 step 直接飙到 nan,因为预训练权重的尺度经不起大梯度。
如果发现 loss 前 100 step 不降反升,先查三件事:lr 是不是太大、归一化是不是和预训练不匹配、标签是不是有越界或错位。我一般会先用一个极小 lr(1e-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(dim=1).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_names=class_names, digits=4)) 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, axis=0) emb = TSNE(n_components=2, perplexity=30, random_state=42).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], s=8, label=str(cls)) plt.legend() plt.savefig("tsne.png", dpi=150)perplexity=30是 t-SNE 的常用值,样本少可以降到 10。random_state=42保证每次图一致,方便对比不同模型。看图的诀窍是:先看整体是否分块,再看边界处哪些类贴在一起,那些就是下一步要补数据或加特征的方向。
另一个技巧是冻结 backbone 只训分类头作为对照实验。如果冻结后指标和全量微调差不多,说明你的数据分布和 ImageNet 接近,预训练特征已经够用;如果差很多,说明领域差异大,全量微调是必要的。这个对照能帮你在「要不要花算力微调」上做决策,而不是凭感觉。
我自己踩过最深的一个坑,是早期图省事把验证集也做了随机增强,结果 val 指标忽高忽低,调了两周参数才发现是评估管线的问题。从那以后我养成了一个习惯:任何一次指标异常,先怀疑数据管线,再怀疑模型。希望帮到你。
本文还有配套的精品资源,点击获取