このチュートリアルを一通り行うことで、
というレベルから
自分でモデル作れます!転移学習、ファインチューニングできます!
モデルの中身を可視化できます!
という状態になってもらえたらと思っています。
はじめに
本チュートリアルは、具体的な理解よりも、直感的な理解をして頂くことを目的としています。そのため、厳密性よりも、わかりやすさを優先した表現をすることをご了承願います。
本チュートリアルの対象者
- if, for, 関数, 変数など基本的なプログラミングに関する知識は記憶の彼方にわずかにあるが、pythonおよびディープラーニングに関する知識は全く無い。
- だけれども、ディープラーニングに興味がある!
- 自力でモデルを作って評価して、またモデルを作ってという一連の流れを身につけたい!!
みたいな方にオススメです。
参考図書
チュートリアルを作成するに当たって参考にさせて頂いた本です。
この本は、個人的にかなりおすすめの本です。
理論的な部分と、実践的な部分が半々です。
しかも、コンピュータビジョン、自然言語、画像生成と幅広く対応しているので、いろんな分野を触って勉強してみたい!という方に向いていると思います。
本チュートリアルで扱う問題
「車の画像から、その画像に写っている車種を当てる」という問題を解くためにモデルを作成します。
本チュートリアルの構成
- 自作モデルで車種当て問題をといてみよう!
- 転移学習で車種当て問題をといてみよう!
- ファインチューニングで車種当て問題をといてみよう!
- 完成したモデルをテストデータで評価してみよう!
- 精度の高いモデルと低いモデルの違いを覗いてみよう!
本記事は4について解説をします。
全てのコードは僕のgithubに上がっているのでcloneをしていただければ実行することが可能です。
今回のコードはこちらです。
それでは、チュートリアルを始めます!
4.完成したモデルをテストデータで評価してみよう!
4-1. テストデータを使ってモデルの評価をしよう!
今までの評価は学習の際に見ることができる、train, validの正解率とlossで判断をしていました。
大まかにはそれで問題はありませんが、自分以外の人に説明をする際、それでは不十分になってしまいます。
学習をする際、validのlossと正解率が低くなるようにモデルを変更して様々なパラメータを調整していきました。
モデル自体の学習には直接は影響ありませんが、間接的に制作者の意図としてデータが漏れてしまっているからです。
なので、制作者の意図が全く介入することがなかったテストデータで比較をする必要があります。
以下では、検証をするために役立つ関数を作ります。
関数の仕様
モデルのパスとtestディレクトリのパスを引数に入れたらテストデータの画像で検証し、結果を、csvで保存した後に返り値でも渡す。
入力
- モデルの保存先のパス文字列
- テスト画像の全パスが入っている配列
- 正解ラベル名の配列
- 保存する際の名前の文字列
返り値
- 全体における正解率 scaler
- 各予測のone hotベクトルの累積和(20, 20)
- 各画像ごとの分布確率(20, 20)
- 正解ラベル名、画像のパス、間違えた画像の全てのラベルに対する予測確率(dataframe)(N, 22)
関数の定義
import os
import numpy as np
import pandas as pd
from keras import models
from keras.preprocessing import image
from tqdm import tqdm
import matplotlib.pyplot as plt
%matplotlib inline
INPUT_SIZE = 256
def evaluate_model(model_path, test_pathes, classes, fname):
model = models.load_model(model_path)
classes_num = len(classes)
cumulative_sum = np.zeros((classes_num, classes_num))
probability = np.zeros((classes_num, classes_num))
all_pictures_num = 0
accuracy = 0
columns = classes + ["LABEL", "PATH"]
df1 = pd.DataFrame(columns=columns)
df_all1 = pd.DataFrame(columns=columns)
acc_dist = np.array([])
for i, test_path in tqdm(enumerate(test_pathes),total=len(classes)):
pictures_num = len(test_path)
all_pictures_num += pictures_num
for img_path in test_path:
img = image.load_img(img_path, target_size=(INPUT_SIZE, INPUT_SIZE))
x = image.img_to_array(img)/255.0
x = np.expand_dims(x, axis=0)
preds = model.predict(x)
ans = np.argmax(preds[0])
acc_dist = np.insert(acc_dist, len(acc_dist), preds[0][i])
cumulative_sum[i][ans] += 1
probability[i][ans] += 1
accuracy += int(i == ans)
content = list(preds[0])+[classes[i],img_path]
df_all2 = pd.DataFrame( [content], columns=columns)
df_all1 = pd.concat([df_all1, df_all2])
if i != ans:
# content = list(preds[0])+[classes[i],img_path]
df2 = pd.DataFrame( [content], columns=columns)
df1 = pd.concat([df1, df2])
probability[i] /= pictures_num
np.savetxt('acc_matrix/acc_matrix'+fname+'.csv', probability)
df1.to_csv('incorrect/df'+fname+'.csv', index=False)
df_all1.to_csv('probabilities_df_all/df_all'+fname+'.csv', index=False)
np.savetxt('acc_dist/acc_dist'+fname+'.csv', acc_dist)
return accuracy/all_pictures_num, cumulative_sum, probability, df1, df_all1, acc_dist
関数の定義が済んだので、前準備をしていきます。
前準備
base_dir = 'mini_pictures'
test_dir = os.path.join(base_dir,'test')
mini_metadata = pd.read_csv('mini_metadata.csv',index_col=0)
classes = list(mini_metadata["make_model"].value_counts().index)
classes = sorted(classes)
classes_num = len(classes)
print(classes)
# print(classes_num) # = 20
test_pathes = []
for i in range(classes_num):
pre_path = os.path.join(test_dir, classes[i])
test_pathes.append([os.path.join(pre_path, fname) for fname in os.listdir(pre_path)])
['Audi-a3', 'Audi-a5', 'Audi-q5', 'BMW-1-series', 'BMW-4-series', 'BMW-x3', 'Honda-pilot', 'Jeep-wrangler', 'MINI-clubman', 'MINI-countryman', 'Mazda-mazda5', 'Mercedes-Benz-gla', 'Mercedes-Benz-glk', 'Mitsubishi-outlander', 'Nissan-370z', 'Nissan-quest', 'Nissan-rogue-select', 'Subaru-outback', 'Toyota-tacoma', 'Volkswagen-cc']
ファインチューニングモデル
model_path = 'models/VGG16_mini_extended_fine2.h5'
acc, cumulative_sum, probability, df1, df_all1, acc_dist1 = evaluate_model(model_path, test_pathes, classes, '1')
pd.options.display.float_format = '{:.2f}'.format
pd.options.display.max_columns = None print(acc)
df_all1.reset_index(drop=True,inplace=True)
print(df_all1.shape)
fname = 'acc_matrix1'
probability = pd.read_csv('./acc_matrix/'+fname+'.csv',header=None,sep=' ')
fig = plt.figure() im = plt.imshow(probability, "rainbow")
plt.title("VGG16_mini_extended_fine2")
plt.xlabel("predicted results")
plt.ylabel("car label")
fig.colorbar(im)
plt.savefig('./acc_matrix/'+fname+'.png')
0.995475113122172
(663, 22)
テストデータので正解率が驚異の99.5%!!
自分でも正直びっくりです。
テストデータ663枚中間違えたのは、3枚のみ!
こうやって、確認をしてあげることで、本当に汎用的なモデルが完成したと判断することができるので、いいですね。
ここで、左:間違えたテストデータの写真、右:何の車種と間違えたのか、を比較してみます。
#左:間違えた写真、右:間違えた車種の写真
df_name = 'df1'
df = pd.read_csv('incorrect/'+df_name+'.csv')
plt.figure(figsize=(12, 16))
for i in range(3):
for j in range(20):
if classes[j] == list(df.iloc[i][df.iloc[i]==df.iloc[i][:20].max()].index)[0]:
real_path = test_pathes[j][0]
break
img1 = image.load_img(df['PATH'][i], target_size=(INPUT_SIZE,INPUT_SIZE))
img2 = image.load_img(real_path, target_size=(INPUT_SIZE,INPUT_SIZE))
plt.subplot(3,2,1+i*2)
plt.imshow(img1)
plt.title('missed picture: '+df['LABEL'][i])
plt.subplot(3,2,2+i*2)
plt.imshow(img2)
plt.title('missed as: '+classes[j])
plt.show
plt.savefig('incorrect/'+df_name+'.png')



