拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

小样本缺陷检测实战:PyTorch数据增广与漏检控制全流程

小样本缺陷检测实战:PyTorch数据增广与漏检控制全流程

产线跑不起来的时候,谁也顾不上什么“算法优雅”。缺陷检测这个项目,难的地方从来不是模型有多先进,而是你在现场拿到的数据就那么几百张,废品率要压到千分之一以内,漏检一多,客户那边直接就不干了。我做过的几个工业视觉项目几乎都是这个套路开局:甲方给一批良品图和少量缺陷图,标注质量参差不齐,缺陷类型五花八门,然后要求算法上线后不能漏、不能多报。这篇文章就是把我在小样本条件下做缺陷检测的完整流程和个人经验写出来,从数据怎么扩、模型怎么选、阈值怎么定,到漏检怎么控制和排查,一条线串清楚。

我会结合PyTorch这套主流技术栈来讲,同时也给出部署和联调阶段的实操细节。如果你是做算法开发、质检系统实施,或者是工厂里要自己搭视觉方案的工程师,照着这个流程走一遍,至少能少踩一半的坑。

1. 整体设计与方案选型

1.1 小样本缺陷检测的难点在哪

缺陷检测跟通用分类任务有个本质区别——缺陷的出现是有长尾特征的。同一个产品上,划痕可能有几十种形态,脏污又有几十种形态,而且这些形态彼此之间还有交叉。你要是用常规的思路,把缺陷检测当成一个单纯的“分类问题”来做,就会立刻卡在样本采集上:每个类别至少要几百张图才能训得像样,但产线上真正能采到的缺陷样本,十个类别加起来可能就两三百张。

小样本问题的第一层难点是数据量不够,第二层难点是缺陷的形态方差太大。同样叫“划痕”,有的是细线状,有的是块状,有的高亮,有的暗纹。如果模型只见过其中一部分,上线后遇到没见过的形态就很容易漏检。所以我做这个项目的第一个决定就是:不把缺陷检测当单纯的分类来做,而是当成“异常检测+细分类”的组合问题来解——先用一个能区分“正常”和“不正常”的模型把大面挡住,再在这个基础上去做缺陷类型的判定。这样做的好处是,正常样本大量可采,异常检测模型对“未知缺陷”的容忍度天然就高,比一头扎进小样本分类任务要稳得多。

第二层难点是标注噪声。工业现场的标注通常都很粗糙,同一个缺陷,A标注员画了个框,B标注员画了半个框,还有的缺陷类间边界模糊,标注本身就有争议。你用小样本去训一个有监督模型,这些标注噪声会被模型成倍放大,导致训练过程震荡。所以数据清洗和预处理这一步,在小样本场景下比模型结构都重要。

1.2 方案选型:模型结构与预训练策略

选型上我用了两条腿走路:

  • 有监督路线:对于有明确标注的缺陷数据,用轻量级卷积模型做分类和定位。考虑到产线上的显卡资源通常很紧张,我一般选ResNet系列作为主干,如果缺陷目标很小再叠加FPN结构做特征融合。
  • 无监督/自监督路线:用大量正常样本做自监督预训练,让模型先学会“什么是正常的”,再拿少量缺陷样本微调。这一步非常关键,因为小样本场景下,直接随机初始化从头训模型,模型学不到足够的底层视觉特征,收敛极慢且效果差。

自监督预训练这里我推荐用MAE(Masked Autoencoder)或者SimCLR这类方法,它们不需要标签,只需要把正常样本喂进去学习特征。一个比较实在的做法是:先用正常样本训练MAE重建任务,训练200个epoch左右,把Encoder部分拿出来做下游任务的初始化权重,再用少量缺陷样本微调分类头。实测下来,这个策略能把小样本分类的准确率提升10到15个点,尤其在缺陷种类超过5类的时候,收益更明显。

