このチュートリアルを一通り行うことで、
というレベルから
自分でモデル作れます!転移学習、ファインチューニングできます!
モデルの中身を可視化できます!
という状態になってもらえたらと思っています。
- はじめに
- CNNの中間層の活性化部分を可視化
- ライブラリのインポート
- モデルの読み込み
- 活性化させる画像の読み込み
- 入力テンソルと出力テンソルのリストに基づいてモデルをインスタンス化
- モデルを予測モードで実行
- 第一層の最後のチャネルを可視化
- 全ての中間層の活性化マップごとに全てのチャネルを抽出してプロットする
- CNNのフィルタ(カーネル)の可視化
- フィルタを可視化するための損失テンソルの定義
- 入力に関する損失関数の勾配を取得
- 勾配の正規化
- 入力画像に基づいて損失テンソルと勾配テンソルの値を計算するkerasのバックエンド関数を自作で定義する。
- 確率的勾配降下法を用いて損失値を最大化
- テンソルを有効な画像に変換する関数
- フィルタを可視化するための関数
- 各層の最初から64個のフィルタを調べる
- 画像におけるクラス活性化のヒートマップの可視化
- 学習済みモデルの重みを読み込む
- この画像において最もBMW-x3ぽく見える場所を可視化するためにGrad-CAMプロセスを設定する
- openCVを用いてヒートマップを元の画像にスーパーインポーズ
- 完成した画像を表示
- 最後に
はじめに
本チュートリアルは、具体的な理解よりも、直感的な理解をして頂くことを目的としています。そのため、厳密性よりも、わかりやすさを優先した表現をすることをご了承願います。
本チュートリアルの対象者
- if, for, 関数, 変数など基本的なプログラミングに関する知識は記憶の彼方にわずかにあるが、pythonおよびディープラーニングに関する知識は全く無い。
- だけれども、ディープラーニングに興味がある!
- 自力でモデルを作って評価して、またモデルを作ってという一連の流れを身につけたい!!
みたいな方にオススメです。
参考図書
チュートリアルを作成するに当たって参考にさせて頂いた本です。
この本は、個人的にかなりおすすめの本です。
理論的な部分と、実践的な部分が半々です。
しかも、コンピュータビジョン、自然言語、画像生成と幅広く対応しているので、いろんな分野を触って勉強してみたい!という方に向いていると思います。
本チュートリアルで扱う問題
「車の画像から、その画像に写っている車種を当てる」という問題を解くためにモデルを作成します。
本チュートリアルの構成
- 自作モデルで車種当て問題をといてみよう!
- 転移学習で車種当て問題をといてみよう!
- ファインチューニングで車種当て問題をといてみよう!
- 完成したモデルをテストデータで評価してみよう!
- 精度の高いモデルと低いモデルの違いを覗いてみよう!
本記事は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)



入力テンソルと出力テンソルのリストに基づいてモデルをインスタンス化
# 出力側の8つの層から出力を抽出
out_num = 8
layer_outputs = [layer.output for layer in model.layers[:out_num]]
# 特定の入力をもとに、これらの出力を返すモデルを作成
activation_model = models.Model(inputs=model.input, outputs=layer_outputs)
モデルを予測モードで実行
# 8つのNumpy配列(層の活性化ごとに1つ)のリストを返す
activations = activation_model.predict(img_tensor)
print(len(activations)) #=8
第一層の最後のチャネルを可視化
first_layer_activation = activations[0]
print(first_layer_activation.shape)
plt.matshow(first_layer_activation[0,:,:,-1], cmap="viridis")
plt.show()
plt.savefig('results_pictures/midle_activetion.png')



縁の部分が黄色や赤になって反応していることがわかります。
なので、このチャネルでは対角検出器として機能していると考えられます。
これと同じことを、全ての層の第一層で行ってみます!
全ての中間層の活性化マップごとに全てのチャネルを抽出してプロットする
# プロットの一部として使用する層の名前
layer_names = []
for layer in model.layers[:out_num]:
layer_names.append(layer.name)
images_per_row = out_num
# 特徴マップを表示
for layer_name, layer_activation in zip(layer_names, activations):
# 特徴マップに含まれている特徴量の数
n_features = layer_activation.shape[-1]
print("n_features :",n_features,layer_activation.shape)
# 特徴マップの形状(1, size, size, n_features)
size = layer_activation.shape[ 1]
# この行列で活性化チャネルのタイルを表示
n_cols = n_features // images_per_row
display_grid = np.zeros((size*n_cols, images_per_row*size))
# 各フィルタを1つの大きな水平グリッドでタイル表示
for col in range(n_cols):
for row in range(images_per_row):
channel_image = layer_activation[0,:,:,col*images_per_row + row]
# 特徴量の見た目を良くするための後処理
channel_image -= channel_image.mean()
channel_image /= channel_image.std()
channel_image *= 64
channel_image += 128
channel_image = np.clip(channel_image, 0, 255).astype('uint8')
display_grid[col*size:(col+1)*size, row*size:((row+1)*size) ] = channel_image
# グリッドを表示
scale = 1./size
plt.figure(figsize=(scale*display_grid.shape[ 1], scale*display_grid.shape[ 0])) # html/cssの関係でインデックスの手前に空白入っています
plt.title(layer_name)
plt.grid(False)
plt.imshow(display_grid, aspect='auto', cmap='viridis')
plt.savefig('results_pictures/midle_activetions.png')
plt.show()
表示しているのは第8層目の活性化です。



