AnyTech Engineer Blog

AnyTech Engineer Blogは、AnyTechのエンジニアたちによる調査や成果、Tipsなどを公開するブログです。

PatchCoreのバックボーンについて

こんにちは、AnyTechの嶋田と申します。

画像系の異常検知にはPatchCoreを使う機会が多いかと思いますが、PatchCoreのバックボーンは色々なモデルを使用することが可能です。
defaultではWideResNet50が使われていますが、他のバックボーンに変更した際の精度や特性について調べてみました。



目次


バックボーンのリスト

PatchCoreは公式実装が存在し、URLは下記になります。 github.com

上記リポジトリのsrc/patchcore/backbones.pyに使えそうなバックボーンの一覧がリストアップされています。

import timm  # noqa
import torchvision.models as models  # noqa

_BACKBONES = {
    "alexnet": "models.alexnet(pretrained=True)",
    "bninception": 'pretrainedmodels.__dict__["bninception"]'
    '(pretrained="imagenet", num_classes=1000)',
    "resnet50": "models.resnet50(pretrained=True)",
    "resnet101": "models.resnet101(pretrained=True)",
    "resnext101": "models.resnext101_32x8d(pretrained=True)",
    "resnet200": 'timm.create_model("resnet200", pretrained=True)',
    "resnest50": 'timm.create_model("resnest50d_4s2x40d", pretrained=True)',
    "resnetv2_50_bit": 'timm.create_model("resnetv2_50x3_bitm", pretrained=True)',
    "resnetv2_50_21k": 'timm.create_model("resnetv2_50x3_bitm_in21k", pretrained=True)',
    "resnetv2_101_bit": 'timm.create_model("resnetv2_101x3_bitm", pretrained=True)',
    "resnetv2_101_21k": 'timm.create_model("resnetv2_101x3_bitm_in21k", pretrained=True)',
    "resnetv2_152_bit": 'timm.create_model("resnetv2_152x4_bitm", pretrained=True)',
    "resnetv2_152_21k": 'timm.create_model("resnetv2_152x4_bitm_in21k", pretrained=True)',
    "resnetv2_152_384": 'timm.create_model("resnetv2_152x2_bit_teacher_384", pretrained=True)',
    "resnetv2_101": 'timm.create_model("resnetv2_101", pretrained=True)',
    "vgg11": "models.vgg11(pretrained=True)",
    "vgg19": "models.vgg19(pretrained=True)",
    "vgg19_bn": "models.vgg19_bn(pretrained=True)",
    "wideresnet50": "models.wide_resnet50_2(pretrained=True)",
    "wideresnet101": "models.wide_resnet101_2(pretrained=True)",
    "mnasnet_100": 'timm.create_model("mnasnet_100", pretrained=True)',
    "mnasnet_a1": 'timm.create_model("mnasnet_a1", pretrained=True)',
    "mnasnet_b1": 'timm.create_model("mnasnet_b1", pretrained=True)',
    "densenet121": 'timm.create_model("densenet121", pretrained=True)',
    "densenet201": 'timm.create_model("densenet201", pretrained=True)',
    "inception_v4": 'timm.create_model("inception_v4", pretrained=True)',
    "vit_small": 'timm.create_model("vit_small_patch16_224", pretrained=True)',
    "vit_base": 'timm.create_model("vit_base_patch16_224", pretrained=True)',
    "vit_large": 'timm.create_model("vit_large_patch16_224", pretrained=True)',
    "vit_r50": 'timm.create_model("vit_large_r50_s32_224", pretrained=True)',
    "vit_deit_base": 'timm.create_model("deit_base_patch16_224", pretrained=True)',
    "vit_deit_distilled": 'timm.create_model("deit_base_distilled_patch16_224", pretrained=True)',
    "vit_swin_base": 'timm.create_model("swin_base_patch4_window7_224", pretrained=True)',
    "vit_swin_large": 'timm.create_model("swin_large_patch4_window7_224", pretrained=True)',
    "efficientnet_b7": 'timm.create_model("tf_efficientnet_b7", pretrained=True)',
    "efficientnet_b5": 'timm.create_model("tf_efficientnet_b5", pretrained=True)',
    "efficientnet_b3": 'timm.create_model("tf_efficientnet_b3", pretrained=True)',
    "efficientnet_b1": 'timm.create_model("tf_efficientnet_b1", pretrained=True)',
    "efficientnetv2_m": 'timm.create_model("tf_efficientnetv2_m", pretrained=True)',
    "efficientnetv2_l": 'timm.create_model("tf_efficientnetv2_l", pretrained=True)',
    "efficientnet_b3a": 'timm.create_model("efficientnet_b3a", pretrained=True)',
}

