PatchCore原理与代码解读

news2025/7/18 9:49:19

paper:Towards Total Recall in Industrial Anomaly Detection

code:GitHub - amazon-science/patchcore-inspection 

存在的问题 

目前无监督缺陷检测常用的一种方法是直接利用在ImageNet上预训练的模型中的表示,而不专门进行目标分布的迁移和适配,比如PaDiM。由于这类方法是non-adaptive的,因此网络在更深更抽象层上的适配置信度有限,因为从ImageNet上学习到的高级抽象特征和工业环境中所需要的抽象特征相关度不高。此外,由于可提取的高维特征表示较少,这类方法在测试时可用的nominal context也受到了限制。

本文的创新点

针对上述问题,本文提出了一种新的无监督缺陷检测算法PatchCore,它具有以下特点

  1. 最大化测试阶段可用的nominal information
  2. 减少对ImageNet数据的bias
  3. 保持高推理速度、

具体包括

  1. 使用局部聚合的,mid-level的特征patch
    a. 特征的抽象语义信息太少,深层特征对ImageNet数据的bias又太大,采用mid-level的特征可以在细节信息、抽象语义信息、对ImageNet的bias之间获得很好的平衡。
    b. 在局部邻域上进行特征聚合可以保留足够的spatial context
  2. 引入greedy coreset subsampling
    显著降低存储内存,提高推理速度

方法与实现

Locally aware patch features

首先为了保留足够的细节信息同时又不会让提取的抽象信息太过于偏向ImageNet的数据,作者选用中间层的特征表示,对于ResNet模型选用第2、3层的特征图。

作者通过在局部邻域上进行特征聚合的方式来提取特征

这里 \(\mathcal{N}^{(h,w)}_{p} \) 表示特征图上位置 \((h,w)\) 处大小为 \(p\times p\) 的一块patch,文中取p=3。则位置 \((h,w)\) 处的locally aware features如下所示 

其中 \(f_{agg}\) 是邻域特征向量的聚合函数,文中采用adaptive average pooling。

提取邻域特征向量的代码如下,首先提取预训练模型中的layer2、layer3。对于原始输入经过预处理后送入模型的大小为 224 x 224,假设 batch_size=2,则输入大小为 (2, 3, 224, 224),layer2、layer3的输出大小分别为(2, 512, 28, 28)、(2, 1024, 14, 14)。然后通过patchfiy函数提取局部邻域内的特征,这里通过torch.nn.Unfold实现,这个函数的用法见torch.nn.functional.unfold 用法解读_00000cj的博客-CSDN博客。这里和PaDiM中不一样的是,PaDiM中的stride=patchsize,也就是每个patch之间是互不重合的,对于28x28的feature map,patch_size=2,stride=2,padding=0,输出14x14。而这里patch_size=3,stride=1,padding=1,输出28x28。layer2、layer3经过patchify提出的邻域特征表示维度分别为(2, 784, 512, 3, 3)、(2, 196, 1024, 3, 3),其中784=28x28, 196=14x14。

features = [features[layer] for layer in self.layers_to_extract_from]
# {'layer2': torch.Size([2, 512, 28, 28])
#  'layer3': torch.Size([2, 1024, 14, 14])}

features = [
    self.patch_maker.patchify(x, return_spatial_info=True) for x in features
]

class PatchMaker:
    def __init__(self, patchsize, stride=None):
        self.patchsize = patchsize  # 3
        self.stride = stride  # 1

    def patchify(self, features, return_spatial_info=False):
        """Convert a tensor into a tensor of respective patches.
        Args:
            x: [torch.Tensor, bs x c x w x h]
        Returns:
            x: [torch.Tensor, bs * w//stride * h//stride, c, patchsize,
            patchsize]
        """
        padding = int((self.patchsize - 1) / 2)  # 1
        unfolder = torch.nn.Unfold(
            kernel_size=self.patchsize, stride=self.stride, padding=padding, dilation=1
        )
        unfolded_features = unfolder(features)  # (2,512,28,28)->(2,4608,784)
        number_of_total_patches = []
        for s in features.shape[-2:]:  # [28,28]
            n_patches = (
                s + 2 * padding - 1 * (self.patchsize - 1) - 1
            ) / self.stride + 1
            number_of_total_patches.append(int(n_patches))  # [28,28]
        unfolded_features = unfolded_features.reshape(
            *features.shape[:2], self.patchsize, self.patchsize, -1
        )  # (2,512,3,3,784)
        unfolded_features = unfolded_features.permute(0, 4, 1, 2, 3)  # (2,784,512,3,3)

        if return_spatial_info:  # True
            return unfolded_features, number_of_total_patches
        return unfolded_features

