Grad-CAM(Gradient-weighted Class Activation Mapping)はCAM(Class Activation Mapping)という画像AIの判断の可視化を行う手法を改良したものです。CAMは画像解析モデルの判断根拠を可視化する技術です。しかし、CAMは可視化方法の性質から適用できるモデルの構造がかなり制限されます。そこで、モデルの構造にとらわれず画像解析AIの判断根拠を可視化する技術としてGrad-CAMが考案されました。

Grad-CAMとは

Grad-CAM は、CAM が抱えていた制約を取り除くために考案された手法 です。CAM では、可視化のためには、モデルが直感的に重要度が分かるような全結合層を使用している必要がありました。そのため、単純な全結合層(FC層)を持つ CNN にしか適用できず、VGG のような複雑な全結合層を持つネットワークでは使用できないという問題があったのです。

具体的には、下記の画像のような 単純な全結合層を持つ場合は、最終畳み込み層がどれほどクラスへ寄与しているかが分かりやすい構造(最終層の畳み込み結果の加重平均を取っているだけ)になっています。CAMはこのわかりやすさを前提にした方法を取っています。

一方で下記の画像のように、複数の全結合層を持つ画像解析モデルの場合には、全結合層が複数あることで、最終的な畳み込み結果がどのようにクラスへ寄与しているのか判断が非常に難しくなり、CAM を使用することができないというわけです。

そこで登場したのが Grad-CAM(Gradient-weighted Class Activation Mapping)です。
Grad-CAMは勾配を使用してクラス判定への寄与を評価するため、画像解析モデルがどれほど複雑な全結合層を持っているかに影響されません。畳み込み層と全結合層の間に大きな FC 層があろうと、あるいは別の構造を取っていようと、最終畳み込み層と勾配さえ利用できれば可視化が可能になります。このようにGrad-CAM はほとんどの CNN アーキテクチャに適用できるより汎用性の高い方法 です。

Grad-CAMの使用結果

下記の画像はGrad-CAMを猫の画像に適用した結果です。画像の結果からモデルは猫の顔に注目して画像を猫と判断していることが分かります。このようにGrad-CAMは画像解析モデルを可視化することができます。

Grad-CAMの仕組み

Grad-CAMの概要

Grad-CAM は勾配を活用した手法です。CAM の場合は、全結合層では最終畳み込み層の結果にただ重みを掛けただけであったため、その重み自体が各最終畳み込み層の結果がクラスの判別にどれくらい貢献しているかを表していました。一方で複雑な全結合層の場合は、各ノードが複雑に関係しており、直接的に各最終畳み込み層の結果がクラスの判別にどれくらい貢献しているかを判断することはできません。そこでGrad-CAMは勾配に注目しました。

Grad-CAMがどのような勾配を活用するか

Grad-CAMでは注目するクラスと各特徴マップのピクセルとの勾配を利用します。この勾配は、各特徴マップの値が増加した際にどのくらいクラススコアが変化するかを表した値です。つまり、この値が大きいほどクラスの判断に貢献してるといえます。

もう少し細かい話をすると、CAMでは各特徴マップの重要度を直接かけられる重みで決めていたが、下記の式のようにその特徴マップに含まれるすべてのピクセルの勾配の大きさの平均を特徴マップの重要度としています。具体的には下記のような式になります。このようにしてGrad-CAMでは全結合層の構造に依存せずに各特徴マップの重要度を決定しています。

やってみよう

コード

以下はGrad-CAMのコードおよびその実験結果です。

import numpy as np
import tensorflow as tf
import cv2
import matplotlib.pyplot as plt

from tensorflow.keras.applications.resnet50 import ResNet50, preprocess_input, decode_predictions

# 1. ResNet50をロード (ImageNet学習済み)
base_model = ResNet50(weights="imagenet")
last_conv_layer_name = "conv5_block3_out"
last_conv_layer = base_model.get_layer(last_conv_layer_name)

# 2. 勾配を取得するためのサブモデル (最終畳み込み層 + 全体の出力)
grad_model = tf.keras.models.Model(
    inputs=[base_model.inputs],
    outputs=[last_conv_layer.output, base_model.output]
)

def preprocess_image(image_path, target_size=(224,224)):
   
    img = cv2.imread(image_path)              # (H,W,3) BGR
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)# RGB
    img = cv2.resize(img, target_size)        # 224x224にリサイズ
    img = img.astype(np.float32)[None, ...]   # バッチ次元 (1,224,224,3)
    img = preprocess_input(img)               # ResNet50前処理
    return img