还有一个可行但工业界用得还不太多的方案,是直接用CLIP这类图文预训练模型做zero-shot。如果产线采集的图片配合了文本描述(比如“表面浅划痕”、“边缘破损”),可以用CLIP做初步缺陷筛选,再结合手工规则过滤。不过这个方案依赖对缺陷语义的准确描述,在复杂纹理场景下表现不稳定,我一般只用它来做快速原型验证,不直接用于生产。

2. 数据准备与增广的细节

2.1 可控增广是唯一的出路

小样本条件下,数据增广不是“锦上添花”,是“保命手段”。但增广不是无脑堆,堆错了反而会让模型学到错误的不变性。

我自己的经验是把增广分为三类:

  • 几何增广:翻转、旋转、缩放、裁剪。这组增广对工业场景基本都能用,因为缺陷在图像里的位置往往是随机的、角度也是随机的。
  • 颜色/光照增广:亮度、对比度、饱和度、色调调整。产线的光源稳定性虽然比自然光好,但不同班次、不同设备、不同老化程度的灯源还是会导致图像整体分布偏移,这组增广能大幅提升模型对不同采图环境的适应能力。
  • 缺陷专用增广:混合切割(Cutmix)、粘贴复制。特别说明一下粘贴复制,就是把缺陷区域从一张图里裁剪出来,随机贴到正常样本的随机位置。这个方案在工业场景下极其好用,因为很多缺陷本质上就是局部纹理异常,并不依赖全局上下文。

缺陷专用增广里有个关键细节:如果你用随机粘贴的方式生成训练样本,一定要记录粘贴的位置和对应的标签,这样可以在训练时让模型只关注缺陷区域,而不是把整个图都当成“有缺陷”。我用的是给粘贴区域生成一个mask的方案——粘贴完缺陷区域后,同时生成一个二值mask,模型在计算loss的时候,背景区域的梯度清零,只让前景缺陷区域回传。这个做法对小目标缺陷特别有效,不然模型会被大量正常背景淹没,根本学不到缺陷特征。

另外一个非常实用的技巧是缺陷融合:从两张缺陷图里各取一块区域,融合在一张图上。这能用有限的缺陷数据创造出组合形态,尤其适用于像“划伤+脏污”这类复合缺陷。融合时要控制好两个区域的透明度和边界平滑度,否则生成出来的样本会带明显的拼接痕迹,模型学到的就不是缺陷特征而是拼接痕迹了。

2.2 数据清洗与标注校准

小样本场景下,标注质量对最终效果的影响远大于数据量。我踩过的坑是:有一批样本,标注人员把“正常纹理极值”标成了“划伤”,这一类别里混入了差不多10%的错误样本,结果训练出来的模型在验证集上反复出现误检,怎么调阈值都没用。

所以我定了一个流程:先做数据预清洗,再做标注校准。

  • 用异常检测模型(比如用一个简单的AE重建误差)跑一遍所有样本,把正常样本里重建误差特别高的挑出来,人工确认是不是漏标的缺陷;
  • 把所有同类型缺陷图放到一起做聚类,肉眼过一遍聚类后的特征分布,把离群样本找出来逐张确认——这时你会发现有不少实际是错标或者混类的情况;
  • 对有争议的类间边界,统一口径。比如“浅划伤”和“正常纹理波动”的分界线是什么,要在动手训练前就定清楚,否则模型会学到你这个判定标准以外的东西。

上面这三步听起来繁琐,但每一步都能实打实降低漏检和误检。

3. 训练策略与模型调优实战

3.1 模型训练关键参数与流程

我以PyTorch为例,整理了一份在小样本缺陷检测任务上跑得比较稳的流程,代码结构和参数可以直接拿来参考。

首先是数据集划分。小样本场景下不能按常规比例划分训练集、验证集,因为缺陷样本本来就少,再拿20%去做验证,训练数据就所剩无几了。我建议采用K-Fold交叉验证,K取3或者5,每一轮用一部分样本做验证,最后对结果取平均。这样能充分利用有限的缺陷数据,也能比较稳定地评估模型效果。

