芯片缺陷检测这几年卷得很厉害,尤其是深度学习大规模进场之后,传统视觉那套阈值分割加人工特征组合,在复杂缺陷面前越来越吃力。缺陷类型多、形态不规则、背景噪声大,良率压力下还要兼顾产线节拍,检测算法既要准又要快,整个方向就成了工业视觉里最硬的骨头之一。这篇论文精读笔记,我挑了自己最近认真读过的一篇代表性前沿工作,完整拆一遍问题建模、网络设计、训练技巧和复现过程,给正在做半导体工艺算法、AOI设备软件或者良率分析的同学做个参考。
我读的这篇工作,核心是把晶圆图上每个缺陷模式当成一个序列来建模,用Transformer提取全局空间特征,再用对比学习拉近同类、推开异类,专门解决长尾分布下的缺陷分类问题。这个方法结构不算复杂,但设计很巧,我在代码层面完整复现过一遍,训练过程踩了不少坑,所以这篇会把论文里没明说的细节也尽量补上。
1. 读这篇论文之前,先想清楚芯片缺陷检测到底卡在哪
1.1 为什么前沿论文都盯着这个老问题
芯片缺陷检测不是新方向,传统机器视觉在产线上已经用了很多年,常规做法是先通过滤波、阈值、形态学操作把可疑区域从图像里抠出来,再靠人工设计的特征判断缺陷类型。这套思路对边缘清晰、形态固定的缺陷问题不大,比如划痕、灰尘颗粒,稍微调调参数就能跑得很好。但实际产线上的缺陷通常非常“碎”,同一个类型在不同批次的晶圆上长得完全不一样,有些缺陷在灰度图上只差几个像素的亮度,人眼都不一定能马上分辨,手工特征很快就会到瓶颈。
另一个绕不开的问题是数据分布严重偏斜。正常晶圆占绝大多数,少数几种缺陷又占了缺陷样本的大部分,真正需要重点识别的罕见缺陷反而样本极少。如果直接用原始分布训练一个分类模型,模型几乎会偷懒到把所有样本都预测成多数类,准确率看起来很高,实际一点用没有。这也是为什么近几年深度学习方案在芯片缺陷检测里越来越热,因为卷积、Transformer这类方法能把特征学习本身交给数据,相比手工特征有明显的表示能力优势,但如何处理好长尾分布、小缺陷漏检这些问题,依旧是论文和方法的核心争夺点。
说回到这篇工作,它选择的任务是晶圆图缺陷模式分类。这个任务在学术上很有代表性,有公开的WM-811K数据集,产线上也有非常强的应用背景,算是“既好评估、又贴实战”的中间地带。所以前沿论文喜欢盯这个方向不奇怪,它既能验证模型设计的新思路,又能输出产线可用的结论。
1.2 我挑论文的三个标准
前沿论文很多,但不是每一篇都值得花时间去精读。我的筛选标准其实很朴素:第一,问题有没有代表性,读完之后能不能迁移到自己的场景;第二,方法是不是真的有新东西,而不是换了一个骨干网络、涨了一个点就发出来;第三,有没有可复现性,结构是否清晰,训练设置是否合理到我可以照着跑一遍。这三点缺一个,我都会放弃精读。
按这个标准,我把最近看到的芯片缺陷检测相关文献粗分成几类。经典CNN分类方法用ResNet等主干对缺陷图分类,实现简单、基线好建立,但长尾和小缺陷表现一般。目标检测和分割方法用YOLO类模型直接定位缺陷区域,适合需要定位的场景,但晶圆图缺陷本身是空间分布模式,不是普通目标框能描述的。Transformer和注意力方法把全局空间关系建模进来,适合空间分布型缺陷,但需要更多数据、训练更容易过拟合。自监督和对比学习方法从无标注数据学习表征,对长尾和少样本友好,难点是设计合理的正负样本对。
这篇精读的工作属于第三类和第四类的结合,用Transformer建模全局特征,再用对比学习做表征约束。选它的原因很直接:它没有回避长尾和小样本这两个最痛的点,而且公开数据集上报告的指标比CNN基线明显高出一截,值得花时间验证。论文里还给了消融实验,能清楚看到每个模块的贡献,这种“可拆解”的论文最适合用来精读。
2. 论文思路拆解:把晶圆图当成一个Token序列来看
2.1 核心贡献一句话说清
如果用一句话概括这篇论文,我会说:它把晶圆图切成一堆局部Patch,用Transformer编码这些Patch之间的全局依赖关系,同时增加了一个对比学习分支,让同类型缺陷在特征空间里聚成一团、不同类型互相推开,从而提升长尾类别的分类效果。
这里的关键点有两个。一个是“Patch序列”的建模方式。之前的很多工作直接对整个晶圆图跑CNN,局部感受野决定了模型必须堆很深才能看到较大范围的缺陷分布。而Transformer在每一层都能直接看到所有Patch,对于“缺陷呈环形分布”“缺陷集中在中心区域”这类强空间模式的识别非常有优势。另一个关键是“对比学习”。芯片缺陷检测的标注成本很高,尤其罕见缺陷样本少,对比学习的正负样本构造方式能在这个约束下尽可能把特征空间拉出清晰边界,这是单靠交叉熵损失很难做到的。
我在复现的时候,明显感觉到这两个设计是相辅相成的。Transformer负责把空间结构吃进去,对比学习负责把特征分布捋平,少一个都会让最终指标掉一截。论文里给的消融实验数据也印证了这个判断。
2.2 输入侧:晶圆图怎么变成Patch序列
大部分晶圆图数据在原始状态下不是一张干净的自然图像,而是包含测试芯片位置的离散坐标和每个位置的良率结果。把坐标数据转成图像矩阵后,会得到类似二值图或者灰度图的形态,其中每个像素点对应一个芯片位。论文里的预处理流程,我照着还原大概是这么几步:首先把原始坐标映射到一个固定大小的图像,比如整张图统一缩放到宽高128像素,保证每一张输入尺寸一致;然后对空白的晶圆边缘位置补齐像素值,避免模型被边缘噪声干扰;最后做了归一化,让输入口径一致。
为什么要把晶圆图统一缩放,而不是像目标检测那样保留原始分辨率,这是我一开始没想明白的地方。后面看代码才意识到,不同批次晶圆图的芯片数量差别很大,有的图有几百颗Die,有的上千颗,分辨率不一致会导致Patch切分数量不稳定,Transformer的可学习位置编码就没法共用。统一缩放到固定尺寸,虽然丢失了一些像素级细节,但换来了整个训练和推理过程的结构化,这个取舍在分类任务里是完全值得的。
Patch的尺寸也很讲究。这篇工作里用的是16x16像素的Patch,128x128的输入最终切成64个Token,每个Token在序列里的位置信息通过位置编码保留下来。Patch太小会让序列变长、计算量上去,Patch太大又会丢掉局部缺陷的细粒度特征。我在同样的数据上对比过8x8和32x32的效果,8x8的准确率略高但训练时间翻了快两倍,32x32省时间但局部划痕和环形缺陷容易搞混,所以16x16是论文里调得比较平衡的一个点。
2.3 网络结构:主干、分类头、对比头都怎么搭
网络主体不算复杂,基本是标准的Transformer Encoder结构:输入Patch先过一个线性映射变成Embedding,加上位置编码后进入多层Encoder,每个Encoder包含自注意力层和前馈网络,前馈网络的隐藏层维度一般取Embedding维度的四倍,也就是常说MLP ratio为4。论文里用的Embedding维度是256,Encoder层数取了8层,注意力头数8个,这几组参数配合128x128的输入在精度和显存之间取了一个平衡点。
真正值得琢磨的是两个“头”的设计。分类头就是常规的LayerNorm加全连接层,输出对应每个缺陷类别的概率。对比学习头不在分类阶段用,它接在Encoder输出的特征向量后面,先经过一个非线性投影层,把特征映射到一个低维空间,再在低维空间里计算样本之间的相似度。这样做的原因是,如果直接用分类特征去算对比损失,模型容易只抓住与分类最相关的信息,反而把更广泛的分布结构丢掉。投影层相当于一个缓冲,让特征本身更通用。
推理阶段,对比头会被整个摘掉,只用主干输出接分类头做预测。这算是一个很典型的“训练时辅助、推理时精简”思路,不会增加部署负担,也让论文的方法更容易落到实际工程里。我在复现时严格保留了这个设计,实测推理速度和同等规模的CNN模型基本持平,没有因为多一个辅助头而变慢。
3. 训练细节和Loss设计:论文性能上限藏在哪
3.1 别只盯着交叉熵,对比学习才是抗长尾的关键
只看网络结构,很多人会觉得这篇论文的核心是Transformer,但实际上它的性能上限很大程度是由Loss设计决定的。论文的总损失大概可以写成一个联合形式,包括常规的Softmax交叉熵分类损失、对比学习损失和一个很轻的辅助正则项。分类损失负责把模型往正确类别方向推,对比学习损失负责在特征空间上做约束,它们的分工其实很清楚:交叉熵告诉模型“你要分对”,对比学习告诉模型“同类要长得像、异类要离得远”。
为什么单独交叉熵不行,我在长尾数据上体会特别深。当某个类别的样本只有几十张时,交叉熵会在这些少数样本上迅速过拟合,训练集损失能压得很低,但验证集上该类别的召回率一塌糊涂。对比学习就不一样,它不直接驱动模型记住某个样本的标签,而是拉近同类样本在特征空间里的距离,这样即使某个类样本很少,只要正负样本对构造合理,模型也能学到更有区分度的表示。论文里把分类损失和对比损失联合在一起,相当于既利用标签信息,又利用特征分布信息,两者互补。
温度系数是对比学习里面一个非常关键的超参。论文里默认取了0.1,我试着在0.05到0.5之间扫了一遍。温度太低,相似度会被压得很尖锐,模型只能看到非常近的样本对,容易忽略远一点的同类;温度太高,所有样本的分布又会被拉得很平,区分度下降。实测下来0.1确实是比较稳的选择,和论文复现值一致。
3.2 数据增强和采样策略不能照搬自然图像
芯片缺陷检测的数据增强和自然图像分类有本质区别,很多在ImageNet上有效的操作直接搬过来会适得其反。原因很简单:晶圆图里每个像素的位置是有物理含义的,旋转90度还能保持物理意义,但随机裁剪到很碎的局部、加上强烈的颜色抖动,会让一个本来看起来很像环形缺陷的图变成乱七八糟的噪声。论文里用的增强组合相对克制,我复现时把它整理成了固定几项:水平翻转、垂直翻转、90度旋转、小范围平移,以及针对局部高亮噪声的Cutout。
采样策略上,训练集采用的不是完全随机采样,而是做了一个类均衡采样。每个batch会尽量保证每个类别都出现,数量上以类别数量倒数为权重来抽样,这样稀有缺陷在每一步训练中都有机会被模型看到。类均衡采样配合对比学习,能明显缓解长尾问题,但也带来了一个小毛病:训练时batch里同类样本变多了,模型见过的全局数据分布和真实分布不一致,所以推理时要有意调高分类损失的权重,让模型不能完全依赖对比学习的分布假设。
我的建议是刚开始复现先别改论文的超参,第一轮按照它的完整配置完整跑完,拿到一个可以对照的基线之后,再尝试改成自己的采样策略和增强组合。这样后面调参时才知道改动到底起了什么作用,不然混在一起改,出了问题根本不知道是谁的锅。
3.3 评估指标千万别只看准确率
芯片缺陷检测论文里经常遇到一种现象:准确率98%以上,看起来非常惊艳,但我从来不会只拿这个数字判断方法好坏。原因在长尾分布上,绝大多数样本是正常类和少数高频缺陷,只要把正常类全预测对,准确率就能被顶到很高,但真正需要解决的罕见缺陷可能一个都没抓住。所以这篇论文在评估时使用的是宏平均F1、各类别召回率以及混淆矩阵,这些指标更能反映真实能力。
我复现时把准确率和macro-F1放在一起看,差别特别直观。直接训练出来的基线准确率能到96%以上,但macro-F1只有0.84左右,翻开混淆矩阵一看,长尾类别几乎全是被预测成正常类或中心类。加完论文这套对比学习和类均衡策略之后,准确率大概涨到97%,macro-F1能到0.89上下,提升的主要不是准确率,而是那些小众类别的召回率,这才是精读这篇论文真正值钱的地方。
| 指标 | CNN基线 | 论文方法(论文报告值) | 我的复现值 |
|---|---|---|---|
| Accuracy | 95.8% | 97.4% | 97.1% |
| Macro-F1 | 0.84 | 0.90 | 0.89 |
| 样本量最少类别的F1 | 0.31 | 0.67 | 0.64 |
反过来说,我始终觉得以后自己在论文里报告结果时,最好也把长尾类别的指标单独列出来,单放一个准确率不仅没法让别人判断方法好坏,也容易被当成“刷分”。这个习惯放到实际产线上更关键,产线关心的永远是“容易漏的那几类”,不是整体平均。
4. 实操过程与复现记录
4.1 环境配置与数据准备
复现之前先把环境固定住,免得后面被乱七八糟的依赖问题干扰判断。我的环境是Python 3.10、PyTorch 2.0.1、CUDA 11.8,单张RTX 3090就够跑完整套实验。WM-811K的官方数据是以numpy数组形式提供的,包含Key(坐标)、Label等字段,需要先转成图像矩阵再缩放到128x128。有一个特别要注意的点:原始数据里很多图是缺边角的,坐标体系不完整,转化成图像时一定要对空白像素做统一填充,否则同一类型缺陷的位置信息会漂移,模型训练起来会很别扭。
数据划分上,论文用的是官方默认的训练测试比例,我为了避免复现偏差,完全按照论文里的方式划分,不做二次随机分割。每个类别单独按比例切分,保证训练集和测试集的类别分布基本一致。跑之前我先做了个简单的类别数量统计,确认最长尾的类别和我预期的一致,再开始后续工作。
4.2 关键代码实现
下面是我复现时整理出来的核心代码,去掉了与论文无关的工程细节,保留了最主要的流程。模型部分主要是一个可配置的Transformer Encoder,加上两个头。这里直接给出训练循环里最关键的分类损失和对比损失两个部分。
import torch import torch.nn as nn import torch.nn.functional as F class DefectTransformer(nn.Module): def __init__(self, patch_size=16, in_chans=1, embed_dim=256, depth=8, num_heads=8, num_classes=9, proj_dim=128): super().__init__() self.patch_size = patch_size self.num_patches = (128 // patch_size) ** 2 self.patch_embed = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size) self.pos_embed = nn.Parameter( torch.zeros(1, self.num_patches + 1, embed_dim)) self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim)) encoder_layer = nn.TransformerEncoderLayer( d_model=embed_dim, nhead=num_heads, dim_feedforward=embed_dim * 4, batch_first=True, activation="gelu", dropout=0.1) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=depth) self.norm = nn.LayerNorm(embed_dim) # 分类头 self.head = nn.Linear(embed_dim, num_classes) # 对比学习投影头:非线性映射到低维空间 self.proj = nn.Sequential( nn.Linear(embed_dim, embed_dim), nn.GELU(), nn.Linear(embed_dim, proj_dim), ) def forward(self, x): B = x.shape[0] tokens = self.patch_embed(x).flatten(2).transpose(1, 2) tokens = torch.cat([self.cls_token.expand(B, -1, -1), tokens], dim=1) tokens = tokens + self.pos_embed tokens = self.encoder(tokens) feat = self.norm(tokens[:, 0]) logits = self.head(feat) feat_proj = self.proj(feat) return logits, feat_proj def contrastive_loss(feat_proj, labels, temperature=0.1): feat_norm = F.normalize(feat_proj, dim=1) sim = feat_norm @ feat_norm.T / temperature labels = labels.contiguous().view(-1, 1) mask = labels == labels.T mask.fill_diagonal_(False) # 简化版InfoNCE:先累加正样本对,再用logsumexp做归一化 positives = sim[mask] loss = -positives.mean() + torch.logsumexp(sim, dim=1).mean() return loss先说明,上面的对比学习实现已经做了简化,正式训练里还需要处理分母中排除自身、正样本对数量归一化这些细节。如果要做严谨复现,建议把损失写成样本级的正负样本掩码形式,每个样本的loss只累加与它同类别的样本对,再除以该样本的正样本对数。这样能避免样本多的类别在损失里占据太大比重,尤其长尾场景差别很大。
4.3 训练参数与调优记录
训练超参我基本沿用了论文的报告值。优化器用AdamW,初始学习率1e-4,权重衰减0.05,batch size是64,总共训练100个epoch。学习率调度用的是Warmup加余弦衰减,前10个epoch学习率从极小值线性升到1e-4,之后按余弦曲线衰减到接近0。这里Warmup对Transformer尤为重要,因为Transformer的自注意力模块对学习率很敏感,直接在大的初始学习率下训练更容易震荡甚至爆炸。
| 超参数 | 值 | 备注 |
|---|---|---|
| 输入尺寸 | 128x128 | 统一缩放 |
| Patch尺寸 | 16x16 | 共64个Token |
| Embedding维度 | 256 | 可调高但显存增加 |
| Encoder层数 | 8 | 再深收益变小 |
| 注意力头数 | 8 | 与embed_dim匹配 |
| 优化器 | AdamW | 权重衰减0.05 |
| 初始学习率 | 1e-4 | 前10个epoch warmup |
| Batch size | 64 | 单张3090实测OK |
| 分类损失权重 | 1.0 | 论文报告值 |
| 对比损失权重 | 0.3 | 论文报告值 |
| 正则项权重 | 0.01 | 很轻的辅助约束 |
实际训练时间方面,单张3090上跑100个epoch大概要40到50分钟,如果改成8x8的Patch,训练时间会明显增加,换来的是很小的精度提升,我会建议多数场景先保持16x16。训练到中期时,我明显观察到分类损失和对比损失下降不同步,对比损失在前20个epoch下降很快,后面趋缓,而分类损失更平滑。这也是联合训练时常见的情况,不用太担心。
4.4 结果对比:论文报告和我的复现值
复现实验最紧张的部分就是把结果和论文报告对齐。先跑没有对比学习分支的CNN基线,准确率约95.8%,macro-F1约0.84。再把Transformer主干替换进去,只保留分类头,不加入对比学习,macro-F1升到0.86左右,说明全局注意力本身对空间模式是有帮助的。最后加了对比学习联合训练,macro-F1稳定在0.89上下,最后一个epoch最好的一次到过0.90,和论文报告的0.90基本一致,但还是有一点点差距。
差距主要来自几方面。一是数据增强的随机种子不一样,虽然增强本身比较克制,但翻转和Cutout的随机性还是会对最终结果有细微干扰;二是训练batch里正负样本对的选择,我的实现偏简化,样本级mask处理和原论文不一定完全一致;三是PyTorch版本可能带来注意力dropout行为的微小差别。这类差距在论文复现里是正常的,只要整体量级对得上、趋势一致,说明方法论本身没有本质问题。
5. 复现中常见的坑
5.1 长尾类别训不起来怎么办
这是绝大多数第一次复现都会遇到的问题。现象很典型:训练loss在下降,准确率看起来也正常,但测评时某一个或者两个罕见类别的召回率几乎为零。我踩这个坑时一度以为是模型表达能力不够,后来看了每一类的训练样本量才意识到是采样的问题。解决办法是先用类均衡采样跑一轮,让模型在每个batch里都能看到所有类别,然后再把对比学习加上,因为对比学习的效果高度依赖batch里有没有同类的正样本对。
如果均衡采样后长尾类别还是很弱,可以试着把该类别在损失函数里的权重抬高,或者混合使用“类均衡采样+较高对比损失权重”两个策略。还有一个很容易忽略的做法:把训练集里明显标注错误的样本先清理掉,因为长尾类别本来样本就少,几个错样本的负面影响会非常大。
5.2 小缺陷被全局注意力吞掉
Transformer的优势在于全局感受野,但它的另一个副作用是注意力可能会被大块正常区域主导,局部的小缺陷在计算注意力时因为信息占比太低,容易被当成背景忽略掉。我复现时发现,某些只有几个像素宽的划痕类缺陷,即使训练集里样本不少,模型依然会漏掉,这基本就是全局注意力“顾大不顾小”的典型表现。
论文的做法是在结构上保留了多个层级的特征,具体来说就是在Transformer Encoder中间层引出一个辅助特征,和最后层的输出做融合。我复现时也照着这个思路加了中间层特征,效果确实有,但会增加一些代码复杂度。如果不想改动主干结构,另一个更简单的办法是把输入图放大,让Patch数量变多,小缺陷对应的Token在序列里的响应也会变大,计算量和显存会高一些,算是一种用算力换精度的折衷。
5.3 标签噪声把模型带偏
芯片缺陷数据集的标签噪声比很多人想象中大。产线标注往往依赖工艺工程师的经验,不同批次的标注标准未必完全一致,同一个缺陷类型在不同工厂里甚至有重叠的定义。WM-811K虽然是公开数据集,我抽查了一部分明显预测错误且置信度很高的样本,发现有几张是标签本身标错了。
处理标签噪声,我现在习惯用置信学习先过一遍数据。做法很简单:先用当前模型对训练集做预测,找出那些预测为A类但置信度极高、实际标签却是B类的样本,人工抽看这些样本的原始晶圆图像,把确认是标错的标签修正掉。做完这一步,长尾类别的小类指标通常能再往上提一两个点。需要注意的是,抽看样本数量不用太多,每类看几十张就够了,关键是找对修正的方向。
5.4 显存、速度和处理瓶颈
训练时显存瓶颈主要来自序列长度和batch size。128x128输入配16x16 Patch只有64个Token,显存压力其实不大,就算序列更长一点也能跑。真正吃显存的是batch size和Encoder层数,两者一上去,显存会加速上涨。如果显存不够,优先减batch size而不是减层数,同时把训练改成混合精度,显存占用能降掉近一半,训练速度还能快一些。
推理端要关心的是速度。Transformer在GPU上多头并行有优势,但如果在CPU或者边缘盒子上部署,性能不一定比同规模CNN好。我实测过把训练好的模型转成ONNX再部署到CPU,单张图的推理时间比CNN慢差不多30%。如果产线节拍卡得紧,这一步要充分评估,别等到上产线才发现速度不达标,那时候再回头改模型结构很被动。
6. 论文到产线,中间还有多远
6.1 训练环境和部署环境不是一回事
精读完论文,拿到好的指标,只能说明在公开数据集上方法有效。芯片缺陷检测真正要落地,横在中间的差距至少还有三块。第一是数据差异,自己产线上的晶圆图分布、相机型号、光照条件、测试芯片尺寸和公开数据集完全不一样,直接拿过去推理基本不可行,必须用产线数据做微调。第二是缺陷定义差异,论文里的缺陷类别和生产线上实际关心的缺陷未必是一一对应的,类别体系要重新梳理。第三是性能边界,离线评测时跑一个模型慢慢调很正常,产线上几秒甚至几百毫秒就得给结果,还得给后续处理方法留出时间,延迟预算要更严格。
所以我会建议,读这类前沿论文时,别只满足于复现精度,要多问一句:如果我要把这个结构搬到自己场景,数据和推理链路要做哪些调整。带着任务去读论文,收获远比单纯看公式大得多。
6.2 下一步可以怎么扩展
这篇论文的很多思路可以继续往外延伸。首先是自监督预训练,既然对比学习在长尾分类里效果这么好,那么完全可以先把对比学习用在无标注晶圆图数据上做预训练,然后用少量标注数据做微调,解决产线上标注样本少的问题。其次是少样本学习,把论文里的对比头换成双塔结构做原型学习,就能比较自然地扩展到新缺陷类型的增量识别。再一个是结合工艺信息,晶圆图不是孤立的,背后还有对应的工艺步骤、设备参数、批次信息,把这些信息作为多模态的辅助输入,理论上能进一步提升检测和根因分析能力。
我个人的下一步计划里,准备先在产线小批量数据上做一轮微调实验,同时把对比学习头保留下来,尝试拿它做一些缺陷聚类,看能不能帮助工艺工程师快速发现新的异常模式。这个方向如果验证顺利,后续还能把聚类结果反馈到标注系统,形成数据闭环。
这篇论文精读让我印象最深的,不是Transformer结构本身有多复杂,而是它在长尾分布、小样本这些实际生产约束下,通过一个辅助对比头就能带来可感知的指标提升,而且推理期还完全不增加成本。读论文和写代码一样,看得再多不如亲自复现一遍,只有亲手把训练跑起来、把混淆矩阵翻开,才能真正理解方法里哪些设计是关键的,哪些只是锦上添花。