☰
小样本油污缺陷分类模型训练:基于660张图的迁移学习实操
2026/10/20 1:36:39 网站建设 项目流程

简介:这份数据集聚焦三星油污缺陷检测场景,以头发丝状缺陷和小黑点状缺陷两类目标为核心,共收录六百六十幅真实工业采集图像,覆盖正常样本与缺陷样本,适合用于基于深度学习的目标检测模型训练、验证与迁移学习,尤其适用于半导体、精密制造等对缺陷定位精度要求较高的外观质检任务。压缩包内共含一千三百二十四个文件,其中六百六十个XML标注文件与六百六十张JPG原图一一对应,标注文件记录目标类别与边界框位置,另有四个TXT文件提供类别列表或数据划分说明,整体大小约314.54MB,可直接接入YOLO、Faster R-CNN等主流目标检测框架。这份数据集已有一千四百一十二人浏览学习,面向具备一定目标检测基础的算法工程师、科研人员与高校学生。借助完整标注,读者可快速搭建基线模型,开展数据增强、超参数调优以及精度提升对比实验,也可结合作者博客中的优化思路,进一步扩展小目标检测效果与模型鲁棒性。

1. 660张油污缺陷图能做出能用的分类模型吗:先说结论

某屏幕模组项目的产线检测环节,AOI设备拍回的图像里经常出现两类油污缺陷:一类是头发丝,细长丝状脏污,多来自擦拭布纤维或手指带起的油痕;另一类是小黑点,点状凝集的油污,大小从几个像素到几十像素不等。原始数据集只有660张,看起来少得可怜,但先别急着否定:如果目标只是区分“正常 / 头发丝 / 小黑点”这三类,这批数据完全够起步。关键在于数据划分和迁移学习怎么做,方法对了,小样本照样能训练出一个可上线的分类模型;方法错了,验证集准确率再高,到了产线也会翻车。这篇笔记就从数据打理讲到训练参数,再讲到验证技巧,整个过程按一线实操路径来走,新手能跟着复现,熟手可以直接对照参数和避坑清单。

2. 打理这660张数据:头发丝与小黑点的划分、标注策略和增强边界

2.1 先摸清头发丝和小黑点的真实分布再划线

拿到660张图,第一件事不是选模型,而是把数据底账盘清楚。常见做法是写个脚本遍历所有样本,统计每类数量、图像尺寸、缺陷大概出现的区域。我一般会把统计结果打印成一个表格,重点看两个数字:头发丝和小黑点各占多少张,以及有没有“一张图里同时出现两种缺陷”的样本。这个信息直接决定后面是二分类还是三分类。

如果产线只关心“有没有油污”,那二分类就够了,头发丝和小黑点可以合并成一类;如果缺陷后续要分流处理,比如头发丝需要返工擦拭、小黑点需要判定是否影响显示,那就必须做三分类。660张做三分类也够,但有一个硬性建议:每个类别最好不低于150张。假设你数完发现头发丝320张、小黑点220张、正常120张,这个分布可以做三分类;如果小黑点只有60张,那最好先合并成“有缺陷 / 无缺陷”二分类,或者去产线再补拍一批小黑点样本,而不是强行让模型区分一个样本量严重不足的类别。

还有一种情况经常被忽略:同一张图里既有头发丝又有小黑点。这种样本在分类任务里会干扰标签定义,我建议单独挑出来,归入“混合油污”类,或者干脆从训练集剔除,避免模型在训练时学到互相矛盾的映射。这时候人工看图虽然费时间,但比模型学错特征之后再返工便宜得多。

2.2 分训练/验证/测试集:按来源分组而不是随机切

这是660张小样本项目里最容易被低估的一步。很多人直接使用随机划分,把660张按7比2比1切成训练、验证、测试集,表面上看没问题,但实际生产中,同一片区域的多帧图像往往高度相似,甚至是从连续视频帧里截出来的。如果这些相似图像一部分进了训练集、一部分进了验证集,模型其实是在“背答案”,验证集准确率会虚高,产线上一跑真实缺陷就露馅。

我一般会按“来源批次”分组切分,也就是先按关拍摄时间、机台编号或者批次ID把样本分组,再整组分配到训练、验证、测试集。这样可以保证验证集里的图像和训练集里的图像在拍摄条件上有真正的差异。分组后切分的代码可以参考下面这个脚本。