かなり沢山あるので、全部は試していませんが今回はあまりバックボーンとしては使われなさそうなViT系も試してみました。
ちなみに、バックボーンの指定はコマンドラインで下記のように指定します。

python run_patchcore.py \
patch_core \
-b \
wideresnet50 \
-le \
layer2 \
-le \
layer3 \

WideResNet50の場合

特徴量からパッチの生成

バックボーンで生成した特徴量からパッチをどのように作成しているかを、基本形のWideResNet50で見ていきましょう。

バックボーンで生成した特徴量をパッチに変換

モデルに入力した画像: [b, 3, 224, 224]はWideResNet50により特徴量に変換されます。(b: batch size, 3: channel, 224: height, 224: width)
WideResNet50はlayerごとに異なった空間情報、チャンネルをもつ特徴量を出力しますが、PatchCoreではlayer2: [b, 512, 28, 28], layer3: [b, 1024, 14, 14]を使用します。
layer1は局所的な情報量は多いですが、抽象化があまり進んでおらず、layer4は情報の抽象化は進んでいますが、局所的な情報がかなり薄れています。
従ってPatchCoreでは局所的な情報と抽象化された情報を程良く保持していると考えられるlayer2, layer3を使用する事としています。

次に特徴量からパッチを作成します。
layer2の特徴量であれば、[b, 512, 28, 28] --> [b, 784(28 * 28), 512, 3, 3]、
layer3では[b, 1024, 14, 14] --> [b, 196(14 * 14), 1024, 3, 3]となります。
これは各ピクセルの周辺情報を集めることによって受容野を広くする狙いがあります。


最終的に、layer2とlayer3の特徴量から作成したパッチをadaptive average poolingでマージしたいのですが、layer3のパッチは空間情報がlayer2と異なるので、196(14 * 14)から784(28 * 28)へbilinear補間を行いアップサンプリングします。
空間情報が揃えばあとはadaptive average poolingを行い、最終的なパッチ[b * 28 *28, 1024]を出力します。
これがバッチ単位でのパッチになります。

WideResNet50の精度

画像系の異常検知でよく使用されるMVTec ADでPatchCoreを実行すると精度(画像単位のAUROC)は下記のようになりました

WideResNet50のAUROC
全体的に精度は高いですが、capsule, grid, pill, screw等が多少苦手なようです

ViTの場合

ViTをバックボーンに指定する

ViTも色々ありますが、まずはvit_largeを使用します。 下記のようにコマンドラインパラメータを指定します。

python run_patchcore.py \
patch_core \
-b \
vit_large \
-le \
blocks.0 \
-le \
blocks.1 \

上記で指定したblocks.0や1はどのような構造になっているのでしょうか? timmの実装によると、次のようになっています。

