python

keras初心者チュートリアル【No.5 モデルの内部を可視化】~CNNで車種判別モデルを作成~

このチュートリアルを一通り行うことで、

超初心者
超初心者
pythonってなに?ディープラーニングって美味しいの?

 

というレベルから

自分でモデル作れます!転移学習、ファインチューニングできます!

モデルの中身を可視化できます!

 

という状態になってもらえたらと思っています。

はじめに

本チュートリアルは、具体的な理解よりも、直感的な理解をして頂くことを目的としています。そのため、厳密性よりも、わかりやすさを優先した表現をすることをご了承願います。

本チュートリアルの対象者

友人
友人
  • if, for, 関数, 変数など基本的なプログラミングに関する知識は記憶の彼方にわずかにあるが、pythonおよびディープラーニングに関する知識は全く無い。
  • だけれども、ディープラーニングに興味がある!
  • 自力でモデルを作って評価して、またモデルを作ってという一連の流れを身につけたい!!

 

みたいな方にオススメです。

参考図書

チュートリアルを作成するに当たって参考にさせて頂いた本です。

この本は、個人的にかなりおすすめの本です。

理論的な部分と、実践的な部分が半々です。

しかも、コンピュータビジョン、自然言語、画像生成と幅広く対応しているので、いろんな分野を触って勉強してみたい!という方に向いていると思います。

本チュートリアルで扱う問題

「車の画像から、その画像に写っている車種を当てる」という問題を解くためにモデルを作成します。

本チュートリアルの構成

  1. 自作モデルで車種当て問題をといてみよう!
  2. 転移学習で車種当て問題をといてみよう!
  3. ファインチューニングで車種当て問題をといてみよう!
  4. 完成したモデルをテストデータで評価してみよう!
  5. 精度の高いモデルと低いモデルの違いを覗いてみよう!

 

本記事は5について解説をします。

全てのコードは僕のgithubに上がっているのでcloneをしていただければ実行することが可能です。

今回のコードはこちらです。

 

それでは、チュートリアルを始めます!

CNNの中間層の活性化部分を可視化

目的:CNNの一連の層によって入力がどのように変換されるのかを理解し、CNNの個々のフィルタの意味を把握するため

ライブラリのインポート

from keras import models
import matplotlib.pyplot as plt
from tqdm import tqdm 
from keras.applications.vgg16 import (
    VGG16, preprocess_input, decode_predictions)
from keras.preprocessing import image
from keras.layers.core import Lambda
from keras.models import Sequential
from tensorflow.python.framework import ops
import keras.backend as K
import tensorflow as tf
import numpy as np
import pandas as pd
import keras
import sys
import cv2
import os
input_size = 256
%matplotlib inline

モデルの読み込み

model = models.load_model('models/VGG16_mini_extended_fine2.h5')
print(len(model.layers))
model.summary()
22
Model: "model_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 256, 256, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 256, 256, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 256, 256, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 128, 128, 64)      0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 128, 128, 128)     73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 128, 128, 128)     147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 64, 64, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 64, 64, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 64, 64, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 64, 64, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 32, 32, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 32, 32, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 32, 32, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 32, 32, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 16, 16, 512)       0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 16, 16, 512)       2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 16, 16, 512)       2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 16, 16, 512)       2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 8, 8, 512)         0         
_________________________________________________________________
global_average_pooling2d_1 ( (None, 512)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 1024)              525312    
_________________________________________________________________
dense_2 (Dense)              (None, 20)                20500     
=================================================================
Total params: 15,260,500
Trainable params: 7,625,236
Non-trainable params: 7,635,264
_________________________________________________________________

 

活性化させる画像の読み込み

base_dir = "mini_pictures"
train_dir = os.path.join(base_dir,"train")
valid_dir = os.path.join(base_dir,"valid")
test_dir = os.path.join(base_dir,"test")

Audi_a3_dir = os.path.join(test_dir,"Audi-a3")
fnames = [os.path.join(Audi_a3_dir,fname) for fname in os.listdir(Audi_a3_dir)]