import os import random import shutil from collections import defaultdict root_dir = "oil_stain_dataset" groups = defaultdict(list) # 假设文件名格式为 batch_001_img_003.jpg,按 batch 号分组 for fname in os.listdir(root_dir): if fname.endswith(".jpg"): batch_id = fname.split("_")[1] groups[batch_id].append(fname) group_ids = list(groups.keys()) random.shuffle(group_ids) # 按 70/20/10 划分批次,而不是按单张图片划分 n_train = int(len(group_ids) * 0.7) n_val = int(len(group_ids) * 0.2) train_groups = group_ids[:n_train] val_groups = group_ids[n_train:n_train + n_val] test_groups = group_ids[n_train + n_val:] for split_name, selected_groups in [ ("train", train_groups), ("val", val_groups), ("test", test_groups), ]: out_dir = os.path.join("split", split_name) os.makedirs(out_dir, exist_ok=True) for g in selected_groups: for fname in groups[g]: shutil.copy( os.path.join(root_dir, fname), os.path.join(out_dir, fname) )

这段代码的关键是按批次 ID 整组划分,而不是对单张图片做随机采样。好处是避免同源图像跨集合泄漏,尤其适合工业质检里同一批次样本光照、角度都高度相似的场景。划分比例上,我习惯用 70/20/10,而不是常规的 80/10/10,因为小数据集里验证集的波动本来就大,验证集多留一点,早停判断会更稳。如果发现某个批次里两种缺陷比例极度不均衡,比如一个批次全是头发丝,可以把这个批次整体划入训练集,再从其他批次里匀一些小黑点进验证集,保证验证集保留两类缺陷的代表性。

2.3 增强参数怎么设:细长缺陷最怕被截断

数据增强是小样本项目的救命稻草,但油污缺陷有特殊性:头发丝是非常细长的线状结构,小黑点是紧凑的斑块。这两类对增强操作的敏感度完全不同。

头发丝最怕随机裁剪把丝截断。PyTorch 的 RandomResizedCrop 默认 scale 范围是 0.08 到 1.0,对小样本来说太激进,裁剪出来的区域很可能只截到头发丝的一段,模型会以为“短线段”也是完整缺陷。我一般把 scale 设到 0.5 到 1.0,保证裁剪后缺陷主体基本完整。旋转角度控制在 ±15 度,角度太大会让丝状结构扭曲得不像真实缺陷。

小黑点本身形态简单,平移、裁剪、轻微旋转都不影响语义,但它最怕被 RandomErasing 这样的操作直接抹掉。很多开源项目默认会加 RandomErasing,但在油污检测场景里,把小黑点擦掉就等于把正样本改成负样本,模型会被带偏。我的习惯是彻底关掉 RandomErasing,或者把概率压到 0.1 以下,并且只擦除背景区域。

下面是一组我常用的增强参数,可以直接照抄进训练脚本。

增强操作推荐参数说明
RandomResizedCropscale=[0.5, 1.0], size=224保护头发丝完整性
RandomRotationdegrees=[-15, 15]角度太大会扭曲丝状形态
ColorJitterbrightness=0.2, contrast=0.2模拟不同光照下的油污差异
RandomHorizontalFlipp=0.5通用增强,安全
RandomVerticalFlipp=0.5通用增强,安全
RandomErasing关闭或 p=0.1擦除小黑点会污染标签

增强不是越多越好。660张的小数据集,增强的目的是模拟真实拍照条件的变化,而不是把图变得花哨。建议在调参前先用一批不增强的数据训练一个基线,再逐步加增强,每次只对比验证集准确率有没有实际提升。

3. 选型:先分类验证可行性,再谈检测与定位

3.1 为什么660张不适合直接上目标检测

很多人看到“油污缺陷”第一反应就是上目标检测,用 YOLO 系列框出头发丝和小黑点的位置。这个想法在数据量充足时没错,但放到660张的规模上,问题很明显。头发丝是细长丝状,标注框需要拉得很长才能包住整根丝,框里绝大部分区域是正常背景,模型学到的更多是“框内有根线”,而不是“线具体在哪里”。小黑点则相反,只有几个像素大小,在检测任务里属于小目标,对特征分辨率要求高,小样本下很难训练出稳定的检测头。

