はじめに
本記事はNTTテクノクロス Advent Calendar 2023の20日目です。
みなさん、どうもこんにちは。NTTテクノクロスの下本です。
Deep Learningに興味がある方たちに向けて結果が可視化できるGrad-CAMを紹介したいという所から始まり、遂行するまでの奮闘記です。AIを用いた画像認識、物体検出の入り口のお話となっています。
自分自身が習熟者でない、かつAdvent Calendarで執筆させていただくのが初めての経験です。拙い部分が多いと思いますがご容赦ください。
対象読者
- Deep Learning、AIに興味あるけど難しそうと思っている方
- 兎にも角にも結果が出てくるコードを動かしてみたい方
- Deep Learningって中身どうなっているの?と疑問に思っている方
技術紹介
Grad-CAM
Grad-CAMとはGradient-weighted Class Activation Mappingの略称です。画像認識で広く使われている手法の一つであるCNN(Convolutional Neural Network)が画像のどこに注目してクラス分類を行っているかを可視化する技術です。
CNNで畳み込みを行った特徴量の出力を抽出して画像にします。詳細は 以下の記事 や 論文 1で説明されています。
左画像を入力例とします。犬というクラスに分類する際に、どこを重要視したかを確認できます。赤色、また赤色に近い色の部分に注目しており、青色に近づくにつれて注目していない部分になります。このような画像をヒートマップと言います。右画像は入力画像とヒートマップを重畳したものとなっています。
きちんと犬の部分を見ていることが分かります。本記事ではこのようなヒートマップを作成することがゴールです。
(画像は ここ から引用)
VGG16
VGG16はCNNのアルゴリズムの一つです。畳み込み層が13層、全結合層が3層、計16層から構成されているネットワークです。 論文 2はこちらです。Image Netという大規模データセットで学習しています。
VGGのアルゴリズムも詳細は省略します。気になる方は 以下の記事 が参考になります。
今回の実装ではVGG16を使用します。選んだ理由はずばり、Pythonで学習済みモデルが実装されているからです。これは非常に便利で、使いやすいです。必要モジュールをインポートするだけで、数行で呼び出すことができます。モデルの構造を出力してみます。
Model: "vgg16"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) [(None, 224, 224, 3)] 0
block1_conv1 (Conv2D) (None, 224, 224, 64) 1792
block1_conv2 (Conv2D) (None, 224, 224, 64) 36928
block1_pool (MaxPooling2D) (None, 112, 112, 64) 0
block2_conv1 (Conv2D) (None, 112, 112, 128) 73856
block2_conv2 (Conv2D) (None, 112, 112, 128) 147584
block2_pool (MaxPooling2D) (None, 56, 56, 128) 0
block3_conv1 (Conv2D) (None, 56, 56, 256) 295168
block3_conv2 (Conv2D) (None, 56, 56, 256) 590080
block3_conv3 (Conv2D) (None, 56, 56, 256) 590080
block3_pool (MaxPooling2D) (None, 28, 28, 256) 0
block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160
block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808
block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808
block4_pool (MaxPooling2D) (None, 14, 14, 512) 0
block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808
block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808
block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808
block5_pool (MaxPooling2D) (None, 7, 7, 512) 0
flatten (Flatten) (None, 25088) 0
fc1 (Dense) (None, 4096) 102764544
fc2 (Dense) (None, 4096) 16781312
predictions (Dense) (None, 1000) 4097000
=================================================================
Total params: 138357544 (527.79 MB)
Trainable params: 138357544 (527.79 MB)
Non-trainable params: 0 (0.00 Byte)
以下実装パートです。
Kerasで実装
Pythonのニューラルネットワークのライブラリでまず思いつくのがkerasです。tensorflowがバックで動作しています。公開されている記事やドキュメントも多いです。VGG16も実装されています。さっそく実装していきます。参考にした 記事 と コード は以下です。
GitHubのリンク/vense/keras-grad-cam
結論: うまくいかない。。。
しかも最初のモデルの生成が通らない。
from keras.applications.vgg16 import VGG16
from keras.preprocessing import image
from keras.layers.core import Lambda
from tensorflow.python.keras import backend as K
import keras
import numpy as np
import cv2
from PIL import Image
import tensorflow as tf
def create_model():
model = VGG16(weights='imagenet') #ここでエラーが出る
#model.summary()
return model
def target_category_loss_output_shape(input_shape):
return input_shape
def normalize(x):
# utility function to normalize a tensor by its L2 norm
return x / (K.sqrt(K.mean(K.square(x))) + 1e-5)
def grad_cam(input_model, image, category_index, layer_name):
nb_classes = 1000
target_layer = lambda x: target_category_loss(x, category_index, nb_classes)
x = input_model.layers[-1].output
x = Lambda(target_layer, output_shape=target_category_loss_output_shape)(x)
model = keras.models.Model(input_model.layers[0].input, x)
loss = K.sum(model.layers[-1].output)
#conv_output = [l for l in model.layers[0].layers if l.name is layer_name][0].output
conv_output = [l for l in model.layers if l.name is layer_name][0].output
grads = normalize(K.gradients(loss, conv_output)[0])
gradient_function = K.function([model.layers[0].input], [conv_output, grads])
output, grads_val = gradient_function([image])
output, grads_val = output[0, :], grads_val[0, :, :, :]
weights = np.mean(grads_val, axis = (0, 1))
cam = np.ones(output.shape[0 : 2], dtype = np.float32)
for i, w in enumerate(weights):
cam += w * output[:, :, i]
cam = cv2.resize(cam, (224, 224))
cam = np.maximum(cam, 0)
heatmap = cam / np.max(cam)
# Return to BGR [0..255] from the preprocessed image
image = image[0, :]
image -= np.min(image)
image = np.minimum(image, 255)
cam = cv2.applyColorMap(np.uint8(255*heatmap), cv2.COLORMAP_JET)
cam = np.float32(cam) + np.float32(image)
cam = 255 * cam / np.max(cam)
return np.uint8(cam), heatmap
エラーメッセージはこんな感じ
AttributeError: module 'tensorflow' has no attribute 'get_default_graph'
要するにtensorflowとのバージョンが悪くてエラーが発生している。この時点で使用していたバージョンはこの通り
- Python 3.8.10
- keras 2.0.9
- tensorflow 2.13.0
対策1:kerasのバージョンを上げてみる
2.15.0まで上げたが、違う部分に不整合が発生して断念。元のコードがkerasの2.0.9を推奨している。
対策2:tensorflowのバージョンを下げてみる
Python3.8だと下げてみたいtensorflowのバージョン(2.2系以下や1系)にそもそも対応していない。執筆時点(2023/12/20)でPythonの最新系は3.12なのでこれ以上Pythonのバージョンを下げた環境で作成する意味はないので断念。
じゃあなぜ3.8系で試しているのか。。
Python3.8系も2024年にはサポートが終了します。手元の環境がこの検証に適している、とは言えませんでしたね。
解決策ご存知の方がいれば、是非温かいご指導ご鞭撻のほどコメントしてもらえると嬉しいです。
Pytorchで実装
Pytorchも同様にPython内の機械学習ライブラリです。2016年頃にリリースされ、活発に開発されているライブラリです。PytorchでもVGG16は扱えるので挑戦してみます。
しかも調べていくとPytorchには pytorch-gradcam というモジュールが存在しました。これを活かして実装していきたいと思います。参考にした 記事 にて分かりやすく解説されていますので、確認してください。
結論: できた
import torch
from torchvision import transforms, datasets
import torchvision.transforms as transforms
import torchvision.models as models
from PIL import Image
from gradcam.utils import visualize_cam
from gradcam import GradCAM, GradCAMpp
#device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
def prepare_data(device, file_path):
"""
入力画像をtorch.Tensor型にする
"""
input_img = Image.open(file_path)
torch_img = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor()
])(input_img).to(device)
return torch_img
def create_model(device):
"""
モデルを指定して作成する
alexnet, vgg, resnet, densenet, squeezenetに対応
"""
model = models.vgg16(pretrained=True)
model = torch.nn.DataParallel(model).to(device)
return model
def create_heatmap(input_model, image):
"""
ヒートマップを作成する
ヒートマップ単体と入力画像にヒートマップを重ねた画像を返す
"""
# 推論状態にする
input_model.eval()
# モデルや構築によって抽出する層を変更する
target_layer = input_model.module.features
# Grad-CAM
gradcam = GradCAM(input_model, target_layer)
gradcam_pp = GradCAMpp(input_model, target_layer)
normed_img = transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])(image)[None]
# 推論結果確認
#output = input_model(normed_img)
#id = torch.argmax(output[0])
mask, _ = gradcam(normed_img)
heatmap, result = visualize_cam(mask, image)
mask_pp, _ = gradcam_pp(normed_img)
heatmap_pp, result_pp = visualize_cam(mask_pp, image)
return heatmap, heatmap_pp, result, result_pp
def save_img(image, file_path):
"""
画像を保存する
"""
img = transforms.ToPILImage()(image)
img.save(file_path)
pytorch-gradcamにはGrad-CAMの拡張系のGrad-CAM++も機能として実装されています。上記コードにおける、
mask_pp, _ = gradcam_pp(normed_img)
この関数で呼び出しています。今回はGrad-CAM++の結果をみていこうと思います。
動作したなら、さっそく動かしたい。
入力画像には家にあったみかんを使用することにします。できるだけオブジェクトと背景に差が出るように白色を基調とした床の上で撮影しています。画像はGrad-CAMに入力できるように224×224にリサイズしています。若干つぶれたように見えるのはそのせいです。
推論して、ヒートマップと重畳したものがこちらです。
所感ですが、
・はっきりとみかんを捉えている。
・オブジェクトが存在しない画面隅は見ていない。
・オブジェクト上部と接している、フローリングの隙間を少し注目している。
でしょうか。AIがどこに注目しているかが分かると、分析も進むので面白いですね。無事ゴールできて良かったです。
まとめ
Grad-CAMの実装をやってみました。皆さんも色々な画像で試してみてください。
総括としては「情報をアップデートしよう」というお話でした。流行り廃りが速い分野だけに常に情報のインプットは必要であることを痛感しました。
無事に初心者がはまるミスにはまり、抜け出せずに抜け道を使って対処しました。動くものができたのは一安心です。
開発、研究が盛んなだけにAIの技術は簡単に使用したり、触れることができます。最近だとChatGPTなんかが良い例です。それ故に理論や構造を理解せずに利用している人が多いです。難しいのはその通りなのですが、それ以上に奥深い分野だとも思います。この記事を読んで興味を持った方たちと一緒に自分も詳しくなれていけたら嬉しいです。以上で本記事を〆させていただきます。
明日は@subutakahiroさんの記事が投稿されます。visonOSについて調べる、という内容ですので興味がある方はそちらの記事も是非ご覧ください。
では、またどこかで。
論文のリンク
-
Ramprasaath R. Selvaraju , Michael Cogswell , Abhishek Das , Ramakrishna Vedantam , Devi Parikh , Dhruv Batra "Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization"arXiv 2019.
https://arxiv.org/pdf/1610.02391.pdf ↩ -
Karen Simonyan, Andrew Zisserman "Very Deep Convolutional Networks for Large-Scale Image Recognition"arXiv 2015.
https://arxiv.org/abs/1409.1556 ↩