class Block(nn.Module):

    def __init__(
            self,
            dim,
            num_heads,
            mlp_ratio=4.,
            qkv_bias=False,
            drop=0.,
            attn_drop=0.,
            init_values=None,
            drop_path=0.,
            act_layer=nn.GELU,
            norm_layer=nn.LayerNorm
    ):
        super().__init__()
        self.norm1 = norm_layer(dim)
        self.attn = Attention(dim, num_heads=num_heads, qkv_bias=qkv_bias, attn_drop=attn_drop, proj_drop=drop)
        self.ls1 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
        # NOTE: drop path for stochastic depth, we shall see if this is better than dropout here
        self.drop_path1 = DropPath(drop_path) if drop_path > 0. else nn.Identity()

        self.norm2 = norm_layer(dim)
        self.mlp = Mlp(in_features=dim, hidden_features=int(dim * mlp_ratio), act_layer=act_layer, drop=drop)
        self.ls2 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
        self.drop_path2 = DropPath(drop_path) if drop_path > 0. else nn.Identity()

    def forward(self, x):
        x = x + self.drop_path1(self.ls1(self.attn(self.norm1(x))))
        x = x + self.drop_path2(self.ls2(self.mlp(self.norm2(x))))
        return x

論文「AN IMAGE IS WORTH 16 X 16 WORDS : TRANSFORMERS FOR IMAGE RECOGNITION AT SCALE」の下図に相当する部分になるかと思います。

論文「AN IMAGE IS WORTH 16X16 WORDS」のFigure 1より抜粋

特徴量からパッチの生成

ViTはWideResNet50とは異なり、各blockから出力される特徴量のサイズは全て同じになっています。
しかも次元数もバッチの次元を除くと2次元となっており、WideResNet50の3次元とは異なっています。

ViT largeの各層での出力

上の図のように層の出力は全て[b, 197, 1024]となっており、このままでは既存のコードでエラーが発生します。
1次元目(197)の0番目の要素はclass tokenなのでこれを除いて196とし、さらに形状を[b, 1024, 14, 14]に変形します。

画像形式への変換

ここまで変形できたら、既存のコードに合流してもエラーは発生しなくなります。
WideResNet50の場合は特徴量の空間的なサイズが異なるのでF.interpolate()でサイズを揃えていましたが、ViTの場合はそういった処理を行わず、F.adaptive_avg_pool1d()でパッチをマージします。

ViT Largeの精度

WideResNet50と同様にMVTec ADで精度(画像単位のAUROC)を確認しました。
黄色のセルはWideResNet50以上の精度、緑のセルはViT Largeの中でのベストモデルになります

ViTの精度
上の表でvit_large_16_17_18というのはViTの16,17,18層を使用したという意味になります。
ViTの場合もWideResNet50のように複数の層を使用した方が精度が良いかと考えていましたが、16層を1つだけ使用した場合が最も良い精度となりました。
しかし、WideResNet50の精度には及ばないようです。
特にscrewが苦手なようで、0.9を超えることはありませんでした。

SwinTransformerの場合

SwinTransformerをバックボーンに指定する

今回はvit_swin_largeを使用するので、下記のようにコマンドラインパラメータを指定します。

python run_patchcore.py \
patch_core \
-b \
vit_swin_large
-le \
layers.0 \
-le \
layers.1 \

SwinTransformerの層は下記のようになっています。

論文「Swin Transformer: Hierarchical Vision Transformer using Shifted Windows」のFigure 3より抜粋

SwinTransformerはStage1〜4までの層があり、今回はStageの出力をバックボーンとして使用します。
ViTとは異なり、各Stageのサイズは異なります。

特徴量からパッチの生成

SwinTransformerの層からの出力は下図のようになっています。

SwinTransformerでの層の出力

これを見るとCNN系のPyramid Featureと似たような特徴量となっていることが分かります。
このままだとPatchCoreのコードでエラーが発生するので、画像形式に変換します。
ViTの場合はとは異なり、class tokenが含まれていないので単純に変化を行います。

画像形式への変換

ここまで変換を行えば後の流れはWideResNet50と同様となります。

SwinTransformerの精度

MVTec ADでの精度(画像単位のAUROC)は下表のようになっています。

SwinTransformerの精度

WideResNet50、ViTの場合は中間にある層を使用した場合が最も精度が良かったですが、SwinTransformerの場合は先頭の2層を使用するのが良いようです。
精度もViTよりも良く、wood、zipperではWideResNet50を上回る精度を出しています。
しかし、全体的な精度ではWideResNet50には及ばず、screwに関してはViT同様0.9を超えることはできませんでした。 バックボーンに使用する層数は1層よりも、2層の方が良いという結果になっています。

