Posted at

素人がAI部門に配属されてやること4ヶ月目(grad-cam、tsne)


初めに

 前回は学習のための工夫を入れて、より精度を出せるようにしました。ここで今一度考える必要があるのは、精度は出ているけど、AIは実際にキャラの特徴を捉えて判断しているのだろうかということです。

 もし精度が高くても、実は背景を基準にして判断していたとかだと話にならないです。そのまま使うと汎用性がないので必ず精度が下がってしまいます。なのでAIがそれぞれの画像とどう捉えているのかを確認するのは非常に重要と言えます。

 また、AIが各画像をきちんと分けられているかを2次元マッピングしてみます。これにより、どのあたりが学習が進んでいるか、どの画像が間違えやすそうかをある程度予測することができます。


4ヶ月目でやること


  • CAMでAIがどこに着目しているかを確認する

  • t‐SNEでAIの考える画像の近さを可視化する

  • 精度について

とりあえずこんなところです。ココらへんまでマスターできれば、社内で発表するには問題ないくらいの知識を得ていると言えるでしょう。


モデルのセーブ・ロード

重みのセーブ・ロードはやりましたが、モデルの方は忘れてました。

Keras のモデルと学習結果を保存して利用するを参考にモデルを.jsonファイルにしておきましょう。

from keras.models import Sequential, load_model, model_from_json, Model

from keras.optimizers import Adam

model_path = path+'/weights/xxxx.json'
model_weight_path = path+'/weights/xxxx.h5'
json_string = open(model_path).read() # モデルをロード準備
model = model_from_json(json_string) # モデルをロード
model.load_weights(model_weight_path) # 重みロード
model.compile(loss='categorical_crossentropy', optimizer=Adam(), metrics=['accuracy']) # コンパイル

model.summary()

 こんな感じでできます。重みとモデルを別々に保存した場合は、モデルがコンパイルされていないので、学習・推論させる場合にmodel.compile()が必要です。

 ちなみに重みを保存するときにモデルごと保存(引数にweight_only=Falseを指定)もできて、この場合は.jsonの方はいらないですしロードしたあとにコンパイルもいらないです。が、ファイルはその分重いです。


cam

 kerasでGrad-CAM 自分で作ったモデルでを参考にさせていただいてcamをやります。camとはClass Activation Mapの略で、「クラス分けするときに重要だと思ったところに色塗るよ」って技術です。これをやることで、色んな人に「今回作ったAIは、ちゃんと画像の重要なところを見て判断してます!」と言うことができます。

深層学習は画像のどこを見ている!? CNNで「お好み焼き」と「ピザ」の違いを検証

この記事も参考になります。

def Grad_Cam(input_model, x, layer_name):

K.set_learning_phase(1)
x = np.expand_dims(x, axis=0)
predictions = model.predict(x)
class_idx = np.argmax(predictions[0])

class_output = model.output[:, class_idx]
conv_output = model.get_layer(layer_name).output # layer_nameのレイヤーのアウトプット
grads = K.gradients(class_output, conv_output)[0] # gradients(loss, variables) で、variablesのlossに関しての勾配を返す
gradient_function = K.function([model.layers[0].input, K.learning_phase()], [conv_output, grads]) # model.inputを入力すると、conv_outputとgradsを出力する関数

output, grads_val = gradient_function([x])
output, grads_val = output[0], grads_val[0]

# 重みを平均化して、レイヤーのアウトプットに乗じる
weights = np.mean(grads_val, axis=(0, 1))
cam = np.dot(output, weights)
cam = cv2.resize(cam, (x.shape[2], x.shape[1]), interpolation=cv2.INTER_NEAREST) # ブロック
# cam = cv2.resize(cam, (x.shape[2], x.shape[1])) # いい感じ

cam = np.maximum(cam, 0) # マイナス削除
cam = cam / cam.max() # 正規化

jetcam = cv2.applyColorMap(np.uint8(255*cam), cv2.COLORMAP_JET) # モノクロ画像に疑似的に色をつける
jetcam = cv2.cvtColor(jetcam, cv2.COLOR_BGR2RGB) # 色をRGBに変換
jetcam = np.clip(np.float32(jetcam) + x[0]*255*0.5, 0, 255)# もとの画像に合成
jetcam = np.uint8(jetcam)
return jetcam.reshape(x.shape[1:])