然后对layer3的输出进行bilinear插值使之与layer2匹配,得到features如下,其中1568=2x28x28,将batch_size维度和spatial维度合并到了一起。

features = [x.reshape(-1, *x.shape[-3:]) for x in features]  # [(1568,512,3,3),(1568,1024,3,3)]

然后通过自适应平均池化进行特征聚合,即上面提到的 \(f_{agg}\),这样对于预训练模型输出feature map上的每个位置(h, w),都得到一个预先设定维度 \(d\) 的单一表示,文中 \(d=1024\)。

代码如下

调用 features = self.forward_modules["preprocessing"](features)  # (1568,2,1024)

class MeanMapper(torch.nn.Module):
    def __init__(self, preprocessing_dim):
        super(MeanMapper, self).__init__()
        self.preprocessing_dim = preprocessing_dim

    def forward(self, features):
        features = features.reshape(len(features), 1, -1)  # (1568,512,3,3)->(1568,1,4608)
        return F.adaptive_avg_pool1d(features, self.preprocessing_dim).squeeze(1)  # (1568,1,4608)->(1568,1024)


class Preprocessing(torch.nn.Module):
    def __init__(self, input_dims, output_dim):
        super(Preprocessing, self).__init__()
        self.input_dims = input_dims  # [512,1024]
        self.output_dim = output_dim  # 1024

        self.preprocessing_modules = torch.nn.ModuleList()
        for input_dim in input_dims:
            module = MeanMapper(output_dim)
            self.preprocessing_modules.append(module)

    def forward(self, features):  # [(1568,512,3,3),(1568,1024,3,3)]
        _features = []
        for module, feature in zip(self.preprocessing_modules, features):
            _features.append(module(feature))  # [(1568,1024),(1568,1024)]
        return torch.stack(_features, dim=1)  # (1568,2,1024)

这样layer2、layer3的聚合特征[(1568, 512, 3, 3), (1568, 1024, 3, 3)]经过预处理,即分别经过自适应均值池化然后stack一起得到 (1568,2,1024)的输出特征。

然后再进一步进行聚合,得到(1568, 1024)的输出。

features = self.forward_modules["preadapt_aggregator"](features)  # (1568,1024)

class Aggregator(torch.nn.Module):
    def __init__(self, target_dim):
        super(Aggregator, self).__init__()
        self.target_dim = target_dim  # 1024

    def forward(self, features):  # (1568,2,1024)
        """Returns reshaped and average pooled features."""
        # batchsize x number_of_layers x input_dim -> batchsize x target_dim
        features = features.reshape(len(features), 1, -1)  # (1568,1,2048)
        features = F.adaptive_avg_pool1d(features, self.target_dim)  # (1568,1,1024)
        return features.reshape(len(features), -1)  # (1568,1024)

Coreset-reduced patch-feature memory bank

上面的代码中batch_size=2,一个batch的输出为(1568, 1024),其中1568=2x784=28x28,MVTec数据集中的bottle类别训练集共209张,因此整个训练集最终得到的memory bank \(\mathcal{M} \) 的维度为(163856, 1024),其中163856=28x28x209,随着训练集 \(\mathcal{X}_{N} \) size的增大,\(\mathcal{M} \) 也变得越来越大,最终的推理时间和存储空间也随之增大,因此通常需要对 \(\mathcal{M} \) 进行降维,且尽可能保存 \(\mathcal{M} \) 中编码的nominal feature。随机下采样会丢失 \(\mathcal{M} \) 中的有用信息,本文使用coreset subsampling方法来减小 \(\mathcal{M} \),coreset selection旨在找到一个子集 \(\mathcal{S}\subset  \mathcal{A}\),对于通过 \(\mathcal{A}\) 得到的解,通过 \(\mathcal{S}\) 可以快速得到最近似解。根据不同的问题,coreset selection的目标也不同,因为PatchCore采用的是nearest neighbour computation,因此本文选用minmax facility location coreset selection来寻找子集 \(\mathcal{M}_{C}\),为了减少coreset selection的时间,本文通过random linear projection \(\psi :\mathbb{R} ^{d}\to\mathbb{R} ^{d^{*}},d^{*}<d\) 来减小元素 \(m\in\mathcal{M}\) 的维度,具体的步骤如下所示

实现代码如下,其中percentage=0.1表示维度缩减为十分之一,\(d^{*}=128\),实现中为了减小内存采用的ApproximateGreedyCoresetSampler的实现,从维度163856中随机挑选10个作为初始点,这样distance matrix的计算就从163856x163856减小到了163856x10。

