今回は、モデルの最適化に関する基本的な方法を説明します。いつも通りバックエンドのチェックから。GPUを使う場合は、plaidmlが返って来ればokです。
# kerasのバックエンドをチェック
import plaidml.keras
import keras
print(keras.backend.backend())
今回は、CNNでMnistの数字を当てるという課題を行います。前処理などは、Keras03で行なっていますので、そちらを参照ください。 まず、Mnistのデータロードを行います。
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.optimizers import RMSprop
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# 各種サイズ取得
num_train = len(y_train) # 教師データ数
num_test = len(y_test) # テストデータ数
print("教師データサイズ: ", num_train)
print("テストデータサイズ: ", num_test)
size_0 = x_train.shape[1] # 横サイズ
size_1 = x_train.shape[2] # 縦サイズ
dim_input_vec = size_0 * size_1
# 規格化
# グレースケール画像の場合、0が黒、255が白となっている。
# ニューラルは値のレンジが0〜1が望ましいので、そのように規格化
x_train = x_train / 255
x_test = x_test / 255
# クラス分類問題なので、クラスラベルをカテゴリカル変数に変換
num_class = 10
y_train = keras.utils.to_categorical(y_train, num_class)
y_test = keras.utils.to_categorical(y_test, num_class)
一応、画像の表示
import matplotlib.pyplot as plt
print("0枚目の画像に書かれている数字: ", y_train[0])
plt.imshow(x_train[0, :, :], cmap='gray')
plt.show()
CNNのためのサイズ変換
import numpy as np
num_train = len(x_train[:, 0,0]) # 教師データの数
num_test = len(x_test[:, 0,0]) # テストデータの数
size_rows = len(x_train[0, :,0]) # 画像、行サイズ
size_cols = len(x_train[0, 0, :]) # 画像、列サイズ
print("x_trainのshape: ", np.shape(x_train))
x_train_cnn = np.reshape(x_train, [num_train, size_rows, size_cols, 1])
x_test_cnn = np.reshape(x_test, [num_test, size_rows, size_cols, 1])
print("x_train_cnnのshape: ", np.shape(x_train_cnn))
続いて、学習の再現性について説明します。ニューラルネットワーク系のモデルは、学習時に乱数に身を委ねる箇所がいくつもあります。したがって、同じ構造のモデル、同じ学習条件を指定しても、まったく同じモデルはほぼ確実にできません。しかし、過去のモデルを再現できないというのは、よくありません。
これを体感するために、まず、同じモデルなのに違う判定結果が出るコードを示します。はじめに、構築したいモデルの構造を、定義しておきます。
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D
def model_making(mod):
# モデル構造の定義
mod.add(Conv2D(filters=1, kernel_size=(5, 5), activation='relu', input_shape=(28, 28, 1)))
mod.add(Conv2D(filters=1, kernel_size=(5, 5), activation='relu'))
mod.add(Conv2D(filters=1, kernel_size=(5, 5), activation='relu'))
mod.add(Dropout(0.25))
mod.add(Flatten())
mod.add(Dense(30, activation='relu'))
mod.add(Dropout(0.5))
mod.add(Dense(10, activation='softmax'))
# コスト関数などの設定
mod.compile(loss='categorical_crossentropy', optimizer=RMSprop(lr=10**-3), metrics=['accuracy'])
# lr=10**-3 は学習係数
return mod
続いて、まったく同じ構造のモデルを2つ定義します。
# 1つめ
m1 = Sequential()
m1 = model_making(m1)
# 2つめ
m2 = Sequential()
m2 = model_making(m2)
モデルが定義できたところで、お互いのモデルを学習させてみます(備考: validation_splitとは、教師データの「後ろ」から、指定された割合だけ、学習に使用しないvalidation dataに割り当てることを意味します。「後ろ」からですので、教師データの並び順が、クラスラベルで綺麗に並んでいると、ひどい目にあうので注意してください)。
batch_size = 129
epochs = 2 # 学習回数(適宜、大きくすること)
print("\n ### モデル1")
m1_lp = m1.fit(x_train_cnn, y_train, batch_size=batch_size,
epochs=epochs, verbose=1, validation_split=0.1)
print("\n ### モデル2")
m2_lp = m2.fit(x_train_cnn, y_train, batch_size=batch_size,
epochs=epochs, verbose=1, validation_split=0.1)
さて、accとval_acc(正答率)がm1とm2で異なることがわかります。同じデータの正答率が異なるということは、異なる判定結果が得られていることになります。すなわち、m1とm2は異なるモデルに仕上がったことを意味します。これを回避するには、
良い例:
という流れのコードを書く必要があります。この順番を厳密に守らないとNGなので、注意してください。例えば、
ダメな例:
とか
ダメな例:
とか
ダメな例:
とかは、(おそらく)すべてNGです。記載する位置がまあまあシビアなので、慣れるまではきちんと再現されているかチェックしないと危険です。
import os
batch_size = 129
epochs = 1 # 学習回数(適宜、大きくすること)
se = 5 # 乱数シード番号(任意の自然数、なんでもok)
print("\n ### モデル1")
os.environ['PYTHONHASHSEED'] = str(se)
np.random.seed(se)
m1 = Sequential()
m1 = model_making(m1)
m1_lp = m1.fit(x_train_cnn, y_train, batch_size=batch_size,
epochs=epochs, verbose=1, validation_split=0.1)
print("\n ### モデル2")
os.environ['PYTHONHASHSEED'] = str(se)
np.random.seed(se)
m2 = Sequential()
m2 = model_making(m2)
m2_lp = m2.fit(x_train_cnn, y_train, batch_size=batch_size,
epochs=epochs, verbose=1, validation_split=0.1)
モデル1, 2でまったく同じ結果になりました。すなわち、同じ判定構造を持つモデルということになります。時間がある人はepoch=1ではなく、100くらいでやってみても良いかもしれません。
再現可能なモデルが作れるようになったところで、学習係数の最適化を行ってみます。学習係数は、1回のパラメータ更新に対する、変化の大きさを意味します。このためにまず、学習係数を自由に変えることのできる関数を用意します。
def mod_lr(mod, learning_rate):
# モデル構造の定義
mod.add(Conv2D(filters=1, kernel_size=(5, 5), activation='relu', input_shape=(28, 28, 1)))
mod.add(Conv2D(filters=1, kernel_size=(5, 5), activation='relu'))
mod.add(Conv2D(filters=1, kernel_size=(5, 5), activation='relu'))
mod.add(Dropout(0.25))
mod.add(Flatten())
mod.add(Dense(30, activation='relu'))
mod.add(Dropout(0.5))
mod.add(Dense(10, activation='softmax'))
# コスト関数などの設定
mod.compile(loss='categorical_crossentropy', optimizer=RMSprop(lr=learning_rate), metrics=['accuracy'])
# mod_lr関数の第二引数が、RMSpropの引数に入力される
return mod
続いて、3つの学習係数を持つモデルを定義し、学習させます。
batch_size = 129
epochs = 10 # 学習回数(適宜、大きくすること)
se = 0 # 乱数シード番号(任意の自然数、なんでもok)
print("\n ### モデル1: 学習係数=10^-1")
os.environ['PYTHONHASHSEED'] = str(se)
np.random.seed(se)
mm1 = Sequential()
mm1 = mod_lr(mm1, 10**-1)
mm1_lp = mm1.fit(x_train_cnn, y_train, batch_size=batch_size,
epochs=epochs, verbose=1, validation_split=0.1)
print("\n ### モデル2: 学習係数=10^-2")
os.environ['PYTHONHASHSEED'] = str(se)
np.random.seed(se)
mm2 = Sequential()
mm2 = mod_lr(mm2, 10**-2)
mm2_lp = mm2.fit(x_train_cnn, y_train, batch_size=batch_size,
epochs=epochs, verbose=1, validation_split=0.1)
print("\n ### モデル3: 学習係数=10^-3")
os.environ['PYTHONHASHSEED'] = str(se)
np.random.seed(se)
mm3 = Sequential()
mm3 = mod_lr(mm3, 10**-3)
mm3_lp = mm3.fit(x_train_cnn, y_train, batch_size=batch_size,
epochs=epochs, verbose=1, validation_split=0.1)
学習が終わったところで、acc(正答率)の推移を見てみましょう。loss(今回のモデルではクロスエントロピー)も大事ですが、最終的な評価は正答率で行うことが多いので、accだけをみます。
plt.figure()
plt.plot(mm1_lp.epoch, np.array(mm1_lp.history['acc']),label='Train $10^{-1}$', c="b", ls="-.")
plt.plot(mm2_lp.epoch, np.array(mm2_lp.history['acc']),label='Train $10^{-2}$', c="r", ls="-.")
plt.plot(mm3_lp.epoch, np.array(mm3_lp.history['acc']),label='Train $10^{-3}$', c="g", ls="-.")
plt.title("Training data")
plt.xlabel('Epoch')
plt.ylabel('Acc')
plt.grid()
plt.legend()
plt.figure()
plt.plot(mm1_lp.epoch, np.array(mm1_lp.history['val_acc']),label='Valid. $10^{-1}$', c="b", ls="-")
plt.plot(mm2_lp.epoch, np.array(mm2_lp.history['val_acc']),label='Valid. $10^{-2}$', c="r", ls="-")
plt.plot(mm3_lp.epoch, np.array(mm3_lp.history['val_acc']),label='Valid. $10^{-3}$', c="g", ls="-")
plt.title("Validation data")
plt.xlabel('Epoch')
plt.ylabel('Acc')
plt.grid()
plt.legend()
ということがわかりました。したがって、学習係数が0.001のモデルが良いことになります。もちろん、epochが10でいいわけがないですし、学習係数が0.0001や0.00001の方がいいかもしれません。本番の場合はもうちょっとepochを増やして、いろいろなパターンの学習係数を試すと良いでしょう。
良い学習係数がわかったところで、次は、良いepochを考えてみます。epochは大きければ大きいほど、教師データの正答率は高くなる傾向にあるものの、ときとして過学習を引き起こし、validation dataの性能低下をもたらします。そのため、良いepochの探索もとても大事になります。
このために、良い学習係数の探索で構築したモデルを再現させた上で、良いepochの探索を行います。必ずしも再現させる必要はありませんが、良い学習係数が0.001であるという事実は、その乱数シードの中だけで成立することかもしれないため、まったく同じモデルで各種のパラメータを最適化していくことが大事になります。
そういうわけで、良いepochの探索を始めます。ここではepochを500回、少し多めにとってみます。結果を見るとき、epochが1〜10のとき、学習係数の探索のときと同じ値を示しているかについても、注意深く見るようにしてください。違うなら、どこかで失敗しています。
epochs = 200 # 学習回数(適宜、大きくすること)
se = 0 # 乱数シード番号(任意の自然数、なんでもok)
print("\n ### モデル3: 学習係数=10^-3")
os.environ['PYTHONHASHSEED'] = str(se)
np.random.seed(se)
mm3 = Sequential()
mm3 = mod_lr(mm3, 10**-3)
mm3_lp = mm3.fit(x_train_cnn, y_train, batch_size=batch_size,
epochs=epochs, verbose=1, validation_split=0.1)
学習が終わったところで、学習過程を可視化してみます。
plt.figure()
plt.plot(mm3_lp.epoch, np.array(mm3_lp.history['acc']),label='Train $10^{-3}$', c="g", ls="-.")
plt.plot(mm3_lp.epoch, np.array(mm3_lp.history['val_acc']),label='Valid. $10^{-3}$', c="g", ls="-")
plt.xlabel('Epoch')
plt.ylabel('Acc')
plt.grid()
plt.legend()
Validation data の正答率(val_acc)が重要になります(学習に使用していないデータの正答率のため)。そして、val_accは、75%〜95%くらいの範囲にあることがわかります。また、epochが多い方がval_accが必ずしも高くないことが見て取れます。したがって、epoch=200のモデルが最適ではないことになります。そのため、val_accが最大となるepochを求めてみます。
val_acc = np.array(mm3_lp.history['val_acc'])
ep_argmax = np.argmax(val_acc)
best_epoch = ep_argmax + 1
print("最適なepoch: ", best_epoch)
print("そのときの正答率(val_acc)", val_acc[ep_argmax])
# 配列は0スタートだが、epochは1スタートのため、1を足す
print("\n最後のepoch: ", len(val_acc))
print("最後の正答率(val_acc)", val_acc[-1])
一番良いepochを採用した場合と、最後まで学習させた場合で、val_accの間に5%近くの差があることがわかりました。したがって、epochは133を採用すべきと判断できます。このepochを用いて、モデルを作ってみましょう。
epochs = best_epoch # 学習回数
se = 0 # 乱数シード番号(再現したいモデルと同じ値)
print("\n ### モデル3: 学習係数=10^-3")
os.environ['PYTHONHASHSEED'] = str(se)
np.random.seed(se)
mm3_best = Sequential()
mm3_best = mod_lr(mm3_best, 10**-3)
mm3_lp_best = mm3_best.fit(x_train_cnn, y_train, batch_size=batch_size,
epochs=epochs, verbose=1, validation_split=0.1)
ポイントは、モデルを再現するために、同じシードを設定することです。再現しない場合は、仮にモデルの構造が同じでも、最適なepochを採用する意味が消失します(validation dataの最大精度が保証されないため)。
最後に、テストデータを利用した場合の性能を求めてみます。これまで、validation dataの精度を見てきましたが、それは教師データから取り出された10%のデータのことでした。validation dataを使って良いパラメータを探してきましたが、逆に言えば、validation dataのみに適合するパラメータでしかありません。したがって、validation data以外、本当に最後まで一度も使わなかったデータで精度をチェックしてみます。
mm3_acc_test = mm3.evaluate(x_test_cnn, y_test, verbose=1, batch_size=32+1) # バッチサイズには2^n+1
print("epoch=200のモデルの正答率:")
print("・テストデータ: ", mm3_acc_test[1])
print("")
mm3best_acc_test = mm3_best.evaluate(x_test_cnn, y_test, verbose=1, batch_size=32+1) # バッチサイズには2^n+1
print("epoch=133のモデルの正答率:")
print("・テストデータ: ", mm3best_acc_test[1])
上が、epochを最適化していないモデル(epoch=2000)で、下がvalidation dataによりepochを最適化したモデル(epoch=133)です。これを見るとわかるように、epochを最適化した方が、最後まで一切手をつけなかったテストデータにおいても、性能が高いことがわかります。これは、validation dataに基づく最適化が、ある程度意味があることを示しています。
とはいえ、validation data とテストデータは、まったくの別物です。validation dataで最適化したからといって、テストデータでも良い性能が保証されるわけではありません。あくまで、その傾向があるだけです。
なります。たとえば、1月1日にモデルを作って、プログラムを閉じて、2月1日にもう一回モデルを作ったとします。このとき、乱数シードを正しく設定していれば、まったく同じモデルが出来上がります。ただ、バージョンを変えたりすると、どうなるかわかりません。
accは学習時の正答率です。ここで、レイヤの間にdropoutをつけた場合、そのレイヤの出力値が確率に依存して0に書き換えられます。これは過学習の抑止になりますが、学習時の「見かけ上の」正答率は下がります。本当はdropoutされない正答率を表示して欲しいところですが、残念ながら、そうなっていません。したがって、accが低い値を示します。一方、val_accは学習に使用しないデータの正答率ですから、dropoutにより出力値が書き換えられていません。このため、accが低く出て、val_accが高く出るわけです。モデル構造のところで、dropoutをコメントアウトすると、accがval_accよりも大幅に低いという現象がなくなることを確認できると思います。
50回学習させた後に、やっぱり100回くらい学習させればよかったなどと思うことがよくあります。この場合、最初から学習を開始すると、時間が勿体無いと思うことがあります。このときは、SequentialやCompileを経由させずに、もう一度fit関数を実行すると、途中から学習を開始してくれます(でも、これをすると何回学習したのかわからなくなる場合があります、あくまで試行錯誤時のテクニックと捉えてください)。
クラス0とクラス1の2択問題を考えます。このとき、クラス0が4500データ、クラス1が500データしかなかったとします。明らかにクラス1の方が少ないことになります。この場合、すべてクラス0と判断するダメなモデルがあったとしても、正答率が90%となってしまい、見かけ上うまくいっているように見えてしまいます。また、学習もうまくいかないことが多いです(クラス1と出力するパラメータを獲得しようとしても、結果的に正答率が減少してしまうことが多いため、適切な学習が行われにくい)。このようなデータを不均衡データと呼びます。この場合は、
のように、fit関数のclass_weight引数に、不均衡を解消するような重みを与えます。上の例では、学習時、クラス1の正答に対して8倍のボーナスがかかります。これで、学習が適切に行われる場合があります。ただし、これはあくまでソフト上の改善案であって、たとえ集めにくいという事情があったとしても、本来はクラス1のデータを集めることが真っ当な解決方法ですから、それを心に留めておきましょう。なお、この処理で表示されるaccやval_accなどは、重みづいたものではないので、注意してください。epochを重ねても正答率が90%のまま推移しますが、それをみても、すべてクラス0と回答することによる90%か、クラス1と回答する場合があった上での90%か、みてもわかりません。学習後、実際に自分の目で一つ一つの判定結果を見る必要があります。