图像分类实战:从数据准备到小样本评估的完整流程 简介这份资源面向机器学习入门者与图像分类方向的开发者围绕SVM与贝叶斯分类器展开帮助读者理解如何用经典机器学习方法完成图像分类任务并提供一个可直接运行的图形界面工具用于对比不同算法的分类效果。压缩包共216个文件约12.87MB以bmp图像样本、h头文件与cpp源码、obj与sbr编译中间文件为主另含exe可执行程序、txt说明、ico与rc等界面资源以及vcproj、sln、dsp等工程文件构成一套完整的VC项目结构便于二次编译与调试。目前已有2483人学习下载。资源核心价值在于将SVM超平面分类、贝叶斯先验与条件概率计算等理论落地为可操作软件用户可通过界面直观比较算法表现同时源码中保留了特征提取与分类流程的实现细节适合作为课程设计、实验验证或算法入门的参考素材。1. 从一堆图片到能跑的模型这套图像分类实战资源到底值不值得拆手头有一批图片想训一个能区分猫狗、缺陷、车型或者森林植被的分类器结果卡在第一步——环境装完不知道从哪下手数据集摆在那不知道目录怎么组织模型跑起来 loss 不降反升。这套「机器学习方法的图像分类」资源解决的就是从零散图片到可复现分类流程的完整链路问题。它不挑框架核心是用经典机器学习与深度学习方法做图像分类的通用范式数据组织、特征提取、模型选型、训练调参、评估验证。适合两类人一是刚入门机器学习、想拿图像分类当第一个完整项目练手的二是已经会调库、但每次换数据集就翻车、想理清标准流程的。下面按「资源是什么 → 怎么用 → 坑在哪」推一遍能抄的代码和参数我都标出来。2. 图像分类的数据准备与特征工程目录结构、划分比例和三种特征提取路线图像分类翻车八成不是模型的问题是数据没整明白。这一章把数据准备和特征工程讲透因为后面模型再强喂进去的是乱数据结果就是玄学。2.1 数据集目录结构与训练验证测试划分绝大多数图像分类框架PyTorch 的 ImageFolder、TensorFlow 的 image_dataset_from_directory都认一种目录结构每个类别一个文件夹文件夹名就是标签。这是最省事的做法也是我一般会优先采用的。# 标准图像分类数据集目录结构 dataset/ ├── train/ # 训练集 │ ├── cat/ # 类别1文件夹名即标签 │ │ ├── 001.jpg │ │ └── 002.jpg │ └── dog/ # 类别2 │ ├── 001.jpg │ └── 002.jpg ├── val/ # 验证集结构同上 │ ├── cat/ │ └── dog/ └── test/ # 测试集结构同上 ├── cat/ └── dog/划分比例没有绝对标准常见做法是 7:1.5:1.5 或 8:1:1。小样本场景比如 1-shot、5-shot要另说后面第 5 章会展开。这里有个血泪经验划分必须在类别层面分层抽样不能随机切。如果某个类别只有 20 张图随机切可能导致验证集里一张都没有评估结果直接失真。import os import shutil import random from pathlib import Path def split_dataset(src_dir, dst_dir, ratios(0.7, 0.15, 0.15), seed42): 按类别分层划分数据集 src_dir: 原始数据每个类别一个子文件夹 ratios: (train, val, test) 比例和为1 random.seed(seed) src Path(src_dir) dst Path(dst_dir) classes [d.name for d in src.iterdir() if d.is_dir()] for cls in classes: imgs list((src / cls).glob(*.jpg)) list((src / cls).glob(*.png)) 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_name, files in splits.items(): out dst / split_name / cls out.mkdir(parentsTrue, exist_okTrue) for f in files: shutil.copy2(f, out / f.name) print(f划分完成共 {len(classes)} 个类别) split_dataset(./raw_images, ./dataset)这段脚本的关键参数是ratios和seed。seed固定后每次划分结果一致方便复现。ratios根据数据量调整数据量少于 1000 张时验证集和测试集各留 10% 就够训练集尽量大数据量过万时1.5:1.5 的验证测试比例更稳。2.2 传统机器学习路线的特征提取HOG、SIFT 与颜色直方图如果不用深度学习传统机器学习做图像分类的流程是「特征提取 分类器」。特征提取决定了上限分类器SVM、随机森林只是在这个上限内找最优边界。三种常用特征特征方法适合场景维度对光照/旋转的鲁棒性HOG形状明显的目标行人、车辆中对光照较鲁棒对旋转一般SIFT纹理丰富、需要尺度不变性高对旋转尺度鲁棒计算慢颜色直方图颜色区分度高的场景森林、植被低对光照敏感对旋转不敏感import cv2 import numpy as np from skimage.feature import hog from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline def extract_hog(img_path, size(128, 128)): 提取HOG特征 img cv2.imread(img_path) img cv2.resize(img, size) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # orientations9: 梯度方向分9个bin # pixels_per_cell(8,8): 每个cell 8x8像素 # cells_per_block(2,2): 每2x2个cell组成一个block做归一化 features hog(gray, orientations9, pixels_per_cell(8, 8), cells_per_block(2, 2), visualizeFalse) return features # 构建SVM分类流水线 clf Pipeline([ (scaler, StandardScaler()), # HOG特征做标准化 (svm, SVC(kernelrbf, C10, gammascale)) ])HOG 的参数里orientations控制梯度方向的分辨率9 是经典值pixels_per_cell越小特征越细但维度越高8x8 是速度和精度的平衡点。SVM 的C越大越容易过拟合gamma控制 RBF 核的作用范围。传统方法在数据量小于 5000 张、类别少于 10 类时训练速度比深度学习快一个数量级但精度通常低 5 到 15 个百分点。2.3 深度学习路线的数据增强与归一化深度学习做图像分类数据增强是标配。常见做法是随机裁剪、水平翻转、颜色抖动。归一化必须和预训练模型匹配——用 ResNet50 的预训练权重就得用 ImageNet 的均值和标准差。from torchvision import transforms # ImageNet标准归一化参数 IMAGENET_MEAN [0.485, 0.456, 0.406] IMAGENET_STD [0.229, 0.224, 0.225] train_transform transforms.Compose([ transforms.RandomResizedCrop(224), # 随机裁剪到224x224 transforms.RandomHorizontalFlip(p0.5), # 50%概率水平翻转 transforms.ColorJitter(brightness0.2, contrast0.2), # 颜色抖动 transforms.ToTensor(), transforms.Normalize(meanIMAGENET_MEAN, stdIMAGENET_STD) ]) val_transform transforms.Compose([ transforms.Resize(256), # 先缩到256 transforms.CenterCrop(224), # 中心裁剪224 transforms.ToTensor(), transforms.Normalize(meanIMAGENET_MEAN, stdIMAGENET_STD) ])验证集和测试集不能做随机增强只能用确定性的 resize center crop。这个坑我见过太多次有人在验证集上也加了 RandomHorizontalFlip结果每次评估精度都在跳还以为是模型不稳定。归一化参数不匹配预训练权重会导致训练初期 loss 震荡甚至不收敛这是黑匣子式的翻车排查起来很费时间。3. 模型选型与训练从 ResNet50 到 ViT 的分类头调整数据整好了下一步是选模型和训模型。这一章讲清楚两条主流路线的选型逻辑和训练细节尤其是迁移学习场景下分类头怎么改。3.1 ResNet50 迁移学习冻结策略与分类头替换ResNet50 是图像分类的万金油预训练权重在 ImageNet 上训过特征提取能力已经很强。迁移学习的标准做法是替换最后的全连接层改成自己类别的数量然后分阶段解冻。import torch import torch.nn as nn from torchvision import models def build_resnet50(num_classes, freeze_backboneTrue): 构建ResNet50分类模型 num_classes: 自己的类别数 freeze_backbone: 是否冻结主干网络 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) if freeze_backbone: # 冻结所有主干参数 for param in model.parameters(): param.requires_grad False # 替换最后的全连接层 in_features model.fc.in_features # ResNet50是2048 model.fc nn.Sequential( nn.Dropout(0.3), # 防过拟合 nn.Linear(in_features, num_classes) ) return model model build_resnet50(num_classes10, freeze_backboneTrue) # 只有fc层的参数需要训练 optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3)冻结主干时只有model.fc的参数在更新学习率可以设大一点1e-3。训练几轮后如果精度上不去再解冻最后几个 block学习率降到 1e-4 到 1e-5。这个「先冻结后解冻」的策略比一上来就全量微调更稳尤其是在目标数据集很小的时候。Dropout(0.3)是经验值类别越少、数据越少dropout 可以适当调大。3.2 ViT 评估时分类头要不要调整用 ViTVision Transformer做迁移学习时分类头的处理方式和 ResNet 不一样。ViT 的输出是一个[CLS]token 经过 MLP head 得到分类结果。常见做法是替换整个 head而不是只换最后一层。import timm import torch.nn as nn def build_vit(num_classes, model_namevit_base_patch16_224): 构建ViT分类模型 timm库会自动处理预训练权重加载 model timm.create_model( model_name, pretrainedTrue, num_classesnum_classes # 直接指定类别数timm会替换head ) return model model build_vit(num_classes10) # ViT微调学习率要小通常1e-5到5e-5 optimizer torch.optim.AdamW(model.parameters(), lr2e-5, weight_decay0.01)关于「用 ViT 评估时分类头用调整吗」这个问题答案是训练时必须替换分类头因为预训练模型的 head 输出是 1000 类ImageNet你的类别数不是 1000。评估时用的是替换后的 head不需要额外调整。但有一个细节如果做的是 1-shot 或 5-shot 小样本评估常见做法是冻结整个 ViT 主干只训练一个线性分类器甚至直接用原型网络Prototypical Network的思路不训练 head而是用支持集的特征均值做分类。这个后面第 5 章展开。ViT 微调比 ResNet 更吃数据数据量少于 1000 张时ViT 通常打不过 ResNet50。如果非要用 ViT建议用 DINO 或 MAE 预训练的权重它们在小样本上表现更好。3.3 训练循环与学习率调度训练循环里学习率调度和早停是两个关键机制。常见做法是用 CosineAnnealing 或 ReduceLROnPlateau。from torch.optim.lr_scheduler import CosineAnnealingLR from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder # 数据加载 train_dataset ImageFolder(./dataset/train, transformtrain_transform) val_dataset ImageFolder(./dataset/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) # 训练配置 device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr2e-5, weight_decay0.01) scheduler CosineAnnealingLR(optimizer, T_max50) # 50个epoch周期 best_acc 0.0 patience 10 # 早停耐心值 counter 0 for epoch in range(50): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() # 验证 model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) preds outputs.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) acc correct / total scheduler.step() if acc best_acc: best_acc acc torch.save(model.state_dict(), best_model.pth) counter 0 else: counter 1 if counter patience: print(f早停于 epoch {epoch}最佳精度 {best_acc:.4f}) break print(fEpoch {epoch}: val_acc{acc:.4f}, best{best_acc:.4f})batch_size32是常见起点显存不够就降到 16 或 8。num_workers4在 Linux 上加速数据加载Windows 上如果报错就改成 0。T_max50要和总 epoch 数匹配CosineAnnealing 会在 50 个 epoch 内把学习率从初始值降到接近 0。早停的patience10表示验证精度连续 10 轮不提升就停防止过拟合。4. 避坑与排查图像分类训练中最容易翻车的五个地方这一章全是踩坑记录每条按「现象 → 原因 → 解决」写。这些坑我基本都亲自踩过有些排查了大半天才找到原因。4.1 现象loss 不降或震荡剧烈原因最常见的是归一化参数不匹配。用 ImageNet 预训练权重但归一化用了mean[0.5,0.5,0.5]特征分布和预训练时不一致模型相当于在噪声上训练。其次是学习率太大尤其是解冻主干后还用 1e-3 的学习率梯度爆炸。解决检查归一化参数是否和预训练权重匹配。ResNet50 和 ViT 的 ImageNet 权重都用mean[0.485,0.456,0.406]、std[0.229,0.224,0.225]。解冻主干后学习率降到 1e-4 到 1e-5。如果 loss 还是震荡加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。4.2 现象训练精度很高验证精度很低原因过拟合。数据量太少、模型太大、训练太久都会导致。另一个容易被忽略的原因是数据泄露——训练集和验证集有重复图片或者同一张图的不同增强版本分别进了训练集和验证集。解决先查数据泄露用图片的 MD5 或感知哈希去重。确认没有泄露后加数据增强、加 Dropout、加权重衰减weight_decay0.01到0.1、早停。如果数据量确实太少每类少于 100 张考虑用更强的预训练权重或做小样本学习。4.3 现象验证集精度波动大每次评估结果不一样原因验证集用了随机增强。有人在val_transform里加了RandomHorizontalFlip或RandomResizedCrop导致每次评估的输入不同。另一个原因是 BatchNorm 在验证时没有切换到 eval 模式model.eval()没调用。解决验证和测试的 transform 只用确定性的ResizeCenterCropNormalize。评估前必须调model.eval()并用torch.no_grad()包住推理过程。4.4 现象GPU 显存溢出OOM原因batch_size 太大、图片分辨率太高、模型参数量太大。ViT 比 ResNet 更吃显存因为注意力机制的计算复杂度是序列长度的平方。解决先降 batch_size从 32 降到 16 或 8。如果还不行降输入分辨率ViT 可以从 224 降到 192 或 160。再不行就用梯度累积accumulation_steps4表示每 4 个 batch 才更新一次参数等效于 batch_size 翻 4 倍但显存不变。# 梯度累积示例 accumulation_steps 4 optimizer.zero_grad() for i, (imgs, labels) in enumerate(train_loader): outputs model(imgs) loss criterion(outputs, labels) / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()4.5 现象类别不平衡导致少数类精度极低原因某个类别样本数远少于其他类别模型倾向于预测多数类。比如 1000 张猫、50 张狗模型全预测猫也有 95% 的精度但狗一张都认不出来。解决三种常见做法。一是重采样对少数类过采样或对多数类欠采样。二是加类别权重CrossEntropyLoss(weightclass_weights)权重和类别频率成反比。三是用 Focal Loss降低易分类样本的权重让模型关注难样本。# 类别权重计算 from collections import Counter import torch labels [label for _, label in train_dataset] counts Counter(labels) total sum(counts.values()) class_weights torch.tensor( [total / (len(counts) * counts[i]) for i in range(len(counts))], dtypetorch.float32 ).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)5. 小样本图像分类实战1-shot 与 5-shot 的评估流程和 ResNet50 完整步骤小样本图像分类是这两年图像分类里热度很高的方向1-shot 和 5-shot 是最常见的评估设定。这一章讲清楚评估流程并给出用 ResNet50 做小样本分类的完整步骤。5.1 1-shot 与 5-shot 的评估逻辑小样本评估的核心是「N-way K-shot」从数据集中抽 N 个类别每个类别抽 K 张支持集support set图片和若干查询集query set图片。模型只看支持集然后对查询集分类。1-shot 就是 K15-shot 就是 K5。常见做法是原型网络Prototypical Network用模型提取支持集特征每个类别的特征均值作为该类的原型查询集样本离哪个原型近就归哪类。这个过程不需要训练分类头直接用预训练模型的特征提取能力。import torch import torch.nn.functional as F def evaluate_few_shot(model, dataset, n_way5, k_shot1, n_query15, n_episodes100): 小样本评估N-way K-shot n_way: 每轮抽取的类别数 k_shot: 每类支持集样本数 n_query: 每类查询集样本数 n_episodes: 评估轮数 model.eval() accs [] for _ in range(n_episodes): # 随机抽n_way个类别 classes random.sample(range(len(dataset.classes)), n_way) support, query [], [] for cls in classes: indices [i for i, (_, label) in enumerate(dataset) if label cls] selected random.sample(indices, k_shot n_query) support_idx selected[:k_shot] query_idx selected[k_shot:] for idx in support_idx: img, _ dataset[idx] support.append((img, cls)) for idx in query_idx: img, _ dataset[idx] query.append((img, cls)) # 提取支持集特征计算原型 prototypes {} with torch.no_grad(): for cls in classes: cls_imgs torch.stack([img for img, c in support if c cls]).to(device) features model(cls_imgs) prototypes[cls] features.mean(dim0) # 查询集分类 correct 0 for img, true_cls in query: feat model(img.unsqueeze(0).to(device)) # 找最近的原型 dists {cls: F.pairwise_distance(feat, proto.unsqueeze(0)) for cls, proto in prototypes.items()} pred min(dists, keydists.get) if pred true_cls: correct 1 accs.append(correct / len(query)) return sum(accs) / len(accs)这段代码的关键参数是n_way、k_shot、n_query和n_episodes。标准设定是 5-way 1-shot 和 5-way 5-shotn_query通常取 15n_episodes取 100 到 1000 轮取平均。评估时模型必须冻结不能更新参数。5.2 用 ResNet50 做小样本分类的完整步骤用 ResNet50 做小样本分类完整流程分五步第一步加载预训练 ResNet50去掉最后的全连接层。只保留特征提取部分输出 2048 维特征向量。import torch.nn as nn from torchvision import models def build_feature_extractor(): 构建ResNet50特征提取器去掉fc层 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) # 去掉最后的fc层保留avgpool输出 model nn.Sequential(*list(model.children())[:-1]) model.eval() return model feature_extractor build_feature_extractor().to(device)第二步准备小样本数据集。用ImageFolder加载但评估时按 episode 抽样不用 DataLoader 的 batch 机制。第三步计算原型。对每个类别的支持集图片提取特征取均值作为原型。第四步查询集分类。对每张查询集图片提取特征计算与各原型的欧氏距离取最近的原型类别。第五步多轮评估取平均。单轮评估随机性太大必须跑 100 轮以上取平均。1-shot 的精度通常比 5-shot 低 10 到 20 个百分点这是正常的。5.3 小样本评估的常见参数与结果解读参数常见值说明n_way5每轮类别数也有 10-way、20-wayk_shot1 或 5支持集每类样本数n_query15查询集每类样本数n_episodes100~1000评估轮数越多越稳距离度量欧氏距离也有用余弦相似度的结果解读时注意1-shot 精度在 5-way 设定下ResNet50 通常在 50% 到 70% 之间5-shot 能到 70% 到 85%。如果 1-shot 精度低于 40%检查特征提取器是否加载了预训练权重以及归一化参数是否正确。如果 5-shot 比 1-shot 还低大概率是支持集抽样有 bug比如同一张图被抽进了多个类别。6. 把分类器推到能用的最后一公里混淆矩阵、置信度校准与一个习惯模型训完、精度看着还行不代表能直接用。最后一公里要做三件事看混淆矩阵找系统性错误、做置信度校准、固定随机种子复现。混淆矩阵能暴露精度数字掩盖的问题。比如 10 类分类整体精度 90%但混淆矩阵显示类别 3 和类别 7 互相误判率高达 40%这两个类在实际场景里可能恰好是需要区分的。常见做法是用sklearn.metrics.confusion_matrix画出来重点看非对角线上的大值。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt def evaluate_with_confusion(model, test_loader, class_names): 评估并输出混淆矩阵 model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in test_loader: imgs imgs.to(device) outputs model(imgs) preds outputs.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)) plt.figure(figsize(10, 8)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsclass_names, yticklabelsclass_names) plt.xlabel(Predicted) plt.ylabel(True) plt.savefig(confusion_matrix.png, dpi150, bbox_inchestight) return cm置信度校准是另一个容易被忽略的点。模型输出的 softmax 概率往往偏高比如预测猫的概率是 0.95但实际正确率只有 0.8。常见做法是用温度缩放Temperature Scaling在验证集上拟合一个温度参数 T推理时用softmax(logits / T)。T 大于 1 会让概率分布更平滑更接近真实正确率。class TemperatureScaler(nn.Module): 温度缩放校准 def __init__(self): super().__init__() self.temperature nn.Parameter(torch.ones(1) * 1.5) def forward(self, logits): return logits / self.temperature # 在验证集上优化temperature参数 scaler TemperatureScaler().to(device) optimizer torch.optim.LBFGS([scaler.temperature], lr0.01, max_iter50) criterion nn.CrossEntropyLoss() def closure(): optimizer.zero_grad() calibrated scaler(all_logits) loss criterion(calibrated, all_labels) loss.backward() return loss optimizer.step(closure) print(f最优温度: {scaler.temperature.item():.3f})最后是一个习惯每次实验固定所有随机种子。PyTorch、NumPy、Python 内置的 random 都要固定否则同一份代码跑两次结果不一样排查问题时根本分不清是改动生效了还是随机波动。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 False set_seed(42)cudnn.deterministic True会让训练稍慢但结果可复现。benchmark False也是同样目的。从那以后我每次跑图像分类实验都强制走一遍set_seed、检查归一化参数、确认验证集没有随机增强这三步做完再开始训省下的排查时间比训练本身还多。希望帮到你。本文还有配套的精品资源点击获取