features = self.featuresampler.run(features)  # (16385, 1024) 

调用coreset sampler,实现中ApproximateGreedyCoresetSampler类继承的GreedyCoresetSampler,并覆盖了_compute_greedy_coreset_indices方法,这里为了方便显示把GreedyCoresetSampler类中的run和_compute_batchwise_differences方法复制到ApproximateGreedyCoresetSampler中了。

class ApproximateGreedyCoresetSampler(GreedyCoresetSampler):
    def __init__(
        self,
        percentage: float,  # 0.1
        device: torch.device,  # cuda:0
        number_of_starting_points: int = 10,  # 10
        dimension_to_project_features_to: int = 128,  # 128
    ):
        """Approximate Greedy Coreset sampling base class."""
        self.number_of_starting_points = number_of_starting_points
        super().__init__(percentage, device, dimension_to_project_features_to)
    
    def run(
        self, features: Union[torch.Tensor, np.ndarray]
    ) -> Union[torch.Tensor, np.ndarray]:
        """Subsamples features using Greedy Coreset.

        Args:
            features: [N x D]
        """
        if self.percentage == 1:
            return features
        self._store_type(features)
        if isinstance(features, np.ndarray):
            features = torch.from_numpy(features)
        reduced_features = self._reduce_features(features)  # (163856, 1024) -> (163856, 128)
        sample_indices = self._compute_greedy_coreset_indices(reduced_features)  # (16385,)
        features = features[sample_indices]  # (16385, 1024)
        return self._restore_type(features)

    @staticmethod
    def _compute_batchwise_differences(
        matrix_a: torch.Tensor, matrix_b: torch.Tensor  # (163856, 128),(10,128)
    ) -> torch.Tensor:
        """Computes batchwise Euclidean distances using PyTorch."""
        # (163856,1,128).bmm(163856,128,1)->(163856,1,1)
        a_times_a = matrix_a.unsqueeze(1).bmm(matrix_a.unsqueeze(2)).reshape(-1, 1)  # (163856,1)
        # (10,1,128).bmm(10,128,1)->(10,1,1)
        b_times_b = matrix_b.unsqueeze(1).bmm(matrix_b.unsqueeze(2)).reshape(1, -1)  # (1,10)
        a_times_b = matrix_a.mm(matrix_b.T)  # (163856,10)

        return (-2 * a_times_b + a_times_a + b_times_b).clamp(0, None).sqrt()  # (163856,10)

    def _compute_greedy_coreset_indices(self, features: torch.Tensor) -> np.ndarray:
        """Runs approximate iterative greedy coreset selection.

        This greedy coreset implementation does not require computation of the
        full N x N distance matrix and thus requires a lot less memory, however
        at the cost of increased sampling times.

        Args:
            features: [NxD] input feature bank to sample.
        """
        number_of_starting_points = np.clip(
            self.number_of_starting_points, None, len(features)
        )  # 10
        start_points = np.random.choice(
            len(features), number_of_starting_points, replace=False  # 163856
        ).tolist()  # [61587, 130619, 91549, 30689, 32225, 130105, 25966, 96545, 31837, 4447]

        approximate_distance_matrix = self._compute_batchwise_differences(
            features, features[start_points]  # (163856,128),(10,128)
        )  # (163856,10)
        approximate_coreset_anchor_distances = torch.mean(
            approximate_distance_matrix, axis=-1
        ).reshape(-1, 1)  # # torch.Size([163856]) -> torch.Size([163856,1])
        coreset_indices = []
        num_coreset_samples = int(len(features) * self.percentage)  # 16385

        with torch.no_grad():
            for _ in tqdm.tqdm(range(num_coreset_samples), desc="Subsampling..."):
                select_idx = torch.argmax(approximate_coreset_anchor_distances).item()
                coreset_indices.append(select_idx)
                coreset_select_distance = self._compute_batchwise_differences(
                    features, features[select_idx : select_idx + 1]  # noqa: E203
                )  # (163856,128),(1,128)->(163856,1)
                approximate_coreset_anchor_distances = torch.cat(
                    [approximate_coreset_anchor_distances, coreset_select_distance],
                    dim=-1,
                )  # (163856,2)
                approximate_coreset_anchor_distances = torch.min(
                    approximate_coreset_anchor_distances, dim=1
                ).values.reshape(-1, 1)  # (163856)->(163856,1)

        return np.array(coreset_indices)  # (16385,)

Anomaly Detection with PatchCore

