[Tensorflow2系]Grad-CAMについて0から勉強をやり直す

# 目的
- Grad-CAMの処理内容を理解する
- Tensorflow 2系の書き方を把握する

# 使用する文献
[Visualizing Activation Heatmaps using TensorFlow \| by Areeb Gani \| Analytics Vidhya \| Medium](https://medium.com/analytics-vidhya/visualizing-activation-heatmaps-using-tensorflow-5bdba018f759)

# ソースコードの読解
### インポート
```
import tensorflow as tf
import tensorflow.keras.backend as K
from tensorflow.keras.applications.inception_v3 import InceptionV3
from tensorflow.keras.preprocessing import image
from tensorflow.keras.applications.inception_v3 import preprocess_input, decode_predictions
import numpy as np
import os
import matplotlib.pyplot as plt
import cv2
from google.colab.patches import cv2_imshow
```
---
### モデル
```
# imagenetの重みを初期値として用いる
model = InceptionV3(weights='imagenet')
model.summary()
```
Grad-CAMでは**モデルの最終畳み込み層**を用います。
InceptionV3では`conv2s_93`が**モデルの最終畳み込み層**になります。

## 学習準備
### 画像のダウンロード
```
!wget https://indiasendangered.com/wp-content/uploads/2011/09/elephant.jpg
!wget https://s3.amazonaws.com/cdn-origin-etr.akc.org/wp-content/uploads/2017/11/12234558/Chinook-On-White-03.jpg
!wget https://icatcare.org/app/uploads/2018/07/Thinking-of-getting-a-cat.png
```
学習用に像や猫などの画像をダウンロードします。
読解するコードでは像の画像のみ使用します。
* `!`は Google Colabでソースコード以外を実行するときのコマンドです

### 画像の表示
```
ORIGINAL = 'elephant.jpg'

DIM = 299
# 第一引数は画像パス名,第二引数は画像サイズ
img = image.load_img(ORIGINAL, target_size=(DIM, DIM))

cv2_imshow(cv2.imread(ORIGINAL)) # Visualize image
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472955/977ae0b5-728b-8e77-0bb7-4c3a33fc1664.png)

---

### 学習のための前処理
```
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)

preds = model.predict(x)
print(decode_predictions(preds))
```
現在の画像の状態では学習に使用することができないので**前処理**を行います。
前処理について具体的に記述します。

```
print(type(img))　# <class 'PIL.Image.Image'>
x = image.img_to_array(img) 
print(type(x)) # <class 'numpy.ndarray'>
print(x.shape) # (299, 299, 3)
```
print分の実行結果を`#`の後ろに記述しました。

最初に`img`のクラスタイプは`PIL.Image.Image`となっています。
処理をするために画像データを加工します。
最初に`image.img_to_array(img)`を用いてNumpy配列に変更します。

結果、`x`のタイプは`<class 'numpy.ndarray'>`となっています。
`x`の配列の形も`(299, 299, 3)`になっています。
これは(縦,横:299,299)の画像サイズ分の次元数であり、最後の`, 3）`はRGBの3つの次元数を持っているという意味です。

---

```
x = np.expand_dims(x, axis=0)　
print(x.shape)# (1, 299, 299, 3)
x = preprocess_input(x)
```
`np.expand_dims`を用いてaxisで指定したところ(今回は0次元)に1つ次元を追加します。
そうすることによって[1, height, width, channels]という形になります。

これはGrad-CAMでは1枚の画像しか用いませんが、通常ディープラーニングでは
たくさんの画像を使用します。そのため、100枚の画像を使用するとなると[100, height, width, channels]という風にデータの形が変換されるため、次元の形を合わせるために`np.expand_dims`を使用します。次元拡張後、`preprocess_input`に画像の情報を入れることによって前処理を行っています。

前処理としては以下のことを行います。
・モデルによって画像の正規化
・ImageNetデータセットのRGB各チャンネルごとの平均値を引く

また、無駄な訓練を行わないという点でも使うメリットはあるそうです。