DeiTの場合

DeiTをバックボーンに指定する

他のバックボーンと同様にコマンドラインパラメータで指定を行います。

python run_patchcore.py \
patch_core \
-b \
vit_deit_distilled \
-le \
blocks.0 \
-le \
blocks.1 \

DeiTの場合、基本的なネットワークのアーキテクチャはViTと同様ですが、蒸留のためdistillation tokenが追加されている事が異なります。

論文「Training data-efficient image transformers & distillation through attention」のFigure2より抜粋

特徴量からパッチの生成

上記のようにDeiTにはclass tokenとdistillation tokenが画像の空間情報に追加されて出力されます。

timmのコードを見ると出力テンソルの1次元目の0と1の要素はclass tokenとdistillation tokenとなっています。

class VisionTransformerDistilled(VisionTransformer):

    def forward_features(self, x) -> torch.Tensor:
        x = self.patch_embed(x)
        x = torch.cat((
            self.cls_token.expand(x.shape[0], -1, -1),
            self.dist_token.expand(x.shape[0], -1, -1), x), dim=1)
        x = self.pos_drop(x + self.pos_embed)
        if self.grad_checkpointing and not torch.jit.is_scripting():
            x = checkpoint_seq(self.blocks, x)
        else:
            x = self.blocks(x)
        x = self.norm(x)
        return x

DeiTの各層の出力は下図のようになっています。
ViT largeとは異なり、2次元目のサイズは768と少し少なくなっています。

DeiTの各層の出力

class token、distillation tokenを除いた部分が画像情報となるので、その部分を画像形式に変換します。

画像形式への変換

ここまで変換できたら、後はViTの場合と同じになります。

DeiTの精度

MVTec ADでの精度(画像単位のAUROC)は下表のようになっています。

DeiTの精度
ViT largeと同様に1つの層のみの方が精度が良く、screwの精度が他のカテゴリーよりも悪いという特徴が見てとれます。
特徴量のサイズはViTが[b, 196, 1024]、DeiTは[b, 196, 768]となっており、DeiTの方が小さいですが精度は同等となっているので、効率的な特徴量の収集ができていると考えられます。

EfficientNet-B7の場合

EfficientNet-B7をバックボーンに指定する

下記のように指定することでバックボーンにEfficient-B7を使用することができます。

python run_patchcore.py \
patch_core \
-b \
efficientnet_b7 \
-le \
blocks.0 \
-le \
blocks.1 \

EfficientNetはベースモデルをスケールアップする際に下記式によって係数を決めることにより、精度と計算量のバランスが取れた効率的なモデルを実現しています。

論文「EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks」の式(3)より抜粋
 \alpha, \beta, \gammaはモデル固有の定数で、 \phiを変更してモデルのスケールアップを行います。
論文によると \alpha = 1.2, \beta = 1.1, \gamma = 1.15となっています。

特徴量からパッチの生成

EfficientNetは公式実装もありますが、バックボーンのリストはtimmのEfficientNetを使用しています。
timmのEfficientNetの各層は下表のようになっています。

EfficientNet-B7の各層の出力
EfficientNetの場合はWideResNet50の場合と同様に特徴量が画像形式となっているので、コードを修正することなく動作させることが可能です。

EfficientNet-B7の精度

MVTec ADでの精度(画像単位のAUROC)は下表のようになっています。

EfficientNet-B7での精度
色々な組み合わせを試してみましたが、WideResNet50と同等の空間的サイズ(28 * 28, 14 * 14)を持つblock2, 3, 4を含める組合せにて、精度が向上するようでした。
ViT系とは異なり、screwに関しても大幅な精度低下は見られませんが、総合的にWideResNet50の精度を超えることはできませんでした
個別のカテゴリーではgrid、zipperでWideResNet50の精度を上回りました。

考察

各バックボーンの精度比較

各バックボーンのパターンの中で一番精度の良い結果だけを表にまとめると下記になります。

