ARTICLE DETAIL

资讯详情

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

从CLIP到AnomalyCLIP:零样本异常检测的进阶之路

从CLIP到AnomalyCLIP:零样本异常检测的进阶之路 如果你在工业视觉这行待过两年以上大概率被一个问题折磨过换一条产线之前的异常检测模型基本作废又要重新标正常样本、重新训模型、重新上线调参。这种“一物一模型”的开发方式几乎是所有落地异常检测项目里最贵的一环。但这两年CLIP的出现让这件事有了完全不同的解法——模型第一次能靠文字描述去理解“什么是正常、什么是坏”AnomalyCLIP则是在这条路上把CLIP推向真正可用状态的典型工作。我们先不急着贴公式先聊清楚这条路为什么要这么走。这篇文章我会站在一个过去踩过不少坑的工程师视角把从CLIP到AnomalyCLIP的进化路线完整捋一遍。包括CLIP双塔结构的底层逻辑、text encoder是如何影响异常检测的、AnomalyCLIP做了哪些关键改进、以及基于CLIP做零样本检测时最常见的几个坑。如果你正在调研工业质检的零样本方案或者想搞清楚这两个名词到底是什么关系这篇文章应该能帮你省不少时间。1. 零样本异常检测一项“反直觉”的任务1.1 传统异常检测到底在做什么先说清楚一个概念大多数人理解的异常检测其实是“无监督异常检测”也就是训练集里只有正常样本模型学习“正常长什么样”推理时和正常分布偏离太大的就判为异常。这个范式在过去十年里非常成熟典型代表是PatchCore、PaDiM、SPADE这一脉。它们的基本逻辑不复杂用预训练网络比如ResNet或WideResNet提取特征然后对每个位置的特征建立正态分布模型或者干脆存成一个内存库推理时算距离。PatchCore的核心就是一个coreset采样加上最近邻距离PaDiM则是用多元高斯分布对不同位置的特征建模。这类方法在MVTec AD这种标准数据集上表现很好图像级AUROC能做到95%以上像素级的定位也算精准。但这里有一个所有实际项目都绕不开的痛点它要求“训练类”和“测试类”是同一类物体。你用一个热水瓶内胆的正常样本训好的模型放到不锈钢保温杯上效果立刻崩盘。因为特征分布变了之前拟合的“正常”根本不适用。我见过很多项目就是这样模型在实验室指标漂亮换一个SKU就废掉最后维护成本比开发成本还高。1.2 零样本到底在解决什么场景零样本异常检测要做的事情和传统范式有本质区别在没见过任何目标类别的正常样本或异常样本的情况下直接对新类别做判断。换句话说模型需要具备一种“看一眼就知道这个东西正常不正常”的通用能力而不是去记忆某类物体的正常形态。这个需求在真实场景里非常普遍。一条产线可能同时跑几十种型号的零件每种型号产量不大或者客户打样阶段根本没有足够的样本量来训一个专用模型又或者产品迭代太快训练数据的生命周期比开发周期还短。这些情况下传统“一物一模型”的路径在经济上就走不通了。但零样本检测有一个天然的难点异常是开放类别。你不可能预先把所有缺陷类型写进标签里因为实际中你根本不知道会碰到什么——可能是划痕、凹坑、污渍也可能是一个你从未定义的物理现象。所以零样本异常检测必须解决的是“开放集识别”问题而不是简单的“分类问题”。1.3 为什么“跨模态语义”会是答案想通这个问题得先想一想人是怎么判断异常的。一个质检员第一次看到某种零件他不需要先看一万个样本他只要知道“这是一个金属垫片”和“垫片表面应该是光滑的”再瞄一眼当前这个垫片是不是光滑就能做出初步判断。这个过程依赖的是语言语义带来的先验知识而不是统计分布。CLIP类模型的出现正好把这种先验知识带进了视觉系统。它通过海量的图文对训练把视觉特征和文本语义对齐到了一个空间里。于是“这是一张航空发动机叶片图”和“这是一个表面有裂纹的航空发动机叶片图”这两段文本就能对图像形成两个不同的语义锚点模型要做的事情变成判断图像更接近哪个锚点。这就是零样本异常检测能被CLIP解出来的根本原因。2. CLIP的前世今生双塔结构如何改变视觉识别2.1 CLIP的核心机制与训练过程CLIP的全称是Contrastive Language-Image Pre-training对比语言-图像预训练OpenAI在2021年发布。它做的事情可以概括成一句话用4亿对图文数据训练两个编码器让它们把图片和对应的文本映射到同一个向量空间里并且在这个空间里“配对”的图文向量靠得近“不配对”的离得远。训练过程用的是对比学习最核心的损失是InfoNCE。具体来说一个batch里有N对图文图像编码器把每张图编码成向量文本编码器把每段文字编码成向量然后计算一个N×N的相似度矩阵。矩阵对角线上是配对的正样本其他位置都是负样本训练目标就是让对角线上的相似度尽量大其他位置尽量小。这个过程不需要给数据打精细的类别标签只要图文对本身是配对的就行所以数据规模可以做得非常大。CLIP的训练里有一个容易被忽略但极其重要的细节温度系数是可学习的。相似度矩阵在计算时除以一个可学习的温度参数这个参数决定了模型对正负样本差异的“尖锐程度”。温度控制了特征分布的聚拢程度如果温度学得很小模型就会把同类特征拉得特别紧但泛化能力可能会受影响。2.2 text encoder的模型结构到底有什么特别很多人用CLIP时只关注image encoder把text encoder当成一个固定的文本特征提取器。但如果你想深入理解AnomalyCLIP的改进逻辑text encoder的结构细节反而更重要。CLIP的text encoder是一个标准的Transformer解码器结构但它有几个独特设计。第一它用的是BPEByte Pair Encoding分词词表大小49408比BERT的30522大不少能更好地覆盖各种视觉描述词汇。第二文本序列的开头和结尾分别有[SOS]和[EOS]两个特殊token最终用于分类或匹配的文本特征不是取平均池化而是取[EOS]位置对应的输出向量。第三CLIP的文本输入长度上限是77个token超出部分直接截断这个限制在写prompt模板时需要特别注意。text encoder对异常检测的影响比你想象中大得多。它的语义空间是在“图片描述”这种任务上塑造出来的所以它天然更擅长理解“a photo of...”“an image of...”这种视觉描述句式。如果你直接写一个很抽象的词比如“good”和“bad”CLIP对它们的区分度其实很差因为这两个词太泛了视觉上没有明确的锚定。相反如果你写成“a photo of a metal nut without defect”和“a photo of a metal nut with a crack”特征区分度就会好很多。这就是为什么在CLIP时代prompt工程不是锦上添花而是决定方法能不能work的关键。2.3 拿着CLIP去做异常检测三个坑CLIP看起来很适合做零样本异常检测但真拿它直接上会撞到三个很实在的坑我做实验时都踩过。第一个坑是粒度问题。CLIP是整图级别的对齐它训练时看到的是“一张完整的图”和“一句完整的描述”所以它天然对全局语义敏感对局部小缺陷不敏感。一个只有几十个像素的划痕在整图的特征池化之后几乎会被淹没。你在MVTec上做整图零样本检测每个类别只用一张图和三个文本模板去比相似度AUROC大概只能到70%多根本达不到可用标准。第二个坑是文本类别的开放性问题。传统分类任务里类别是固定的但异常检测里的“异常”是一个开放概念。你告诉模型“this is an anomaly”模型无从下手因为anomaly这个词没有具体的视觉锚定。CLIP的训练分布里不可能覆盖所有工业异常形态所以必须通过prompt设计来激活它已有的语义理解。第三个坑是背景干扰。CLIP很擅长回答“这是什么物体”但不擅长回答“这个物体坏没坏”。在整图匹配时模型的注意力几乎全放在物体类别上背景成分也会参与特征计算结果就是物体稍微换个角度或者光照变一下相似度分数就大幅波动而真正的缺陷反而不太影响结果。这也是为什么早年基于CLIP的异常检测方法效果一直不温不火直到WinCLIP和AnomalyCLIP出现之后情况才有了质的改变。3. AnomalyCLIP进化路线全拆解3.1 思路基点把“正常/异常”从死模板变成活语义AnomalyCLIP之前业界已经有一些基于CLIP的零样本异常检测尝试。WinCLIP是其中比较有代表性的工作它的做法是设计很多hand-crafted的文本模板比如“a photo of a {} without defect”“a photo of a {} with defect”然后对图像做滑窗采样每个窗口都送入CLIP计算和两条文本的相似度最后把所有窗口的分数汇总。WinCLIP能work但存在一个致命问题文本模板是静止的。不管图像里实际是什么内容模板里描述缺陷的词就只有缺陷本身那么几个。但工业异常是千变万化的——同样是“defect”半导体晶圆上的defect和纺织品上的defect视觉形态差了十万八千里。AnomalyCLIP的核心洞察就是prompt需要根据图像内容动态调整才能做到真正的跨领域通用。3.2 通用异常提示跨模态引导的prompt是怎么生成的AnomalyCLIP提出的核心机制叫作“跨模态引导的通用异常提示”。原理可以理解为图像特征不再只做匹配的一方而是反过来参与文本prompt的生成。具体实现上模型先从image encoder拿到局部特征对这些局部特征做池化得到一个条件向量然后通过一个小型映射网络把这个条件向量转换成与文本token同维度的引导信号再叠加到可学习的prompt token上。这个叠加后的prompt再送进text encoder生成一个“看过图像之后才确定语义”的文本特征。也就是说输入一张带划痕的金属件图片prompt会偏向表达“划痕”这种异常输入一张变色的PCB板prompt又会被拉向“变色”的方向。这样做的好处非常明显。第一prompt不再是死板的模板而是有了视觉上下文的信息注入第二异常语义的表达不再依赖训练时见过的具体缺陷类型而是由当前图像的局部特征来自适应激活第三整个模块是端到端设计的和CLIP原本的特征空间兼容性很好。根据AnomalyCLIP公开的实验结果在MVTec AD上它的零样本图像级检测AUROC可以做到90%以上比原始CLIP基线有显著提升。3.3 局部上下文增强救回被全局注意力淹没的细节跨模态prompt解决了“文本侧怎么表达异常”的问题但图像侧还有一个问题悬而未决局部缺陷的信息在学习过程中被全局注意力稀释了。ViT的全局注意力天然偏向于建模“整体长什么样”而不是“哪里和周围不一样”。针对这个问题AnomalyCLIP在image encoder的特征输出上增加了一个局部上下文增强模块。模块的做法是在token序列上引入局部操作比如卷积或邻域注意力让相邻patch之间的特征能感知到彼此从而把局部的纹理差异、颜色跳变、边缘断裂这些关键线索保留下来。我个人的理解是这个模块是在做“注意力之外的补救”。CLIP的全局注意力看的是long-range dependency局部增强模块看的是short-range pattern。两者互补之后图像特征既知道“这是一个什么物体”也知道“这个物体上哪一块的纹理不太对劲”。后面做像素级的异常定位时这一步几乎决定了定位精度能到多少。3.4 从模型设计到评测指标AnomalyCLIP拿到了什么样的效果AnomalyCLIP还有几个配套设计值得一提。比如它支持同时输出图像级异常分数和像素级异常图前者用于判定这个产品要不要被踢出产线后者用于告诉维修人员缺陷具体在哪个位置。在MVTec-AD、VisA等几个主流数据集上它的zero-shot检测和分割指标都做到了当时的SOTAState of the Art而且不需要在目标数据集上做任何微调。但我想提醒一点不要只看总平均AUROC数。拆开看不同类别的结果你会发现方法的强项和弱项非常明显。在MVTec的“纹理类”类别比如地毯、木材、瓷砖AnomalyCLIP的效果通常很好因为纹理异常非常依赖局部上下文增强而这个模块正好解决了这个问题。但在“物体类”类别里比如螺丝钉、胶囊、药片异常往往很小且形态单一单纯靠CLIP的语义对齐有时候反而不如传统特征建模方法去拟合正常分布来得稳。这说明AnomalyCLIP不是银弹它本质上是用语义先验换来了通用性代价是某些极端小缺陷场景的灵敏度下降。4. 实操验证从CLIP到可复现的异常检测pipeline4.1 环境准备与数据准备理论说再多不如动手跑一遍。这里我给出一个基于CLIP原生能力做零样本异常检测的最小可复现实现以及在此基础上引入AnomalyCLIP式改进的实践路线。环境方面不需要太复杂Python 3.8以上PyTorch 1.10以上再加一个开源的CLIP库就行。我推荐用open_clip因为它对不同backbone的支持比原版更全尤其是ViT-L/14、ViT-H这些大模型。显存方面ViT-B/16推理一张224×224的图大概要2-3GB显存如果你要做滑窗或patch级别的特征提取建议上12GB以上的卡不然batch size会非常受限。数据准备要走的是MVTec-AD或者你自己的产品图像。用MVTec的话可以直接下载官方数据集里面包含15个类别的正常与异常样本。注意MVTec的训练集只有正常样本测试集包含正常和异常两类这是标准的异常检测评估协议别和常规分类任务搞混了。4.2 基于CLIP原生能力的零样本检测实现下面的代码实现一个最简单的版本对测试集中的每一张图提取图像特征和“正常”文本特征、“异常”文本特征各算一个余弦相似度最后用softmax得到一个异常概率。import torch import clip from PIL import Image device cuda if torch.cuda.is_available() else cpu model, preprocess clip.load(ViT-B/16, devicedevice) # 注意class_name需要根据你要检测的类别替换比如 metal nut class_name metal nut texts clip.tokenize([ a photo of a {} without defect.format(class_name), a photo of a {} with defect.format(class_name), ]).to(device) with torch.no_grad(): text_features model.encode_text(texts) text_features / text_features.norm(dim-1, keepdimTrue) image preprocess(Image.open(test_image.png)).unsqueeze(0).to(device) image_features model.encode_image(image) image_features / image_features.norm(dim-1, keepdimTrue) similarity image_features text_features.T anomaly_prob similarity[:, 1].item()这段代码跑出来的效果在大多数MVTec类别上AUROC会在70%到80%之间浮动比随机猜测好不少但离可用还差得远。问题出在哪就是我前面说的整图特征丢失了局部细节同时单模板的文本语义不够尖锐。所以下一步我们要做两个改进方向和AnomalyCLIP是一致的。第一个改进是改用patch级特征把图像切成一个7×7的patch网格每个patch单独和文本计算相似度再取最高分或者平均分作为图像级分数。这样局部缺陷在某个patch里的响应不会被整体平均掉。第二个改进是扩充文本模板把“with defect”换成更具体的描述集合比如“with a scratch”“with a dent”“with a crack”“with discoloration”取多个模板下的最大异常响应。注意这里是在模拟AnomalyCLIP的“动态prompt”思想——虽然我们用的是手工模板枚举而不是跨模态生成但方向是一致的。def patch_anomaly_score(model, image, text_features, patch_size7): # 这里简化处理用resize局部区域模拟patch采样 # 更严谨的做法是取ViT倒数第二层的token特征 scores [] W, H image.size for sy in range(patch_size): for sx in range(patch_size): box (sx*W//patch_size, sy*H//patch_size, (sx1)*W//patch_size, (sy1)*H//patch_size) patch image.crop(box).resize((224, 224)) patch_input preprocess(patch).unsqueeze(0).to(device) with torch.no_grad(): feat model.encode_image(patch_input) feat / feat.norm(dim-1, keepdimTrue) sim feat text_features.T anomaly_prob sim[:, 1].item() scores.append(anomaly_prob) return max(scores)实际跑一下你会发现patch级分数的引入会把AUROC从70多拉到85左右扩充实模板又能再涨2到4个点。这就是AnomalyCLIP里“局部上下文增强”和“通用异常提示”两个模块在直觉层面的有效性验证。4.3 加上AnomalyCLIP式改进的实践路线如果你想在真实项目里复现AnomalyCLIP的效果我建议不要直接从零实现全文的cross-modal prompt模块而是分三步走。第一步用现成的CLIP backbone把baseline搭起来用patch-level特征把检测能力摸到85%左右。第二步复现跨模态引导的prompt生成模块。核心思路是取image encoder倒数第二层的特征图做一个adaptive avg pool得到条件向量再通过一个线性层映射到text token的embedding空间和可学习的prompt embedding相加后过text encoder。这个模块的训练不需要缺陷标注可以在ImageNet的类别描述上做弱监督预训练或者干脆用CLIP本身的zero-shot能力来初始化。第三步加局部上下文增强。做法是在image encoder的最后一层输出上叠加一个3×3的depth-wise卷积残差块再把增强后的特征图和原来CLS token的特征拼在一起参与最后的相似度计算。这已经是论文级别的复现工作量了如果你的目标只是验证“这个方向对我的业务有没有价值”4.2节的最小实现就足够说明问题。如果你真的要在产线上落地那就需要考虑部署环境里的显存开销和batch sizeCLIP ViT-L/14加异常定位头一张16G的卡同时处理8路视频流会非常吃力通常要切换到TensorRT做量化。5. 常见问题与排查技巧实录5.1 新手最容易踩的四个坑第一个坑是想当然地认为CLIP的文本特征对所有描述词都敏感。我试过直接用中文prompt比如“一个没有缺陷的金属螺母”效果非常差。原因很简单CLIP的训练数据以英文为主中文描述在文本空间里根本落不到对应的视觉锚点附近。建议无论是做研究还是做项目都用英文写prompt至少先验证英文prompt能work再考虑多语言方案。第二个坑是忽略温度系数。很多人加载CLIP模型后直接用原始的logit scale但开源的CLIP实现里temperature已经在训练时学好了你在做相似度计算时直接用model.logit_scale.exp()得到的值即可不要再手动除以一个自己设定的温度。如果手动改了温度不同类别间的分数分布会完全失衡严重的时候正常样本比异常样本分数还高。第三个坑是只用CLS token做定位。CLIP的CLS token经过最后一层attention后已经积累了全局信息它的特征向量几乎没有空间分辨率。你拿它最多做图像级检测做不了像样的像素级定位。要定位就老老实实取倒数第二层的patch token或者用注意力图加权不要图省事。第四个坑是对所有类别用同一套prompt模板。我在项目里发现MVTec的15个类别里同样的“without defect/with defect”模板在瓶盖这种类别上效果好在二极管上就很差。原因是不同工业产品的缺陷形态差异巨大单一模板无法覆盖。排查方向是给每个类别手工调3到5个专属描述词这会比总想着调模型结构更立竿见影。5.2 问题速查表现象可能原因排查方向所有图像异常分数都接近0.5prompt中类别名和图像内容不匹配检查class_name是否和实际物体一致是否用了正确的英文术语正常样本分数比异常样本高patch特征被全局特征淹没改用倒数第二层token特征避免使用CLS token细粒度缺陷检测不到模板语义太宽泛增加具体缺陷描述词如scratch、crack、stain、discoloration不同类别效果波动大类别间的视觉差异大为每个类别单独配置prompt不要使用全局统一模板定位结果像噪点无连续性特征图分辨率太低去掉最后一层下采样或改用ViT的patch size更小的backbone显存不足无法训练图像尺寸和batch size冲突先用224×224跑通流程再考虑大分辨率优化方案5.3 我的一点实操体会最后分享一个我在实际项目中收获很大的认知CLIP系方法的调试逻辑和传统CNN时代完全不同。以前做异常检测调的是特征维度、高斯分布、内存库大小这些统计量现在调的是prompt语义、token粒度、跨模态对齐方式这些语言-视觉交互层面的东西。这意味着团队里最好有人能理解文本语义对视觉特征空间的影响而不是把它当黑盒。我自己在几个项目里用AnomalyCLIP式方案的效果是在新增SKU的零样本检测场景下能覆盖大约80%到90%的检测需求剩下10%到20%极难检测的微小缺陷我会退回给传统无监督方案去兜底。这不是模型不够好而是零样本本身就意味着信息瓶颈——你不可能完全不看目标数据就解决所有问题。所以合理的工程策略是用CLIP系方法负责通用性和快速响应用传统方法负责特定类别的高精度兜底两者互补而不是互斥。
返回列表