🚨

画像系異常検知モデルの仕組みについて調べてみた 2 - PaDiM編 -

2024/12/25に公開

はじめに

皆さんこんにちは。株式会社アイデミー・データサイエンティストの藤井(X | LinkedIn)です。
本記事は、画像系異常検知モデルの中身について解説するシリーズの第二弾です。

第一弾では、画像系異常検知モデルの始祖・SPADEについて解説しました。画像系異常検知モデルの概要も説明していますので、まだ読まれていない方はぜひ先にこちらを読んでみてください。

https://zenn.dev/aidemy/articles/ccad49f8cf78e8

SPADEは、画像のピクセル単位の異常度を判定できる点が画期的でした。これによって、その画像が正常か異常かだけでなく、異常の位置やサイズなどの推定も可能になりました。

今回紹介するモデルPaDiMは、このSPADEをベースに改良を加えたモデルとなっています。

GitHubリポジトリ

前回同様、こちらのGitHubリポジトリを使用します。

https://github.com/rvorias/ind_knn_ad

SPADEの問題点

SPADEは全てのピクセルについてkNNによるクラスタリングに基づく距離情報を算出している都合上、推論に大きな計算コストがかかる点が課題でした。後継モデルであるPaDiM(および次回紹介するPatchCore)は、この問題を軽減することを主なスコープとしています。

PaDiMの概要

PaDiMは、推論時間短縮&精度向上のために、主に以下の2点を工夫しています。

  • 特徴量マップの次元数を削減
  • 異常度の計算方法を、各ピクセルごとの特徴量の平均共分散行列を使用する形に変更


図1 PaDiMの概要(arXiv:2011.08785v1より引用)

公式の論文から図を引用して補足します。
図1は、PaDiMモデルが抽出する特徴量マップの構造を示しています。
図中、青、緑、オレンジで示されている箱は、それぞれ学習済みCNNモデルの第1、2、3層から出力された特徴量マップに該当します。
WHはそれぞれ画像の幅方向と高さ方向に対応する次元であり、青、緑、オレンジの特徴量マップおよび元画像でこれらの数値は異なっています。
また、各特徴量マップはその枚数も異なっています。青→緑→オレンジの順で画像サイズが小さくなる反面、その枚数は増加していきます。

PaDiMは以下のプロセスで異常度判定パッチを作成します。

  1. 各特徴量マップの幅方向と高さ方向のサイズを揃える(緑、オレンジのサイズを最も大きい青のサイズに拡大)
  2. 特徴量マップの枚数を間引いて削減
  3. 特徴量マップの平均と共分散行列を計算して保持

次章以降で、実際のコードや出力を見ながら各処理を追っていきます。

モデルの詳細解説

特徴量抽出器

画像から特徴量を抽出する部分については、前回記事と同じです。

PaDiMモデル

モデルのコード全体が見たい場合は以下のトグルを展開してください。