这一部分原文没太看懂,官方实现中最近邻检索和距离计算直接调用的第三方库faiss,对faiss的原理不太了解。并且实现中好像并没有用到式(7),等后续看懂了再来补充吧。这里贴一下原文

代码实现

整个训练集经过coreset selection得到的memory bank \(\mathcal{M}\) 的维度为(16385, 1024)。然后送入faiss的search index中,核心代码就是下面两行

search_index = faiss.IndexFlatL2(features.shape[-1])
search_index.add(features)

在测试时,假设batch_size=2,提取的聚合特征维度为(1568, 1024),其中1568=2x28x28,然后从训练集 \(\mathcal{M}\) 中找到nearest distance and indice,然后沿特征维度取均值就得到了异常得分anomaly scores,代码如下

query_distances, query_nns = search_index.search(query_features, n_nearest_neighbours=1)  # (1568,1024),(1568,1)
anomaly_scores = np.mean(query_distances, axis=-1)  # (1568,)

异常得分reshape成(2, 28, 28),沿spatial维度取最大值就得到了整张图片的异常分数,shape=(2, )。进行bilinear插值上采样,然后高斯滤波得到(2, 224, 224)的输出mask,即整张图片每个像素点的异常得分,用于进行异常区域的分割。 

实验结果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/8090.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

从零开始将图片信息和空间信息绑定,并在前端展示到地图

作者&#xff1a;xiaoyan 关键词&#xff1a;前端查询时展示和空间数据绑定的图片资源 本文适合零基础入门 背景&#xff1a;iServer支持空间查询&#xff0c;可以将空间数据属性表中的属性查询出来&#xff0c;如通过SQL语句查询出某地大楼实际层高&#xff0c;或者查询出某…

RHCE实验--配置nfs服务

1、开放/nfs/shared目录&#xff0c;供所有用户查询资料&#xff1b; 2、开放/nfs/upload目录&#xff0c;供所有用户上传下载资料&#xff1b; 服务器与客户端都写好yum源以及挂载光盘&#xff0c;然后安装服务包 [rootserver ~]# yum install rpcbind -y [rootserver ~]# y…

Vue3基础

Vue 官网 https://cn.vuejs.org/ https://v3.cn.vuejs.org/ https://staging-cn.vuejs.org/api/ 1、环境 1.1、nodejs node node -vnpm #当前版本 npm -v #升级npm版本 npm install -g npm1.2、vue #安装vue npm install -g vue-cli #安装最新版本 npm install -g vu…

LQ0197 锦标赛【程序填空】

题目来源&#xff1a;蓝桥杯2014初赛 C A组E题 题目描述 本题为代码补全填空题&#xff0c;请将题目中给出的源代码补全&#xff0c;并复制到右侧代码框中&#xff0c;选择对应的编译语言&#xff08;C/Java&#xff09;后进行提交。若题目中给出的源代码语言不唯一&#xff0…

Python简单实现人脸识别检测, 对照片进行评分

大家好&#xff0c;今天和大家说说如何用Python简单实现人脸识别检测, 对照片进行排名&#xff0c;看看自己有多漂亮。 [开发环境]: Python 3.8 Pycharm 2021.2 [模块使用]: requests >>> pip install requeststqdm >>> pip install tqdm 简单实现进度条效果…

Arduino从零开始(1)——按钮控制LED

0.前言 本文主要介绍Arduino对于开关和条件判断函数的使用。 目录 0.前言 1.介绍 2.按钮控制LED 2.1下拉模式&#xff1a; 2.2上拉模式 3.扩展实验&#xff1a; 1.介绍 前篇介绍了点亮LED&#xff0c;这次案例我们尝试通过一个简单的传感器——按钮&#xff0c;来实现…

Ubuntu20.04离线安装Vmware tools

参考连接&#xff1a;在 Linux 虚拟机中手动安装 VMware Tools 从 Workstation Pro 菜单栏中选择虚拟机 > 安装 VMware Tools。 如果安装了早期版本的Vmware tools&#xff0c;则菜单项是更新Vmware tools如果这个安装Vmware tools 是灰色的&#xff0c;进行如下的处理方式…

HTML基本骨架与编辑器选择

HTML基本骨架与编辑器选择 文章目录HTML基本骨架与编辑器选择1.HTML基本了解1.1 什么是HTML1.2 HTML标签1.3 HTML元素1.4 Web浏览器1.5 HTML网页结构1.6 HTML版本了解2.HTML基本骨架介绍3.HTML编辑器的下载与使用1.HTML基本了解 1.1 什么是HTML HTML 是用来描述网页的一种语言…

双链表的基本操作

