ディープラーニングの畳み込みニューラルネットワーク(CNN)のヒートマップ(CAM)を可視化してみました。可視化に使用したのは、5種類の花の分類に使用したVGG16を転移学習したモデルです。VGG16は、ディープラーニングによる画像応用の代表的なモデルの一つです。ヒートマップにより、テスト画像のどの部分が分類の決め手になったのかが分かります。

 

VGG16(転移学習)モデルのヒートマップを表示してみる。

In [1]:
import os

import keras
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential, Model
from keras.layers import Input, Dense, Dropout, Activation, Flatten
from keras import optimizers
from keras.applications.vgg16 import VGG16
Using TensorFlow backend.
In [2]:
keras.__version__
Out[2]:
'2.2.4'

訓練画像、検証画像、テスト画像のディレクトリ

In [3]:
# 分類クラス
classes = ['daisy', 'dandelion','rose','sunflower','tulip']
nb_classes = len(classes)
batch_size_for_data_generator = 20

base_dir = "."

test_dir = os.path.join(base_dir, 'test_images')

test_daisy_dir = os.path.join(test_dir, 'daisy')
test_dandelion_dir = os.path.join(test_dir, 'dandelion')
test_rose_dir = os.path.join(test_dir, 'rose')
test_sunflower_dir = os.path.join(test_dir, 'sunflower')
test_tulip_dir = os.path.join(test_dir, 'tulip')

# 画像サイズ
img_rows, img_cols = 200, 200

VGG16モデル

In [4]:
input_tensor = Input(shape=(img_rows, img_cols, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)
#vgg16.summary()

VGG16モデルに全結合分類器を追加する

In [5]:
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(nb_classes, activation='softmax'))
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))
model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 200, 200, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 200, 200, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 200, 200, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 100, 100, 64)      0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 100, 100, 128)     73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 100, 100, 128)     147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 50, 50, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 50, 50, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 50, 50, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 50, 50, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 25, 25, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 25, 25, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 25, 25, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 25, 25, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 12, 12, 512)       0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 12, 12, 512)       2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 12, 12, 512)       2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 12, 12, 512)       2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 6, 6, 512)         0         
_________________________________________________________________
sequential_1 (Sequential)    (None, 5)                 4720133   
=================================================================
Total params: 19,434,821
Trainable params: 19,434,821
Non-trainable params: 0
_________________________________________________________________
In [6]:
model.compile(loss='categorical_crossentropy',optimizer=optimizers.RMSprop(lr=1e-5), metrics=['acc'])

学習結果を読み出す

In [7]:
hdf5_file = os.path.join(base_dir, 'flower-model.hdf5')
model.load_weights(hdf5_file)
In [8]:
import matplotlib.pyplot as plt
In [9]:
%matplotlib inline

実際にテスト画像を分離してみる

In [10]:
import numpy as np
from keras.preprocessing.image import load_img, img_to_array
from keras.applications.vgg16 import preprocess_input
In [11]:
filename = os.path.join(test_dir, 'sunflower')
filename = os.path.join(filename, '3681233294_4f06cd8903.jpg')
filename
Out[11]:
'.\\test_images\\sunflower\\3681233294_4f06cd8903.jpg'
In [12]:
from PIL import Image
In [13]:
img = np.array( Image.open(filename))
plt.imshow( img )
Out[13]:
<matplotlib.image.AxesImage at 0x18f2d9fd748>
テストデータの画像

テストデータ

In [14]:
img = load_img(filename, target_size=(img_rows, img_cols))
x = img_to_array(img)
x = np.expand_dims(x, axis=0)

predict = model.predict(preprocess_input(x))
for pre in predict:
    y = pre.argmax()
    print("test result=",classes[y], pre)
test result= sunflower [0.01896724 0.         0.         0.9810327  0.        ]

ヒートマップを出力する予測を選択する

In [15]:
from keras import backend as K
In [16]:
img = load_img(filename, target_size=(img_rows, img_cols))
x = img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
In [17]:
np.argmax(predict[0])
Out[17]:
3
In [18]:
model_output = model.output[:,3]
In [19]:
last_conv_layer = model.get_layer('block5_conv3')
In [20]:
grads = K.gradients(model_output, last_conv_layer.output)[0]
In [21]:
pooled_grads = K.mean(grads, axis =(0,1,2))
In [22]:
iterate = K.function([model.input], [pooled_grads, last_conv_layer.output[0]])
In [23]:
pooled_grads_value, conv_layer_output_value = iterate([x])
In [24]:
for i in range(512):
        conv_layer_output_value[:,:,i] *= pooled_grads_value[i]
In [25]:
heatmap = np.mean(conv_layer_output_value, axis = -1)
In [26]:
heatmap = np.maximum(heatmap, 0)
heatmap /= np.max(heatmap)
In [27]:
plt.matshow(heatmap)
Out[27]:
<matplotlib.image.AxesImage at 0x18f2db470f0>
テストデータのひまわりクラスの活性化のヒートマップの画像

テストデータのひまわりクラスの活性化のヒートマップ

ヒートマップを入力画像にスーパーインポーズする

In [28]:
import cv2
In [29]:
img = cv2.imread(filename)
In [30]:
heatmap = cv2.resize(heatmap,(img.shape[1],img.shape[0]))
heatmap = np.uint8(255 * heatmap)
In [31]:
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
superimposed_img = heatmap * 0.4 + img
In [32]:
cv2.imwrite('flower_cam.jpg', superimposed_img)
Out[32]:
True
In [33]:
img = np.array( Image.open('flower_cam.jpg'))
plt.imshow( img )
Out[33]:
<matplotlib.image.AxesImage at 0x18f41e504a8>
ひまわりクラスの活性化のヒートマップをスーパーインポーズの画像

ひまわりクラスの活性化のヒートマップをスーパーインポーズ

 


<参考>
Francois Chollet 著、株式会社クイープ 翻訳、巣籠悠輔 監訳、「PythonとKerasによるディープラーニング」、マイナビ出版