def grad_cam(grad_model, image_path, class_index=None):

    # 1. 入力画像の前処理
    preprocessed_img = preprocess_image(image_path)

    # 2. tf.GradientTape で勾配を計算
    with tf.GradientTape() as tape:
        conv_outputs, predictions = grad_model(preprocessed_img)
        if class_index is None:
            class_index = tf.argmax(predictions[0])
        target_class_score = predictions[:, class_index]

    grads = tape.gradient(target_class_score, conv_outputs)

    # conv_outputs, grads: shape=(1,H,W,C)
    conv_outputs = conv_outputs[0].numpy()  # (H,W,C)
    grads = grads[0].numpy()               # (H,W,C)

    # 3. チャネル方向に平均 -> alpha_k
    pooled_grads = np.mean(grads, axis=(0,1))  # (C,)

    # 4. conv_outputs × alpha_k → チャネル方向に合計
    grad_cam_map = np.sum(conv_outputs * pooled_grads, axis=-1)
    grad_cam_map = np.maximum(grad_cam_map, 0)

    # 5. 正規化 (0~1)
    eps = 1e-8
    grad_cam_map -= grad_cam_map.min()
    grad_cam_map /= (grad_cam_map.max() + eps)

    return grad_cam_map

def overlay_gradcam(image_path, grad_cam_map, alpha=0.4):
    raw_img = cv2.imread(image_path)           # BGR
    raw_img = cv2.cvtColor(raw_img, cv2.COLOR_BGR2RGB) # → RGB
    H, W, _ = raw_img.shape

    # (7,7) -> (H,W)
    heatmap = cv2.resize(grad_cam_map, (W, H))
    heatmap = (heatmap * 255).astype(np.uint8)

    # BGR形式のカラーマップ
    heatmap_color = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
    # BGR -> RGB
    heatmap_color = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2RGB)

    # α合成: raw_img(RGB) & heatmap_color(RGB)
    overlay_img = cv2.addWeighted(raw_img, 1 - alpha, heatmap_color, alpha, 0)
    return raw_img, overlay_img

if __name__ == "__main__":
    # 画像パスを合わせてください
    image_path = ""

    # (A) 予測を確認
    input_img = preprocess_image(image_path)
    preds = base_model.predict(input_img)
    top_pred_index = np.argmax(preds[0])
    decoded = decode_predictions(preds, top=5)
    print("Top-5 Predictions:")
    for (imagenet_id, label_name, score) in decoded[0]:
        print(f"{imagenet_id} : {label_name} (score={score:.4f})")

    # (B) Grad-CAM 計算
    grad_cam_map = grad_cam(grad_model, image_path, class_index=top_pred_index)

    # (C) オーバーレイ可視化
    raw_img, cam_img = overlay_gradcam(image_path, grad_cam_map, alpha=0.5)

    # (D) 結果表示
    plt.figure(figsize=(10,5))
    plt.subplot(1,2,1)
    plt.imshow(raw_img)
    plt.title("Original Image")
    plt.axis("off")

    plt.subplot(1,2,2)
    plt.imshow(cam_img)
    plt.title("Grad-CAM")
    plt.axis("off")
    plt.show()

実験結果

Grad-CAMの結果を見ると範囲が広くやや曖昧性が強いと感じました。これは勾配の大きさが重要度と直結しているとは限らないために起こっていると考えられます。その問題に関してのちにCNNの構造に依存せず、より直接的にモデルの寄与度を表す手法が登場します。

まとめ

本記事では、CNNモデルが画像をどのように判断しているかを視覚的に説明する手法「Grad-CAM」について解説しました。Gtad-CAMはCNNのアーキテクチャに依存しない手法として非常に重宝されてきた手法ですので是非覚えておいてください。

\ シェア /

E資格スピードパッケージ2023#2修了者合格率100%達成

zero to one E資格 jdla

zero to oneの「E資格」向け認定プログラム

日本ディープラーニング協会の実施するE資格の受験ならzero to oneの「E資格」向け認定プログラム (税込165,000円) をおすすめします。当講座は、東京大学大学院工学系研究科の松尾豊教授と東北大学大学院情報科学研究科の岡谷貴之教授が監修する実践的なプログラムとなっています。
厚生労働省の教育訓練給付制度対象のE資格認定プログラムの中では最安値※となり、実質負担額49,500円~(支給割合70%の場合)で受講可能です。※2023年弊社調べ zero to one E資格 jdla

人工知能基礎講座を提供中

人工知能の第一人者である東京大学の松尾豊教授が監修した人工知能基礎講座を受講してみませんか? 人工知能の歴史から自然言語処理、機械学習、深層学習といった最先端のトピックやAIに関わる法律問題まで網羅しているので全てのビジネスパーソン・AIの初学者におすすめです。

サンプル動画

人工知能基礎講座はこちら↓ zero to one G検定 人工知能基礎 jdla

AI初学者・ビジネスパーソン向けのG検定対策講座

G検定受験前にトレーニングしたい方向けの問題集「G検定実践トレーニング」も提供中です。 zero to one E資格 jdla