所以我一般会建议先用分类模型验证这批数据的可学性。分类任务只需要判断“这张图里有没有油污,是哪一种”,不需要框位置,模型复杂度低得多,660张完全喂得饱。而且分类结果本身就是产线最关心的信息。先做分类还有一个实际收益:如果分类模型在验证集上准确率一直上不去,说明数据本身有问题,比如标签乱、缺陷特征不明显,这时候去做检测只会更糟。

3.2 迁移学习基线:轻量预训练模型的取舍

小样本深度学习最忌讳从头训练。660张图远远不够训练一个深度卷积网络,必须用 ImageNet 预训练权重做迁移学习。选什么骨干网络也有讲究,我常用的基线是 ResNet18、EfficientNet-B0、MobileNetV3-Small,全部是轻量级网络,参数量在几百万级别。相比之下,ResNet50 参数量 2500 万以上,在 660 张数据上基本必过拟合;Vision Transformer 这类需要海量数据预训练加微调的架构,小样本下收敛慢且不稳定,也不推荐。

三个基线里,我最常用 EfficientNet-B0。它在 ImageNet 上的精度高于 ResNet18,参数规模又小于 ResNet50,对油污这种纹理性强的缺陷特征提取比较友好。MobileNetV3-Small 适合要部署到边缘设备的情况,精度会略低一点,但推理速度快,现场验证方便。微调策略上,我习惯分两步走:第一阶段冻结骨干网络,只训练分类头,用较小的学习率跑 10 轮左右;第二阶段解冻最后两到三个 block,用更低的学习率全参微调。这样既避免特征被破坏,又让模型能适应油污域的特殊纹理。

3.3 常见误用与合理的后续扩展

这个环节值得单独说,因为我在实际沟通中经常看到同类误用。

第一个误用是“小样本直接上 YOLO”。YOLO 确实能训练,但训练出来的模型大概率对头发丝这种细长目标漏检严重,验证集 mAP 低,调试成本远高于分类方案。第二个误用是用大模型加粗暴的数据增强硬扛过拟合,扛不住就加更多增强,最后增强把缺陷特征都冲淡了。第三个误用是完全不微调,直接把预训练模型当特征提取器,分类头换成自己的一层,然后只训练分类头。这种做法在医学影像等域差距大的场景偶尔好用,但油污缺陷纹理和自然图像差异也很大,冻结全部骨干往往学不到油污特有的细丝和斑块特征,精度上限很低。

合理的路径是先分类、后定位。分类模型跑通后,再采集更多带位置标注的数据,积累到 2000 张以上,转用分割网络做缺陷区域分割,或者用带 FPN 结构的轻量检测模型。660 张阶段硬做定位不是不行,但投入产出比很低。

4. 训练配置与参数:让模型学到油污而非噪点

4.1 最小可复现训练脚本

这一节直接给一个可跑的 PyTorch 训练脚本,模型用 EfficientNet-B0,数据集按照第 2 章划分好的目录结构存放。代码里每一段关键逻辑都加了注释,方便对照修改。

import torch import timm from torch import nn from torch.utils.data import DataLoader from torchvision import datasets, transforms from torch.optim import AdamW # 数据增强:按第2章的参数表配置 train_transform = transforms.Compose([ transforms.RandomResizedCrop(size=224, scale=(0.5, 1.0)), transforms.RandomRotation(degrees=(-15, 15)), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomVerticalFlip(p=0.5), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) val_transform = transforms.Compose([ transforms.Resize(size=256), transforms.CenterCrop(size=224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) train_ds = datasets.ImageFolder("split/train", train_transform) val_ds = datasets.ImageFolder("split/val", val_transform) # 加权采样:根据每类样本数计算权重 class_counts = torch.tensor([len(train_ds.targets) for _ in range(3)]) class_weights = 1.0 / (class_counts.float() + 1e-6) weights = class_weights[torch.tensor(train_ds.targets)].float() sampler = torch.utils.data.WeightedRandomSampler( weights, num_samples=len(weights), replacement=True ) train_loader = DataLoader(train_ds, batch_size=16, sampler=sampler, num_workers=4) val_loader = DataLoader(val_ds, batch_size=16, shuffle=False, num_workers=4) # 迁移学习:加载预训练权重,替换分类头 model = timm.create_model("efficientnet_b0", pretrained=True, num_classes=3) model.train() # 第一阶段:冻结骨干,只训练分类头 for param in model.parameters(): param.requires_grad = False for param in model.classifier.parameters(): param.requires_grad = True optimizer = AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3, weight_decay=1e-4) criterion = nn.CrossEntropyLoss(label_smoothing=0.1) # 第二阶段使用的低学习率,在第10轮后切换 def set_lr(optimizer, lr): for g in optimizer.param_groups: g["lr"] = lr best_acc = 0.0 for epoch in range(30): total_loss = 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() # 第10轮后解冻最后两个block,降学习率微调 if epoch == 9: for param in model.parameters(): param.requires_grad = True set_lr(optimizer, 1e-4) # 验证集评估 model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in val_loader: outputs = model(images) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) acc = correct / total print(f"epoch {epoch+1}, loss {total_loss:.4f}, val acc {acc:.4f}") if acc > best_acc: best_acc = acc torch.save(model.state_dict(), "oil_stain_best.pt") model.train()