然后是训练流程。我用的方案是:先用大量正常样本跑自监督预训练(MAE或对比学习),再用少量缺陷样本微调分类模型。

import torch import torch.nn as nn import torchvision.models as models from torch.utils.data import DataLoader from torchvision import transforms # 自监督预训练阶段(MAE 或 SimCLR 以 SimCLR 为例) # 这里简化为:用正常样本训练一个子网络,输出为特征向量 class ProjectionHead(nn.Module): def __init__(self, in_dim=2048, out_dim=128): super().__init__() self.layers = nn.Sequential( nn.Linear(in_dim, in_dim, bias=False), nn.BatchNorm1d(in_dim), nn.ReLU(inplace=True), nn.Linear(in_dim, out_dim, bias=False) ) def forward(self, x): return self.layers(x) backbone = models.resnet50(weights=None) backbone.fc = nn.Identity() # 去掉分类头,只保留特征 # 自监督训练时用对比损失,输入为正常样本的两个不同增强视角 # 伪代码示意:view1, view2 是同一个样本经过不同增广后的结果 # feat1 = backbone(view1); feat2 = backbone(view2) # loss = contrastive_loss(feat1, feat2) # 训练完成后,backbone 即为预训练权重

微调阶段加载上述预训练权重,替换分类头,用有标注的缺陷数据训练:

# 微调阶段 model = models.resnet50(weights=None) pretrained_dict = torch.load("pretrained_backbone.pth", map_location="cuda") model.load_state_dict(pretrained_dict, strict=False) model.fc = nn.Linear(2048, num_classes) # num_classes = 缺陷类别数 + 1类正常 criterion = nn.CrossEntropyLoss() optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50) # 训练循环略,建议微调 50-100 个epoch,并用K-Fold交叉验证

微调阶段有3个参数值得注意:

  • 学习率:预训练权重的学习率要比随机初始化小一个量级,我习惯用1e-4起步,按CosineAnnealing衰减;
  • weight_decay:小样本场景下正则化很重要,1e-4的weight_decay是默认值,如果出现过拟合可以适度加大;
  • Batch Size:因为总样本量不大,batch size建议设小一点,16或者32,太大容易在局部震荡。

3.2 loss函数与类别不平衡处理

小样本缺陷检测另一个常见问题是类别不平衡。极端情况下,正常样本几万张,缺陷样本只有两三百张,缺陷里还分了好几个子类,有的子类可能只有三四十张。直接拿CrossEntropy去训,模型会无脑倾向多数类。

我建议的组合方案是:

  • 使用Focal Loss替代普通CrossEntropy,Focal Loss通过调制因子让模型关注难分类的少样本类别;
  • 对于极度不平衡的子类,用Class-Balanced Loss或者给每个类设置权重,权重跟该类样本数量的平方根成反比;
  • 对于需要定位缺陷的任务,还可以在分类loss之外叠加一个基于特征距离的度量学习loss,拉近同类特征、推远异类特征。

这里给出Focal Loss的一个简洁实现,可以直接嵌入训练代码:

class FocalLoss(nn.Module): def __init__(self, gamma=2.0, alpha=0.25, num_classes=5): super().__init__() self.gamma = gamma self.alpha = alpha self.ce = nn.CrossEntropyLoss(weight=None) def forward(self, logits, targets): ce_loss = self.ce(logits, targets) pt = torch.exp(-ce_loss) focal_loss = (1 - pt) ** self.gamma * ce_loss return focal_loss

gamma=2.0会让模型更关注那些预测概率低的难样本。alpha是类别权重参数,如果是多分类建议按类别样本数的倒数归一化后传入CrossEntropy的weight参数,效果会更好。

在验证和调参阶段,我强烈建议不要只看准确率,要同时关注每类的Precision、Recall和F1。尤其是在漏检控制方面,Recall的意义远大于Accuracy。如果你发现某个缺陷类别的Recall一直上不去,先不要急着调模型结构,可以先看这一类样本的特征分布是否太少,或者增广策略是不是有问题。