うーん。似てるような、似ていないような…
なんとも、言えない感じです。
でも、たったこれだけしか間違えていないとなるととても優秀ですね。
ちなみに、確率を可視化するときに好きな表示方法は、上記のようにマトリクスで表現するやり方です。なぜなら、見やすいから!
これは、3年の5月からお世話になっている研究所の先生からの継承です。笑
転移学習モデル
model_path2 = 'models/VGG16_mini_3.h5'
acc2, cumulative_sum2, probability2, df2, df_all2, acc_dist2 = evaluate_model(model_path2, test_pathes, classes,'2')
# np.set_printoptions(precision=2, floatmode='fixed')
pd.options.display.float_format = '{:.2f}'.format
pd.options.display.max_columns = None
print(acc2)
df_all2.reset_index(drop=True,inplace=True)
fname = 'acc_matrix2'
probability2 = pd.read_csv('./acc_matrix/'+fname+'.csv',header=None,sep=' ')
fig = plt.figure()
im = plt.imshow(probability2, "rainbow")
plt.title("VGG16_mini_3")
plt.xlabel("predicted results")
plt.ylabel("car label")
fig.colorbar(im)
plt.savefig('./acc_matrix/'+fname+'.png')
0.9834087481146304



これも、98.3%とかなり高いですが、マトリクスで比較をすると8番目のMINI-clubmanの予測が苦手みたいですね。
さっきと同様に比べてあげます。