バックボーンの精度比較

PatchCoreのバックボーンとして、ViT系はCNN系よりも精度が落ちる結果となっています。
ViT系の中で一番精度が良いのはSwinTransformerですが、ネットワーク構造はCNNのpyramid featureと似ており、window内でのみpatchのattention計算を行うといったローカルな情報の関連性を考慮している点もCNNに近い情報処理を行っていると考えられます。
また、ViT系の特徴として、screwカテゴリーの精度がかなり悪いという点も挙げられます。
screwは他のカテゴリーとは異なり、対象のネジが様々な方向に回転しており、向きが一定ではありません。
この特徴がTransformerと相性が悪いのかもしれません。

次に各バックボーンの特徴量のサイズと精度について考えてみます。

特徴量のサイズと精度
WideResNet50は最も精度が良いですが、特徴量のサイズも最大となっていて、最も特徴量のサイズが小さいvit_deit_distilled_6と比べると4倍程となっています。
vit_deit_distilled_6の精度は0.95と高くありませんが、少ない特徴量でそこそこの精度を出しているので効率的な情報の抽象化を行えていると考えられます。
同様のことはEfficientNet-B7にもあてはまり、CNN系としてはWideResNet50に一歩及びませんが特徴量のサイズの少なさは効率的と言えます。
ViT系はやみくもに特徴量のサイズを増やしても精度の向上が見られなかったので、Transformerのローカルの情報処理がCNNよりやや劣る場合があるといった特性が影響しているのかもしれません。

バックボーンの特徴量の可視化

ここでは各バックボーンについて特徴量の可視化を行い、それぞれの特徴を見ていきたいと思います。

WideResNetの特徴量の可視化

特徴量の可視化ですが、下図のように行います。

特徴量の可視化

[b, 512, 28, 28]という特徴量があった場合、チャンネル方向に加算を行い[b, 1, 28, 28]となります。
次に、各レイヤーの特徴量の最大値、最小値を使って特徴量の値を0〜1の範囲に正規化を行い、最後に特徴量を元の画像の大きさにリサイズします。
リサイズした画像をOpenCV等を用いてヒートマップ化し、元画像に重ねます。 可視化した特徴量はMVTechのテスト画像になります

bottle/test/good/000.png

まずはbottleの正常品について可視化します。
左から元画像、layer2の特徴量、layer3の特徴量、layer2と3をパッチ化してマージした特徴量になります

元画像 b, 512, 28, 28 b, 1024, 14, 14 マージした特徴量

bottle/test/broken_large/000.png

元画像 b, 512, 28, 28 b, 1024, 14, 14 マージした特徴量

layer2は活性箇所(赤い部分)が小さく、周辺領域に散らばり、layer3では少し大きく、中央よりに集まってきています。
これはlayer2の方が解像度が高く細かい部分が活性化し、layer3になると少し抽象度が上がり広めの範囲で活性化しやすくなったからと考えることができます。
マージした特徴量について見ると、正常品と異常品の違いがある程度現れています。
PatchCoreはマージした特徴量とMemory bank(正常品のパッチ集合)との距離によって異常箇所を判定するので、厳密に言うと正常品の1つと異常品を比べる事は実際の処理とは異なる、かつ、実際にはマージ前の特徴量の差分集計から異常箇所を炙り出す点からも実際の処理とは異なるのですが、視覚的に分かりやすい結果となっています。

leather/test/good/000.png

次にbottleのような単体の部品ではなく、判定箇所が画像の全面に広がるleatherを可視化します。

元画像 b, 512, 28, 28 b, 1024, 14, 14 マージした特徴量

leather/test/cut/000.png

元画像 b, 512, 28, 28 b, 1024, 14, 14 マージした特徴量

こちらのケースでもマージした特徴量では正常品と異常品の差が顕著に出ています。

screw/test/good/000.png

バックボーンにWideResNet50を使用した際に最もAUROCの値が低いscrewについて可視化を行います。

元画像 b, 512, 28, 28 b, 1024, 14, 14 マージした特徴量