3.3 阈值选择与置信度校准

模型输出的是每个类别的概率分布,但部署时给不给过、给不给判废,完全取决于阈值怎么设。很多新手工程师会把0.5作为默认阈值,这在工业场景是行不通的——如果某项缺陷的漏检率要求是控制在0.1%以内,0.5的阈值通常太激进,模型稍微犹豫一下就把缺陷漏过去了。

我的做法是:在训练完成后,单独跑一遍验证集,统计每个类别在不同置信度阈值下的Recall和Precision曲线,然后根据客户对漏检和误检容忍度的具体要求去选阈值。这个步骤通俗讲叫“置信度校准后的阈值搜索”。

举个具体的例子,假设客户要求“缺陷检出率不低于99.9%,误检率不做硬性要求”,那你的阈值就要往低里调,一般调到0.2甚至0.1,把大包围打出来,宁可多报也不能漏。如果客户同时要求误检率不能超过某个值,那就要做一个阈值搜索的最优化:遍历0.05到0.95之间的每个值,计算每个阈值下Recall和Precision的变化,找到两者trade-off最平滑的那个点。这个过程我习惯写成脚本,每次模型更新后自动跑一遍,输出一张阈值-指标曲线表。

3.4 验证策略:别被单次划分骗了

小样本场景下,模型的验证结果方差极大。同一个模型,换一组验证集,准确率可能从95%掉到80%。所以我强烈建议用小样本专用的验证策略——K-Fold交叉验证是必须的。K=5的时候,每次拿80%的缺陷样本训练、20%验证,五次平均后的指标才有参考价值。

还有一个来自实际项目的细节:为了模拟真实产线场景,我在划分数据时会把“同一产品同一批次的样本尽量放进同一个fold”,防止模型利用批次背景信息做“作弊式”分类。这个坑很隐蔽,如果不去控制,交叉验证结果会虚高,上线后立刻现原形。

4. 漏检率控制与问题排查思路

4.1 为什么漏检比误检更难处理

在工业检测场景里,漏检和误检的代价完全不对等。一次漏检可能导致整批次产品流向客户端,后面就是客诉甚至召回,代价可能是百万级的。误检只是让产线多停机检查几秒钟,代价相对可控。所以工程上几乎所有客户都会优先压漏检率。

但从算法角度看,漏检的优化空间比误检小得多。误检可以通过后处理规则轻松压掉,比如限定缺陷的最小面积、最小对比度、与周围纹理亮度差的阈值等。而漏检意味着模型压根没“看懂”某个缺陷,这时候你要么提供更多样化的缺陷样本去扩分布,要么降低阈值把模型预测的模糊地带全部暴露出来,再配合规则过滤把误检收回来。

这个思路总结起来就是一句话:“先放开,再收敛”。先把阈值压低,让模型尽可能多地输出候选缺陷,然后靠规则和人工确认来收敛,这是工业界最稳的漏检控制路线。

4.2 多层过滤:规则后处理控制误检

模型输出后,经过一层业务规则过滤,能显著降低误检率。我总结了一套常用的规则优先级,按从简单到复杂排列:

  • 面积过滤:缺陷连通域面积小于某一个阈值的直接忽略;
  • 位置过滤:某些产品边缘区域的缺陷判定标准可以放宽,或者反过来收紧;
  • 对比度/梯度过滤:缺陷区域与背景的灰度差、梯度强度不够的忽略;
  • 形状过滤:按连通域的长宽比、凸度、圆形度做判定,滤掉一些“伪缺陷点”;
  • 多模型投票过滤:训练两个不同结构的模型(比如一个CNN、一个Transformer),两个模型都判为缺陷才给出缺陷告警。这个方案能显著降低偶然性误检,但会轻微牺牲召回,适合误检要求极高的场景。