実際のコードはこちら。
indad/models.py
class PaDiM(KNNExtractor):
    def __init__(
        self,
        d_reduced: int = 100,
        backbone_name: str = "resnet18",
    ):
        super().__init__(
            backbone_name=backbone_name,
            out_indices=(1,2,3),
        )
        self.image_size = 224
        self.d_reduced = d_reduced # your RAM will thank you
        self.epsilon = 0.04 # cov regularization
        self.patch_lib = []
        self.resize = None

    def fit(self, train_dl):
        for sample, _ in tqdm(train_dl, **get_tqdm_params()):
            feature_maps = self(sample)
            if self.resize is None:
                largest_fmap_size = feature_maps[0].shape[-2:]
                self.resize = torch.nn.AdaptiveAvgPool2d(largest_fmap_size)
            resized_maps = [self.resize(fmap) for fmap in feature_maps]
            self.patch_lib.append(torch.cat(resized_maps, 1))
        self.patch_lib = torch.cat(self.patch_lib, 0)

        # random projection
        if self.patch_lib.shape[1] > self.d_reduced:
            print(f"   PaDiM: (randomly) reducing {self.patch_lib.shape[1]} dimensions to {self.d_reduced}.")
            self.r_indices = torch.randperm(self.patch_lib.shape[1])[:self.d_reduced]
            self.patch_lib_reduced = self.patch_lib[:,self.r_indices,...]
        else:
            print("   PaDiM: d_reduced is higher than the actual number of dimensions, copying self.patch_lib ...")
            self.patch_lib_reduced = self.patch_lib

        # calcs
        self.means = torch.mean(self.patch_lib, dim=0, keepdim=True)
        self.means_reduced = self.means[:,self.r_indices,...]
        x_ = self.patch_lib_reduced - self.means_reduced

        # cov calc
        self.E = torch.einsum(
            'abkl,bckl->ackl',
            x_.permute([1,0,2,3]), # transpose first two dims
            x_,
        ) * 1/(self.patch_lib.shape[0]-1)
        self.E += self.epsilon * torch.eye(self.d_reduced).unsqueeze(-1).unsqueeze(-1)
        self.E_inv = torch.linalg.inv(self.E.permute([2,3,0,1])).permute([2,3,0,1])

    def predict(self, sample):
        feature_maps = self(sample)
        resized_maps = [self.resize(fmap) for fmap in feature_maps]
        fmap = torch.cat(resized_maps, 1)

        # reduce
        x_ = fmap[:,self.r_indices,...] - self.means_reduced

        left = torch.einsum('abkl,bckl->ackl', x_, self.E_inv)
        s_map = torch.sqrt(torch.einsum('abkl,abkl->akl', left, x_))
        scaled_s_map = torch.nn.functional.interpolate(
            s_map.unsqueeze(0), size=(self.image_size,self.image_size), mode='bilinear'
        )

        return torch.max(s_map), scaled_s_map[0, ...]

    def get_parameters(self):
        return super().get_parameters({
            "d_reduced": self.d_reduced,
            "epsilon": self.epsilon,
        })

