EfficientNetV2の学習済みモデルでGrad-CAMを試す

# はじめに

CNN可視化手法であるGrad-CAMを試してみようと思います。
画像分類AIの予測根拠の可視化などに使われたりしている技術です。

- 環境
OS: Ubuntu18.04LTS
CPU: Intel® Core™ i7-8700 CPU @ 3.20GHz × 12
GPU: GeForce RTX2080
Python: Python3.7.9(Anacondaで作成した仮想環境)
tensorflow:2.8.0
matplotlib:3.5.1
numpy:1.21.5
opencv-python:4.5.5.64

- 参考記事
① [kerasとtensorflowでGrad-CAMを実装してみた](https://qiita.com/Kensuke-N/items/cea3920f19bebc9e0d19)

# 学習済みモデルの読み込み
```python
# ライブラリの読み込み
import tensorflow as tf
import numpy as np
import cv2
import matplotlib.pyplot as plt

# 学習済みEfficientNet_v2の読み込み
model = tf.keras.applications.efficientnet_v2.EfficientNetV2B0(weights="imagenet")
```
# Grad-CAMのアルゴリズム
Grad-CAMの関数です。[参考記事①](https://qiita.com/Kensuke-N/items/cea3920f19bebc9e0d19)を参考にしました。
```python
def grad_cam(input_model, image, layer_name):
    # 前処理
    img_arr = tf.keras.preprocessing.image.img_to_array(image)
    x = img_arr[tf.newaxis]
    x = tf.keras.applications.efficientnet_v2.preprocess_input(x)

    grad_model = tf.keras.models.Model([input_model.inputs], [input_model.get_layer(layer_name).output, input_model.output])

    with tf.GradientTape() as tape:
        conv_outputs, predictions = grad_model(x)
        class_idx = np.argmax(predictions[0])
        loss = predictions[:, class_idx]

    # 勾配を計算
    output = conv_outputs[0]
    grads = tape.gradient(loss, conv_outputs)[0]

    gate_f = tf.cast(output > 0, 'float32')
    gate_r = tf.cast(grads > 0, 'float32')

    guided_grads = gate_f * gate_r * grads

    # 重みを平均化して、レイヤーの出力に乗じる
    weights = np.mean(guided_grads, axis=(0, 1))
    cam = np.dot(output, weights)

    # 画像を元画像と同じ大きさにスケーリング
    cam = cv2.resize(cam, (x.shape[1], x.shape[2]), cv2.INTER_LINEAR)
    # ReLUの代わり
    cam  = np.maximum(cam, 0)
    # ヒートマップを計算
    heatmap = cam / cam.max()

    # モノクロ画像に疑似的に色をつける
    jet_cam = cv2.applyColorMap(np.uint8(255.0*heatmap), cv2.COLORMAP_JET)
    # RGBに変換
    rgb_cam = np.float32(cv2.cvtColor(jet_cam, cv2.COLOR_BGR2RGB))
    # もとの画像に合成
    output_arr = cv2.addWeighted(src1=img_arr, alpha=0.7, src2=rgb_cam, beta=0.3, gamma=0)
    output_image = tf.keras.preprocessing.image.array_to_img(output_arr)
    
    return output_image
```
前処理の部分や、最後の画像合成のところは少し変えています。　

# 実施結果
予測画像には以下の画像を使用します。
![Penguin](https://cdn.pixabay.com/photo/2014/07/04/21/59/king-penguins-384252_960_720.jpg "penguin.jpg")
学習済みモデルによる予測結果は以下のようになります。
> [[('n02056570', 'king_penguin', 0.8875521), ('n01798484', 'prairie_chicken', 0.0006106787), ('n02071294', 'killer_whale', 0.0005540343), ('n03743016', 'megalith', 0.0005340129), ('n01629819', 'European_fire_salamander', 0.00049976655)]]

Grad-CAMを実行します。
```python
# 画像読み込み
base_input_shape = model.input_shape[1:]
img_pil = tf.keras.preprocessing.image.load_img("./penguin.jpg", target_size=(base_input_shape[0], base_input_shape[1]))
# Grad-CAM
ｌayer_name = 'top_activation'
cam = grad_cam(model, img_pil, layer_name)
```
`layer_name`は一番最後の層の名前を指定します。`model.summary()`で確認することができます。
grad_camの出力(元の画像とヒートマップを合成した画像)は↓のようになりました。
![output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2604433/891b1611-0dea-478a-14e2-f962daab2204.png)
全てのペンギンの顔部分と、一番左にいるペンギンの全身が判断根拠となっていますね。
顔部分の黄色い毛は確かに皇帝ペンギンの特徴的な部分ですし、一番左のペンギンは横を向いていて黒い毛と白い毛がどちらも見えているのでこちらもペンギンの特徴としては重要そうです。

# おわりに
EfficientNetV2の学習済みモデルを使ってGrad-CAMを試してみました。
Grad-CAM自体は別にEfficientNetじゃなくても使えるので、画像分類AI案件で性能を説明する際などに使用できればなと思います。