后处理逻辑需要在产线试运行阶段持续调参,不能一梭子打完就撒手。我通常安排两周的试运行期,前一周每天和质检员对一遍误检结果,把误检案例攒下来,逐条分析是阈值问题、规则问题还是模型问题。这套机制看着笨,但它是真正能把漏检率打下去的土办法,比任何复杂的算法理论都管用。

4.3 常见问题速查与实战排障表

跑小样本缺陷检测项目,遇到最多的问题其实就那几类。我整理了一个速查表,你在排障的时候可以按图索骥:

症状可能原因排查顺序与解法
训练loss不下降学习率过大或过小、数据归一化错误先检查数据预处理,再调整学习率,尝试warm-up
训练集准确率很高,验证集很差过拟合,常见于小样本增加数据增广、加大weight_decay、引入Dropout
某类缺陷召回率始终上不去该类样本形态方差太大,或者样本量太少重点增强该类别的增广,粘贴复制,缺陷融合
误检集中在某一批正常图正常样本的纹理特征和缺陷样本太接近收集误检图,加入训练集作为硬负样本重新微调
线上效果与验证集差异大图像采集环境不一致(光源、角度、分辨率)验证集加入跨环境样本,图像预处理统一规范化
推理速度太慢模型太大或后处理太重换轻量级backbone,改TensorRT半精度推理

第二行说的过拟合,我再展开一句。小样本训练时,过拟合是最常见的问题。除了增广和正则化,还有一个我经常用的办法:早停。在验证集上跑几个epoch后,如果loss不再下降或者开始回升,果断把训练停掉,不要让它继续磨。多磨几轮,看着训练精度在涨,其实模型已经退化到只会背训练集了。

4.4 上线前的压测与回归

项目上线不是训练结束就完事,上线前必须做一轮压测和回归。压测主要测两点:一是推理耗时,看单张图的检测时间能不能跟上产线节拍;二是稳定性,用一段长时间采集的产线真实数据去跑,观察模型是否有偶发性的输出异常。

我把压测标准定为三个维度:

  • 单张推理耗时<100ms(视具体硬件而定);
  • 连续跑24小时,单卡显存占用稳定,无内存泄漏;
  • 漏检率、误检率在客户要求的指标范围内波动,且不能出现“时好时坏”的抖动。

回归测试指的是:每次更新模型或调参后,把历史积累的验证集全部重跑一遍,对比新旧版本在每个类别上的指标变化。这样能防止为了修一个bug把另一个bug引进来。

回归测试的数据集是项目执行过程中需要“滚雪球”一样积累的。凡是线上出现过误检、漏检的案例图,确认后全部归档进回归库。这个库越厚,模型迭代就越稳,后面每一个新版本上线前心里都有底。

多提一句,我遇到很多项目在试运行阶段发现漏检率“跑到后面又升回来了”,这类问题大多不是模型退化,而是产线工艺变化了——换了材料批次、调了注塑参数、设备磨损导致纹理变了。针对这种情况,光调模型没用,要让现场工程师同步关注工艺参数变化,必要的时候把新工况下的正常样本也拉回来做增量更新。模型上线不是一次性的交付,而是和产线“共同演进”的过程。

从项目里带走的经验

小样本缺陷检测这个方向,说到底比的不是谁的模型结构花哨,而是谁在小数据条件下更懂取舍、更会在工程链路里找漏洞。数据增广、预训练策略、阈值控制、多层过滤,每一环都是系统工程的一部分。

我个人体会最深的一点是:把验证体系建好,比训练出一个漂亮模型更重要。当你的验证集、回归库、指标采集机制足够扎实,每个版本迭代的收益会被严格量化,项目就不会陷入“感觉变好了、又感觉变差了”的泥潭。

最后分享一个带过很多项目的工程师都认可的土办法:把所有漏检案例打印出来贴在工位上,每天看一遍,逼自己去想为什么模型没抓到。这个过程虽然原始,但比翻论文有用得多。缺陷检测这个行业,真正的干货从来都在现场,不在纸上。

返回列表