img_path = fnames
print(img_path)
img = image.load_img(img_path, target_size=(input_size,input_size))
img_tensor = image.img_to_array(img)
# モデルが読み込めるようにするため次元拡張する
img_tensor = np.expand_dims(img_tensor, axis=0)
img_tensor /= 255.
print(img_tensor.shape)
plt.imshow(img_tensor[0])
plt.show
mini_pictures\test\Audi-a3\Audi-a3_5ee6ce7e5db6_11.jpg
(1, 256, 256, 3)
# 除算の前に1e-5を足すことで0割を回避
grads /= (K.sqrt(K.mean(K.square(grads)))+1e-5)

入力画像に基づいて損失テンソルと勾配テンソルの値を計算するkerasのバックエンド関数を自作で定義する。

関数iterateは入力としてNumpyテンソル(サイズ1のテンソルのリスト)を受け取り、出力として2つのNumpyテンソル(損失値と勾配値)のリストを返す関数。

# 入力値をnumpy配列で受け取り、出力値をnumpy配列で返す関数の定義
iterate = K.function([model.input], [loss, grads])

import numpy as np
loss_value, grads_value = iterate([np.zeros((1,input_size,input_size,3))])

確率的勾配降下法を用いて損失値を最大化

# 最初はノイズが含まれたグレースケール画像を使用
input_img_data = np.random.random((1, input_size, input_size, 3)) * 20 + 128.

# 勾配上昇法を40ステップ実行
step = 1. #各勾配の更新の大きさ
for i in range(40):
    # 損失値と勾配値を計算
    loss_value, grads_value = iterate([input_img_data])
    # 損失値が最大になる方向に画像を調整
    input_img_data += grads_value * step

結果として得られる画像テンソルは、(1, input_size, input_size, 3)のfloat型なので、[0, 255]の範囲の整数に変換します。

テンソルを有効な画像に変換する関数

def deprocess_image(x):
    #テンソルを正規化:中心を0、標準偏差を0.1にする
    x -= x.mean()
    x /= (x.std() + 1e-5)
    x *= 0.1
    
    # [0, 1]でクリッピング
    x += 0.5
    x = np.clip(x, 0, 1)
    
    # RGB配列に変換
    x *= 255
    x = np.clip(x, 0, 255).astype('uint8')
    return x

フィルタを可視化するための関数

def generate_pattern(layer_name, filter_index, size=256):
    # ターゲット層のn番目のフィルタの活性化を最大化する損失関数を構築
    layer_output = model.get_layer(layer_name).output
    loss = K.mean(layer_output[:,:,:,filter_index])
    
    # この損失関数を使って入力画像の勾配を計算
    grads = K.gradients(loss, model.input)[0]
    
    # 勾配を正規化
    grads /= (K.sqrt(K.mean(K.square(grads)))+1e-5)
    
    # 入力画像に基づいて損失値と勾配値を返す関数
    iterate = K.function([model.input], [loss, grads])
    
    # 最初はノイズが含まれたグレースケール画像を用いる
    input_img_data = np.random.random((1,size,size,3)) * 20 + 128.
    
    # 勾配上昇法を40ステップ実行
    step = 1.
    for i in range(40):
        loss_value, grads_value = iterate([input_img_data])
        input_img_data += grads_value *step
    
    img = input_img_data[0]
    return deprocess_image(img)
plt.imshow(generate_pattern('block3_conv1', 0, 64))
plt.savefig('result_pictures/block_conv1_sample.png')
plt.show()

層block3_conv1の0番チャネルの出力

水玉模様のパターンに応答していることが分かる

各層の最初から64個のフィルタを調べる

また、ここでは、5つのたたみ込みブロックの最初の層(block1_conv1, block2_conv1, block3_conv1,block4_conv1,block5_conv1)を調べる

出力は64×64のフィルタパターンからなる8×8のグリッドにまとめる。
各フィルタパターンを黒で縁取りする。

layers = ["block1_conv1","block2_conv1","block3_conv1","block4_conv1","block5_conv1"]
for layer_name in tqdm(layers):
    size = 64
    margin = 5
    
    # 結果を格納する空(黒)の画像 
    results = np.zeros((8 * size + 7 * margin, 8 * size + 7 * margin, 3), np.uint8)
    
    for i in range(8): # resultsのグリッドの行を順番に処理
        for j in range(8): # resultsのグリッドの列を順番に処理
            
            # layer_nameのフィルタ i + (j * 8)のパターンを生成
            filter_img = generate_pattern(layer_name, i+(j*8),size=size)
            
            # resultsグリッドの短形(i,j)に結果を表示
            horizontal_start = i * size + i * margin
            horizontal_end = horizontal_start + size
            vertical_start = j * size + j * margin
            vertical_end = vertical_start + size
            results[horizontal_start:horizontal_end,vertical_start:vertical_end,:] = filter_img
            
    # resultsグリッドを表示
    plt.figure(figsize=(20,20))
    plt.imshow(results)
    plt.show()

