CAM/Grad-CAMによる画像認識モデルの信頼性向上

# はじめに
◆この記事で伝えたいこと
画像認識モデルのXAI(説明可能なAI)であるCAM/Grad-CAMの仕組みと実装方法

◆対象読者
* 機械学習に興味のあるエンジニア
* XAIに興味のあるエンジニア

◆この記事のねらい
CAM/Grad-CAMの仕組みおよび実装方法を理解し、画像認識モデルの信頼性評価に活用できるようになること

# CAM/Grad-CAM
CAM(Class Activation Mapping)/Grad-CAMは、畳み込みニューラルネットワーク(CNN)による画像識別モデルの説明手法です。

CAM/Grad-CAMは、局所説明の手法でCNNの判断が合理的かどうかの確認に活用できます。

https://qiita.com/sasshi_i/items/400b7672d8b80bfc3a28

また、Grad-CAMはCAMの欠点である**適用できる機械学習モデルのアーキテクチャが制限される点**を克服した手法になります。

それでは、CAMとGrad-CAMについて解説していきましょう。

## CAMとは
Class Activation Mapping（CAM）は特定の入力画像において、画像のどの部分がモデルの予測に影響が出ているかをマッピングする手法です。

CAMを利用する制限として、適用する画像認識モデルに**Global Average Pooling層がないと使用できない**という点があります。

CAMがどのようにして予測に影響を与えている画像の部分を可視化しているか説明します。

### CAMの処理の流れ
CAMは下記の流れでモデルの予測に影響している画像の部分を可視化しています。

* 学習済モデルに判断根拠を知りたい画像を入力する
* 学習済モデルから特徴量マップを取得(モデルの最後の畳み込み層など)
* Global Average Pooling(GAP)層の出力する値に乗じる重みを取得(画像の$\mathbf{w}_1$, $\mathbf{w}_2$, $\mathbf{w}_n$のとこ)
    * $k$番目のチャネルに対するGAP後のスカラー値とクラス$c$をつなぐ重み($w_k^c$)
* 特徴量マップのチャネルごとに重みを乗じる
* 特徴量マップに重みを乗じたものを足し合わせる
* 元の画像サイズに戻すことで、重要度を表すヒートマップが完成

![スクリーンショット 2024-05-12 172042.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324032/4ac1b2a8-8214-2482-edf5-e38799332acd.png)
[1]のFigure.2から引用

## Grad-CAMとは
### CAMの欠点
CAMは適用する画像認識モデルに**Global Average Pooling層がないと使用できない**という欠点があります。この欠点を克服したものがGrad-CAMになります。

### CAMの欠点をどのように克服したのか
CAMでは、特徴量マップのチャネルに乗じる重みとして、GAP層の出力に乗じる重みを利用していました。

Grad-CAMでは特徴量マップのチャネルに乗じる重みとして、特徴量マップのチャネルごとの勾配平均を用いています。

これにより、GAP層がなくても重要度を表すヒートマップを作成できるようになりました。

### Grad-CAMの欠点
勾配平均を用いるため、勾配消失が起きた時はGrad-CAMは上手く動作しません。
勾配を用いない手法として、**Score-CAM**があります。

## なぜCAM/Grad-CAMが必要なのか
CAM/Grad-CAMは下記の活用方法があります。
* 予測の妥当性の検証
* 意図とは異なる学習の見直し

例えば、狼の画像を分類する時、狼に注目しているのではなく、背景の雪を見て識別していたという事例があります。
このように画像が正しい分類されていても、妥当な識別方法で分類できているとは限りません。

また、上記のような場合、背景が雪ではない狼の画像が訓練データセットに足りなかったということが推測できます。

このような予測の妥当性から意図とは異なる学習が行われたかどうかを検証することができます。


## CAM/Grad-CAMによる画像認識モデルの信頼性向上
それでは、画像認識モデルの信頼性を評価してみましょう。
下記の通り実装します。