screw/test/thread_top/021.png

元画像 b, 512, 28, 28 b, 1024, 14, 14 マージした特徴量

正常品、異常品共にlayer2ではネジの周辺に注目し、layer3ではネジ山と先端に反応が強く出ています。
マージした特徴量でネジの傷の箇所を見ると、bottle, leatherほど差が見られません。
かろうじて傷のある箇所が少し薄くなっているように見えますが、人間が識別するのは難しい状態となっています。
傷のある箇所はネジの螺旋と比べて図形的特徴が少ないので、あまり注目されず、この事が正常品との違いとなっているのかもしれません。
このように、正常品と異常品の差があまりない場合は異常箇所の検知能力が低くなります。

SwinTranformerの特徴量の可視化

SwinTransformerの中で一番精度が良かった、stage1, stage2の出力を使って特徴量の可視化を行います。
stage1, stage2のテンソルサイズはstage1: [b, 384, 28, 28], stage2: [b, 768, 14, 14]となります。

bottle/test/good/000.png

まずはbottleの正常品について可視化します。
左から元画像、stage1の特徴量、stage2の特徴量、stage1と2をパッチ化してマージした特徴量になります。

元画像 b, 384, 28, 28 b, 768, 14, 14 マージした特徴量

bottle/test/broken_large/000.png

元画像 b, 384, 28, 28 b, 768, 14, 14 マージした特徴量

SwinTransformerの活性状態はWideResNet50とはかなり違った結果となっています。
stage1では正常品、異常品共に四隅と中央の円が活性状態となっていますが、異常品の方は右下の傷部分の活性状態が少しだけ上昇しています。
stage2では右部分の傷は正常品よりも異常品の方が活性状態が下がり、右下から下にかけては異常品の方が多少、活性化しているのが確認できます。
最後のマージした特徴量も同様に右部分の傷は非活性、右下から下にかけては若干の活性化が見られます。

leather/test/good/000.png

次にleatherについて結果を表示します。

元画像 b, 384, 28, 28 b, 768, 14, 14 マージした特徴量

leather/test/cut/000.png

元画像 b, 384, 28, 28 b, 768, 14, 14 マージした特徴量

leatherはstage1, 2, マージした特徴量で、傷のある異常箇所のみ非活性となっています。

screw/test/good/000.png

最後に一番精度が悪いscrewの可視化を見ていきます。

元画像 b, 384, 28, 28 b, 768, 14, 14 マージした特徴量

screw/test/thread_top/021.png

元画像 b, 384, 28, 28 b, 768, 14, 14 マージした特徴量

screwに関しては面白いことにネジ自体は活性化せずにネジ周辺が活性化しています。
活性化関数にswishが用いられている為に、こうした事態が起こり得ます。
正常品と異常品との違いですが、可視化した結果をよく見ると、傷のある部分が正常品よりも少しだけ活性状態となっています。
WideResNet50と同様に、正常品と異常品の差異がなかなか分かりにくいので精度の低下を招いていると考えられます。

EfficientNet-b7の特徴量の可視化

EffcientNet-b7は一番精度が良かった特徴量の組み合わせ(layer2, 3, 4, 5)について可視化を行います
一番左が元画像、layer2, layer3, layer4, layer5, マージした特徴量となります。

bottle/test/good/000.png

元画像 b, 80, 28, 28 b, 160, 14, 14 b, 224, 14, 14 b, 384, 7, 7 マージした特徴量

bottle/test/broken_large/000.png

元画像 b, 80, 28, 28 b, 160, 14, 14 b, 224, 14, 14 b, 384, 7, 7 マージした特徴量

可視化した特徴量が5個もあるので傾向をつかみにくいですが、layer2, 3では傷部分が活性化しており、結果としてマージした特徴量についても傷の箇所が注目を集めています。
マージした特徴量について正常品と異常品を比べると傷の部分に差分が有りこれを元に異常箇所を判定できることが分かります。
ただし、WideResNet50よりは分かりにくい感じとなっています。