上が第一層目のフィルタの中身です。

離散コサイン変換で言うところの「低周波成分に色がついたもの」に似ています。

下が第15層目です。

芸術的なフィルタをしています。

離散コサイン変換で言うところの「高周波成分に色がついたもの」に似ています。

ですが、こちらの方がより繊細な表現をしているように思えます。

それでは、まとめていきます。

フィルタの可視化から分かること
  • CNNの各層は、一連のフィルタを学習することで入力をフィルタの組み合わせとして表現できるようにする。
  • そのため、仕組みとしてはフーリエ変換によって信号がsin, cos関数に変換されることと同じことが起こっている。
  • 出力側に近ければ近いほど複雑なフィルタになる(高周波成分のようなもの)

層を重ねるほど、繊細は表現を獲得していることがわかります。

これが、自作モデルとの大きな違いだということが分かります!

画像におけるクラス活性化のヒートマップの可視化

目的: 画像のどの部分が特定のクラスに属していると見なされたのかを理解するため。これにより、画像内のオブジェクトを局所化できるようになる。

学習済みモデルの重みを読み込む

choice = 1
if choice == 1:
    my_model = models.load_model('models/VGG16_mini_extended_fine2.h5') #ファインチューニング
    save_path = "results_pictures/Fine"
    last_layer_name = 'block5_conv3'
elif choice == 2:
    my_model = models.load_model('models/VGG16_mini_3.h5')# 転移学習モデル
    save_path = "results_pictures/Trans"
    last_layer_name = 'block5_conv3'
else:
    my_model = models.load_model('models/4_dense_mini.h5')# 自作モデル
    save_path = "results_pictures/Original"
    last_layer_name = 'conv2d_3'

 

複数モデルで比較をしたいので、簡易的に定数を定義しています。

# 画像のパスを取得
df2 = pd.read_csv('incorrect/df2.csv')
img_path = df2["PATH"]
mkname = 'BMW-x3'
# データの前処理
img = image.load_img(img_path, target_size=(256, 256))

# xは形状が(256,256,3)のfloat32型のnumpy配列
x = image.img_to_array(img)/255.0

# この配列サイズが(1, 256, 256, 3)のバッチに変換するために次元を追加
x = np.expand_dims(x, axis=0)
preds = my_model.predict(x)
print(np.argmax(preds[0]))
6

画像に学習済みネットワークを適応し、予測ベクトルを人が読めるようにします。

ここからが、本番です!

この画像において最もBMW-x3ぽく見える場所を可視化するためにGrad-CAMプロセスを設定する

# 予測ベクトルのAudi-a3エントリ
output = my_model.output[:,np.argmax(preds[0])]
# simplest_cnn_model_mini_1の最後の層であるconv2d_15の出力特徴マップ
last_conv_layer = my_model.get_layer(last_layer_name)
# conv2d_15の出力特徴マップでの「BMW-x3」クラス勾配
grads = K.gradients(output,last_conv_layer.output)[0]

# 形状が(32, )のベクトル
# 各エントリは特定の特徴マップの平均強度
pooled_grads = K.mean(grads,axis=(0,1,2))
#Audi-a3の画像に基づいてpooled_gradsとconv2d_15の出力マップの値にアクセスするための関数
iterate = K.function([my_model.input], [pooled_grads, last_conv_layer.output[0]])

# 2つの値をNumpy配列で取得
pooled_grads_value, conv_layer_output_value = iterate([x])

# 「BMW-x3」クラスに関する「このチャネルの重要度」を特徴マップ配列の各チャネルに掛ける
for i in range(pooled_grads_value.shape[0]):
    conv_layer_output_value[:,:,i] *= pooled_grads_value[i]

# 最終的な特徴マップのチャネルごとの平均値がクラスの活性化のヒートマップ
heatmap = np.mean(conv_layer_output_value,axis=-1 )

# ヒートマップの後処理
print(heatmap.shape)
heatmap = np.maximum(heatmap, 0)
heatmap /= np.max(heatmap)
plt.matshow(heatmap)
(16, 16)

