目的
- Kerasの習得
- ニューラルネットワークのさらなる理解
- DNNによるクラス分類と手順を解説
概要
データセット:MNIST
ネットワーク:3層ニューラルネットワーク
実行環境:Google Colaboratory(GPU)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%config InlineBackend.figure_formats = {'png', 'retina'}
import os
from keras.models import Sequential, load_model
from keras.layers import Dense, Activation, Dropout
from keras.utils.np_utils import to_categorical
from keras.optimizers import Adam, Adagrad, RMSprop, SGD
from keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau, TensorBoard
from keras.datasets import mnist
データ取得
(X_train, y_train), (X_test, y_test) = mnist.load_data()
print(X_train.shape, y_train.shape, X_test.shape, y_test.shape)
Downloading data from https://s3.amazonaws.com/img-datasets/mnist.npz
11493376/11490434 [==============================] - 1s 0us/step
(60000, 28, 28) (60000,) (10000, 28, 28) (10000,)
画像1次元配列化
28 × 28の2次元配列を784次元ベクトルに変換
X_train = X_train.reshape(X_train.shape[0], 784)
X_test = X_test.reshape(X_test.shape[0], 784)
データ型の変換&正規化
X_train = X_train.astype('float32') / 255
X_test = X_test.astype('float32') / 255
one-hot 変換
# クラス数指定(すべてのクラスが含まれているとは限らないため)
num_classes = 10
y_train = to_categorical(y_train, num_classes = num_classes)
y_test = to_categorical(y_test, num_classes = num_classes)
モデル構築
- アーキテクチャ:architecture(ニューラルネットワークの構造)
- ネットワーク:architecture + optimizer(学習設定)
- モデル:architecture + optimizer + weight(重み、パラメータ)
ネットワーク構造
入力層(784) → 隠れ層(256) → 隠れ層(128) → 出力層(10)の3層ネットワーク
隠れ層やユニット数を多くすると、多彩な関数を表現できるが、
- 隠れ層が多くなると、入力層に近い重みを適切に更新するのが難しく、学習が進みにくくなる。
- ユニット数が多くなると、重要性の低い特徴量を抽出してしまい、過学習しやすくなる。
ネットワーク構造は理論よりも、経験に基づいて決定される傾向にある。
Dropout
概要
-
過学習の緩和
ユニットの一部が学習のたびにランダムに削除(正確には0で上書き)されることで、特定のニューロンの存在に依存されにくくなり、より汎用的な特徴を学習するようになる。 -
精度の向上
ランダムでニューロンを消去していくことで、毎回異なる複数のニューラルネットワークを学習。これにより アンサンブル学習(複数の認識器の組み合わせた学習手法)と同様の効果 が期待できる。
方針
- 小規模なニューラルネットワークを構築
- 徐々にネットワークのサイズを大きくしていく
- 過学習を起こしたら、例えば入力層や畳み込み層にDropout率0.2、全結合層に0.5を適用
- 各種パラメータを微調整
- 高度にチューニングされたニューラルネットワークを使用できるので、検証、評価の際はDropoutを使わない
活性化関数
全結合層では、入力を線形変換したものを出力するが、活性化関数を用いることで **非線形性をもたせる。**これにより、適切に学習が進めば 線形分離不可能なモデルでも分類することが可能。
# インスタンス作成
model = Sequential()
# 入力ユニット数:784
# 全結合層ユニット数:256
model.add(Dense(256, input_dim=784))
# 活性化関数
model.add(Activation("relu"))
# Dropout(rate:削除率)
model.add(Dropout(rate=0.2))
# 全結合層ユニット数:128
model.add(Dense(128))
model.add(Activation("relu"))
model.add(Dropout(rate=0.5))
# 出力ユニット数:10
model.add(Dense(10))
# 活性化関数:softmax(確率値に変換)
model.add(Activation("softmax"))
最適化関数(Optimizer)
重みの更新は、損失関数を各重みで微分した値を元に、更新すべき方向と、どの程度更新するかを決める。最適化関数は、微分によって求めた値を、 学習率、エポック数、過去の重みの更新量などを踏まえて、どのように重みの更新に反映するかを定める。
- 学習率:各層の重みの1回あたりの変更率
損失関数(誤差関数)
正解と予測の差(間違え具合)を評価する関数
損失関数を最小化するように、各層の重みを更新(誤差逆伝播法)
# 最適化関数(Ir:学習率)
optimizer = Adam(lr=0.001)
# モデルコンパイル(学習設定)
model.compile(
optimizer=optimizer,
loss="categorical_crossentropy",
metrics=["accuracy"]
)
モデル要約
model.summary()
- add 順に層を掲出
- Output Shape の None はバッチサイズを指し、可変であることを意味する
- 層数は重み(Param)がある層(Dense層やConv層)の数と一致
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 256) 200960
_________________________________________________________________
activation_1 (Activation) (None, 256) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 256) 0
_________________________________________________________________
dense_2 (Dense) (None, 128) 32896
_________________________________________________________________
activation_2 (Activation) (None, 128) 0
_________________________________________________________________
dropout_2 (Dropout) (None, 128) 0
_________________________________________________________________
dense_3 (Dense) (None, 10) 1290
_________________________________________________________________
activation_3 (Activation) (None, 10) 0
=================================================================
Total params: 235,146
Trainable params: 235,146
Non-trainable params: 0
Param の計算
- dense_1: 784 × 256 + 256 = 200960
- dense_2: 256 × 128 + 128 = 32896
- dense_3: 128 × 10 + 10 = 1290
コールバック関数の設定
EarlyStopping
監視する値の変化が停止した時に学習を終了
- monitor:監視する値(既定値:val_loss)
- patience:値が改善しなくなってからのエポック数
ModelCheckpoint
各エポック終了後にモデルを保存
※途中で接続が切れ、学習が中断する可能性がある場合に使用
- save_best_only:Trueの場合、最良モデルが上書きされない
- save_weights_only:Trueの場合、モデルの重みが保存。Falseの場合、モデル全体が保存
- period:チェックポイントの間隔(エポック数)
ReduceLROnPlateau
監視する値の改善が止まった時に学習率を減らす
- factor:学習率を減らす割合
# EarlyStopping
early_stopping = EarlyStopping(
monitor='val_loss',
patience=10,
verbose=1
)
# ModelCheckpoint
weights_dir='./weights/'
if os.path.exists(weights_dir)==False:os.mkdir(weights_dir)
model_checkpoint = ModelCheckpoint(
weights_dir + "val_loss{val_loss:.3f}.hdf5",
monitor='val_loss',
verbose=1,
save_best_only=True,
save_weights_only=True,
period=3
)
# reduce learning rate
reduce_lr = ReduceLROnPlateau(
monitor='val_loss',
factor=0.1,
patience=3,
verbose=1
)
# log for TensorBoard
logging = TensorBoard(log_dir="log/")
モデル学習
- verbose:学習進捗の表示設定
- epochs:同じデータセットでの反復学習回数
- batch_size:バッチサイズ(既定値:32)
- validation_split:validationデータ として抽出する trainデータ の割合
例えば0.2に設定すると、シャッフルされることなく、データの最後の20%を検証 - shuffle:各epoch で trainデータをシャッフル(既定値:True)
ただし validationデータ はシャッフルされない。
バッチサイズ
学習の際、1回あたりのモデルに渡すデータの数。
一度に複数のデータを渡した時、モデルはそれぞれのデータでの損失と損失関数の勾配を求めるが、重みの更新は、求めた勾配の平均を使って1回のみ行われる。
メリット
複数のデータを用いて重みの更新を行うことで、極端に変わったデータの影響をあまり受けない。並列計算が行えるので計算時間を短縮。
デメリット
極端な重みの更新が発生しなくなり、損失関数の局所解から抜け出せなくなる可能性がある。
癖の強いデータが多い時はバッチサイズを大きくする。
同じようなデータが多いときはバッチサイズを小さくする。
などバッチサイズをうまく調整する必要がある。
バッチサイズによる学習手法
- オンライン学習(確率的勾配法):バッチサイズを1とする手法
- バッチ学習(最急降下法):バッチサイズを全データ数とする手法
- ミニバッチ学習:これらの中間となる手法
反復学習
モデルの精度をあげるため同じ trainデータ を使って反復して学習させる。
学習回数を大きくすればモデルの精度が上がり続ける、というものではなく **正解率は途中から伸びなくなるだけでなく、繰り返し学習をすることで損失関数を最小化させようとして過学習が起こる。**そのため適切なタイミングで学習を打ち切ることが必要。
- エポック数:データセットが完全に通過した回数
- イテレーション回数:各ミニバッチにおいて、連続してパラメータが更新された回数=ステップ数
%%time
# モデルの学習
hist = model.fit(
X_train,
y_train,
verbose=1,
epochs=50,
batch_size = 32,
validation_split=0.2,
callbacks=[early_stopping, reduce_lr, logging]
)
Train on 48000 samples, validate on 12000 samples
Epoch 1/50
48000/48000 [==============================] - 12s 247us/step - loss: 0.3589 - acc: 0.8914 - val_loss: 0.1464 - val_acc: 0.9569
Epoch 2/50
48000/48000 [==============================] - 11s 229us/step - loss: 0.1653 - acc: 0.9516 - val_loss: 0.1053 - val_acc: 0.9681
Epoch 3/50
48000/48000 [==============================] - 11s 228us/step - loss: 0.1218 - acc: 0.9628 - val_loss: 0.1074 - val_acc: 0.9692
〜省略〜
Epoch 00018: ReduceLROnPlateau reducing learning rate to 1.0000000656873453e-06.
Epoch 19/50
48000/48000 [==============================] - 11s 227us/step - loss: 0.0181 - acc: 0.9943 - val_loss: 0.0814 - val_acc: 0.9812
Epoch 20/50
48000/48000 [==============================] - 11s 226us/step - loss: 0.0172 - acc: 0.9942 - val_loss: 0.0814 - val_acc: 0.9812
Epoch 21/50
48000/48000 [==============================] - 11s 228us/step - loss: 0.0179 - acc: 0.9944 - val_loss: 0.0814 - val_acc: 0.9812
Epoch 00021: ReduceLROnPlateau reducing learning rate to 1.0000001111620805e-07.
Epoch 22/50
48000/48000 [==============================] - 11s 226us/step - loss: 0.0180 - acc: 0.9939 - val_loss: 0.0814 - val_acc: 0.9812
Epoch 00022: early stopping
CPU times: user 4min 34s, sys: 38.2 s, total: 5min 12s
Wall time: 4min 2s
モデル保存
model.save の保存内容
- 再構築可能なモデルの構造
- ベストの重み
- compileの設定
- optimizerの状態(これにより学習を終えた時点から学習再開可能)
model_dir = './model/'
if os.path.exists(model_dir) == False:os.mkdir(model_dir)
model.save(model_dir + 'model.hdf5')
# optimizerのない軽量モデルを保存(学習や評価は不可だが、予測は可能)
model.save(model_dir + 'model-opt.hdf5', include_optimizer=False)
# ベストの重みのみ保存
# 上記2つのファイルにも重みは保存されているので、load_weights を使って重みを読み込むことが可能
model.save_weights(weights_dir + 'model_weight.hdf5')
学習曲線をプロット
plt.figure(figsize = (18,6))
# accuracy
plt.subplot(1, 2, 1)
plt.plot(hist.history["acc"], label = "acc", marker = "o")
plt.plot(hist.history["val_acc"], label = "val_acc", marker = "o")
#plt.xticks(np.arange())
#plt.yticks(np.arange())
plt.xlabel("epoch")
plt.ylabel("accuracy")
#plt.title("")
plt.legend(loc = "best")
plt.grid(color = 'gray', alpha=0.2)
# loss
plt.subplot(1, 2, 2)
plt.plot(hist.history["loss"], label = "loss", marker = "o")
plt.plot(hist.history["val_loss"], label = "val_loss", marker = "o")
#plt.xticks(np.arange())
#plt.yticks(np.arange())
plt.xlabel("epoch")
plt.ylabel("loss")
#plt.title("")
plt.legend(loc="best")
plt.grid(color = 'gray', alpha = 0.2)
plt.show()
val_acc が acc によりも精度が低い場合
過学習の傾向にある
val_acc、acc ともに精度が低い場合
未学習の傾向にある
val_acc が acc よりも精度が高い場合
- validationデータ が少ない
- 学習が足りていない可能性があるので、epoch数 を上げる
- 隠れ層のユニット数を増やす
グラフが激しく振動している場合
- 学習率が高い
- そもそもデータ数が少ない
学習始めの際、loss が val_loss よりも大きい理由
- Dropout や正則化は、学習の際、汎用性を保持しながら、重みを更新させることを主軸とした手法なので、検証や評価では使われない。
- 各epoch ごとの loss は trainデータの各batch の平均であり、最初と最後の batch の誤差は大きい。それに対して val_loss は各epoch の最後の状態のモデルを使って計算されるため、誤差が小さい。
過学習を防ぐ方法
- trainデータ を増やす(またはData Augmentation)
- モデルの複雑性を減らす(Dropout)
- Batch Normalization(バッチ正規化)を実装
- モデルの複雑性(重み)にペナルティを与える(正則化)
モデル評価
汎化精度の確認
score = model.evaluate(X_test, y_test, verbose=1)
print("evaluate loss: {0[0]}".format(score))
print("evaluate acc: {0[1]}".format(score))
10000/10000 [==============================] - 1s 61us/step
evaluate loss: 0.07180490057299171
evaluate acc: 0.9832
モデル読み込み
model = load_model(model_dir + 'model.hdf5')
# optimaizerがない軽量モデルの場合(予測のみに使用可能)
# model = load_model(model_dir + 'model-opt.hdf5', compile=False)
モデルによる予測
testデータ30件の画像と正解ラベルを出力
# testデータ30件の正解ラベル
# argmax() :多次元配列の中の最大値の要素を持つインデックスを取得
true_classes=np.argmax(y_test[0:30],axis=1)
# testデータ30件の画像と正解ラベルを出力
plt.figure(figsize=(16,6))
for i in range(30):
plt.subplot(3, 10, i+1)
# xy軸消去
plt.axis("off")
plt.title(true_classes[i])
# 28×28の2次元配列に変換
plt.imshow(X_test[i].reshape(28,28), "gray")
plt.show()
testデータ30件の画像と予測ラベル&予測確率を出力
# testデータ30件の予測ラベル
pred_classes=model.predict_classes(X_test[0:30])
# testデータ30件の予測確率
pred_probs=model.predict(X_test[0:30]).max(axis=1)
pred_probs = ['{:.4f}'.format(i) for i in pred_probs]
# testデータ30件の画像と予測ラベル&予測確率を出力
plt.figure(figsize=(16,6))
for i in range(30):
plt.subplot(3, 10, i+1)
# xy軸消去
plt.axis("off")
if pred_classes[i]==true_classes[i]:
plt.title(str(pred_classes[i])+'\n'+pred_probs[i])
else:
plt.title(str(pred_classes[i])+'\n'+pred_probs[i], color="red")
# 28×28の2次元配列に変換
plt.imshow(X_test[i].reshape(28,28), "gray")
plt.show()
testデータ30件でのモデル予測ではあるが、モデル評価のaccuracyが98%、lossが0.07だけあって、高い確率でクラス分類できていることがわかる。今度は、CIFAR-10を使って、CNNによるクラス分類を実装する。