下は第14層目の活性化です



分かることをまとめていきます。
- 最初の層は様々なエッジ検出のコレクションの役割を果たす。この段階では、元の画像に存在している情報はほぼ全てが活性化に含まれている。
- 層から層に進むにつれて、活性化は徐々に抽象化されていくので、視覚的な解釈可能性は低下している。つまりAuid-a3らしさや、BMW-x3らしさなどをエンコードするようになる。
- 活性化の疎性は、層が深くなるほど高くなる。つまり、最初の層では全ての画像が入力画像によって活性化されるが、その後の層では空のフィルタが増えていく。フィルタが空であることはそのフィルタにエンコードされているパターンが入力画像から検出されないことを意味している。(0に近ければ近いほど黒っぽい画像になる)
- 背景のCARVANAの文字に対して活性化を示さなくなる。つまり、 層が深くなるにつれてより車という抽象的な概念をとらえるようになっていることを示している。
可視化をしてあげると、それまで気づかなかったことにたくさん気づけるのでいいですね!
特に、CARVANA(背景)に層の後半で反応しなくなる点はとても興味深いです。
CNNのフィルタ(カーネル)の可視化
目的: CNNの各フィルタが受け入れる視覚パターンや視覚概念がどのようなもので、どのような順序で解析しているのかを理解するため。
フィルタを可視化するための損失テンソルの定義
model = models.load_model('models/VGG16_mini_extended_fine2.h5')
layer_name = 'block3_conv1'
filter_index = 0
layer_output = model.get_layer(layer_name).output
loss = K.mean(layer_output[:,:,:,filter_index])
入力に関する損失関数の勾配を取得
# gradientsの呼び出しはテンソル(この場合はサイズ1)のリストを返すので、最初の要素のみを保持する
grads = K.gradients(loss, model.input)[0]
勾配の正規化
勾配テンソルをそのL2ノルム(テンソルの値の二乗の和の平方根)で割る
これにより、入力画像に対して実行される更新の大きさが常に同じ範囲に収まる
# 除算の前に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[ 1], 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などでは「エンブレムが大事だよ!」「色がたくさん塗ってある部分が大事だよ!」
のように人間がわざわざ選んで教えてあげていたことを、勝手に勉強しているんですよ!
技術の発展を感じます。
でも、中身はちゃんと数学でできているから面白いです。
だからこそ、勉強しがいがあります。
最後に
長かったチュートリアルも終了です!
このような機会を設けることができてとても幸せです!
おかげで、ディープラーニングだけでなく、フーリエ変換などと絡めた、ディジタル信号処理に関して詳しくなることができました。
チュートリアルでは技術的な面にフォーカスしましたが、実験を通じて、実験目的を明確に定義する大切さを痛感しました。
早く実験したい!という一心で、一番最初にその部分を深掘りすることが無かったのですが、やりこんでいくうちに何%まで精度を達成すればいいのか、逆にこれ以上精度を出す必要はないではないのかなど道に迷うことが何度かあったからです。
はやり、技術は目的に対して活かしてこそなので、そこの目的の部分が曖昧になってしまうと努力が無駄になってしまいます。
ですが、しっかりとした目的が定まっていれば、人間を上回る成果を出すことができるのが人工知能です。
それを適切に活用して、より生活が便利になるようなものをたくさん作って、活用していけたらと思います。
最後まで見て頂きありがとうございました!












リクエストやコメントなどをいただけると嬉しいです!
Twitterから更新報告をしております!
いいね・フォローしていただけると泣いて喜びます。(´;ω;`)
タイピングもままならない完全にプログラミング初心者から
たった二ヶ月で
応用も簡単にできる…!!
という状態になるまで、一気に成長させてくれたオススメのプログラミングスクールをご紹介します!


