图像分类项目实战:从模型选型到评估汇报

发布时间:2026/9/19 4:17:10
图像分类项目实战:从模型选型到评估汇报 简介《计算机视觉图像分类》是一份面向人工智能与数据挖掘初学者的教学课件源于《人工智能应用基础》课程聚焦图像分类中卷积神经网络、VGG网络与ResNet网络的核心原理帮助读者理解深度学习处理图像的任务流程。资源包含一个PPTX课件压缩包大小为1.07MB覆盖从输入张量维度、卷积核与滤波器计算、ReLU激活到VGG与ResNet网络架构的讲解。已有358人学习适合课堂教学、自学复习或教师备课查阅。课件以直观图解和逐步推导的方式呈现三维张量在卷积运算中的空间变化对VGG反复堆叠3×3小卷积核与2×2最大池化层、ResNet采用残差块解决退化问题等关键点给出清晰说明读者可据此快速构建从基础卷积到深度网络演进的图像分类知识体系也便于后续拓展到目标检测等视觉任务。1. 图像分类不是写个模型而是先回答两个问题先说一个反直觉的结论做图像分类时模型往往不是瓶颈瓶颈在“你拿什么数据、按什么标准说它准”。当一份名为“计算机视觉图像分类.pptx”的汇报任务放到面前别急着下载最新模型先想清楚两件事这个分类器到底服务谁判断错了之后谁来承担。医疗影像的误判和垃圾图片过滤的误判代价完全不同。图像分类是计算机视觉里最基础的监督学习任务输入一张图像输出一个离散类别标签但近几年的发展已经让它从 AlexNet 一路走到 Vision Transformer关注的焦点也从“谁在数据集上分数高”变成“谁能稳定地跑在业务数据上”。这篇内容适合两类人一类是计算机视觉入门者需要一份能跟着做的项目链路另一类是准备技术汇报、要写大作业或做产品预研的工程师。无论哪一类通用路子都是先定评价标准再选模型再写最小训练工程最后用可视化把结论钉在证据上。2. 图像分类的算法选择CNN与Transformer的边界2.1 图像分类到底在学什么特征的两条路线图像分类的传统路线是“手工特征 分类器”用 SIFT、HOG 这类算法抽取图像的局部纹理、边缘方向再把特征喂给 SVM 或随机森林。这条路线并非完全过时。在背景干净、类间差异明显的小规模工业分拣场景里几百张样本也能做到 95% 以上的准确率而且推理速度快、结果可解释。真正的问题是换一个数据领域就要重新设计特征难以迁移。深度学习路线把“设计特征”这一步交给卷积核自动完成。浅层卷积学习边缘、颜色块深层卷积学习部件和语义模式最后的全局池化把空间信息压缩成一个向量再通过全连接层做分类。CNN 的一个关键优势是平移等变性物体在图里的位置稍微移动卷积输出也相应平移这比全连接网络对空间位置更鲁棒。因此在中等规模数据上CNN 仍然是计算机视觉项目里最值得优先尝试的基座。不过“选哪种模型”不该从一开始就纠结。我一般会先定一个原则迁移学习优先从零训练是最后手段。理由很简单现代图像分类模型动辄上千万参数靠几千张图从零训练很难收敛而 ImageNet 预训练权重已经包含大量通用视觉特征微调后往往能在小数据上取得不错结果。这也是后面所有章节操作的起点。2.2 默认起点ResNet 的残差优势与代码用法ResNet 是过去十年图像分类里地位最稳的 backbone。它的核心贡献是残差连接让某一层的输入能跳过卷积直接加到输出上。这条捷径使梯度可以跨层传播深度增加到 50 层甚至 101 层也不会明显退化。ResNet50 在 ImageNet 上的预训练资源最丰富PyTorch、TensorFlow 里都是一行代码加载所以我把它视为大多数中小型图像分类任务的 baseline。用代码微调 ResNet50 的常见做法如下import torch import torchvision.models as models model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) num_ftrs model.fc.in_features model.fc torch.nn.Linear(num_ftrs, n_classes) # 单卡示例多卡用 model torch.nn.DataParallel(model) print(num_ftrs) # 4096? 不ResNet50 的 fc 输入是 2048这里的weightsIMAGENET1K_V2表示加载官方在 ImageNet-1K 上训练的第二版权重比旧的ResNet50_Weights.IMAGENET1K_V1准确率更高。model.fc.in_features取出原始全连接层的输入维度ResNet50 对应 2048再用新的Linear替换输出层为n_classes。如果样本量只有几百张我通常会冻结前面大部分参数for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True冻结后只训练最后的分类头能显著减少显存占用和训练时间。当数据量超过一万张时再解冻全部参数做完整微调学习率也应降为原来的十分之一。这样分批尝试比一开始就全参数训练更容易定位问题。2.3 Transformer图像分类ViT 与 Swin 的适用边界Vision Transformer 把图像切成固定大小的 patch每个 patch 展平后当作 token再用标准 Transformer 的 self-attention 建模 patch 之间的全局关系。与传统 CNN 相比ViT 感受野更大但代价是需要大规模训练数据才能学到足够的归纳偏置。直接在小数据集上训练 ViT效果通常不如同等规模的 ResNet。Swin Transformer 是另一条路线它引入分层结构和窗口注意力只在局部窗口内做 self-attention再通过移位窗口实现跨窗口信息交换。这样计算复杂度随图像尺寸呈线性增长也更适合做通用视觉 backbone。在图像分类业务里我的选择表大致如下模型数据量参考优点落地注意点ResNet18几百到几千张参数量小训练快精度天花板较低ResNet50数千到数万张预训练权重丰富迁移稳定大图输入时显存占用增加EfficientNet-B0数千张FLOPs 小适合边缘设备训练时对数据增强更敏感ViT-B/16十万张以上或强预训练大模型上限高单机小数据容易欠拟合Swin-T数千到数万张精度与计算量权衡好配置复杂度略高于 ResNet这里说的“数据量”是经验值不是绝对阈值。真正决定选择的不只是原始图片张数还有类间相似度。类内差异很大而类间又接近的任务例如森林图像分类里的树种识别即使数据量不小也需要更大容量的模型和更强的数据增强。反过来如果任务只是区分猫和狗ResNet18 就够用。需要提醒的是最新的图像分类模型很多已经和 CLIP、多模态预训练结合直接迁移到单标签分类时不一定比 Swin 更划算。我在实际项目中会先跑通一个 ResNet50 baseline把数据 pipeline 和评估代码稳定下来再尝试用效果更好的 checkpoint 替换编码器这样能快速判断是模型问题还是数据处理问题。3. 图像分类项目的最小训练工程从目录到 loss 函数3.1 数据目录和标签编码决定了后面所有代码的复杂度图像分类里最常见的坑不是模型而是数据组织。一个可复现、可交接的数据集最好按类别分目录放置。PyTorch 的ImageFolder可以直接利用这种目录结构自动把子目录名映射为类别索引省去手写 CSV 解析的麻烦。最小目录结构示例如下data/ train/ cat/ cat_001.jpg cat_002.jpg dog/ dog_001.jpg val/ cat/ val_cat_001.jpg dog/ val_dog_001.jpg需要特别注意的是ImageFolder会按照目录名的字母顺序生成class_to_idx也就是cat是 0dog是 1。这个索引顺序在读取、评估和画混淆矩阵时必须保持一致。如果后续改用 CSV 标注我会用sklearn.preprocessing.LabelEncoder编码标签并保存一份类名映射表防止重新训练时索引错位。3.2 训练集和验证集要使用不同的图像变换数据增强的作用是增加训练样本的多样性但增强绝不能加到验证集上否则评估结果不稳定。验证集应该只做尺寸调整和归一化用同一张图也能得到相同的输出。训练集则可以使用更激进的随机变换。from torchvision import datasets, transforms transform_train transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.3, 0.3, 0.3), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) transform_val 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]), ]) train_dataset datasets.ImageFolder(data/train, transformtransform_train) val_dataset datasets.ImageFolder(data/val, transformtransform_val)RandomResizedCrop(224)会随机裁剪一块区域并缩放到 224 像素相当于同时做尺度扰动和目标局部性扰动。RandomHorizontalFlip对自然图像通常有效但对文本识别、车牌识别这类左右不对称任务要关闭。ColorJitter调整亮度、对比度、饱和度和色调能够提升模型对光照变化的鲁棒性。归一化的mean和std用的是 ImageNet 统计值因为后续迁移学习使用的是 ImageNet 预训练权重。3.3 训练循环和关键超参数有了数据集接下来就是 DataLoader 和训练循环。DataLoader 参数里最值得调的是batch_size和num_workers。num_workers控制加载图像的进程数建议从 4 开始当数据集很大时CPU 加载速度跟不上 GPU训练会一直等数据。pin_memoryTrue可以加速 CPU 到 GPU 的拷贝。from torch.utils.data import DataLoader import torch.nn as nn batch_size 64 train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_sizebatch_size, shuffleFalse, num_workers4, pin_memoryTrue) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max10)CrossEntropyLoss内部已经包含了 softmax 和交叉熵计算输入模型输出的原始 logits 就可以不要在损失函数前手动做 softmax否则会双重软化导致梯度变小。AdamW把权重衰减从梯度更新中分离比 Adam 对稳定更友好适合微调 Transformer 和 CNN。学习率用3e-4数据量特别小或 batch size 很小时可以降到1e-4。CosineAnnealingLR让学习率按余弦曲线衰减前几个 epoch 较高利于快速收敛后面变小以稳定 loss。训练循环本身不复杂但有几个动作每次都必须做epochs 10 for epoch in range(epochs): model.train() train_loss 0.0 for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() outputs model(x) loss criterion(outputs, y) loss.backward() optimizer.step() train_loss loss.item() model.eval() correct, total 0, 0 with torch.no_grad(): for x, y in val_loader: x, y x.to(device), y.to(device) outputs model(x) preds outputs.argmax(dim1) correct (preds y).sum().item() total y.size(0) acc correct / total print(fepoch{epoch1} loss{train_loss/len(train_loader):.4f} acc{acc:.4f}) scheduler.step()model.train()和model.eval()的切换尤其重要。BatchNorm 层在训练时使用当前 batch 的均值和方差在评估时使用训练阶段累计的统计量如果验证时忘记切回eval模式同一个 batch 的预测结果会随着 batch 组成变化指标忽高忽低。torch.no_grad()关闭梯度计算能减少验证阶段显存占用也避免反向传播图被意外存储。训练过程中的打印信息建议包含 train loss 和 val acc这两个指标足够判断早期问题。具体超参数可参考下表参数推荐值说明batch size32 或 648GB 显存建议 3216GB 以上可到 64epochs10 到 30主要看验证集是否继续提升learning rate3e-4全量微调用 1e-4 更稳weight decay1e-4太大导致欠拟合太小容易过拟合optimizerAdamW对噪声梯度更鲁棒学习率策略CosineAnnealing比固定学习率上限更高4. 图像分类的评估、调参与排错为什么模型没涨点4.1 准确率会骗人评估指标必须看全二分类场景里正负样本比例 9:1模型把所有样本预测为负类准确率是 90%但毫无业务价值。这种情况在缺陷检测、森林图像分类等场景里非常常见坏样本少正常样本多。只看准确率的坏习惯会让调参方向完全走偏。我一般会把准确率作为顶层指标同时输出每个类别的精确率、召回率和 F1。精确率表示“预测为这个类别的样本里有多少是真对的”召回率表示“真实是这个类别的样本里有多少被找回来了”。在类别不平衡时宏平均 F1 比准确率更能反映小类别的真实表现。评估代码可以这样写from sklearn.metrics import classification_report, confusion_matrix import numpy as np model.eval() pred_all, true_all [], [] with torch.no_grad(): for x, y in val_loader: x x.to(device) pred model(x).argmax(dim1).cpu().numpy() pred_all.extend(pred) true_all.extend(y.numpy()) print(classification_report(true_all, pred_all, target_namestrain_dataset.classes)) print(confusion_matrix(true_all, pred_all))train_dataset.classes就是前面ImageFolder解析出来的类别名列表顺序和class_to_idx一致。classification_report会输出每个类别的 precision、recall、f1-score 和样本数confusion_matrix则返回一个二维计数矩阵行是真实类别列是预测类别。如果某个类别的 recall 远低于其他类别说明模型对该类别的特征学得不够需要补样本而不是盲目加深网络。4.2 训练曲线怎么读区分欠拟合和过拟合训练时的 loss 和验证准确率曲线是排错的第一现场。常见的三种情况如下现象原因处理方式train loss 和 val loss 都很高欠拟合模型容量不够或学习率太小换更大模型、增大网络宽度、调高学习率train loss 持续下降val acc 开始回落过拟合增加数据增强、提高 weight decay、加 Dropouttrain loss 不降或变为 NaN学习率过大或数据里有异常值降低学习率检查归一化和标签如果训练 loss 下降缓慢先看学习率。学习率过小时模型几乎原地踏步每个 epoch 都有损失但减少幅度微小学习率过大loss 会剧烈震荡甚至直接变 NaN。一个快捷做法是先用一个小批量跑 5 个 epoch学习率从 1e-5 到 1e-2 做指数扫描画出 loss 曲线找到下降最快的区间再定为初始学习率。这个技巧在调 Transformer 时尤其有效。4.3 三个最容易忽视的坑第一个坑是数据泄漏。如果原始数据是按时间顺序采集的随机划分训练集和验证集时同一个来源的相似图片可能同时出现在两边导致验证准确率虚高。更隐蔽的是增强泄漏在划分之前做数据清洗时已经看过全部图片或者用验证集图片参与计算归一化统计量。稳妥做法是先按文件、时间或设备维度划分再写增强代码。第二个坑是验证集忘记切换模型状态。典型表现是验证准确率每次运行都不一样或者一张图单独测试和批量测试结果不同。原因就是没有调用model.eval()BatchNorm 还在用当前 batch 的统计量。写一个工具函数evaluate(model, val_loader)内部强制model.eval()并开启no_grad()可避免遗忘。第三个坑是类别索引混乱。使用 CSV 标签时如果直接对字符串做排序然后手工指定类别索引训练和验证可能对应不同的数字映射导致混淆矩阵看起来“错得离谱”。正确做法是只使用LabelEncoder或ImageFolder这类统一编码工具训练完成后用同一个对象转换新数据。每次重新训练前检查train_dataset.class_to_idx并保存为 JSON 文件后续部署预测时直接加载同一个映射。5. 把模型结果变成汇报材料三张图就够5.1 训练曲线图给人看也给自己看与其贴一堆命令行日志不如输出一张训练曲线图在“计算机视觉图像分类.pptx”里放上 train loss 和 val acc 两条曲线。这张图能说明训练过程是否稳定、何时开始过拟合比“训练了 20 个 epoch最终准确率 95%”这种描述有说服力得多。import matplotlib.pyplot as plt history { train_loss: [1.2, 0.8, 0.5, 0.3, 0.2], val_acc: [0.72, 0.84, 0.89, 0.91, 0.92], } fig, ax1 plt.subplots() ax1.plot(history[train_loss], labeltrain_loss, colorC0) ax1.set_xlabel(epoch) ax1.set_ylabel(loss) ax2 ax1.twinx() ax2.plot(history[val_acc], labelval_acc, colorC1) ax2.set_ylabel(accuracy) fig.legend(loccenter right) plt.savefig(training_curve.png, dpi150, bbox_inchestight)twinx()让 loss 和 accuracy 共用横轴但使用左右两个纵轴适合展示不同量纲的指标。训练代码里在每个 epoch 结束后把数值 append 到history最后保存图片即可。输出图片的分辨率建议 150 dpi 以上否则投影到屏幕上会发虚。5.2 混淆矩阵直接指出错误集中在哪混淆矩阵是图像分类汇报里最容易被重视的一页。使用ConfusionMatrixDisplay可以一行生成但需要处理好类别名顺序from sklearn.metrics import ConfusionMatrixDisplay ConfusionMatrixDisplay.from_predictions( true_all, pred_all, display_labelstrain_dataset.classes, cmapBlues, normalizetrue, ) plt.xticks(rotation45, haright) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)normalizetrue会对每一行做归一化让矩阵每一行加起来是 1这样更容易看出某个真实类别被错分到哪个目标类别的比例。当类别名较长时旋转横轴刻度为 45 度可避免标签重叠。这张图最好配合几个典型误判样本一起看因为混淆矩阵只告诉你错误比例不能告诉你为什么错。5.3 bad case 抽样给模型“挑错”并解释原因最后一页建议放 6 到 12 张错分样本标注预测类别、真实类别和置信度。一段简单代码可以输出这些样本val_samples val_dataset.samples errors [] for idx, (pred, true) in enumerate(zip(pred_all, true_all)): if pred ! true: path val_samples[idx][0] errors.append((path, pred, true)) errors errors[:12] for path, pred, true in errors: print(path, pred:, train_dataset.classes[pred], true:, train_dataset.classes[true])val_dataset.samples是(图片路径, 类别索引)的列表与val_loader未打乱时的顺序一致所以可以按索引拿回原图路径。逐个展示图片前先看路径是否对应真实内容避免数据本身标错。真正有价值的 bad case 通常集中在遮挡、模糊或者类别本身外观相似的地方把这些样本在 PPT 里标出来比任何指标都更能说明后续优化方向。制作汇报材料时我一般会坚持“一张训练曲线、一张混淆矩阵、一页 bad case”的组合。这个组合能覆盖训练过程是否正常、整体准确率多少、错误模式是什么三个问题。图像分类项目的收尾不是模型文件而是这套能把结果讲清楚的证据链。建议把 bad case 图片按预测顺序排列并在每张图上直接绘制预测文本例如用 Pillow 在图片上方写上pred: cat / true: dog。这样生成的 png 可以直接嵌入 PPT不用再对着表格手写说明。整个汇报材料只保留和业务结论相关的内容训练代码细节放附录就好。本文还有配套的精品资源点击获取