脚本核心逻辑分三块:数据加载、加权采样、分阶段微调。数据加载部分要注意路径结构,ImageFolder 要求 train 和 val 目录下按类别建子目录,这是最省事的读数据方式。加权采样是很关键的一步,当头发丝和小黑点数量不均衡时,直接随机采样会导致模型偏向大类;用 WeightedRandomSampler 让小类被抽到的概率提高,能明显改善小类召回率。分类头替换成num_classes=3,对应三类输出,如果业务只需要二分类,改成 2 即可。

分阶段微调是这个脚本最值得借鉴的地方。前 10 轮只训练分类头,让新分类头先适应预训练特征;第 10 轮后解冻全网络,把学习率降到 1e-4,让骨干特征向油污域偏移。这种做法的核心思想是:刚开始就全参微调,预训练特征很容易被 660 张小样本带偏,分阶段可以让模型稳定落地到一个较好的局部最优。

4.2 学习率、批次与轮次的设定逻辑

小样本训练里,学习率是最敏感的参数。我默认用 1e-3 作为第一阶段学习率,这个值基于预训练模型微调的经验值。第二阶段降到 1e-4,因为此时骨干参数刚开始更新,过大的学习率会把预训练特征冲毁。如果发现训练 loss 震荡明显,建议先降到 5e-4 再跑一遍,而不是去调模型结构。

批次大小方面,16 是 660 张数据的安全选择。批次太小,BatchNorm 统计不稳定;批次太大,比如 64,小数据集下一轮迭代次数太少,模型还没看够样本就过拟合了。轮次我设了 30,但真正的早停机制靠验证集判断,不是等 30 轮跑完。可以加一个早停逻辑:连续 8 个 epoch 验证集准确率没有刷新,就终止训练。660 张数据过拟合很快,通常在第 15 到 25 轮之间就能看到验证集波动。

![训练参数速查表]

参数推荐值说明
输入尺寸224x224平衡细长缺陷完整性与计算量
batch size16小数据下稳定 BatchNorm
第一阶段 lr1e-3只训练分类头时的安全值
第二阶段 lr1e-4全参微调时避免破坏预训练特征
weight decay1e-4正则化,抑制过拟合
label smoothing0.1缓解训练集标签噪声
早停 patience8防止无效训练浪费时间

4.3 类别不平衡时如何让两种缺陷都被学到

油污数据集里两类缺陷数量经常不平衡,比如头发丝 320 张、小黑点只有 180 张,差距不算极端,但已经足以让模型偏向头发丝。除了脚本里用的加权采样,还可以在损失函数层面做文章。CrossEntropyLoss 支持直接传weight参数,把小黑点类的权重提到 1.5,头发丝类保持 1.0,等价于让模型更重视小黑点的分类错误。

不过要提醒一句,类别权重不能设得太大,否则模型会过度拟合小黑点的细节,在正常样本上产生更多误检。我很少把权重超过 2.0,除非某一类样本少于另一类的一半。还有一个容易被忽略的细节:加权采样和损失函数权重不要同时用很大强度,否则正样本被过度强调,训练曲线反而会不稳定。二选一即可,脚本里用的加权采样在多数情况下已经够用。

标签平滑在小样本场景值得开,它可以把 one-hot 标签软化,减少模型对训练集中一些错误标注的过度自信。油污缺陷的人工标注本身存在灰度地带,尤其是那些“刚形成的小油痕”,标签平滑能从优化目标上降低这种不确定性带来的影响。设为 0.1 是常见做法,不会明显损害精度,还能让模型输出的概率分布更平滑,方便后续设置判定阈值。