こっちのモデルの方が、間違えている理由に納得できます!
たしかに、人間でも間違えそうな間違え方をしています。
ですが、ここで面白いのは、ファインチューニングモデルで間違えていた写真がこのモデルでは間違えていないのです!
モデルごとに個性があるのがいかにもディープラーニングらしい特徴ですね。
自作モデル
今回は比較を簡単にするために、精度がそこそこなものでやってみます。
model_path3 = 'models/simplest_cnn_model_mini_1.h5'
acc3, cumulative_sum3, probability3, df3, df_all3, acc_dist3 = evaluate_model(model_path3, test_pathes, classes, "3")
# np.set_printoptions(precision=2, floatmode='fixed')
pd.options.display.float_format = '{:.2f}'.format
pd.options.display.max_columns = None
print(acc3)
fname = 'acc_matrix3'
probability3 = pd.read_csv('./acc_matrix/'+fname+'.csv',header=None,sep=' ')
fig = plt.figure()
im = plt.imshow(probability3, cmap="rainbow")
plt.title("simplest_cnn_model_mini_1")
plt.xlabel("predicted results")
plt.ylabel("car label")
fig.colorbar(im)
plt.savefig('./acc_matrix/'+fname+'.png')
0.8431372549019608



上の2つと比較をすると一目瞭然ですね。
画像の比較をします。



Audi-A3を他のドイツ車と間違えていますね。
日本車と間違えていないので、まあ、努力は認めましょうと言ったところでしょうか。
このモデルからしたら、「そんな違いなんて、わかんねーよ!パッと見一緒じゃんか!」なんて思っているかもしれません。
まあ、ただ重みとバイアスで計算された結果なので、なんの感情もないんですけどね。
3つのモデルをひとつのグラフで比較
今は1つのモデルを詳しく見ていました。
詳しくみることで、そこで生まれる新たな発見がたくさんあることが分かったと思います。
順番に見ていけば、一番最初が一番良いことは明らかでしたが、もっと比較対象が増えると結局どれが一番良かったのかがわかりにくくなってしまいます。
特に、スライドで発表するときは3枚前のスライドの数値は視聴者の記憶から抜け落ちていると言っても過言ではないです。
残っているのは印象だけです。
そのため、パッと一瞬で理解してもらう必要があります。
そこで次は、3つのモデルでどれが一番良いモデルか一目で理解してもらうために使う手法を紹介します。
acc_dist1 = np.array(pd.read_csv('acc_dist/acc_dist1.csv'))
acc_dist2 = np.array(pd.read_csv('acc_dist/acc_dist2.csv'))
acc_dist3 = np.array(pd.read_csv('acc_dist/acc_dist3.csv'))
acc1_m = np.mean(acc_dist1)
acc2_m = np.mean(acc_dist2)
acc3_m = np.mean(acc_dist3)
acc1_s = np.std(acc_dist1)
acc2_s = np.std(acc_dist2)
acc3_s = np.std(acc_dist3)
print(acc1_m,acc2_m,acc3_m)
print(acc1_s,acc2_s,acc3_s)
label=['Original','VGG16TransferLearning','VGG16FineTuning ']
x = [1,2,3]
y = [acc3_m, acc2_m, acc1_m]
yerr = np.array([acc3_s, acc2_s,acc1_s ])
plt.bar(x, y, yerr=yerr, ecolor="black", color=["red", "blue", "green"],tick_label=label)
plt.title('Mean of probabilities')
plt.ylabel('probability')
plt.savefig('results_pictures/acc_bars.png')
0.9955801101277647 0.9683156347526884 0.8201834722272378
0.060243361434697204 0.1108814340073005 0.3234513735050927



このグラフは平均値と分散を使って「モデルの正解率」と「モデルの確からしさ」を表しています。
これとほとんど同じ意味を持つグラフをもう一つ紹介します。
a_min = 0.975
a_max = 1.0
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
x = [acc_dist3.reshape(-1,),acc_dist2.reshape(-1,),acc_dist1.reshape(-1,)]
ax.hist(x, bins=16, color=['red', 'blue', 'green'],range=(a_min, a_max), label=label)
ax.set_ylim(0,662)
ax.set_title('Distribution of predictions')
ax.set_xlabel('probability')
ax.set_ylabel('counts (sample num is 662)')
plt.legend(loc='upper left')
fig.show()
plt.savefig("results_pictures/acc_dists.png")



これは、「何パーセントの自信をもって確率を出力したか」を表しています。
緑はほとんど、100%の確信を持って正解を出していますが、青や赤は確信度が低いことがわかります。
これらのグラフをみることで、ファインチューニングモデルが転移学習モデルに対して偶然ではなく本当に精度が良くなったのかを一瞬で判断することができます。
先生に説明をするときはt検定をしたりしますが、統計がわからない人に説明するときはこっちの方がわかりやすいのかなと思ってます。
最後に
人に伝えてこその実験結果
せっかく実験をしたなら、その結果を知ってもらい世のために活用してもらいたいです。
でないと、自己満足で終わってしまいます。
日々、伝える努力もしていかないといけないです…!
次回予告!
次回が、ついに最終章です!
最後は、ブラックボックスとされるディープラーニングの中身を覗いて行きたいと思います!



最後に、こんな画像を生成するのでご期待ください!












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


