今回は、以前の記事
Kerasで自作画像を用いてAlexNetで訓練するソースコード(Windows10)
に引き続いて、KerasでのDeep Learningをやっていきます。
今回は、学習したモデルのアーキテクチャや重みを保存し、それを読み込んで使う方法をやっていきたいと思います。
深層学習のプログラムを書いていると、結構プログラムを分けたい場面に遭遇します。例えば
① 訓練用のプログラム
② ①で訓練したモデルを用いたテストプログラム(正解不明のデータに適用) ⇒ 所謂本番用
みたいな感じに二つのプログラムになったりします。
このときに、①のプログラムでモデルのアーキテクチャや重みを保存しておいて、②のプログラムで読み込んで使うという流れになります。
今回は、アーキテクチャや重みの保存と読み込みの方法について見ていきましょう。
今回の環境
・OS : Windows10(64bit)
・GPU: GeForce GTX 1060
・Anaconda
・CUDA 9.0
・cuDNN v7.0.5
・Tensorflow 1.11.0
・Keras 2.2.4
上記全て環境構築済
モデルの保存(Save)方法
ここでは、過去記事である
Kerasで自作画像を用いてAlexNetで訓練するソースコード(Windows10)
で組んだAlexNetのプログラムを使い、このコードにモデルのアーキテクチャ・重みを保存するコードを加えることで、モデルを保存してみましょう。
ソースコードは以下となります。
import numpy as np | |
from keras.utils import np_utils | |
from keras.models import Sequential | |
from sklearn.model_selection import train_test_split | |
import glob | |
from keras.preprocessing.image import load_img, img_to_array | |
from keras.initializers import TruncatedNormal, Constant | |
from keras.layers import Dropout, Flatten, Conv2D, MaxPooling2D, Dense, BatchNormalization | |
from keras.optimizers import SGD | |
# ------------------------------------------------------------------------------------- | |
# 初期設定部 | |
# ------------------------------------------------------------------------------------- | |
# GrayScaleのときに1、COLORのときに3にする | |
COLOR_CHANNEL = 3 | |
# 入力画像サイズ(画像サイズは正方形とする) | |
INPUT_IMAGE_SIZE = 224 | |
# 訓練時のバッチサイズとエポック数 | |
BATCH_SIZE = 32 | |
EPOCH_NUM = 100 | |
# 使用する訓練画像の入ったフォルダ(ルート) | |
TRAIN_PATH = "..\training_dataset\training_anime1" | |
# 使用する訓練画像の各クラスのフォルダ名 | |
folder = ["000_hatsune_miku", "001_kinomoto_sakura", "002_suzumiya_haruhi", | |
"003_fate_testarossa", "004_takamachi_nanoha", "005_lelouch_lamperouge", | |
"006_akiyama_mio", "007_nagato_yuki", "008_shana", "009_hakurei_reimu"] | |
# CLASS数を取得する | |
CLASS_NUM = len(folder) | |
print("クラス数 : " + str(CLASS_NUM)) | |
# ------------------------------------------------------------------------------------- | |
# 訓練画像入力部 | |
# ------------------------------------------------------------------------------------- | |
# 各フォルダの画像を読み込む | |
v_image = [] | |
v_label = [] | |
for index, name in enumerate(folder): | |
dir = TRAIN_PATH + "\" + name | |
files = glob.glob(dir + "\*.png") | |
print(dir) | |
for i, file in enumerate(files): | |
if COLOR_CHANNEL == 1: | |
img = load_img(file, color_mode = "grayscale", target_size=(INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE)) | |
elif COLOR_CHANNEL == 3: | |
img = load_img(file, color_mode = "rgb", target_size=(INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE)) | |
array = img_to_array(img) | |
v_image.append(array) | |
v_label.append(index) | |
v_image = np.array(v_image) | |
v_label = np.array(v_label) | |
# imageの画素値をint型からfloat型にする | |
v_image = v_image.astype('float32') | |
# 画素値を[0~255]⇒[0~1]とする | |
v_image = v_image / 255.0 | |
# 正解ラベルの形式を変換 | |
v_label = np_utils.to_categorical(v_label, CLASS_NUM) | |
# 学習用データと検証用データに分割する | |
train_images, valid_images, train_labels, valid_labels = train_test_split(v_image, v_label, test_size=0.20) | |
# ------------------------------------------------------------------------------------- | |
# モデルアーキテクチャ定義部 | |
# ------------------------------------------------------------------------------------- | |
def conv2d(filters, kernel_size, strides=1, bias_init=1, **kwargs): | |
trunc = TruncatedNormal(mean=0.0, stddev=0.01) | |
cnst = Constant(value=bias_init) | |
return Conv2D( | |
filters, | |
kernel_size, | |
strides=strides, | |
padding='same', | |
activation='relu', | |
kernel_initializer=trunc, | |
bias_initializer=cnst, | |
**kwargs | |
) | |
def dense(units, **kwargs): | |
trunc = TruncatedNormal(mean=0.0, stddev=0.01) | |
cnst = Constant(value=1) | |
return Dense( | |
units, | |
activation='tanh', | |
kernel_initializer=trunc, | |
bias_initializer=cnst, | |
**kwargs | |
) | |
def AlexNet(): | |
model = Sequential() | |
# 第1畳み込み層 | |
model.add(conv2d(96, 11, strides=(4,4), bias_init=0, input_shape=(INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE, COLOR_CHANNEL))) | |
model.add(MaxPooling2D(pool_size=(3, 3), strides=(2, 2))) | |
model.add(BatchNormalization()) | |
# 第2畳み込み層 | |
model.add(conv2d(256, 5, bias_init=1)) | |
model.add(MaxPooling2D(pool_size=(3, 3), strides=(2, 2))) | |
model.add(BatchNormalization()) | |
# 第3~5畳み込み層 | |
model.add(conv2d(384, 3, bias_init=0)) | |
model.add(conv2d(384, 3, bias_init=1)) | |
model.add(conv2d(256, 3, bias_init=1)) | |
model.add(MaxPooling2D(pool_size=(3, 3), strides=(2, 2))) | |
model.add(BatchNormalization()) | |
# 全結合層 | |
model.add(Flatten()) | |
model.add(dense(4096)) | |
model.add(Dropout(0.5)) | |
model.add(dense(4096)) | |
model.add(Dropout(0.5)) | |
# 出力層 | |
model.add(Dense(CLASS_NUM, activation='softmax')) | |
model.compile(optimizer=SGD(lr=0.01), loss='categorical_crossentropy', metrics=['accuracy']) | |
return model | |
# コンパイル | |
model = AlexNet() | |
# 訓練 | |
history = model.fit(train_images, train_labels, batch_size=BATCH_SIZE, epochs=EPOCH_NUM) | |
# ------------------------------------------------------------------------------------- | |
# 訓練実行&結果確認部 | |
# ------------------------------------------------------------------------------------- | |
# モデル構成の確認 | |
model.summary() | |
score = model.evaluate(valid_images, valid_labels, verbose=0) | |
print(len(valid_images)) | |
print('Loss:', score[0]) | |
print('Accuracy:', score[1]) | |
# ------------------------------------------------------------------------------------- | |
# モデルのアーキテクチャの保存 | |
# ------------------------------------------------------------------------------------- | |
# モデルのアーキテクチャの保存①(JSON版) | |
model_arc_json = model.to_json() | |
open("model_architecture.json", mode='w').write(model_arc_json) | |
# モデルのアーキテクチャの保存②(YAML版) | |
model_arc_yaml = model.to_yaml() | |
open("model_architecture.yaml", mode='w').write(model_arc_yaml) | |
# ------------------------------------------------------------------------------------- | |
# モデルの重みの保存 | |
# ------------------------------------------------------------------------------------- | |
# モデルの重みの保存 | |
model.save_weights("weights.hdf5") |
追加した部分としては、後半の以下の部分のみとなります。
# -------------------------------------------------------------------------------------
# モデルのアーキテクチャの保存
# -------------------------------------------------------------------------------------
# モデルのアーキテクチャの保存①(JSON版)
model_arc_json = model.to_json()
open("model_architecture.json", mode='w').write(model_arc_json)
# モデルのアーキテクチャの保存②(YAML版)
model_arc_yaml = model.to_yaml()
open("model_architecture.yaml", mode='w').write(model_arc_yaml)
# -------------------------------------------------------------------------------------
# モデルの重みの保存
# -------------------------------------------------------------------------------------
# モデルの重みの保存
model.save_weights("weights.hdf5")
(1) モデルのアーキテクチャの保存
まずはモデルのアーキテクチャを保存します。アーキテクチャというのは、最初は畳み込み層があって、次に~層があって、フィルタの数は~で……のようなモデルの構造を示す部分です。
アーキテクチャを保存する方法ですが、JSON形式で保存する方法と、YAML形式で保存する方法の二つがあるようですが、どちらを選択しても特に大きな違いはなさそうです。
一応、上記のコードでは両方書きましたが、どちらか一方で保存すれば十分です。
# -------------------------------------------------------------------------------------
# モデルのアーキテクチャの保存
# -------------------------------------------------------------------------------------
# モデルのアーキテクチャの保存①(JSON版)
model_arc_json = model.to_json()
open("model_architecture.json", mode='w').write(model_arc_json)
# モデルのアーキテクチャの保存②(YAML版)
model_arc_yaml = model.to_yaml()
open("model_architecture.yaml", mode='w').write(model_arc_yaml)
model.to_json()で、モデルのアーキテクチャを示す文字列がmodel_arc_jsonに返却されるので、その文字列を保存してやるだけです。
YAML形式についても関数名が違うだけで全く同じなので割愛します。
せっかくなので、出力されたJSON形式のファイルと、YAML形式のファイルをメモ帳で見てみましょう。
<1> JSON形式
<2> YAML形式
JSON形式は改行されておらず見づらいですが、正常に出力されているようです。
(2) モデルの重みの保存
次に重みの保存方法について見ていきます。
重みを保存することで、一度訓練することによって得た重みを保存し、次回その地点から訓練を再開したり、あるいは別のプログラムで使ったりすることができるようになります。
# -------------------------------------------------------------------------------------
# モデルの重みの保存
# -------------------------------------------------------------------------------------
# モデルの重みの保存
model.save_weights("weights.hdf5")
重みの保存も非常に簡単で、save_weightsを使い、HDF5(Hierarchical Data Format 5)で保存します。フォーマットの詳細については割愛します。
モデルの読み込み(Load)方法
上で保存したアーキテクチャに関するJSONファイルと、重みのファイルを基に、何か1枚の適当な画像(正解ラベルなし)を認識させるコードを作ってみました。
import numpy as np | |
from keras.models import model_from_json | |
from keras.preprocessing.image import load_img, img_to_array | |
# ------------------------------------------------------------------------------------- | |
# モデルの読み込み部 | |
# ------------------------------------------------------------------------------------- | |
# 入力画像サイズ - 訓練時の画像サイズと合わせる | |
INPUT_IMAGE_SIZE = 224 | |
# GrayScaleのときに1、COLORのときに3にする - 訓練時のカラーチャンネル数と合わせる | |
COLOR_CHANNEL = 3 | |
# 確認したい画像へのフルパス | |
TEST_PATH = "..\test_dataset\test_image.jpg" | |
# 今回利用するアーキテクチャと重みのファイルへのパス | |
MODEL_ARC_PATH = 'model_architecture.json' | |
WEIGHTS_PATH = 'weights.hdf5' | |
# ------------------------------------------------------------------------------------- | |
# テスト画像入力部 | |
# ------------------------------------------------------------------------------------- | |
# 今回は1枚の画像だが複数画像対応も可能 | |
test_images = [] | |
# テスト画像の入力部分 | |
if COLOR_CHANNEL == 1: | |
img = load_img(TEST_PATH, color_mode = "grayscale", target_size=(INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE)) | |
elif COLOR_CHANNEL == 3: | |
img = load_img(TEST_PATH, color_mode = "rgb", target_size=(INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE)) | |
array = img_to_array(img) | |
test_images.append(array) | |
test_images = np.array(test_images) | |
# imageの画素値をint型からfloat型にする | |
test_images = test_images.astype('float32') | |
# 画素値を[0~255]⇒[0~1]とする | |
test_images = test_images / 255.0 | |
# ------------------------------------------------------------------------------------- | |
# モデル読み込み部分 | |
# ------------------------------------------------------------------------------------- | |
# JSONファイルからモデルのアーキテクチャを得る | |
model_arc_str = open(MODEL_ARC_PATH).read() | |
model = model_from_json(model_arc_str) | |
# モデル構成の確認 | |
model.summary() | |
# モデルの重みを得る | |
model.load_weights(WEIGHTS_PATH) | |
# ------------------------------------------------------------------------------------- | |
# テスト実行部 | |
# ------------------------------------------------------------------------------------- | |
# テストの実行 | |
result = model.predict(test_images, batch_size=1) | |
# 結果の表示 | |
print('result : ', result) | |
max_index = np.argmax(result) | |
print('max_index : ', max_index) |
コードは上記のようになります。
最初に説明したセーブのコードと対応しているので、そちらで保存したアーキテクチャや重みをそのまま使えるはずです。
重要な部分だけ取り出して説明すると、
# JSONファイルからモデルのアーキテクチャを得る
model_arc_str = open(MODEL_ARC_PATH).read()
model = model_from_json(model_arc_str)
# モデル構成の確認
model.summary()
# モデルの重みを得る
model.load_weights(WEIGHTS_PATH)
上記の部分にて、JSONファイルからモデルのアーキテクチャと重みを得ています。
アーキテクチャの方は、一度openで開くことでアーキテクチャの文字列を得て、その文字列をmodel_from_json()で読み込むことでモデルのアーキテクチャを取得できます。この流れは保存の部分と同じです。
実行結果
私の手元での実行結果です。
まず、ソースコード53行目のmodel.summary()の結果、全部は写していませんが、以下のように正常にモデルのアーキテクチャが表示されました。
また、今回使用したテスト画像(..\test_dataset\test_image.jpg)ですが、以下の画像を使用しました。
こちらの画像を認識させた結果、コンソールの表示が以下となりました。
見方ですが、まずresultの方には、各クラスの確率が入っています。resultを全て足すと1になるはずですが、最初の項目が最も高いスコアを示していることがわかります。
max_indexは、resultの中の0番のクラスが最高値でしたということを示しています。配列なので、インデックスは基本的に0番から9番までの10クラスです。
さて、肝心の0番が何だったかということですが、これは訓練したときのソースコードの
folder = ["000_hatsune_miku", "001_kinomoto_sakura", "002_suzumiya_haruhi",
"003_fate_testarossa", "004_takamachi_nanoha", "005_lelouch_lamperouge",
"006_akiyama_mio", "007_nagato_yuki", "008_shana", "009_hakurei_reimu"]
を見ればよく、0番のクラスは「“000_hatsune_miku“」です。
つまり、このテスト画像は初音ミクであると認識されており、概ね結果は正しいのではないでしょうか。
今回は訓練に使っていない画像をテストに使ったので、モデルにある程度の汎化性能があることもわかります。
おそらくですが、多少訓練画像とのタッチが違っても、髪色が実際の初音ミクと一致しているのが大きいのではないかと思います。
まとめ
今回はモデルのアーキテクチャと重みを一度外部ファイルに吐き出して、他のコードでそれを読み込んで使うということをやりました。
これで、一度訓練に使ったモデルや、取得した重みを保存しておくことが可能になります。