5. 油污缺陷训练避坑记录:过拟合与误检的5个案例

5.1 头发丝被当成背景纹理

现象:训练了 20 轮,验证集准确率到了 92%,但把几张正常的屏幕纹理图送进去,模型有 30% 的概率把它们判成头发丝。看了 Grad-CAM 的热力图才发现,模型高亮区域根本不在缺陷位置,而是集中在整个图像的像素阵列纹理上。

原因:屏幕本身有规则的点阵或栅格纹理,和头发丝在频域上都是高对比度图案。训练集里如果恰好有大量背景纹理清晰但无缺陷的负样本,模型会偷懒,直接拿背景纹理当作分类依据。

解决:在负样本里增加了 60 张带强纹理、但人工确认无缺陷的图。同时修改增强策略,把 ColorJitter 的对比度扰动加大到 0.25,迫使模型不能只依赖静态纹理。最关键的一步是检查训练集里“背景纹理+无缺陷”组合是否够多,不够就回产线补拍。

5.2 小黑点与正常噪点混淆

现象:小黑点这类召回率只有 70%,大量小黑点被漏报,而正常样本里一些灰尘颗粒、微小气泡反而被报成小黑点。把预测错误的图片逐一翻出来,发现被漏报的小黑点普遍偏小,只有 3 到 5 个像素。

原因:模型学到的“小黑点”特征里混入了环境噪点特征,比如光源反射造成的暗点。标注时只关注了形态,没有区分“真正的油污”和“看起来像的杂质”,导致正负样本边界模糊。

解决:重新制作了小黑点的标注规范,给标注人员出示了一组典型负例,明确边缘模糊、高光居中、形状不规则的暗点不算油污。同时用这些负例单独组成一个“难例验证集”,每次训练完都跑一遍,专门看误检率有没有下降。

5.3 验证集准确率很高,现场一试就翻车

现象:660 张按随机切分后训练,验证集准确率 94%,乐观地部署到产线试运行,第一天误报率就超过 40%,现场基本没法用。同类油污在产线新机台上的表现和训练集完全不同,模型就像变成了另一个模型。

原因:随机切分导致同一批次出图的相似图像同时进了训练集和验证集,模型记住了具体批次的纹理特征和光照条件,不是通用的油污本质。新机台的光照角度、相机增益一变化,模型就失效。

解决:这是我最深刻的一次教训。后来把数据划分改成按批次整组切分,并且从另一个机台补拍了 100 多张图像专门做跨场景测试,模型准确率跌到 88%,但至少是真实水平。从那以后,小样本项目一律先确认数据来源分组,再谈训练。

5.4 增强过度把油污样本增强没了

现象:为了提高泛化能力,我把增强开得很猛,RandomResizedCrop 的 scale 改成 0.2,还加了 RandomErasing p=0.5。训练 30 轮后验证集准确率反而比弱增强时低了 6 个百分点,特别是头发丝类,几乎全是漏报。

原因:scale 0.2 意味着裁剪区域可以只有原图的一小部分,头发丝经常被截断,模型学到的是“半截丝”。RandomErasing 直接把很多小黑点擦除,正样本变成负样本,标签被污染。

解决:回退到第 2 章参数表里的保守增强设置,关闭 RandomErasing。这里要认清一个现实:660 张数据上,增强的角色是模拟光照和角度变化,而不是制造新的缺陷形态。想用增强弥补数据量不足,是这条路里最大的玄学误区。

5.5 标注不一致:两种缺陷的边界漂移

现象:第一次标注的数据集里,同一张图被两个标注人员分别标成“头发丝”和“小黑点”,原因是有一处油污既呈细长状,末端又有斑点凝集。训练时这个样本被强制归为一类,模型的分类边界被拉偏,验证集上两类之间的混淆持续存在。

原因:标注规范里没有定义“混合形态”怎么处理,也没有给出边界案例的判定规则。头发丝和小黑点在物理成因上本来就可能叠加。

解决:在数据准备阶段增加了标注复核环节,把两类标签争议大的样本单独抽出来,人工讨论后统一归入一个更合理的类。我在规范里明确了一条:如果细长和点状同时存在且区域重叠,统一归为“混合油污”,单独建类,实在没有条件就做二分类,不细分这两类。660 张数据的项目,标签一致性对最终模型准确率的影响大于绝大多数模型调参技巧。