leather/test/good/000.png

元画像 b, 80, 28, 28 b, 160, 14, 14 b, 224, 14, 14 b, 384, 7, 7 マージした特徴量

leather/test/cut/000.png

元画像 b, 80, 28, 28 b, 160, 14, 14 b, 224, 14, 14 b, 384, 7, 7 マージした特徴量

leatherはなかなか分かりにくいですが、layer3では傷のある箇所の活性度が低く、layer5では傷周辺部部の活性度が高くなっています。
マージした特徴量に関しても傷の周辺の活性度が高くなっていますが、こちらもWideResNet50と比べるとかなり分かりにくい状態となっています。

screw/test/good/000.png

元画像 b, 80, 28, 28 b, 160, 14, 14 b, 224, 14, 14 b, 384, 7, 7 マージした特徴量

screw/test/thread_top/021.png

元画像 b, 80, 28, 28 b, 160, 14, 14 b, 224, 14, 14 b, 384, 7, 7 マージした特徴量

screwに関してはどのlayer、マージした特徴量を見ても傷部分の活性化や正常品と異常品の差分が見当たらないようです。

可視化したバックボーンの比較

WideResNet50とEfficientNet-b7は同じCNN系なので、活性化のパターンが似ていますが、WideResNet50の方が傷以外の活性度合いは低く、傷部分の活性化が強いという傾向が見られます。
EfficientNetは少ないchannelで効率的な特徴量を生成しますが、WideResNet50は非常にchannel数が多く、それが故に重複している特徴量が多数あると想定されます。
重複している特徴量によって傷の部分がEfficientNetより活性化され、PatchCoreでは高い精度を出すバックボーンになっているのかもしれません。

一方、Transformer系のSwinTransformerは傷部分が非活性でそれ以外の箇所が活性化するという逆転現象が見られる傾向にあります。
Transformerの本質的な処理はself-Attentionによって関連するデータをグローバルな範囲で共起することですが、傷部分は周辺の図形的な特徴と比べると特殊で法則性のある図形的な特徴が途切れ、あまり共起する対象がありません。
このことが、逆転現象(傷部分は非活性で、それ以外が活性化)を引き起こしている要因と考えられるかもしれません。

MemoryBankの可視化

MemoryBankとは正常品だけから抽出した特徴量をパッチ化して集めたもので、K-Centerなどでデータ量を減らしていますが、正常品を一般的に表すデータになります。
下記は論文から抜粋した図になります。

MemoryBank

ヒストグラム

MemoryBankは[n, 1024]の次元数を持ち、nはパッチの個数となります。
可視化にあたっては、1024次元をを合計し正規化した結果を0〜255までの値に変換しています。
ヒストグラムの横軸は0〜255の値をもつピクセルの輝度値で、縦軸は頻度になります。

WideResNet50

bottle leather screw

一番精度の良いWideResNet50では輝度値が高い範囲(200前後)に山の頂点が来ていて、分散も他のバックボーンと比べると狭くなっています
この形状により、異常品のパッチと重なりにくく精度が高くなっていると考えられます
カテゴリの中で一番精度が悪いscrewを見ると、頂点の山が2つ存在し、分散も他のカテゴリよりは広くなっています。
その結果、異常品との重複部分が多くなり、精度の低下に繋がったと想定されます。

EfficientNet-b7

bottle leather screw

WideResNet50とは異なり、山の頂点が中央(125前後)あたりに来ており、分散も全体的に広くなっています。
MemoryBankのこの形状からWideResNet50よりは精度が悪くなることが想像できます。

SwinTransformer

bottle leather screw

SwinTransformerもWideResNet50と比較すると分散が広めとなっています。
特にAUROCの精度が極端に悪いscrewを見ると2つの頂点が存在し、輝度値も全域に渡って存在しています。
screwのAUROCはWidreResNet50が0.986、EfficientNet-b7が0.963ですが、SwinTransformerは0.845と他のカテゴリと比べると極端に差が出ています。

PCA

