资讯详情

机器学习图像分类实战:从数据准备到模型复现的完整路径

📅 2026/10/10 15:14:22 | 华诺云谱 👁 阅读
机器学习图像分类实战:从数据准备到模型复现的完整路径
简介这份资源面向机器学习入门者与图像分类方向的开发者围绕SVM与贝叶斯分类器展开帮助读者理解如何从图像特征中自动完成类别判定。压缩包共216个文件以102个bmp图像样本、16个cpp源码与18个h头文件为核心辅以obj、sbr等编译中间文件及exe可执行程序整体约12.87MB构成一套可直接运行的图像分类实验工程。资源配套图形界面用户可直观操作并对比不同算法的分类效果省去手动搭建环境的繁琐。已有2483人学习下载适合希望快速验证SVM、贝叶斯等经典方法在图像分类中表现的研究人员与开发者也可作为课程实验或算法对比的参考素材。1. 机器学习做图像分类从调包侠到能复现的完整路径你手里有一批图想训练一个模型把它们分到不同类别里。可能是工业质检的缺陷图可能是遥感影像的地物分类也可能是医学影像的良恶性判断。不管哪种场景核心问题都一样怎么用机器学习方法把图像分类这件事做出来并且做到能复现、能调优、能排错。很多人第一次接触图像分类是从调sklearn的SVC或者跑一遍ResNet50的预训练权重开始的。跑通了准确率看着还行但换一批数据就翻车。问题出在图像分类不是把图丢进模型就完事它涉及数据组织、预处理、模型选型、训练策略、评估方式这一整条链路。任何一个环节没对齐结果就是玄学。这篇笔记面向两类人一是刚入门机器学习、想用图像分类做第一个完整项目的工程师二是已经会调包、但遇到小样本、类别不均衡、评估指标异常时不知道怎么排查的熟手。我会按「数据怎么组织 → 模型怎么选 → 训练怎么跑 → 坑怎么避 → 进阶怎么玩」的顺序把每一步的参数、命令和判断依据写清楚。不堆概念只写能直接抄作业的流程。2. 数据准备与预处理图像分类的地基怎么打2.1 目录结构决定你能不能跑通第一个脚本图像分类最常见的数据组织方式是按类别分文件夹。这不是随便定的torchvision.datasets.ImageFolder、TensorFlow的image_dataset_from_directory都默认这个结构。你如果自己写DataLoader也建议沿用省得后面换框架时重新整理。标准结构长这样dataset/ ├── train/ │ ├── cat/ │ │ ├── 001.jpg │ │ └── 002.jpg │ └── dog/ │ ├── 001.jpg │ └── 002.jpg ├── val/ │ ├── cat/ │ └── dog/ └── test/ ├── cat/ └── dog/训练集、验证集、测试集必须物理分开。我见过太多人把全部数据放在一个文件夹里用train_test_split随机切分结果同一张图的增强版本同时出现在训练和验证集里准确率虚高到 99%上线就崩。正确做法是先按文件夹分好 train/val/test再在 train 内部做增强。划分比例没有绝对标准。数据量在 1 万张以下时我一般用 7:1.5:1.5数据量超过 10 万张测试集可以降到 5%。验证集不能太小否则早停和调参的方差会很大建议至少每类 50 张。2.2 预处理流水线三个必须对齐的参数图像分类的预处理不是「随便 resize 一下」。训练和推理阶段的预处理必须严格一致否则模型看到的分布变了精度直接掉。核心参数有三个尺寸、归一化均值方差、插值方式。import torch from torchvision import transforms # 训练阶段带随机增强 train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), # 随机裁剪并缩放到224 transforms.RandomHorizontalFlip(p0.5), # 水平翻转概率0.5 transforms.ColorJitter(brightness0.2, contrast0.2), # 颜色抖动 transforms.ToTensor(), # 转成Tensor像素值归到[0,1] transforms.Normalize(mean[0.485, 0.456, 0.406], # ImageNet均值 std[0.229, 0.224, 0.225]) # ImageNet标准差 ]) # 验证/推理阶段只做确定性变换 val_transform transforms.Compose([ transforms.Resize(256), # 先缩到256 transforms.CenterCrop(224), # 中心裁剪到224 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])逻辑说明RandomResizedCrop的scale(0.6, 1.0)表示随机裁剪原图 60% 到 100% 的区域再缩放到 224。这个参数控制增强强度小样本时可以调到(0.3, 1.0)增加多样性但别低于 0.3否则裁出来的图可能只剩背景。Normalize的均值和方差必须和预训练权重一致——如果你用的是 ImageNet 预训练的 ResNet就用上面这组如果是从头训练可以自己算数据集的均值和方差但大多数情况下直接用 ImageNet 的就行。插值方式容易被忽略。Resize默认是双线性插值但如果你做的是医学影像或遥感图像像素值有物理意义建议改成transforms.InterpolationMode.NEAREST或BICUBIC具体看任务。这个参数在训练和推理时必须一致否则边缘像素的响应会不同。提示验证集和测试集的预处理必须完全一致不要给验证集加任何随机增强。我见过有人在验证集上也加RandomHorizontalFlip结果验证精度波动很大早停点选不准。3. 模型选型与训练从 ResNet 到小样本的落地路径3.1 常规图像分类ResNet50 还是 ViT如果你的数据量在每类 500 张以上直接上预训练模型微调。ResNet50 和 ViT 是当前最常用的两个基线。选哪个看数据量和算力。ResNet50 的优势是成熟、稳定、显存占用低。在 224×224 输入下batch size 64 大概占 6GB 显存单卡 1080Ti 就能跑。ViT-Base 同样输入下batch size 64 要 12GB 以上而且对数据量更敏感——数据少于 1 万张时ViT 从零训练很难超过 ResNet必须用预训练权重。import torchvision.models as models import torch.nn as nn # 加载预训练ResNet50 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) # 替换最后的全连接层类别数改成自己的 num_classes 10 model.fc nn.Linear(model.fc.in_features, num_classes) # 冻结前几层只训练后面的层小样本时常用 for name, param in model.named_parameters(): if layer4 not in name and fc not in name: param.requires_grad False # 优化器只更新需要梯度的参数 optimizer torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4 )参数说明weightsResNet50_Weights.IMAGENET1K_V2是 torchvision 新版的写法旧版用pretrainedTrue。冻结策略上小样本每类少于 100 张建议只训练layer4和fc学习率设 1e-3数据量充足时解冻全部层学习率降到 1e-4 避免破坏预训练特征。weight_decay用 1e-4 是常规起点过拟合严重时加到 1e-3。ViT 的微调略有不同。ViT 的分类头是一个Linear层输入维度是hidden_sizeViT-Base 是 768。用预训练权重时分类头需要重新初始化。有人问「用 ViT 评估时分类头用调整吗」——如果你是在做线性探测linear probing只训练分类头主干冻结如果是微调分类头和主干一起训练但主干学习率要设小一些通常是分类头的 1/10。from transformers import ViTForImageClassification model ViTForImageClassification.from_pretrained( google/vit-base-patch16-224-in21k, num_labelsnum_classes, ignore_mismatched_sizesTrue # 分类头维度不匹配时重新初始化 ) # 分层学习率主干小分类头大 optimizer torch.optim.AdamW([ {params: model.vit.parameters(), lr: 1e-5}, {params: model.classifier.parameters(), lr: 1e-3} ], weight_decay1e-4)3.2 小样本图像分类1-shot 和 5-shot 怎么做小样本图像分类是另一个问题。1-shot 意味着每类只有 1 张标注图5-shot 是每类 5 张。这时候不能直接微调因为数据量根本不够。常见做法是用预训练模型提取特征然后做最近邻分类或原型网络。流程分三步第一用 ImageNet 预训练的主干网络提取所有图的特征向量第二对每个类别的支持集support set特征取平均得到类原型第三对查询集query set的每张图计算其特征与各类原型的距离取最近的类。import torch import torch.nn.functional as F from torchvision.models import resnet50, ResNet50_Weights # 加载预训练主干去掉分类头 backbone resnet50(weightsResNet50_Weights.IMAGENET1K_V2) backbone.fc torch.nn.Identity() # 输出2048维特征 backbone.eval() def extract_features(images): with torch.no_grad(): features backbone(images) return F.normalize(features, dim1) # L2归一化 def few_shot_classify(support_images, support_labels, query_images): # support_images: [N*K, C, H, W]N类K shot # query_images: [M, C, H, W] support_features extract_features(support_images) query_features extract_features(query_images) # 计算每个类的原型 classes torch.unique(support_labels) prototypes [] for c in classes: mask support_labels c prototypes.append(support_features[mask].mean(dim0)) prototypes torch.stack(prototypes) # [N, 2048] # 余弦相似度分类 sim query_features prototypes.T # [M, N] preds classes[sim.argmax(dim1)] return preds逻辑说明F.normalize做 L2 归一化后点积等价于余弦相似度。原型计算用均值这是原型网络的核心思想。1-shot 时每个类只有一个样本原型就是该样本本身5-shot 时取 5 个样本的均值能降低噪声。这个方法的精度高度依赖主干特征的质量所以必须用预训练权重不能从头训练。如果要做更复杂的小样本方法比如 Matching Network 或 MAML代码量会大很多但核心思路不变在特征空间里做度量学习。实际落地时原型网络 预训练特征已经能覆盖大部分场景没必要一上来就上元学习。注意小样本分类的评估必须用 episodic 方式即每次随机采样 N 类 K shot 作为支持集再从这些类里采样查询集。如果直接用整个测试集算准确率结果会偏乐观。4. 训练过程排查loss 不降、精度震荡、显存爆炸怎么处理4.1 loss 不降的四个检查点训练启动后 loss 不动是最常见的问题。按顺序检查这四项第一学习率是否过大。Adam 默认 1e-3但微调预训练模型时往往要降到 1e-4 或 1e-5。如果 loss 在前几个 batch 就跳到 NaN基本是学习率太大。用torch.optim.lr_scheduler.CosineAnnealingLR做 warmup 能缓解。第二数据标签是否对齐。ImageFolder按文件夹名排序生成类别索引如果你自己写了class_to_idx映射必须和训练时一致。我遇到过验证集标签整体偏移一位的情况loss 完全不降排查了半天。第三归一化参数是否匹配。如果你用了预训练权重但归一化用了自己算的均值方差特征分布会偏移loss 下降很慢。统一用 ImageNet 的均值和方差。第四BatchNorm 的momentum是否合适。小 batch size 时 BatchNorm 的统计量不准建议改用GroupNorm或冻结 BN 层。model.apply(lambda m: m.momentum 0.01 if isinstance(m, nn.BatchNorm2d) else None)可以调小动量。4.2 精度震荡的排查顺序验证精度上下波动超过 5%先看 batch size。batch size 太小小于 16时梯度噪声大精度自然震荡。能加大就加大加不了就用梯度累积。# 梯度累积模拟大batch accumulation_steps 4 optimizer.zero_grad() for i, (images, labels) in enumerate(train_loader): outputs model(images) loss criterion(outputs, labels) / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()如果 batch size 没问题检查学习率调度。固定学习率在后期容易在最优解附近震荡加CosineAnnealing或ReduceLROnPlateau能稳定收敛。另外验证集本身太小也会导致精度波动每类至少 50 张验证图。4.3 显存爆炸的应急手段显存不够时按代价从低到高依次尝试减小 batch size、用混合精度训练、用梯度检查点、换更小的模型。混合精度训练是最划算的几乎不损失精度显存能省 30% 到 50%。from torch.cuda.amp import autocast, GradScaler scaler GradScaler() for images, labels in train_loader: optimizer.zero_grad() with autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()autocast自动把部分运算转成 float16GradScaler防止梯度下溢。注意autocast区域内的 loss 计算要用 float32所以criterion不要放在autocast里面。这个组合在 ResNet50 上能把 batch size 从 64 提到 128。5. 避坑与常见问题图像分类落地时的五个血泪教训5.1 现象训练精度 99%测试精度 60%原因数据泄漏。同一张图的不同增强版本同时出现在训练集和测试集或者训练集和测试集来自同一段视频的相邻帧。模型记住了训练样本没学到泛化特征。解决按时间、按设备、按场景划分数据集确保训练集和测试集在分布上不重叠。如果数据来自视频按视频 ID 划分不要按帧随机划分。5.2 现象模型对某些类别完全预测错原因类别不均衡。某个类别的样本数远少于其他类模型倾向于预测多数类。比如 10 个类其中 9 个类各有 1000 张1 个类只有 50 张模型几乎不会预测那个小类。解决用加权交叉熵损失权重设为类别频率的倒数。或者用重采样对小类过采样。WeightedRandomSampler是常用工具。from torch.utils.data import WeightedRandomSampler class_counts [1000, 1000, 50] # 每个类的样本数 weights [1.0 / c for c in class_counts] sample_weights [weights[label] for _, label in dataset] sampler WeightedRandomSampler(sample_weights, num_sampleslen(dataset), replacementTrue) train_loader DataLoader(dataset, batch_size32, samplersampler)5.3 现象验证 loss 先降后升原因过拟合。模型在训练集上继续优化但验证集开始变差。这是早停的信号。解决加正则化Dropout、Weight Decay、加数据增强、减小模型容量。早停的 patience 一般设 5 到 10 个 epoch监控验证 loss 而不是验证精度因为 loss 更平滑。5.4 现象推理速度远慢于预期原因预处理在 CPU 上做成为瓶颈。transforms默认在 CPU 上执行如果 GPU 利用率只有 30%说明数据加载拖后腿了。解决增加DataLoader的num_workers一般设为 CPU 核数的 2 到 4 倍。用pin_memoryTrue加速 CPU 到 GPU 的传输。如果还慢把预处理放到 GPU 上做用kornia库替代torchvision.transforms。5.5 现象换了随机种子结果差很多原因初始化或数据划分不稳定。小数据集上不同的随机种子会导致不同的训练/验证划分结果自然不同。解决固定所有随机种子包括 Python、NumPy、PyTorch 的种子。但要注意固定种子后如果结果仍然波动大说明模型本身不稳定需要检查数据量是否足够。import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark Falsecudnn.deterministicTrue会降低速度但能保证结果可复现。如果追求速度可以设benchmarkTrue但结果会有微小差异。6. 进阶技巧用特征可视化验证模型到底学到了什么训练完一个模型准确率达标了但你怎么知道它是在看物体还是在看背景我习惯用 Grad-CAM 做一次可视化检查。这个方法不需要改模型结构只要拿到目标层的梯度就能生成热力图。import torch import torch.nn.functional as F import cv2 import numpy as np class GradCAM: def __init__(self, model, target_layer): self.model model self.target_layer target_layer self.gradients None self.activations None target_layer.register_forward_hook(self.save_activation) target_layer.register_full_backward_hook(self.save_gradient) def save_activation(self, module, input, output): self.activations output.detach() def save_gradient(self, module, grad_input, grad_output): self.gradients grad_output[0].detach() def generate(self, input_image, target_classNone): output self.model(input_image) if target_class is None: target_class output.argmax(dim1).item() self.model.zero_grad() output[0, target_class].backward() # 对梯度做全局平均池化得到每个通道的权重 weights self.gradients.mean(dim(2, 3), keepdimTrue) # 加权求和激活图 cam (weights * self.activations).sum(dim1, keepdimTrue) cam F.relu(cam) cam F.interpolate(cam, sizeinput_image.shape[2:], modebilinear) cam cam.squeeze().cpu().numpy() cam (cam - cam.min()) / (cam.max() - cam.min() 1e-8) return cam # 使用示例 grad_cam GradCAM(model, model.layer4[-1]) cam grad_cam.generate(input_tensor) heatmap cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET)逻辑说明register_forward_hook保存前向传播的激活值register_full_backward_hook保存反向传播的梯度。weights是梯度的全局均值代表每个通道对目标类的重要性。加权求和后 ReLU 去掉负响应再插值回原图尺寸。生成的cam是 0 到 1 的热力图叠加到原图上就能看到模型关注区域。参数上target_layer一般选最后一个卷积 stage 的最后一层。ResNet50 选model.layer4[-1]ViT 选最后一个 Transformer block 的norm1或attention模块。层选得太浅热力图太粗糙选得太深感受野太大定位不准。我一般会抽 20 张验证集里预测正确的图和 20 张预测错误的图分别生成热力图。如果正确预测的图热力图集中在目标物体上说明模型学到了正确特征如果集中在背景或边缘说明模型走了捷径换一批背景不同的图就会翻车。错误预测的图如果热力图集中在目标上但分类错了可能是类别定义有歧义需要检查标注。这个检查花不了几分钟但能避免上线后才发现模型在「看背景」的尴尬。我现在的习惯是任何图像分类模型在交付前必须过一遍 Grad-CAM确认关注区域合理。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