---
```
preds = model.predict(x)
print(decode_predictions(preds)) 
#実行結果 [[('n02504013', 'Indian_elephant', 0.9624098), ('n01871265', 'tusker', 0.015191134)
```
model.predictで**モデルに対しての画像の推論を行います。**

## Grad-CAMの実装
ここから(やっと)Grad-CAMの実装に入っていきます。
TensorFlow1系で実装するときと2系で実装する大きな違いの一つとして以下の
`tf.GradientTape(以下:tf.GradientTape)`を使用するという点があります。
`tf.GradientTape`はグラデーション(勾配)を計算するのに使用します。
### Grad-CAMのソースコード
以下がGrad-CAMの一番大事の処理のところです。
一行ずつ見ていきましょう。

```
with tf.GradientTape() as tape:
  last_conv_layer = model.get_layer('conv2d_93')
  iterate = tf.keras.models.Model([model.inputs], [model.output, last_conv_layer.output])
  model_out, last_conv_layer = iterate(x)
  class_out = model_out[:, np.argmax(model_out[0])]
  grads = tape.gradient(class_out, last_conv_layer)
  pooled_grads = K.mean(grads, axis=(0, 1, 2))
  
heatmap = tf.reduce_mean(tf.multiply(pooled_grads, last_conv_layer), axis=-1)
```
### 一行ずつ読み解いていく

```
  last_conv_layer = model.get_layer('conv2d_93')
```
`model.get_layer`を使用して Inception V3の最終畳み込み層を取得します。

```
iterate = tf.keras.models.Model([model.inputs], [model.output, last_conv_layer.output])

model_out, last_conv_layer = iterate(x)
```
`tf.keras.models.Model`ではモデルの入力(第一引数)とモデルの出力(第二引数)を定義します。
今回はmodelの入力はそのままで、出力はmodelの**最終出力と最終畳み込み層**の二つの勾配を出力するモデルを定義しています。
これの出力が`model.out`のみである場合は`iterate`をしても一つしか出力はされません。

```
  class_out = model_out[:, np.argmax(model_out[0])]
```
モデルの最終出力の中で最も高い数値の確信度を`class_out`として出力します。
`model_out`自体は"shape=(1, 1000)"の二次元配列であるため`model_out[0]`として取得します。


```
  grads = tape.gradient(class_out, last_conv_layer)
  print(grads.shape)  #(1, 8, 8, 192)
```

`tape.gradient(class_out, last_conv_layer)`は"class_out(モデルの最終出力勾配)"から"last_conv_layer(最終畳み込み層)"までの勾配です。

```
  pooled_grads = K.mean(grads, axis=(0, 1, 2))
  print(pooled_grads.shape) # (192,)
```
`K.mean(grads, axis=(0, 1, 2))`は先ほどの"class_out(モデルの最終出力勾配)"からlast_conv_layer(最終畳み込み層)までの平均勾配です。

```
heatmap = tf.reduce_mean(tf.multiply(pooled_grads, last_conv_layer), axis=-1)
```
次に最終畳み込み層出力の値に平均勾配を掛けて、さらに平均化します。

---
## ヒートマップを出力する
```
heatmap = np.maximum(heatmap, 0)
heatmap /= np.max(heatmap)
heatmap = heatmap.reshape((8, 8))
plt.matshow(heatmap)
plt.show()
```
### 一行ずつ読み解いていく
```
heatmap = np.maximum(heatmap, 0)
```
ここではリスト内の値を"0~1"にして最小値を0にします。

```
heatmap /= np.max(heatmap)
```
最大値で割ることによってリスト内の最大値を1にします。