学習(fit()

特徴量マップの抽出とリサイズ

PaDiMが用いる学習済みモデルからの出力層は、以下out_indicesで規定する通り第1, 2, 3層です。

indad/models.py
super().__init__(
    backbone_name=backbone_name,
    out_indices=(1, 2, 3),
)

各層の出力の形状を見てみましょう。

Layer 1: torch.Size([1, 256, 56, 56])
Layer 2: torch.Size([1, 512, 28, 28])
Layer 3: torch.Size([1, 1024, 14, 14])

第3, 4次元が画像のタテ、ヨコのサイズに相当し、第2次元が特徴量マップの枚数に対応します。また、第1次元は画像の枚数を示しています。
PaDiMの概要で述べたとおり、深い層からの出力ほど画像サイズが小さくなり、特徴量マップの枚数が大きくなっています。

続いて、全ての特徴量マップの幅方向と高さ方向のサイズを揃える処理が入り、全ての特徴量マップを結合します。コードでは以下の部分が該当します。

indad/models.py
for sample, _ in tqdm(train_dl, **get_tqdm_params()):
    # 特徴量マップの抽出
    feature_maps = self(sample)

    if self.resize is None:
        # 最も大きい特徴量マップのサイズを取得
        largest_fmap_size = feature_maps[0].shape[-2:]
        # 取得したサイズへの変換器self.resizeを定義
        self.resize = torch.nn.AdaptiveAvgPool2d(largest_fmap_size)

    # リサイズ処理
    resized_maps = [self.resize(fmap) for fmap in feature_maps]
    self.patch_lib.append(torch.cat(resized_maps, 1))

# 全ての特徴量マップを結合
self.patch_lib = torch.cat(self.patch_lib, 0)

特徴量マップのリサイズは、torch.nn.AdaptiveAvgPool2d()を用いて行っています。
では最終的に得られる異常度判定パッチself.patch_libの形状を見てみましょう。

self.patch_lib: torch.Size([n, 1792, 56, 56])

第2次元(特徴量マップの枚数)は1〜3層の特徴量マップの枚数の合計になっており、第3, 4次元は最も大きい1層目の特徴量マップのサイズに統一されていることがわかります。また、第1次元のnは学習に用いた画像枚数を表しています。

特徴量マップ枚数の削減

続いて、ランダムサンプリングによって特徴量マップの枚数を削減します。論文にて、主成分分析(PCA)による削減よりもランダムサンプリングの方が最終的な性能が高かったことが報告されています。
該当箇所のコードは以下の通りです。

indad/models.py
if self.patch_lib.shape[1] > self.d_reduced:
    print(f"   PaDiM: (randomly) reducing {self.patch_lib.shape[1]} dimensions to {self.d_reduced}.")
    # ランダムサンプリング
    self.r_indices = torch.randperm(self.patch_lib.shape[1])[:self.d_reduced]
    self.patch_lib_reduced = self.patch_lib[:,self.r_indices,...]
else:
    print("   PaDiM: d_reduced is higher than the actual number of dimensions, copying self.patch_lib ...")
    self.patch_lib_reduced = self.patch_lib

デフォルトでは、1792枚の特徴量マップを350枚まで削減する設定になっています。
削減後の異常度判定パッチの形状は以下の通りです。

self.patch_lib_reduced: torch.Size([n, 350, 56, 56])

平均と共分散行列の計算

最後に特徴量マップの平均と共分散行列を計算して異常度判定パッチとします。
平均についての処理は以下の通りです。

indad/models.py
self.means = torch.mean(self.patch_lib, dim=0, keepdim=True)
self.means_reduced = self.means[:,self.r_indices,...]

dim=0すなわち第1次元についての平均を計算しているので、全画像の平均を出していることになります。その後、self.patch_lib_reducedと同じ位置の特徴量マップのみを抽出してself.means_reducedとしています。異常度の判定にはこのself.means_reducedを用いることになります。
では、それぞれの形状を見てみましょう。

self.means: torch.Size([1, 1792, 56, 56])
self.means_reduced: torch.Size([1, 350, 56, 56])

どちらも画像1枚分のサイズになっていることが確認できました。

続いて、共分散行列を計算します。
該当部分のコードは以下の通りです。

indad/models.py
# 各画像の特徴量マップとそれらの平均との差
x_ = self.patch_lib_reduced - self.means_reduced

# x_を用いて共分散行列を計算
self.E = torch.einsum(
    'abkl,bckl->ackl',
    x_.permute([1,0,2,3]), # transpose first two dims
    x_,
) * 1/(self.patch_lib.shape[0]-1)

# self.Eに微小数を加える
self.E += self.epsilon * torch.eye(self.d_reduced).unsqueeze(-1).unsqueeze(-1)

# self.Eの逆行列を計算
self.E_inv = torch.linalg.inv(self.E.permute([2,3,0,1])).permute([2,3,0,1])

ここでは、各画像の特徴量マップとそれらの平均との差x_を用いて共分散行列self.Eを計算しています。
self.Eself.epsilonに基づく微小数を加えている意図は、数値計算での安定性を高めるためです。具体的には、その後の処理でself.Eの逆行列を計算する際に、0に近い値が存在することによって生じる不安定性を回避するための処理となっています。

共分散行列の逆行列self.E_invは、異常度の判定に用いる「マハラノビス距離」を計算するために用います。

x_self.E、およびself.E_invの形状はそれぞれ以下の通りです。

x_: torch.Size([3, 350, 56, 56])
self.E: torch.Size([350, 350, 56, 56])
self.E_inv: torch.Size([350, 350, 56, 56])

共分散行列とかよく分かんねーよ!って方のために、一連の処理についてChatGPT先生に解説してもらいました。興味のある方は読んでみてください。

まずeinsum()ってなに?

einsum は「アインシュタインの縮約記法」と呼ばれるものを使って、複雑なテンソルの掛け算や和の操作をシンプルに書く方法です。これにより、テンソルの特定の次元をどう操作するかを簡潔に指定できます。

torch.einsum('abkl,bckl->ackl', x1, x2) の例では、2つの4次元テンソル x1x2 が使われています。

ここで使われている記号の意味:
  • x1: 4次元テンソル(サイズは [a, b, k, l]
  • x2: 4次元テンソル(サイズは [b, c, k, l]

それぞれの文字が次元を表しています。たとえば、ax1 の最初の次元のサイズを、bx1x2 の共通の2番目の次元を表しています。

  • a, b, c:テンソルの特定の次元のラベルです(通常、バッチサイズやチャンネル数を表すことが多いです)。
  • k, l:空間的な次元(幅や高さ、特定の特徴マップ)を表すことが多いです。
a, b, c, k, l の対応関係
  • ax1 の最初の次元、バッチサイズやデータセットの個々の要素を表すことが多いです。
  • bx1x2 の共通の2番目の次元、特徴のチャネルや別の次元に対応することが多いです。
  • cx2 の3番目の次元、新たに作り出される次元に対応します。
  • k, l:空間的な次元(たとえば画像の縦横サイズ)で、2つのテンソル x1, x2 に共通の次元です。
einsum の動作の詳細:
  • 'abkl,bckl->ackl' という書き方は、「b, k, l 次元に沿ってテンソル x1x2 を掛け算し、b 次元を取り除く(合計する)」という操作を指示しています。
  • つまり、次のような動作が行われます:
    • x1x2 の2つのテンソルを、b, k, l の位置で掛け算します。
    • その後、b の次元で合計(縮約)し、最終的に a, c, k, l の次元を持つ新しいテンソルが得られます。
まとめ:
  • a, b, c はテンソルのバッチや特徴の次元に対応し、k, l は空間的な次元(画像の高さや幅など)に対応することが多いです。
  • einsum は、共通する次元(この場合は b, k, l)を使ってテンソル同士を掛け算し、その後 b 次元を合計して新しいテンソルを作り出します。
E(共分散行列)にはどんな意味があるの?

x_ が CNN モデルから抽出された特徴量であり、全画像のマップとの「差」を表していることを考えると、E は、これらの特徴量に対して統計的な特徴や関係性を捉えるために計算されたものだと考えられます。

E の役割:

self.E = torch.einsum('abkl,bckl->ackl', x_.permute([1,0,2,3]), x_) * 1/(self.patch_lib.shape[0]-1) という式からわかるように、E は以下のプロセスで計算されています。

  1. einsum を使った計算
    • x_ の 1 次元目と 2 次元目を入れ替えたテンソル(x_.permute([1, 0, 2, 3]))と元の x_ を使って、b, k, l の次元に沿って内積を取ります。
    • 結果として、次元 [a, c, k, l] のテンソルが得られ、これは画像ごとの特徴マップ間の相関や共分散に相当するものと考えられます。
  2. E は共分散行列の一種
    • この計算により、Ex_ の特徴量の相関(または共分散)を捉えた行列、もしくはその一部を表していると考えられます。
    • 具体的には、各特徴マップ(画像ごとの特徴)の間の関係を示すものであり、これを使ってデータの分布や特徴量の変動をモデル化しています。
  3. 正規化
    • 1/(self.patch_lib.shape[0]-1) で割ることにより、E は全体の画像数に基づいて正規化されています。これは、各画像の特徴マップが他の画像との関係でどの程度偏りがあるかを示すものです。
結論:

E は、CNN から抽出された特徴量マップに基づく相関行列(もしくは共分散行列)であり、これを使うことで、画像ごとの特徴量の分布や変動、他の画像との関係性を把握しています。このような行列は、異常検知や画像の特徴比較において重要な役割を果たします。

おそらく E を使って、異常検知における「基準」や「正常な分布」を表し、他の特徴量と比較しているのではないかと推測されます。

E_inv(共分散行列の逆行列)にはどんな意味があるの?

E_invE の逆行列)を計算し、それを推論で使用することは、特徴量間の関係や分布を基準にした異常検知や特徴の評価において、非常に重要な役割を果たしています。ここでは、E_inv の意味やその使用の意義を説明します。

1. E_inv の役割
  • E は、特徴量間の相関や共分散を表しているため、逆行列 E_inv はその逆の操作を表しています。つまり、E_inv を使うことで、特徴量間の関係を「戻す」または「調整」し、基準(通常の分布)と比較するための操作が可能になります。
  • 特徴マップの逆行列を使うことによって、画像の特徴が「通常」と比べてどの程度異なるかを測定できるようになります。特に、異常検知の文脈では、このような操作は「正常」な分布からどれだけ外れているかを評価するために使われます。
2. 共分散行列と逆行列
  • 共分散行列 E は、特徴量の分布の広がりを示しています。E_inv を使うと、この広がりに対して、どれだけ離れた特徴量が存在するかを効果的に計算できるようになります。
  • 特に異常検知の場面では、あるデータ点が通常の分布からどのくらい離れているか(距離やスコア)を計算するために逆行列が使われます。この場合、特徴量 x_E の分布からどの程度外れているかを評価していると考えられます。
3. 異常検知の例

推論時に、E_inv を使うことの具体的な意味としては、次のようなことが考えられます。

  • 正常なデータの特徴量(例えば、正常な画像の特徴マップ)は E で表される分布の中に収まっていると考えられます。
  • しかし、新しいデータや異常なデータがその分布から外れているかどうかを判断するために、逆行列 E_inv を使ってその差異を強調し、その「ずれ」を計算します。
  • 逆行列を使うことで、共分散の影響を取り除き、データが「異常」であるかどうかを効率よく判定できるようになります。これは、異常な特徴量がどれだけ異なるかを正確に測定するための重要な手法です。
4. 具体例:マハラノビス距離
  • 異常検知でよく使われる指標の一つにマハラノビス距離というものがあります。これは、データの分布からどれだけ離れているかを測る距離ですが、この距離を計算する際には共分散行列の逆行列が使われます。
  • E_inv を使うことは、マハラノビス距離のような計算を行い、データが通常の分布からどれだけ外れているかを評価することに似ています。
まとめ:
  • E_inv は、特徴量間の相関関係や分布を基準に、異常な特徴を効果的に検出するためのツールです。
  • 特に、推論時に E_inv を使うことで、新しいデータがどの程度「通常の範囲」から外れているかを計算し、異常検知に役立てています。

E_inv を使うことは、正常なデータのパターンや関係性を基準にして、それに当てはまらないデータを見つけるための手段と言えます。

epsilonを用いた処理の意味は?

この処理は、数値的な安定性を保つためのテクニックです。具体的には、小さな値(self.epsilon)を使って単位行列(アイデンティティ行列)を加えることで、逆行列を計算する際の問題を回避し、モデルの安定性を向上させる効果があります。

1. torch.eye(self.d_reduced):
  • これは、d_reduced サイズの単位行列(アイデンティティ行列)を作成します。単位行列は、対角要素がすべて1で、それ以外の要素がすべて0の行列です。
  • この行列は、特徴量の次元が互いに独立していると仮定した場合の基本的な行列です。
2. self.epsilon:
  • これは非常に小さな値であり、数値計算での不安定性を防ぐために使われます。典型的には、逆行列を計算する際に0に近い値が存在することで生じる不安定さを回避するために使います。
3. unsqueeze(-1).unsqueeze(-1):
  • これは、テンソルの最後の2つの次元に1つずつ次元を追加しています。これにより、アイデンティティ行列が E のテンソル形状と一致し、加算できるようになります。
4. self.E += self.epsilon * ...:
  • 最後に、E に対してこの小さな値を持つ単位行列を加えます。これにより、E の対角要素に self.epsilon が加わることになり、逆行列を計算する際にゼロやゼロに近い値があることで起こり得る計算の不安定さを防ぎます。
この処理の意図:
  1. 逆行列計算の安定性向上
    • E が正則でない(つまり、逆行列を持たない)場合や、極端に小さい固有値を持つ場合、逆行列を計算するときに不安定になる可能性があります。非常に小さな数値 self.epsilon を加えることで、E が正則となり、逆行列が計算できるようになります。
  2. 数値的な問題の回避
    • 行列の逆行列を計算する際、行列がゼロに近い要素を持つと、逆行列の計算が極端に不安定になることがあります。この処理によって、そのような問題を回避し、数値計算の安定性を確保しています。
  3. 正定値行列の保証
    • 共分散行列 E が正定値であることを保証するための処理とも考えられます。正定値行列は、逆行列が存在する条件の一つです。このような小さな値を加えることで、行列の固有値がすべて正の値となり、逆行列を安全に計算できます。
まとめ:

この処理は、数値的な安定性を保つために非常に重要です。特に、共分散行列の逆行列を計算する際には、小さな固有値やゼロに近い要素が存在すると計算が不安定になるため、self.epsilon のような小さな値を加えて、行列が正則であり逆行列を安全に計算できるようにしています。

推論(predict()

では推論を行う関数predict()を見ていきましょう。
以下に示す前半部分では、学習時と同様に特徴量マップの抽出&リサイズ&結合を行い、推論画像の特徴量マップfmapを得ています。

indad/models.py
feature_maps = self(sample)
resized_maps = [self.resize(fmap) for fmap in feature_maps]
fmap = torch.cat(resized_maps, 1)

続いて後半部分で、fmapの特徴量マップを学習時と同数まで削減&self.means_reducedとの差を取り、異常度判定パッチとのマハラノビス距離を計算してピクセル単位の異常度マップを得ます。

indad/models.py
x_ = fmap[:,self.r_indices,...] - self.means_reduced

# マハラノビス距離の算出
left = torch.einsum('abkl,bckl->ackl', x_, self.E_inv)
s_map = torch.sqrt(torch.einsum('abkl,abkl->akl', left, x_))

# 異常度マップを元画像のサイズまで引き伸ばす
scaled_s_map = torch.nn.functional.interpolate(
    s_map.unsqueeze(0), size=(self.image_size,self.image_size), mode='bilinear'
)

return torch.max(s_map), scaled_s_map[0, ...]

それぞれの出力形状も見てみましょう。

x_: torch.Size([1, 350, 56, 56])
self.E_inv: torch.Size([350, 350, 56, 56])
left: torch.Size([1, 350, 56, 56])
s_map: torch.Size([1, 56, 56])
scaled_s_map: torch.Size([1, 1, 224, 224])

なんやかんやあってs_mapでは、56x56の画像1枚分のサイズになっています。
それをtorch.nn.functional.interpolate()による補完を行って、元画像のサイズ224x224まで引き延ばしています。

最終的な返り値は、画像単位の異常度とピクセル単位の異常度マップの2点です。
前者はscaled_s_mapの最大値が、後者は最終的に得たscaled_s_mapがそれぞれの出力となっています。

マハラノビス距離の算出について、地獄のeinsum()2連発かよ勘弁してくれ!でも詳しく知りたい!という勉強熱心な方は以下のChatGPT先生の解説をご覧ください。

マハラノビス距離の算出について
1. left = torch.einsum('abkl,bckl->ackl', x_, self.E_inv)
  • ここでの einsum の意味
    • 'abkl,bckl->ackl' という縮約記法は、x_(サイズ [a, b, k, l])と self.E_inv(サイズ [b, c, k, l])の間で、b, k, l の次元に沿って掛け算をし、b の次元で和を取るという意味です。
    • 具体的には、次元 b, k, l に沿って2つのテンソルの対応する要素を掛け合わせ、その結果を b の次元で合計します。最終的に、次元 [a, c, k, l] のテンソルが残ります。
  • 意味すること
    • この部分は、x_self.E_inv の掛け算を通して、x_ をある変換行列(逆行列 E_inv)を使って変換していると考えることができます。この操作により、新しいテンソル left が作られます。self.E_inv はテンソル self.E の逆行列で、変換の一種を表しています。
2. s_map = torch.sqrt(torch.einsum('abkl,abkl->akl', left, x_))
  • ここでの einsum の意味
    • 'abkl,abkl->akl' は、left(サイズ [a, b, k, l])と x_(同じく [a, b, k, l])の対応する次元の要素を掛け算し、その結果を b の次元に沿って合計するという意味です。結果として、次元 [a, k, l] のテンソルが得られます。
  • 意味すること
    • この部分では、left と元のテンソル x_ の間で内積を計算しています。この操作によって、次元 [a, k, l] のテンソル s_map が得られます。このテンソルは、ある種の「距離」や「スコア」を表している可能性が高いです。
    • 最後に、torch.sqrt を使って、内積の結果に平方根を適用しています。これにより、距離や類似性の計算結果が調整されます。
3. scaled_s_map = torch.nn.functional.interpolate(s_map.unsqueeze(0), size=(self.image_size, self.image_size), mode='bilinear')
  • interpolate の意味
    • この行では、s_map(サイズ [a, k, l])を、size=(self.image_size, self.image_size) の大きさにリサイズしています。bilinear という補間方法を使って、画像の解像度を変更しています。これにより、元の s_map のサイズが image_size に変換されます。
  • unsqueeze(0) の意味
    • unsqueeze(0) は、次元を1つ追加します。s_map[a, k, l] の形をしていますが、unsqueeze(0) によって [1, a, k, l] という次元が1つ追加され、これが interpolate 関数に渡されます。
全体の流れ:
  1. テンソルの変換
    • 最初の行では、x_self.E_inv を使って、テンソル x_ を逆行列を用いて変換し、新しいテンソル left を作成します。
  2. 距離(スコア)計算
    • 次に、leftx_ の内積を計算し、その結果に平方根を適用してスコアマップ(s_map)を作成します。このスコアマップは、入力データのある種の異常スコアや特徴量の変化を表している可能性があります。
  3. スコアマップの拡大
    • 最後に、スコアマップを画像サイズに拡大します。これにより、スコアマップが元の画像サイズに合わせてリサイズされ、視覚的な解析や評価がしやすくなります。

この一連の操作は、データに対して何らかの異常検知や特徴マップの生成を行っていることが考えられます。

出力結果を見てみよう

得られたscaled_s_mapをカラーマップとして元画像にオーバーレイさせた結果を以下に示します。

図2 異常度マップの例
使用データセット: https://www.mvtec.com/company/research/datasets/mvtec-ad

まとめ

以下にPaDiMモデルが行う一連の処理をまとめます。

  1. 学習
    • backendとして学習済みモデルを呼び出し、正常画像を入力する
    • 第1, 2, 3層の出力を取り出して特徴量マップを得る
    • 特徴量マップのサイズを揃える
    • 特徴量マップの枚数を削減
    • 特徴量マップの平均と共分散行列を計算し、異常度判定パッチとして保持
  2. 推論
    • 学習時と同様のプロセスで特徴量マップを処理
    • 異常度判定パッチとのマハラノビス距離を計算して異常度マップを獲得
    • 画像単位の異常度(異常度マップの最大値)とピクセル単位の異常度(異常度マップ)を出力

最後まで読んでいただきありがとうございました。
次回は最終回としてPatchCoreを取り上げたいと思います!

Aidemy Tech Blog

Discussion