* 環境: Google Colab
* モデル: VGG16(ImageNetで訓練済)
* XAI: Grad-CAM
* 説明する画像: トイプードルの画像(https://www.pakutaso.com/20231021284post-48088.html)

まずは、VGG16で画像の分類予測を行います。

```python
import numpy as np
import tensorflow as tf
from tensorflow.keras.applications.vgg16 import VGG16, preprocess_input, decode_predictions
from tensorflow.keras.preprocessing import image
import matplotlib.pyplot as plt
import cv2
import os

def preprocess_image(img_path):
    img = image.load_img(img_path, target_size=(224, 224))
    img = image.img_to_array(img)
    img = np.expand_dims(img, axis=0)
    img = preprocess_input(img) # RGB to BGR. each color channel is zero-centered
    return img

img_path = '/content/sample_data/dog.png'

preprocessed_img = preprocess_image(img_path)

# ImageNetで学習済のモデルを使用
model = VGG16(weights='imagenet')
pred = model.predict(preprocessed_img)
decode_predictions(pred, top=5)
```

```
[[('n02113624', 'toy_poodle', 0.4029886),
  ('n02113712', 'miniature_poodle', 0.2011538),
  ('n02112018', 'Pomeranian', 0.058990967),
  ('n02112137', 'chow', 0.046927843),
  ('n02094433', 'Yorkshire_terrier', 0.042802434)]]
```

トイプードルと分類できているみたいです。  
続いてGrad-CAMによる可視化を行います。

```python
def get_gradcam_heatmap(model, img_array, layer_name):
    grad_model = tf.keras.models.Model(
        [model.inputs], [model.get_layer(layer_name).output, model.output]
    )

    with tf.GradientTape() as tape:
        conv_outputs, predictions = grad_model(img_array)
        loss = predictions[:, tf.argmax(predictions[0])]
    
    grads = tape.gradient(loss, conv_outputs) # 勾配を取得
    pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2)) # チャンネルごとの平均勾配を算出。バッチ、縦、横方向に平均する

    conv_outputs = conv_outputs[0] # layer_nameの出力する特徴量マップ
    heatmap = conv_outputs @ pooled_grads[..., tf.newaxis] # 特徴量マップにチャネルごとの平均勾配をかける(勾配の大きなチャネルがより重視される)
    heatmap = tf.squeeze(heatmap) # 不要な次元の削除。ex. (height, width, 1) -> (height, width)

    heatmap = tf.maximum(heatmap, 0) / tf.math.reduce_max(heatmap)
    return heatmap.numpy()

# ヒートマップを画像に重ねる
def superimpose_heatmap(img_path, heatmap, alpha=0.4):
    img = cv2.imread(img_path)
    heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
    heatmap = np.uint8(255 * heatmap)
    heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
    superimposed_img = heatmap * alpha + img # alphaを大きくするとheatmapの画像が強調される
    return superimposed_img

heatmap = get_gradcam_heatmap(model, preprocessed_img, 'block5_conv3')
superimposed_img = superimpose_heatmap(img_path, heatmap, alpha=0.6)
superimposed_img = np.clip(superimposed_img, 0, 255).astype(np.uint8)

# 結果の表示
plt.figure(figsize=(10, 10))
plt.subplot(1, 2, 1)
plt.title('Original Image')
plt.imshow(image.load_img(img_path))

plt.subplot(1, 2, 2)
plt.title('Grad-CAM')
plt.imshow(cv2.cvtColor(superimposed_img, cv2.COLOR_BGR2RGB))
plt.show()
    
```
![スクリーンショット 2024-06-02 153923.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324032/4fb278b2-4718-715a-ac00-c7bd1f080230.png)

Grad-CAMの結果を見ると、犬の顔に注目して分類していることがわかります。
これにより、正しいところに注目して分類を行っており、信頼できるモデルである可能性が示唆されました。


# 終わりに
今回はCNNに対して使用できるXAIであるCAM/Grad-CAMについて記事を書きました。
CAM/Grad-CAMの仕組みやモデルの信頼性を確認できることを理解いただけたのであれば嬉しいです。

# 参考文献
[1] https://arxiv.org/pdf/1512.04150
[2] https://keras.io/examples/vision/grad_cam/

# 関連記事
https://qiita.com/sasshi_i/items/400b7672d8b80bfc3a28

https://qiita.com/sasshi_i/items/a1cb8339e668f5fd4e19