6. 用Grad-CAM和硬样本集确认模型看的是油污

训练结束不代表完成,模型准确率达到 90% 以上仍要确认它“看”的是不是油污本身。我的习惯是每次训练完都跑一次 Grad-CAM,把热力图叠加到测试集图像上,肉眼检查模型的高亮区域。如果高亮区域和人工标注的缺陷位置吻合,模型学到的特征才是真正的油污;如果高亮区域落在背景纹理或边缘上,说明训练过程有偏差,需要回头查数据。

Grad-CAM 的简版实现可以按下面代码来跑。

import torch import timm import cv2 import numpy as np from torchvision import transforms model = timm.create_model("efficientnet_b0", pretrained=True, num_classes=3) model.load_state_dict(torch.load("oil_stain_best.pt", map_location="cpu")) model.eval() def grad_cam(img_path, layer_name="conv_head"): img = cv2.imread(img_path) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) transform = transforms.Compose([ transforms.ToPILImage(), transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) input_tensor = transform(img_rgb).unsqueeze(0) # 注册 hook 拿到目标层梯度与输出 target_layer = model.get_attention_layer if hasattr(model, "get_attention_layer") else None # 不同 timm 版本的 target_layer 名称不同,这里按 conv_head 处理 activation = {} def forward_hook(module, input, output): activation["value"] = output.detach() def backward_hook(module, grad_input, grad_output): activation["grad"] = grad_output[0].detach() layer = model.conv_head fh = layer.register_forward_hook(forward_hook) bh = layer.register_full_backward_hook(backward_hook) output = model(input_tensor) pred = output.argmax(dim=1).item() model.zero_grad() output[0, pred].backward() grads = activation["grad"] # [1, C, H, W] acts = activation["value"] # [1, C, H, W] weights = grads.mean(dim=(2, 3), keepdim=True) cam = (weights * acts).sum(dim=1, keepdim=True).squeeze() cam = torch.relu(cam) cam = cam / (cam.max() + 1e-6) fh.remove() bh.remove() cam = cam.numpy() cam = cv2.resize(cam, (img.shape[1], img.shape[0])) heatmap = cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET) overlayed = cv2.addWeighted(img, 0.6, heatmap, 0.4, 0) cv2.imwrite("cam_output.jpg", overlayed) grad_cam("test_samples/example_021.jpg")

这段代码从加载模型到输出热力图覆盖图,核心逻辑分为两步:先注册 hook 拿到最后一个卷积层的特征和梯度,再用全局平均池化算出每个通道的权重,加权求和得到 CAM。需要说明的是,不同 timm 版本里 EfficientNet-B0 最后一层名字可能略有差异,有的版本是conv_head,有的版本是get_attention_layer,跑之前先打印一下模型结构确认层名。

除了 Grad-CAM,还要构建一个硬样本集。从产线收集两类图像:一类是“看起来像缺陷但实际正常”的图,比如背景纹理强、有小灰尘颗粒;另一类是“缺陷很小但确实存在”的图,比如只有三五个像素的油点。这些图不进训练集,专门用来检验模型上线后的表现。60 到 80 张就够,每次模型迭代后在硬样本集上跑一遍,记录误检率和漏检率。比验证集准确率更能反映真实水平。

最后关于判定阈值。很多项目直接拿 0.5 当分类阈值,但对油污这种类别边界清晰度不高的任务,我更推荐看置信度分布。把验证集里正常样本和缺陷样本的模型输出置信度分别画出来,如果正常样本普遍在 0.85 以上、缺陷样本集中在 0.4 到 0.7,那阈值可以设到 0.75,能明显压低误报。这个操作不需要重新训练,只是调整部署脚本里的判断逻辑,却常常能让现场误报率降一半。

这类小样本项目走到最后,拼的不是模型结构多新,而是对数据的理解有多深。我第一次做类似项目时,直接随机切分了全部 660 张数据,训练时验证集准确率喜人,上线一周就被现场反馈淹没。后来按批次分组、做 Grad-CAM 验证、建硬样本集,才把误报率压到可接受范围。这套流程踩过的坑,希望这篇笔记能帮你绕过去。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询