from keras import backend as K
plt.figure(figsize=(15, 6))
for i in range(10):
gx=Grad_Cam(model, x_all[550+10*i], 'block5_conv3')
plt.subplot(2,5,i+1),plt.imshow(gx),plt.xticks([]),plt.yticks([])
plt.show()



 結果がこんな感じです。なんか髪の色で判断してる感じになってますね・・・。まあ正解といえば正解なんだけども・・・。こんな感じでAIは判断しやすいところを見つけてくるので、全体としてちゃんと捉えてるかという確認は非常に重要です。

 ちなみにプログラム中のcamのresizeのところの引数をinterpolation=cv2.INTER_NEAREST)にすることで、こんなふうにブロック状にできます。個人的にはこっちのほうが対象特徴マップの大きさとかがわかって好きです。


t-SNE

 t-SNEとはすごいやつです。高次元データの次元削減および2次元プロット手法でも紹介されている通り、数字くらいだときれいに分けてくれます。ただ流石に複雑な画像だときれいに分けてくれないので、学習に使ったCNNを利用して次元を削減します。PCA, Kernel-PCA, t-SNE, CNNによる可視化のための次元削減の比較を参考にCNNを特徴量抽出機として使います。

def make_middle_layer_model(model, layer_name=None):

short_model = Model(input=model.input, output=model.get_layer(layer_name).output)
return short_model

middle_layer_model = make_middle_layer_model(model, layer_name='dense_3')
middle_layer_test = middle_layer_model.predict(x_all)
print(middle_layer_test.shape)

import matplotlib.pyplot as plt
from matplotlib import offsetbox
%matplotlib inline
from sklearn.manifold import TSNE
tsne = TSNE(random_state=42, perplexity=30.0)
X_tsne = tsne.fit_transform(middle_layer_test)

def plot_embedding(X,X2,y, title=None):
x_min, x_max = np.min(X, 0), np.max(X, 0)
X = (X - x_min) / (x_max - x_min)

plt.figure(figsize=(14,13))
ax = plt.subplot(111)
leny=len(set(y))
for i in range(X.shape[0]):
plt.text(X[i, 0], X[i, 1], str(y[i]),
color=plt.cm.rainbow(y[i]/leny),fontdict={'weight': 'bold', 'size': 9})

if hasattr(offsetbox, 'AnnotationBbox'):
# only print thumbnails with matplotlib > 1.0
shown_images = np.array([[1., 1.]]) # just something big
plt.gray()
for i in range(X.shape[0]):
dist = np.sum((X[i] - shown_images) ** 2, 1)
if np.min(dist) < 1e-3 :#or y[i] in [0,1,2,3,4]:
# don't show points that are too close
continue
shown_images = np.r_[shown_images, [X[i]]]
#imagebox = offsetbox.AnnotationBbox(offsetbox.OffsetImage(X2[i].reshape(30,30),cmap=plt.cm.gray_r),X[i])
pic=cv2.cvtColor(cv2.resize(X2[i], (32,32)), cv2.COLOR_BGR2RGB)
imagebox = offsetbox.AnnotationBbox(offsetbox.OffsetImage(pic),X[i])
ax.add_artist(imagebox)
plt.xticks([]), plt.yticks([])
if title is not None:
plt.title(title)
plt.savefig('image2-3.png', bbox_inches='tight')

plot_embedding(X_tsne, x_all, y_all)



 こんな感じで、CNNの出力とtsneを組み合わせることで、近い画像を集めたマップを作ることができます。今回のCNNはキャラごとにクラス分けしたCNNでしたが、別な目的で成長させたCNNを入れることにより、いろんなまとめ方のマップを得られると思います。

 ちなみにこれは最終層の前のマップなので、これをパーセプトロンで線を引くのがCNNの分類ですね。なのでこれを見ることで、どんな画像がわかりやすいか、間違えやすそうかを少し予測できるかなと思います。


精度について

 前回までの学習では普通に精度という形で性能を評価していましたが、本当にそれだけで十分でしょうか。普段の報告ならば精度だけでいいですが、精度は偏りがあるデータだとあまり良い指標とは言えないことがあります。

 詳しくは下記の記事が良いと思います。適合率、再現率、F値、精度、どれを重要視するかは案件によって変わりますが、仲間内で議論するならば直交行列をそのまま見せるのが一番だと思います。

 調和平均は行きと帰りで違う速度で走った車の全行程の平均速度をイメージすると考えやすいです。

機械学習の評価指標を簡単にまとめてみた


まとめ

 これで4ヶ月目は終わりです。ここまでの知識があれば、自分の会社みたいな製造業レベルではもう十分なほどの試行錯誤をできると思います。あとはデータセットの工夫だったり、社内での目標調整、使用方法の打ち合わせの比重が高くなると思います。今回の情報があれば説明資料もかなり面白いのが作れるはずです。頑張りましょう。

 次回は正解画像だけで学習できると評判のVAEをやります。評判なだけで実際使うとなるとかなり厳しいですが。それに伴ってkerasのfunctional的なネットワークの作り方をやって、resnetも説明できたらなと思います。