
1. 先搞清楚你的图像识别为什么准确率低做图像识别的朋友应该都有过这种经历模型在训练集上表现还行一到验证集准确率就崩或者从一开始 loss 就下不去。我也踩过不少坑最初拿到的数据集明明看着挺规整跑出来的结果却让人想摔键盘。说白了图像识别准确率低通常逃不出这几个原因数据质量不行、模型结构选错、训练策略粗糙、评估方式有漏洞。先说数据质量。很多人觉得只要图片够多就行实际上图片的标注一致性、类别均衡性、拍摄环境差异往往才是决定上限的因素。比如做工业质检产品表面划痕在不同光照下差异极大如果训练集里全是同一角度、同一亮度的图片模型学到的就不是“划痕长什么样”而是“这个亮度下有什么”换个环境直接就废。再说模型结构。图像识别有一个经典思路就是从手工特征逐步转向卷积神经网络。传统方法靠人工设计边缘、纹理、颜色直方图这些特征再用 SVM 或决策树做分类特征工程做得好还能出活但遇到复杂背景、姿态变化、光照干扰就非常吃力。CNN 的出现相当于把“怎么提取特征”这件事也交给了网络自己去学你用不着再手工琢磨 SIFT 或 HOG 要怎么调参反而可以把精力放在网络结构设计和训练细节上。分类不准确的第三类原因出在训练环节。学习率设得太大loss 会震荡设得太小收敛慢到怀疑人生。Batch size 选得不对Batch Normalization 的行为也会变化推理时统计量和训练时不匹配准确率照样掉。这里面的细枝末节如果只看理论不实操很难体会得到。如果你已经试过一些现成的分类工具但准确率始终上不去这篇文章会沿着 CNN 的完整实践路径帮你排查从数据准备、网络搭建到训练调参、评估诊断一套流程走下来你大概率能知道问题卡在哪一个环节。2. CNN 是怎么做到“高效分类”的——整体设计与思路拆解2.1 从全连接到卷积为什么图像任务必须换思路最早做图像分类最直接的想法是把每个像素当作一个特征丢进全连接网络。比如一张 256x256 的三通道图展平后就是 19 万多个输入维度。全连接层把所有维度和下一层所有神经元连接起来参数量瞬间爆炸不仅训练慢还特别容易过拟合。更关键的问题是这种展平操作完全丢掉了图像的“空间结构”。猫的眼睛在图的左上角还是右下角对于展平后的向量来说只是特征顺序不同但网络完全没有办法把“相邻像素之间的关联”利用起来。人眼能认出猫靠的是胡须、耳朵、瞳孔这些局部模式的组合而不是孤立地看每个像素值。卷积操作的核心思路就是“局部感受野 权值共享”。用一个小尺寸的卷积核比如 3x3在图像上滑动每次只看这个小窗口里的像素并把同一个卷积核用在整张图上。这样一来参数量大幅减少。一个 3x3 卷积核只有 9 个权重加上通道维度不管输入图像多大同一层的参数是固定的。平移等变性自然获得。猫往左挪了几个像素卷积核照样能捕捉到同样的局部特征只是输出特征图上的位置变了。浅层学边缘和纹理深层学部件和语义这种层次化表达非常契合图像的天然结构。这就是为什么从全连接切到卷积网络图像分类的任务一次质的飞跃。2.2 经典 CNN 结构的演化从 LeNet 到 ResNet题目里提到手写数字识别很多人最早接触 CNN 都是从 LeNet 开始的。LeNet-5 的结构在今天看来很简单两个卷积层、两个池化层、三个全连接层但对 32x32 的灰度图分类效果已经非常好。它确立了“卷积提取特征 池化降维 全连接分类”这个基本范式。后续的结构演进基本是在回答几个问题网络更深能不能更好结果是可以但直接堆层会遇到梯度消失于是出现了 ResNet 的残差连接。能不能把模型变小于是有了 Depthwise Separable ConvolutionMobileNet 系列在移动端大放异彩。多尺度信息怎么融合于是有了 FPN、ASPP 这类结构。对于做实际项目的人来说不需要每次都从零设计网络。工程上有一个非常实用的判断标准数据量只有几千张类别在 10 个以内优先考虑 ResNet-18 或 MobileNetV3配合 ImageNet 预训练权重做迁移学习。数据量达到几万张以上可以尝试 ResNet-50、EfficientNet 系列。如果对推理速度有硬性要求比如嵌入式设备或实时视频流MobileNet 系列是首选必要时再用量化压缩。用不用预训练权重其实很关键。ImageNet 上预训练的模型已经在海量自然图像上学过通用的边缘、纹理、形状特征面对新任务时只需要微调后面的分类层即便新数据集不大也能得到不错的效果。这是图像分类项目里最划算的一个技巧没有之一。但要注意预训练权重对输入图像的尺寸、归一化方式是有约定的。比如 ImageNet 上训练的模型通常要求输入归一化到 mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]。如果你自己随便用什么 mean 和 std 做归一化哪怕网络结构完全一样效果也会打折扣这一点我在新手代码里看到过太多次了。3. 手把手搭建 CNN 分类器——核心细节解析与实操要点3.1 数据准备先别急着写模型很多人做图像分类一上来就搭模型模型写完才回头处理数据最后发现数据才是最大的坑。正确顺序是先做数据探查。第一步统计每个类别的图片数量。类别不均衡是最常见的问题。假设你做一个 10 类分类其中 9 类各有 5000 张剩下 1 类只有 200 张模型为了降低整体 loss会倾向于把所有样本都预测成那 9 类个别类别的准确率会非常难看。解决办法有几种对少数类做过采样重复抽样或者做数据增强来扩充也可以在 loss 函数里给少数类更高的权重。第二步检查图片质量。模糊、过度压缩、标注错误这些都是隐蔽的数据杀手。有个土办法很管用随机抽几百张图把原图和标注信息打印出来人工过一遍虽然费时间但能避免后期返工。如果图片尺寸不统一需要做缩放或填充建议先统一尺寸再进网络省的 DataLoader 里每次做 resize 浪费 I/O。第三步划分数据集。训练集、验证集、测试集要严格分开而且要保证分布一致。怎么保证按类别比例做分层采样。比如每个类别取 70% 进训练集、15% 进验证集、15% 进测试集。千万不要在数据预处理阶段就让验证集的信息泄漏进训练集——比如你用整份数据的均值去做归一化严格来说这不规范虽然影响可能不大但没必要养成这种习惯。3.2 网络搭建用代码看一个实际例子直接用一个可运行的例子来演示。这里用 PyTorch 搭建一个简化版的 CNN用来做一个 10 类彩色图像分类任务。import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes10): super(SimpleCNN, self).__init__() self.features nn.Sequential( # 输入: 3x224x224 nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 32x112x112 nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 64x56x56 nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 128x28x28 nn.Conv2d(128, 256, kernel_size3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 256x14x14 ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(256, 512), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(512, num_classes) ) def forward(self, x): x self.features(x) x self.classifier(x) return x几个细节说明一下。BatchNorm2d 放在卷积层之后、激活函数之前是通用的做法实践下来比放激活后稳定。MaxPool2d 的下采样降低特征图尺寸同时也增大感受野。AdaptiveAvgPool2d((1,1)) 是一个值得养成的习惯——不管输入特征图多大它都能池化成 1x1这样后面的全连接层输入维度就固定了模型可以适应不同尺寸的输入。Dropout(0.5) 在全连接层前加一个能有效缓解过拟合但对于已经用了 BatchNorm 的卷积层通常不需要在卷积层后面加 Dropout两者功能上有重叠叠加反而可能损害信息。3.3 训练策略优化器、学习率与 Loss分类任务最常用的 Loss 是交叉熵损失 CrossEntropyLoss。它内部已经包含了 Softmax 操作所以网络的最后一层不要额外加 Softmax直接输出 logits 就行。如果加了 Softmax再传进 CrossEntropyLoss 会导致数值不稳定准确率莫名其妙地下降这是我见过特别多新手踩的坑。优化器方面Adam 是默认选项收敛快对学习率不那么敏感。但 Adam 有时候会走到尖锐的局部最优泛化能力可能不如 SGD。比较稳妥的做法是先用 Adam 跑几十个 epoch把模型训到 loss 不再明显下降再切换成 SGD momentummomentum0.9把学习率调低一个数量级继续微调。这种“先 Adam 后 SGD”的做法在很多竞赛里被验证有效。学习率可以按迭代步数做余弦退火也可以用阶梯式下降。举例来说初始学习率设成 1e-3每训练 20 个 epoch 学习率乘以 0.1一共训练 60 个 epoch。如果想更省心用 PyTorch 的 ReduceLROnPlateau它会在验证 loss 不再下降时自动调低学习率。Batch size 的选取要同时考虑显存和模型效果。过大的 batch size 会降低梯度噪声可能导致收敛到较差的极小值过小的 batch size 则训练不稳定。如果你用单卡训练建议先从 32 或 64 开始观察显存占用和 loss 曲线再调整。BatchNorm 在 batch size 很小时比如小于 8统计量会不稳定这时可以考虑换成 GroupNorm。训练过程至少每个 epoch 打印一次训练 loss、训练准确率、验证准确率。loss 下降但验证准确率不升说明模型在过拟合两个都不动可能是学习率太低或者网络结构有问题loss 直接变成 NaN多半是学习率太大或数据里有异常值。4. 数据增强与迁移学习——分类效果提升的两张王牌4.1 数据增强让模型“见过更多世面”数据增强的本质是对训练图片做一系列保持语义不变的变换让模型看到同一张图的不同版本。它相当于一种免费的数据扩充能显著提升模型的泛化能力尤其在小数据集上效果明显。常用的增强手段按威力排序RandomResizedCrop: 随机裁剪一块区域再缩放到固定尺寸。这个变换非常强它强迫模型学习目标的整体结构而不是只依赖某个位置的局部特征。RandomHorizontalFlip: 水平翻转。对大多数自然图像分类任务都适用但对文字识别、车牌识别这类方向敏感的任务要关掉。ColorJitter: 随机调整亮度、对比度、饱和度、色相。能增强模型对光照变化的鲁棒性但幅度别太大否则颜色本身就是重要特征的任务比如识别交通信号灯会被干扰。RandomRotation: 小角度旋转±10度到±20度。超过 30 度容易切到图像边缘造成信息丢失需要配合 padding 或 reflection padding 使用。PyTorch 里用 torchvision.transforms 实现增强非常方便。但有一个容易忽略的坑验证集和测试集不要做随机增强。只能做尺寸缩放和归一化因为增强的目的是模拟更多变的训练样本而不是改变真实数据的分布。如果在验证集上也做随机裁剪每次验证结果都会波动根本没法判断模型真的变好了没有。以手写数字识别为例单纯把 MNIST 的图片喂给 CNN不做任何增强大概能达到 99% 左右的准确率。但如果任务是识别票据上手写的数字票据图片可能有倾斜、模糊、噪声——这时候在训练时随机旋转、平移、加少量高斯噪声模型的在真实场景的表现会明显提升。图像识别“准确率低”并不一定是模型不行很可能是训练数据太“干净”了没见过真实世界的脏乱差。4.2 迁移学习站在预训练模型的肩膀上前面提到用 ImageNet 预训练权重做初始化这里把细节说透。ImageNet 数据集包含 1000 类、超过 120 万张自然图像。通过在这个数据集上训练模型已经学会了非常丰富的底层视觉特征比如边缘、角点、纹理模式、颜色分布。对于一个新的图像分类任务这些底层特征绝大多数是可复用的。你要做的只是把最后的分类头换掉用自己的数据集微调。实操上分两种策略特征提取器模式冻结 backbone 的参数requires_gradFalse只训练新加的分类头。适合数据集非常小每类只有几百张且与 ImageNet 图像分布比较接近的情况。训练速度快不容易过拟合。全模型微调模式所有层都参与训练。适合数据量较大或目标图像与 ImageNet 差异大的情况。backbone 用预训练权重初始化但学习率要降低一般 backbone 层用分类头的 1/10 学习率防止预训练特征被破坏。PyTorch 里用 torchvision.models 加载预训练模型非常简单import torchvision.models as models model models.resnet18(pretrainedTrue) # 替换最后一层全连接 num_ftrs model.fc.in_features model.fc nn.Linear(num_ftrs, 10)ResNet18 的最后一层是全连接层新数据集的类别数比如 10在这里指定。如果你的输入是灰度单通道图比如手写数字任务可以把第一个卷积层的输入从 3 改成 1并将预训练权重的对应层做平均处理或者更省事的方式是把灰度图复制成三通道再输入。迁移学习对数据量的要求宽容得多。有人做过对比在只有 500 张图的小数据集上从零训练 CNN 的验证准确率大概在 70% 出头而用 ResNet18 预训练权重微调可以轻松到 90% 以上。这个差距在中小规模数据集上非常稳定所以做实际项目的工程师几乎都会默认使用迁移学习。5. 训练过程全记录——从数据加载到模型评估5.1 训练脚本的完整流程把前面讲的东西串成一个完整可运行的流程。以 PyTorch 为例一个标准的训练脚本包含这些关键步骤数据加载部分。用 torchvision.datasets.ImageFolder 可以非常方便地读取按文件夹结构组织的图片数据。文件夹名就是类别名每个文件夹里的图片属于对应类别。from torch.utils.data import DataLoader from torchvision import datasets, transforms # 训练集增强 归一化 train_transform transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 验证集只做缩放和归一化 val_transform 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, transformtrain_transform) val_dataset datasets.ImageFolder(data/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse, num_workers4)训练循环部分。model models.resnet18(pretrainedTrue) num_ftrs model.fc.in_features model.fc nn.Linear(num_ftrs, len(train_dataset.classes)) model model.cuda() criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lr1e-3, momentum0.9) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.1) def train_one_epoch(model, loader, criterion, optimizer): model.train() running_loss 0.0 correct 0 total 0 for inputs, labels in loader: inputs, labels inputs.cuda(), labels.cuda() optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * inputs.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_loss running_loss / total epoch_acc correct / total return epoch_loss, epoch_acc def validate(model, loader, criterion): model.eval() running_loss 0.0 correct 0 total 0 with torch.no_grad(): for inputs, labels in loader: inputs, labels inputs.cuda(), labels.cuda() outputs model(inputs) loss criterion(outputs, labels) running_loss loss.item() * inputs.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_loss running_loss / total epoch_acc correct / total return epoch_loss, epoch_acc for epoch in range(60): train_loss, train_acc train_one_epoch(...) val_loss, val_acc validate(...) scheduler.step() print(fEpoch {epoch1} | Train Loss {train_loss:.4f} | Train Acc {train_acc:.4f} | Val Acc {val_acc:.4f})这里有个容易忽略的细节模型在训练状态和推理状态的 Dropout、BatchNorm 行为是不同。调用 model.train() 和 model.eval() 切换模式很多人忘了在验证前调用 model.eval()结果 BatchNorm 仍然使用训练时的统计行为验证准确率会偏低且不稳定。5.2 评估指标别只盯着一个准确率“图像识别准确率低”这个说法本身就有点笼统。如果只看总体准确率在类别不均衡的情况下容易被表面数字骗到。一个 10 类分类任务某类占 90%模型把所有样本都预测成这一类就有 90% 的准确率。但你的目标类别可能恰好是那 10% 里的某几个这时候总体准确率毫无意义。看分类效果至少要用到这几个指标Precision 精确率预测为正类的样本中有多少是真正类。它回答“模型说这是猫有多大可能是猫”。Recall 召回率真正的正类样本中有多少被模型找了出来。它回答“所有猫里面模型找到了多少只”。F1-ScorePrecision 和 Recall 的调和平均类别不均衡时比准确率更能反映模型质量。在技术决策上怎么选指标取决于业务需求。医疗影像筛查更看重 Recall因为漏诊的代价远高于误诊垃圾邮件过滤更看重 Precision因为把正常邮件误判为垃圾邮件的代价用户很难接受。图像分类项目开始之前先想清楚你要优化哪个指标不要盲目追求总体准确率。把每一类的 Precision、Recall、F1 都打印出来通常能定位到问题类别。最常见的现象是大部分类别 F1 很高但一两个类别特别低。这时候去看这些类别的训练样本——数量是不是太少图片是不是和其他类别长得太像如果两个类别在视觉上非常接近比如“矿泉水瓶”和“饮料瓶”可以考虑是否合并类别或者在数据收集阶段把它们分开采集中间不要有模糊样本。5.3 用混淆矩阵定位错误模式混淆矩阵是分类问题里最直观的诊断工具。每一行代表真实类别每一列代表预测类别。对角线上的值越大越好非对角线上的亮点就是模型经常混淆的类别对。PyTorch sklearn 画混淆矩阵很简单import numpy as np from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt all_preds [] all_labels [] model.eval() with torch.no_grad(): for inputs, labels in val_loader: inputs inputs.cuda() outputs model(inputs) _, predicted torch.max(outputs, 1) all_preds.extend(predicted.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(cm)实践中有个经验之谈如果混淆矩阵显示“类别 A 经常被预测成类别 B”先去看这两类的原始图片通常能发现它们确实容易混淆比如背景相似、目标太小、拍摄角度刁钻。这种情况下不要急着堆模型结构先把数据里这些模糊样本挑出来或者增加这两类尤其是 A 类的训练数据效果立竿见影。6. 准确率提升路线图——从 70% 到 95% 的实战策略6.1 六条经过验证的优化手段在实际项目里模型从 70% 提到 95%靠的往往不是某个单点的奇技淫巧而是一套组合拳。我按优先级顺序整理如下第一先做数据清洗和标注一致性检查。把标注错误的样本找出来改正这一项有时就能提升三到五个百分点。找错标样本的土办法用一个不算差的模型预测一遍训练集把模型高置信度但和标注不一致的样本打印出来人工复核——这些往往是标注错误或边界样本。第二使用迁移学习加载预训练权重。从零训练 ResNet 在中小规模数据集上的效果远不如微调。第三选对网络结构。ResNet 系列适合通用场景EfficientNet 在精度和计算量的平衡上很出色。这里建议做个候选模型对比实验固定数据集和训练超参数分别跑 ResNet18、ResNet34、MobileNetV3、EfficientNet-B0选验证集 F1 最高的。第四数据增强从“标准三件套”开始加起RandomResizedCrop、RandomHorizontalFlip、ColorJitter。如果还能涨点再考虑 CutMix 或 MixUp 这类高级增强。第五调整训练策略学习率用余弦退火或阶梯下降优化器换成 SGD momentum 做精调增加训练轮数配合早停法。第六类别不均衡时给 Loss 加权重或做类别重采样如果最后还有余力可以尝试各种集成方法比如对不同 epoch 的模型权重做平均或对多个模型的预测概率做平均通常能再涨个 0.5 到 1 个百分点。6.2 不同场景的分类调优差异不同来源的图像面临的难点完全不一样不能一套方案打天下。自然图像分类比如猫狗、花朵、场景识别主要难点在类内差异大、背景复杂。同一个品种的猫在不同姿态、不同光照下可能差异很大这时强数据增强和迁移学习的效果最明显。工业质检类的图像分类难点在缺陷样本稀少、正负样本极度不均衡、缺陷尺度可能很小。这种情况下简单分类网络往往不够可能要先用目标检测模型把缺陷区域定位出来再对局部区域做分类。如果只能用整图分类要注意模型最后几层的 Feature Map 分辨率是否足够必要时去掉一些下采样层来保留更多细节。医学影像分类难点在数据量小、标注成本高、类别极不均衡。这个领域对误判的容忍度极低所以除了准确率更值得关注的是召回率同时要充分验证模型在不同设备、不同参数设置下采集的图像上的稳定性。预训练权重的来源也最好不是 ImageNet 自然图像而是用类似领域的医学影像预训练模型初始化。OCR 相关的文字图像分类比如票据类型识别、验证码识别难点在文字倾斜、形近字、模糊等。给这类任务做数据增强时要格外小心——水平翻转和大幅旋转通常不能开否则“6”和“9”、“b”和“d”这类语义方向会翻转。7. 常见问题与排查技巧实录7.1 现象速查表按照“现象—可能原因—解决方案”的方式来整理是排查图像分类问题最高效的方式。我把工作中常遇到的几类问题整理成一个速查表现象可能原因排查方向Loss 不下降学习率太低/特征损坏先用 1e-3 跑 10 个 epoch 观察曲线Loss 为 NaN学习率过高/数据有异常值调低学习率检查图片像素是否有极端值训练准确率很高验证很低过拟合加 Dropout/数据增强/减小模型容量训练验证都低模型容量不足或特征没学到换更大模型/检查预处理是否匹配某个类别准确率特别差样本太少或类别混淆严重统计各类数量看混淆矩阵定位推理速度和训练时不一致少了 model.eval()检查模型模式切换增广后验证波动很大验证集也做了随机增广验证集只做缩放和归一化7.2 三个我踩过的隐蔽的坑踩坑经验这种东西文档里一般不会写但实际项目中浪费的时间往往就在这些细节上。第一个坑是图像读取通道顺序混乱。用 OpenCV 的 cv2.imread 读图默认是 BGR 顺序而 PyTorch 的 ToTensor 和大多数预训练模型是按 RGB 设计的。如果你用 OpenCV 读图后没做 BGR 转 RGB图片的红色和蓝色通道就互换了。这会让模型看到一张颜色完全错误的图而人眼对着原始图对比半天也不一定能发现问题。症状是训练准确率一直上不去或者 Loss 波动剧烈。解决方案很简单img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 再转 Tensor。第二个坑是标签和类别名没有对齐。用 ImageFolder 读取数据时类别顺序是按文件夹名的字母排序的不是你在文件里看到的顺序。如果在处理 CSV 标签时用了不同的顺序映射训练时的标签可能会整体偏移模型永远学不对。建议每次加载完数据先打印 class_to_idx 和 idx_to_class 确认一遍再开始训练。第三个坑是验证集取了训练集的均值方差做归一化。严格来说归一化参数应该只从训练集统计出来验证集和测试集沿用相同的统计量。如果你图省事拿着整份数据算 mean 和 std 再统一归一化虽然对结果的影响多数时候很小但在某些数据偏移的场景下会带来隐患。养成好习惯总没错。7.3 一个从“59% 到 93%”的真实复盘前段时间做一个产品外观缺陷分类项目起始模型验证准确率只有 59%几乎等于瞎猜。我按下面这个顺序逐项排查第一步看数据。发现有一个类别的图片数量只有其他类别的十分之一而且部分图片的标注明显有误。先清理了标注再用过采样把少数类的有效样本补到接近其他类别的水平。第二步看预处理。原始图片来自不同产线设备分辨率不统一光照条件差异大。统一做了白平衡校正和缩放后光这一项就提升了大约 12 个百分点。第三步换模型。从自定义的浅层 CNN 换成了 ResNet18 预训练权重只训练了 30 个 epoch验证准确率到了 82%。第四步调数据增强。加了 RandomResizedCrop 和 ColorJitter模拟不同拍摄角度和亮度变化准确率提升到 88%。第五步看混淆矩阵。发现两个在产品规格上很接近的类别经常互相误判。联系业务人员后决定把这两个类别合并成一个“疑似缺陷”的粗分类交给人工二次确认。这一步之后准确率到了 93%。整个过程没有用什么高深的算法就是把数据、预训练模型、增强策略和评估方式老老实实过了一遍。图像识别准确率低的时候多数原因是工程细节没到位而不是模型理论不够前沿。8. 一些真心话做图像分类这几年我最大的体会是模型结构只是整个系统的一部分数据质量、预处理流程、评估方式、训练策略这些环节每一个都能造成百分之几甚至百分之几十的准确率差异。遇到准确率低的时候先别急着换更复杂的模型按数据、预处理、模型、训练、评估这条链路逐步排查往往比盲目堆算力更有效。如果你刚入门建议先用一个公开数据集MNIST、CIFAR-10、Food-101 都可以把完整流程跑通数据加载、模型搭建、训练验证、混淆矩阵分析每一步都亲手写一遍再改用现成库。流程熟了之后再用自己的数据做迁移学习你会比一开始就抄一堆复杂代码的人对问题理解得深刻得多。最后再分享一个小技巧每次做实验记录下模型结构、超参数、数据增强方式和最终指标用表格也好用训练日志也好。图像分类的调参空间很大不做记录的话很可能同一个实验重复跑三遍还以为是新方案。实验记录做得仔细后面复盘和写报告都会轻松很多。