次にMemoryBankとtest時の画像1枚をPCAで可視化してみます。
次元削減はMemoryBankを基準に行い、test画像はその次元を使用してプロットを行います。
赤がMemoryBank、青がtest画像となり、test画像については正常品と異常品をそれぞれ可視化します。
正常品はMemoryBankと同じ領域に分布し、異常品は離れた場所に分布しているデータがある程度存在すると考えられます。

WideResNet50

bottle
good broken_large

上記を見ると、異常品(broken_large)の方が中央に青い点が集まっているのが分かります。

leather
good cut

異常品の方が端の上部に青いデータが集まっているのを確認できると思います。
下図の部分が分かりやすいでしょうか。

screw
good thread_top

screwに関しても外側に青い点が集まる傾向があるようです。
こちらも静止画像を載せておきます。

EfficientNet

bottle
good broken_large

WideResNet50の場合と同様に、異常品の方が中央に青い点が集まっています。

leather
good cut

こちらはなかなか分かりにくいので、test画像で次元削減を行い、その次元を使ってMemoryBankを可視化しました。
下図に関しては赤がtest画像、青がMemoryBankとなります。

good cut

異常品はtest画像がMemoryBnakから大きく外れている部分があるのが分かります。

screw
good thread_top

EfficientNet-b7のscrewについては差異が殆ど確認できません。
このことからもscrewの精度が悪くなりがちなのが分かります。

SwinTransformer

bottle
good broken_large

分かりにくいですが、中央部分に青い点が多めに集まっているように見えます。

leather
good cut

異常品の方が下部の端に青い点が集まっています。

screw
good thread_top

screwに関しても異常品の方が外側に青い点が集まっています。

まとめ

色々なバックボーンでPatchCoreの精度を確認しましたが、現状ではWideResNet50が安定して高い精度(AUROC)を出すようです。
Transformer系はCNN系よりも全般的に精度が劣りますが、その中でも一番精度が良いのはSwinTransformerとなっています。
SwinTransformerはCNNと似たアーキテクチャを採用しているので、他のTransformer系のバックボーンよりも優れた結果を出すことができています。

それぞれのバックボーンに正常品と異常品を入力し、活性化状態を可視化しました。
正常品と異常品の差分が視覚的に一番確認しやすいのはWideResNet50で、CNN系は傷などがある異常箇所が活性化する場合が多いようです。
それに対してTransformer系のSwinTransformerは異常箇所が非活性で正常箇所が活性化している状態が見られました。
こちらはバックボーン内で採用されているswish等のマイナスの値を返し得る活性化関数に影響する所もあるとは思いますが、CNNのようなlocalな視点で図形の特徴を重視するモデルとは異なり、データとデータの関連性を強調するTransformerの特性が現れていると考えます。

MemoryBankの可視化によってもWideResNet50がなぜ高い精度になるのかが、明らかとなりました。
WideResNet50のMemoryBankはpatchデータが中央ではなく値が大きい方に偏り、分散も小さい分布となっています。
これと比べると他のバックボーンの分布は中央よりで、分散が大きく、異常データとの重複が起こりやすくなっています。

PCAを使ってMemoryBankとtestデータを可視化しましたが、やはりWideResNet50が視覚的に一番分かりやすい結果となっています。

WideResNet50はなぜ他のバックボーンより精度が良いのでしょうか?
EfficientNetやSwinTransformerはWideReNet50よりも後に登場しているので、なんとなく優れている気がします。
私もこの評価を行う前までは、バックボーンをEfficientNetやSwinTransformerなどに変更すれば精度が上がるだろうと簡単に考えていました。

WideResNet50が他のバックボーンと異なるのは、channel数がかなり多い所です。
バックボーンの可視化でも明らかとなっていますが、多くのchannelによって異常部分の活性化が他のモデルよりも強めにでることによって、正常・異常が見分けやすくなっていると考えることができます。
この多くのchannelの情報は重複した冗長な情報が含まれているかもしれませんが、PatchCoreでの異常検知というタスクでは優れた結果を残しているのだと思われます。