目录 一、双链表的设计 二、双链表的实现和基本操作 1.实现双链表节点以及设置first、last指针 2.获取当前链表中元素的数量 3.获取指定位置的节点 4.在尾部添加结点元素 5.在指定位置添加元素 6.删除指定位置的结点 一、双链表的设计 针对于查询操作&#xff0c;我们可…

计算机操作系统:实验3 【虚拟存储器管理】

计算机操作系统&#xff1a;实验3 【虚拟存储器管理】 文章目录计算机操作系统&#xff1a;实验3 【虚拟存储器管理】一、前言二、实验目的三、实验环境四、实验内容五、实验说明1、设计中虚页和实页的表示2、关于缺页次数的统计3、LRU算法中“最近最久未用”页面的确定4、算法…

删除类及其对象的属性:delattr()函数

【小白从小学Python、C、Java】 【Python-计算机等级考试二级】 【Python-数据分析】 删除类及其对象的属性 delattr()函数 [太阳]选择题 请问对以下Python代码说法错误的是&#xff1f; class MyClass1: x 1 y 2 myObject1 MyClass1() print(【访问】myObject1的属…

Revit中“结构框架显示与剪切“的应用和一键剪切功能

一、Revit关于"结构框架显示与剪切"的应用 结构框架&#xff1a;顾名思义其实它表示的就是结构梁而已&#xff0c;但是我们画图的时候往往会显示"实线"和"虚线"&#xff0c;以至于在出结构图纸的时候&#xff0c;达不到出图要求 NO.2、应用 Part…

ISCTF

upload upload,一道phar文件上传题目 <?php class upload{public $filename;public $ext;public $size;public $Valid_ext;public function __construct(){$this->filename $_FILES["file"]["name"];$this->ext end(explode(".", …

[山东科技大学OJ]1490 Problem F: 该按哪些键

Time Limit: 1 Sec Memory Limit: 128 MB Submit: 1693 Solved: 433 [Submit][Status] Description Peter在手机上打字时一直用全键键盘来输入&#xff0c;但最近不知道怎么搞的&#xff0c;把全键键盘弄丢了&#xff0c;只剩下了9键键盘。一项喜欢高科技的Peter却不会用9键…

彻底搞懂nodejs事件循环

nodejs是单线程执行的&#xff0c;同时它又是基于事件驱动的非阻塞IO编程模型。这就使得我们不用等待异步操作结果返回&#xff0c;就可以继续往下执行代码。当异步事件触发之后&#xff0c;就会通知主线程&#xff0c;主线程执行相应事件的回调。 以上是众所周知的内容。今天…

ASEMI整流桥D3KB100参数,D3KB100规格,D3KB100封装

编辑-Z ASEMI整流桥D3KB100参数&#xff1a; 型号&#xff1a;D3KB100 最大重复峰值反向电压&#xff08;VRRM&#xff09;&#xff1a;1000V RMS反向电压VR(RMS)&#xff1a;700 平均整流输出电流&#xff08;IO&#xff09;&#xff1a;3A 峰值正向浪涌电流&#xff08…

【论文阅读】时序动作检测系列论文精读(2019年)

文章目录1. BMN: Boundary-Matching Network for Temporal Action Proposal Generation论文目的——拟解决问题贡献——创新实现流程详细方法2. MGG: Multi-granularity Generator for Temporal Action Proposal论文目的——拟解决问题贡献——创新实现流程详细方法3. P-GCN: G…

稳压二极管的应用及注意事项

文章目录稳压二极管也被称为齐纳二极管 齐纳二极管和普通二极管的伏安特性曲线 齐纳二极管的工作原理 稳压二极管的伏安特性曲线的正向特性和普通二极管差不多&#xff0c;反向特性是在反向电压低于反向击穿电压时&#xff0c;反向申阳很大&#xff0c;反向漏电流极小。但是…

【学习笔记】AGC028/AGC007

AGC028 Removing Blocks High Elements 好仙啊。 我会转化&#xff01;&#xff01;问题转化为在原序列剩下的数中取ISISIS序列aaa,bbb&#xff0c;满足cx∣a∣cy∣b∣cx|a|cy|b|cx∣a∣cy∣b∣ 。对于没在a,ba,ba,b序列中的数&#xff0c;可以通过恰当放置使其不对前缀最大…

并发编程- synchronized,Lock及volatile的使用

文章目录并发编程的可见性问题解决方法synchronizedLockvolatile并发编程的可见性问题 多线程访问共享变量&#xff0c;造成线程不安全&#xff0c;最后的数值不对 public class VDemo {private static int num 0;public static void add() {num;}public static void main(St…