AIアルゴリズムは、ブラックボックスであるため、なぜその答えを導き出したのかがわからず、問題になることがあります。
画像認識AIがなぜ、その答えを導き出したのかとい説明に役立つGrad-CAMというアルゴリズムが提案されており、様々な分野で活用されています。
Grad-CAMのサンプルコードを初心者の方向けにわかりやすく解説します。
はじめに
今回は、tensorflow kerasのサンプルコードです。参考にしたのは、こちらになります。説明がい少ないので、本ページでは詳しく書いておきます。
非常にシンプルなコードですので、ぜひ身に着けてください。colaboのリンクもあるので便利ですよ。
サンプルコード解説
Set up
import numpy as np
import tensorflow as tf
from tensorflow import keras
# Display
from IPython.display import Image, display
import matplotlib.pyplot as plt
import matplotlib.cm as cm
各種ライブラリのインポートです
- 1~3行目:numpyとtensorflowとkerasをインポート
- 6行目:画像表示関係のライブラリをインポート
- 7行目:matplotのpyplot グラフや画像描画ライブラリをインポート
- 8行目:matplotのカラーマップをインポート
Configurable parameters(パラメータ設定)
model_builder = keras.applications.xception.Xception
img_size = (299, 299)
preprocess_input = keras.applications.xception.preprocess_input
decode_predictions = keras.applications.xception.decode_predictions
last_conv_layer_name = "block14_sepconv2_act"
# The local path to our target image
img_path = keras.utils.get_file(
"african_elephant.jpg", "https://i.imgur.com/Bvro0YD.png"
)
display(Image(img_path))
1行目:学習済みモデルXceptionを使います
2行目:モデルに入力する画像サイズは、幅299、高さ299pixel
3行目:画像の前処理の定義です
4行目:画像の出力層の定義です
6行目:最後の畳み込み層の名前です。この層を取り出します。
8~11行目:試してみる画像です。アフリカゾウですね
13行目:アフリカゾウの画像をみてみます。
The Grad-CAM algorithm
def get_img_array(img_path, size):
# `img` is a PIL image of size 299x299
img = keras.utils.load_img(img_path, target_size=size)
# `array` is a float32 Numpy array of shape (299, 299, 3)
array = keras.utils.img_to_array(img)
# We add a dimension to transform our array into a "batch"
# of size (1, 299, 299, 3)
array = np.expand_dims(array, axis=0)
return array
def make_gradcam_heatmap(img_array, model, last_conv_layer_name, pred_index=None):
# First, we create a model that maps the input image to the activations
# of the last conv layer as well as the output predictions
grad_model = tf.keras.models.Model(
[model.inputs], [model.get_layer(last_conv_layer_name).output, model.output]
)
# Then, we compute the gradient of the top predicted class for our input image
# with respect to the activations of the last conv layer
with tf.GradientTape() as tape:
last_conv_layer_output, preds = grad_model(img_array)
if pred_index is None:
pred_index = tf.argmax(preds[0])
class_channel = preds[:, pred_index]
# This is the gradient of the output neuron (top predicted or chosen)
# with regard to the output feature map of the last conv layer
grads = tape.gradient(class_channel, last_conv_layer_output)
# This is a vector where each entry is the mean intensity of the gradient
# over a specific feature map channel
pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
# We multiply each channel in the feature map array
# by "how important this channel is" with regard to the top predicted class
# then sum all the channels to obtain the heatmap class activation
last_conv_layer_output = last_conv_layer_output[0]
heatmap = last_conv_layer_output @ pooled_grads[..., tf.newaxis]
heatmap = tf.squeeze(heatmap)
# For visualization purpose, we will also normalize the heatmap between 0 & 1
heatmap = tf.maximum(heatmap, 0) / tf.math.reduce_max(heatmap)
return heatmap.numpy()
Grad-CAMのアルゴリズム部分です
- 1行目:画像を取得する関数の定義です
- 3行目:画像データを読み込みます
- 5行目:画像データをnumpy形式に変換します。こっちの方が計算しやすいので
- 8行目:バッチ処理するため、軸を1個追加します。データの個数の次元ですね。axis=0なので、先頭に追加です。
- 9行目:numpyのarrayで返します
- 12行目:本題のGrad-CAMでヒートマップを作成する関数です。
引数は、img_array:画像データをnumpy形式に変換したもの、model:画像認識モデル、last_conv_layer_name:最後の畳み込み層の名前、pred_index:??? - 15~17行目:Grad-CAMのモデル定義。model.inputs画像認識モデルの入力層が入り口で、出力の一つ目が、model.get_layer(last_conv_layer_name).output、画像認識モデルの最後の畳み込み層。出力の二つ目が、model.output、画像認識モデルのフツーの出力層。
ここからは、画像認識の予測ラベルでtopとなったときの、モデルの勾配を計算します
- 21行目:tf.GradientTapeは勾配の自動計算です。openで読み込みます
- 22行目:モデルに画像を入れて、画像認識の予測結果と、最後の畳み込み層を出力します
- 23~24行目:pred_indexを指定しないなら。画像認識の予測結果とします。
- 25行目:class_channelという変数に、画像認識の予測ラベルとpred_indexを保存しておきます
- 29行目:class_channelの時の、最後の畳み込み層までの勾配を計算します。
- 33行目:勾配データの平均を計算します。平均する方向は、0,1,2です。
- 38行目:最後の畳み込み層の出力を取り出します
- 39行目:最後の畳み込み層に勾配計算を実行します。tf.newaxisは、次元を1個増やしてつじつま合わせです。
- 40行目:次元を1個削ってもどします。
- 43行目:標準化しておきます。最大値で割っているだけです。
- 44行目:heatmapをnumpy形式で返します
Let’s test-drive it (実際にやってみよう!)
# Prepare image
img_array = preprocess_input(get_img_array(img_path, size=img_size))
# Make model
model = model_builder(weights="imagenet")
# Remove last layer's softmax
model.layers[-1].activation = None
# Print what the top predicted class is
preds = model.predict(img_array)
print("Predicted:", decode_predictions(preds, top=1)[0])
# Generate class activation heatmap
heatmap = make_gradcam_heatmap(img_array, model, last_conv_layer_name)
# Display heatmap
plt.matshow(heatmap)
plt.show()
実際に画像を入力してためしてみます。
- 2行目:入力する画像を用意します。指定したサイズにリサイズされます。
- 5行目:モデルオブジェクトを作成。重みはimagenetです。
- 8行目:モデルの最後のsoftmax層を削ります
- 11行目:モデルに画像を入力して、出力させます。soft_maxの前までですよね
- 12行目:decode_predictionsで、top1の結果を出して、表示します。確認用です。
- 15行目:heat_mapを作成します。
- 18行目:heatmapを描画します
- 19行目:描画を表示します。見えるようにします。
結果はこんな感じです。10×10データの数値ですよ。
Create a superimposed visualization(スーパーインポーズ画像を作ってみよう)
def save_and_display_gradcam(img_path, heatmap, cam_path="cam.jpg", alpha=0.4):
# Load the original image
img = keras.utils.load_img(img_path)
img = keras.utils.img_to_array(img)
# Rescale heatmap to a range 0-255
heatmap = np.uint8(255 * heatmap)
# Use jet colormap to colorize heatmap
jet = cm.get_cmap("jet")
# Use RGB values of the colormap
jet_colors = jet(np.arange(256))[:, :3]
jet_heatmap = jet_colors[heatmap]
# Create an image with RGB colorized heatmap
jet_heatmap = keras.utils.array_to_img(jet_heatmap)
jet_heatmap = jet_heatmap.resize((img.shape[1], img.shape[0]))
jet_heatmap = keras.utils.img_to_array(jet_heatmap)
# Superimpose the heatmap on original image
superimposed_img = jet_heatmap * alpha + img
superimposed_img = keras.utils.array_to_img(superimposed_img)
# Save the superimposed image
superimposed_img.save(cam_path)
# Display Grad CAM
display(Image(cam_path))
save_and_display_gradcam(img_path, heatmap)
heatmapだけだとわかりずらいので、元の画像データに重ねて、スーパーインポーズ画像を作成します。画像処理の技としてもよく使いますので、覚えておきましょう。
- 1行目:関数ですよ。alpahはスケスケ具合です
- 3行目:画像データを読み込んで
- 4行目:計算しやすいようにnumpy形式に変換
- 7行目:heatmapが0~1の数値なので、画像データのように0~255の8bit整数データに変換
- 10行目:カラーマップを設定します。安定のjetです。
- 13行目:jetのカラーマップを作って
- 14行目:heatmapをカラーマップ変換します
- 17行目:heatmapを画像データに変換します
- 18行目:画像データをリシェイプします。(数値のマトリックスデータは、行→列の順ですが、画像データは幅→高さの順なので、反転させる必要があるのです。。。)
- 19行目:再び、numpy 形式に変換。
- 22行目:ヒートマップ画像に、元画像×0.4にしたものを足します。うっすら、元画像を重ねます
- 23行目:スーパーインポーズデータをnumpy形式から画像データへ変換
- 26行目:保存
- 29行目:表示
- 32行目:関数を呼び出して実行します!
Let’s try another image(他の画像で試してみよう)
img_path = keras.utils.get_file(
"cat_and_dog.jpg",
"https://storage.googleapis.com/petbacker/images/blog/2017/dog-and-cat-cover.jpg",
)
display(Image(img_path))
# Prepare image
img_array = preprocess_input(get_img_array(img_path, size=img_size))
# Print what the two top predicted classes are
preds = model.predict(img_array)
print("Predicted:", decode_predictions(preds, top=2)[0])
heatmap = make_gradcam_heatmap(img_array, model, last_conv_layer_name, pred_index=260)
save_and_display_gradcam(img_path, heatmap)
heatmap = make_gradcam_heatmap(img_array, model, last_conv_layer_name, pred_index=285)
save_and_display_gradcam(img_path, heatmap)
別画像でためしています。。。こちらは試してみてください。
まとめ
画像認識の説明用アルゴリズムGrad-CAMのサンプルコードを説明しました。
シンプルなコードですのでぜひ身に着けてください。
スーパーインポーズ画像の作成は画像処理としても便利な技ですので、こちらも覚えるとスキルUPになりますよ。