```
heatmap = heatmap.reshape((8, 8))
plt.matshow(heatmap)
plt.show()
```
あとは大きさを整形し、plotで表示します。
このとき表示されるものは以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472955/66765e25-c291-58aa-c55f-07ba4d920507.png)
このままでは何かさっぱりわからないので、元の像の画像と
合成することによってヒートマップを完成させます。
## 画像を合成する
```
img = cv2.imread(ORIGINAL)
INTENSITY = 0.5

heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))

heatmap = cv2.applyColorMap(np.uint8(255*heatmap), cv2.COLORMAP_JET)

img = heatmap * INTENSITY + img

cv2_imshow(cv2.imread(ORIGINAL))
cv2_imshow(img)
```
### 一行ずつ読み解いていく
```
img = cv2.imread(ORIGINAL)
INTENSITY = 0.5
```
もと画像を読み込みます。
また`INTENSITY`はヒートマップの強度係数というもので、
値が大きくなるとヒートマップで赤くなるところがより濃くなります。
(末尾に番外編として、強度係数を変更したときの画像を載せておきます)

```
heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
```
先ほど出力したヒートマップをを元画像に合成するためにサイズを元画像と一致させます。

```
heatmap = cv2.applyColorMap(np.uint8(255*heatmap), cv2.COLORMAP_JET)
```
np.uint8(255*heatmap)で`255`を乗算ことによりRGB形式に変化にします。
次にcv2.applyColorMapを用いて疑似カラーマップを生成します。

```
img = heatmap * INTENSITY + img
```
(先ほど記述した)ヒートマップの値、強度係数、もと画像をかけ合わせることにより
ヒートマップを完成させます。

```
cv2_imshow(cv2.imread(ORIGINAL))
cv2_imshow(img)
```
最後に元画像とヒートマップ画像を出力して完成です！

# 最後に
今まではTensorflow1系でプログラムを書いていたのですが、
2系になったことプログラムの記述の仕方が一部が変わりました。

また知識がほんわかとしているところが多々あったため、
勉強をやり直すことを目的にこの記事を作成しました。

わからないところは文献や記事を見ながら自分なりに解釈して記述しました。
しかし、間違っているところや誤解しているところがあると思います。
そのような所はご教授していただけますと幸いです。


# (番外編)強度係数を変更してみる
```
# もと画像
img = cv2.imread(ORIGINAL)
cv2_imshow(img)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472955/11dae179-bc93-537c-a6f3-c391dc4ad6ea.png)

```
INTENSITY = 0.1
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472955/3de982f3-3a47-0e60-a587-d4c5669a0806.png)

```
INTENSITY = 0.5
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472955/71619e85-8031-5825-5b79-0d62f125169f.png)

```
INTENSITY = 0.9
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472955/eac6115c-f81e-c2c6-a23e-7667b2fe7d8f.png)

---
# 参考文献
- [Visualizing Activation Heatmaps using TensorFlow \| by Areeb Gani \| Analytics Vidhya \| Medium](https://medium.com/analytics-vidhya/visualizing-activation-heatmaps-using-tensorflow-5bdba018f759)
- [日本一詳しくGrad\-CAMとGuided Grad\-CAMのソースコードを解説してみる\(Keras実装\) \- Qiita](https://qiita.com/kinziro/items/69f996065b4a658c42e8)
- [TensorFlow メソッド、shape をいじる系メモ \- Qiita](https://qiita.com/cfiken/items/04925d4da39e1a24114e)
- [Kerasで転移学習をする際にはpreprocess\_input\(\)を呼ぼう \| 10001 ideas](https://10001ideas.com/2018/10/24/keras%E3%81%A7%E8%BB%A2%E7%A7%BB%E5%AD%A6%E7%BF%92%E3%82%92%E3%81%99%E3%82%8B%E9%9A%9B%E3%81%AB%E3%81%AFpreprocess_input%E3%82%92%E5%91%BC%E3%81%BC%E3%81%86/)
- [バックエンド \- Keras Documentation](https://keras.io/ja/backend/) 
- [Grad\-CAMでヒートマップを表示 \- Qiita](https://qiita.com/yakisobamilk/items/8f094590e5f45a24b59c)
- [【Python】OpenCVで疑似カラー\(Pseudo color\) \- Qiita](https://qiita.com/Kazuhito/items/fc1cb321eaf4daab2276)