heatmapが完成しました!

このヒートマップを元の画像に重ねて行きます。

openCVを用いてヒートマップを元の画像にスーパーインポーズ

# cv2を使って画像読み込み
img = cv2.imread(img_path)
# 元画像と同じサイズになるようにヒートマップのサイズを変更
heatmap = cv2.resize(heatmap, (img.shape, img.shape[0]))
# ヒートマップをRGBに変換
heatmap = np.uint8(255 * heatmap)
# ヒートマップをもとの画像に適応
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
# 0.4はヒートマップの強度係数
superimporsed_img = heatmap * 0.4 + img
# 画像を保存
cv2.imwrite(save_path+mkname+'1.jpg',superimporsed_img)

完成した画像を表示

img = image.load_img(save_path+mkname+'1.jpg', target_size=(input_size,input_size))
img_tensor = image.img_to_array(img)
img_tensor /= 255.
plt.imshow(img_tensor)
plt.show

これを見てあげることで、どのモデルがどの点に対して反応しているのかが分かります。

特に興味深いのが、このベストモデルでは、律儀にエンブレムを見て判断していることです!

やっぱり、そこ大切だよねという部分に反応するようになっているのです。

これって、とても凄いことで、今までのSVMなどでは「エンブレムが大事だよ!」「色がたくさん塗ってある部分が大事だよ!」

のように人間がわざわざ選んで教えてあげていたことを、勝手に勉強しているんですよ!

技術の発展を感じます。

でも、中身はちゃんと数学でできているから面白いです。

だからこそ、勉強しがいがあります。

最後に

長かったチュートリアルも終了です!

このような機会を設けることができてとても幸せです!

おかげで、ディープラーニングだけでなく、フーリエ変換などと絡めた、ディジタル信号処理に関して詳しくなることができました。

 

チュートリアルでは技術的な面にフォーカスしましたが、実験を通じて、実験目的を明確に定義する大切さを痛感しました。

 

早く実験したい!という一心で、一番最初にその部分を深掘りすることが無かったのですが、やりこんでいくうちに何%まで精度を達成すればいいのか、逆にこれ以上精度を出す必要はないではないのかなど道に迷うことが何度かあったからです。

はやり、技術は目的に対して活かしてこそなので、そこの目的の部分が曖昧になってしまうと努力が無駄になってしまいます。

 

ですが、しっかりとした目的が定まっていれば、人間を上回る成果を出すことができるのが人工知能です。

それを適切に活用して、より生活が便利になるようなものをたくさん作って、活用していけたらと思います。

最後まで見て頂きありがとうございました!

keras初心者チュートリアル【No.1 自作モデルの作成】~CNNで車種判別モデルを作成~このチュートリアルを一通り行うことで、 というレベルから という状態になってもらえたらと思っ...
keras初心者チュートリアル【No.2 転移学習】~CNNで車種判別モデルを作成~このチュートリアルを一通り行うことで、 というレベルから という状態になってもらえたらと思っ...
keras初心者チュートリアル【No.3 ファインチューニング】~CNNで車種判別モデルを作成~このチュートリアルを一通り行うことで、 というレベルから という状態になってもらえたらと思っ...
keras初心者チュートリアル【No.4 モデルの評価と可視化】~CNNで車種判別モデルを作成~このチュートリアルを一通り行うことで、 というレベルから という状態になってもらえたらと思っ...

リクエストやコメントなどをいただけると嬉しいです!

Twitterから更新報告をしております!

いいね・フォローしていただけると泣いて喜びます。(´;ω;`)

オススメのプログラミングスクールをご紹介

タイピングもままならない完全にプログラミング初心者から

アホいぶきんぐ
アホいぶきんぐ
プログラミングってどこの国の言語なの~?

たった二ヶ月で

いぶきんぐ
いぶきんぐ
え!?人工知能めっちゃ簡単にできるじゃん!

応用も簡単にできる…!!

という状態になるまで、一気に成長させてくれたオススメのプログラミングスクールをご紹介します!

テックアカデミーのPython+AIコースを受講した僕が本音のレビュー・割引あり! というプログラミング完全初心者だった僕が Tech Academy(テックアカデミー)のPython×AIコース を二ヶ月間...

COMMENT

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です