学習済モデルの利用とgrad-camを使う


[前回](https://qiita.com/hayato0806/items/a567c87cb470879cea1a)の続きです。
やりっぱなしで月日が経ってしまいうろ覚えですが、後で見返せるように書き残しておきます。
コード自体は半年以上前のものなので、ライブラリの仕様等変わっていたらごめんなさい。

今回の目的は下記2点です。
* 学習済みモデルのファインチューニング
* Grad-CAMで可視化

## 1. ResNet50のファインチューニング
#### ResNetとは
　ResNetは2015年のImageNetコンペティションとCOCOセグメンテーションの最良モデルとして提案され、最大1000層以上の深いニューラルネットワークを構築することを可能としたモデルです。
　ResNetの考え方はそれまでのディープニューラルネットワークとは異なり、「ある層で求める最適な出力を学習するのではなく、層の入力を参照した残差関数を学習する」 ことで最適化を行っています。（詳しくは[こちら](https://deepage.net/deep_learning/2016/11/30/resnet.html)）
　今回使用するResNet50 は深さが50 層の畳み込みニューラルネットワークで、100 万枚を超えるイメージで学習させた事前学習済みのモデルです。
#### ファインチューニングとは
　ファインチューニングは **「一度解いた設問の解法を、別の設問のために微調整すること」** です。似た手法で転移学習がありますが、転移学習は学習済みのモデル部分の重みの再学習は行わないのに対しファインチューニングでは学習済みモデルについても一部重みの再学習を行うという違いがあります。

#### 実装

　今回学習済みのResNet50については最後の畳み込み層3層分のみ重みの更新を行います。重みの更新を行わない層については"layer.trainable = False"で訓練不可（学習済み重みの利用）とします。
~~~python
# import 
from tensorflow.keras.applications.resnet50 import ResNet50
from tensorflow.keras import optimizers
# resnet50読み込み(weights='imagenet'：ImageNetで学習済みの重みのデータが読み込まれる)
base_model = ResNet50(include_top = False, weights = 'imagenet', input_tensor=input_tensor)
# 学習済み重みを利用する場合は層のtrainableを"False"とする
for layer in base_model.layers[:165]:
    layer.trainable = False
# 何層目までFalseとしたか確認用
for layer in base_model.layers:
    print(layer, layer.trainable )
~~~
　次に学習済みモデルからの出力を受け取り、続きを学習する部分を定義します。学習済みモデルをファインチューニングして使用する場合学習率は小さめに設定するのが一般的です。重みを更新しない層が多く学習率が大きいと過学習に陥りやすくなるためです。
~~~python
model = Sequential()

model.add(base_model)
model.add(Flatten())
model.add(Dense(1024, activation = 'relu'))
model.add(Dense(256, activation = 'relu'))
model.add(Dense(128, activation = 'relu'))
model.add(Dropout(0.5))
model.add(Dense(8, activation = 'softmax'))

model.compile(optimizer = optimizers.SGD(learning_rate = 0.001),
              loss = 'categorical_crossentropy',
              metrics = ['accuracy'])
~~~

#### 実行と結果の確認
　学習時検証データの正答率は、、、またテストデータでの正答率は〇〇%でした。

~~~python
history = model.fit(x_train, y_train, batch_size = 32, 
                    epochs = 100, verbose = 1, validation_data = (x_val, y_val),
                    callbacks = [tf.keras.callbacks.EarlyStopping(monitor = 'val_loss', patience = 7)])

#acc, val_accのプロット
plt.plot(history.history["accuracy"], label = "accuracy", ls = "-", marker="o")
plt.plot(history.history["val_accuracy"], label = "val_accuracy", ls = "-", marker = "x")
plt.ylabel("accuracy")
plt.xlabel("epoch")
plt.legend(loc = "best")
plt.show()

#モデルを保存
model.save("my_model_resnet_all.h5")
model.save_weights('model_weight_resnet_all.hdf5')
~~~

　正答率は8割行かない程度でした。前回自作したものよりやや低かったですが諸々の調整を行えば自作した簡単なモデル以上の精度はでるはずです。今回はとりあえず使い方を知ることが目的なのでここまでとします。

## 2. Grad-CAMでの可視化
#### Grad-CAMとは
　ディープラーニングでは何を根拠に答えを出力しているかが不明なため、その根拠を説明できるAI（説明可能なAI：XAI）が近年のトレンドのよう。
Grad-CAMはCNN（畳み込みニューラルネットワーク）による画像の分類を可視化する手法で、入力画像に対して画像のどこが予測に最も影響を与えたかをヒートマップとして出力することができる。
一般的にはニューラルネットワークにおける最後の畳み込み層を出力して確認することが多いそうだ。

#### 実装
関数定義
~~~python
def img_predict(filename):
# 画像を読み込んで予測する関数

    img = cv2.imread(filename)
    x   = np.expand_dims(img, axis=0)
    # 表示
    cv2_imshow(img)
    pred = model.predict(x)[0]

def grad_cam(input_model, x, layer_name = target_layer):
# grad_cam実行関数

    """
    Args: 
        input_model(object): モデルオブジェクト
        x(ndarray): 画像
        layer_name(string): 畳み込み層の名前
    Returns:
        output_image(ndarray): 元の画像に色付けした画像
    """

    # 画像の前処理
    # 読み込む画像が1枚なため、次元を増やしておかないとmode.predictが出来ない
    X = np.expand_dims(x, axis = 0)
    preprocessed_input = X.astype('float32') / 255.0    

    grad_model = 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(preprocessed_input)
        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, IMAGE_SIZE, 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 = cv2.cvtColor(jet_cam, cv2.COLOR_BGR2RGB)
    # もとの画像に合成
    output_image = (np.float32(rgb_cam) + x / 2)  

    return output_image
~~~
画像サイズと見たいレイヤーの名前定義
~~~python
IMAGE_SIZE = (64,64)
# 層を指定して、そこで見ている箇所を可視化
target_layer = 'conv5_block3_3_conv'
~~~
画像と学習したモデルの読込
~~~python
image_path = './voice_picture/test/minaseinori_resize_64/resize_3.jpg'
model1=load_model('my_model_resnet_all.h5')
x = img_to_array(load_img(image_path, target_size=IMAGE_SIZE))
~~~
Grad-CAM実行
~~~python
target_layer = 'conv1_conv'
cam = grad_cam(model, x, target_layer)
array_to_img(cam)
~~~
　最後の層の結果は全体が赤くなっており（それまでの学習結果を踏まえ全体を見ているということ？）、あまり面白くないので他の層の結果をいくつか示します。

・最初の方の層'conv1_conv'
![スクリーンショット 2023-05-01 141005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2768851/e0679eed-7d88-77f4-59ad-ebc36c76235f.png)

・真ん中らへんの層'conv3_block3_3_conv'
![スクリーンショット 2023-05-01 141310.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2768851/8715cd5b-21c2-2b5c-b5f7-4479bc32f992.png)

・最後の方の層'conv5_block3_3_conv'
![スクリーンショット 2023-05-01 141425.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2768851/f22a07fc-1dbb-3e6e-61f0-114271c7c524.png)

層によって全く違うがとりあえずそれらしい結果が確認できた。層ごとに違う観点からみて最後にそれらを総合的に判断する感じかなぁ…。

### まとめ
　今回は学習済みのモデルの再学習の仕方と画像系における出力根拠の可視化を行い、ひとまず使い方を知ることができた。モデルによっては学習の内容が違ったりすると思うので、可視化して比較しても面白そうだと思った。

以上