Kerasによる畳み込みニューラルネットワークの実装
前回(2016/11/9)はMNISTの数字認識を多層パーセプトロンで解いたが、今回は畳み込みニューラルネットを使って解いてみた。このタスクもKerasの例題に含まれている。ソースコードを見れば大体何をやっているかつかめそうだけどポイントを少しまとめておく。畳み込みニューラルネットワーク自体の説明は、参考文献に挙げた「ゼロから作るDeep Learning」の7章が非常にわかりやすいのでおすすめ。
ソースコード: mnist.py
4次元テンソルのチャネル位置
畳み込みニューラルネットでは、入力する画像の形状を保つために画像集合を4次元テンソル(4次元配列)、すなわち画像のサンプル数、画像のチャネル数(白黒画像なら1、RGBのカラー画像なら3など)、画像の縦幅、画像の横幅で入力するのが一般的。Kerasでは、4次元テンソルの各次元の位置がimage_dim_ordering
によって変わる ので要注意。久々にKerasを動かしたら動かなくなっていてはまった。
image_dim_ordering
は、~/.keras/keras.json
でth
(Theano)またはtf
(TensorFlow)のどちらかを指定できる。実際は、バックエンドとは無関係に指定でき、バックエンドにTensorFlowを使ってth
を指定してもよいし、バックエンドにTheanoを使ってtf
を指定してもよい。デフォルトではtf
のようだ。入力画像集合の各次元は
th
(Theano)では(サンプル数, チャネル数, 画像の行数, 画像の列数)tf
(TensorFlow)では(サンプル数, 画像の行数, 画像の列数, チャネル数)
の並び順になる。例えば、image_dim_ordering
がtf
の場合、X_train[sample][row][col][channel]
で画像の画素値にアクセスできる。両方に対応する場合は、下のようなコードを書く必要がある。keras.json
の設定はkeras.backend
モジュールのimage_dim_ordering()
で取得できる。
from keras import backend as K # 画像集合を表す4次元テンソルに変形 # keras.jsonのimage_dim_orderingがthのときはチャネルが2次元目、tfのときはチャネルが4次元目にくる if K.image_dim_ordering() == 'th': X_train = X_train.reshape(X_train.shape[0], 1, img_rows, img_cols) X_test = X_test.reshape(X_test.shape[0], 1, img_rows, img_cols) input_shape = (1, img_rows, img_cols) else: X_train = X_train.reshape(X_train.shape[0], img_rows, img_cols, 1) X_test = X_test.reshape(X_test.shape[0], img_rows, img_cols, 1) input_shape = (img_rows, img_cols, 1)
MNISTの入力数は白黒画像なのでチャネル数は1である。
畳み込みニューラルネットの構築
畳み込み層が2つで、プーリング層が1つ、そのあとに多層パーセプトロンが続くシンプルな構成の畳み込みニューラルネットである。
def build_cnn(input_shape, nb_filters, filter_size, pool_size): model = Sequential() model.add(Convolution2D(nb_filters, filter_size[0], filter_size[1], border_mode='valid', input_shape=input_shape)) model.add(Activation('relu')) model.add(Convolution2D(nb_filters, filter_size[0], filter_size[1])) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=pool_size)) model.add(Dropout(0.25)) model.add(Flatten()) model.add(Dense(128)) model.add(Activation('relu')) model.add(Dropout(0.5)) model.add(Dense(nb_classes)) model.add(Activation('softmax')) return model
畳み込みニューラルネットでは、これまでの全結合層(Dense
)以外に畳み込み層(Convolution2D
)とプーリング層(MaxPooling2D
)が出てくる。畳み込み層やプーリング層も「層」なのでmodel
にadd()
で追加できる。最後に多層パーセプトロンに入力するときはデータをフラット化する必要がある。4次元配列を1次元配列に変換するにはFlatten()
という層を追加するだけでOK。ユニット数などは自動的に計算してくれる。
モデルを図示してみると下のようになる。
(クリックで拡大)
どうやらこの図の4次元テンソルはimage_dim_ordering
をtf
にしていてもth
と同じ(サンプル数, 行数, 列数, チャネル数) になるようだ・・・ちゃんと image_dim_ordering
の設定を見るようにしてほしいところ。
Convolution2D
のborder_mode
をvalid
にすると出力画像は入力画像より小さくなる(2015/6/26)。一方、same
にすると自動的にパディングして出力画像が入力画像と同じサイズになるよう調整される。出力画像のサイズは計算式があるが、Kerasでは自動的に計算してくれている。
畳み込みニューラルネットのパラメータ数はフィルタのパラメータ数になる。例えば、最初の畳み込み層のパラメータ数は、 となる。32を足すのは各フィルタにあるスカラーのバイアス項。二つ目の畳み込み層のパラメータ数は、となる。
残りは前回の多層パーセプトロンとまったく同じなので省略。
実行すると7エポックほどで収束し、精度は99%近く出る。
フィルタの可視化
最後に学習したフィルタを可視化してみた。1つ目の畳み込み層の重みのみ。フィルタは32なので手抜きで固定している。
def visualize_filter(model): # 最初の畳み込み層の重みを取得 # tf => (nb_row, nb_col, nb_channel, nb_filter) # th => (nb_filter, nb_channel, nb_row, nb_col) W = model.layers[0].get_weights()[0] # 次元を並べ替え if K.image_dim_ordering() == 'tf': # (nb_filter, nb_channel, nb_row, nb_col) W = W.transpose(3, 2, 0, 1) nb_filter, nb_channel, nb_row, nb_col = W.shape # 32個(手抜きで固定)のフィルタの重みを描画 plt.figure() for i in range(nb_filters): # フィルタの画像 im = W[i, 0] # 重みを0-255のスケールに変換 scaler = MinMaxScaler(feature_range=(0, 255)) im = scaler.fit_transform(im) plt.subplot(4, 8, i + 1) plt.axis('off') plt.imshow(im, cmap="gray") plt.show()
W
の次元もimage_dim_ordering
によって変わるようだ。th
だと(フィルタ数、チャネル数、行数、列数)となるのだが、tf
だと(行数、列数、チャネル数、フィルタ数)と逆になる。これは内部のアルゴリズムの実装による違いなのだろうか?混乱するなあ。個人的にはバックエンドがTensorFlowでもth
に統一した方が扱いやすいと思ったが、デフォルトはtf
なんだよなあ。
学習前と学習後の重みを比較すると学習前ではモザイクがランダムなのに対し、学習後の方が白と黒が組織化されて何らかの特徴をとらえていることがわかる。実際は、まったく重みが変わらないフィルタもあるようだが・・・EarlyStoppingを使うと数エポックで学習が終了してしまい、更新されないフィルタが多くなっていた。図は50エポックくらい学習させた重み。
次回は、定番のCIFAR-10のデータセットで畳み込みニューラルネットを試してみたい。
参考
- Handwritten Digit Recognition using Convolutional Neural Networks in Python with Keras
- How does border_mode work?
- How convolutional neural networks see the world
- 畳み込みニューラルネットワークの仕組み
ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装
- 作者: 斎藤康毅
- 出版社/メーカー: オライリージャパン
- 発売日: 2016/09/24
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (15件) を見る
KerasでMNIST
今回は、KerasでMNISTの数字認識をするプログラムを書いた。このタスクは、Kerasの例題にも含まれている。今まで使ってこなかったモデルの可視化、Early-stoppingによる収束判定、学習履歴のプロットなども取り上げてみた。
ソースコード: mnist.py
MNISTデータのロードと前処理
MNISTをロードするモジュールはKerasで提供されているので使った。
from keras.datasets import mnist from keras.utils import np_utils # MNISTデータのロード (X_train, y_train), (X_test, y_test) = mnist.load_data() # 画像を1次元配列化 X_train = X_train.reshape(60000, 784) X_test = X_test.reshape(10000, 784) # 画素を0.0-1.0の範囲に変換 X_train = X_train.astype('float32') X_test = X_test.astype('float32') X_train /= 255 X_test /= 255 print(X_train.shape[0], 'train samples') print(X_test.shape[0], 'test samples') # one-hot-encoding Y_train = np_utils.to_categorical(y_train, nb_classes) Y_test = np_utils.to_categorical(y_test, nb_classes)
KerasでダウンロードしたMNISTのデフォルトの形状は (60000, 28, 28)
なので (60000, 784)
に reshape
する。各サンプルが784次元ベクトルになるようにしている。画像データ(X
)は0-255の画素値が入っているため0.0-1.0に正規化する。クラスラベル(y
)は数字の0-9が入っているためone-hotエンコーディング型式に変換する。
nb_classes = 10
を省略するとy_test
に入っているラベルから自動的にクラス数を推定してくれるようだが、必ずしも0-9のラベルすべてがy_test
に含まれるとは限らないためnb_classes = 10
を指定したほうが安全のようだ。
モデルの可視化
from keras.models import Sequential from keras.layers.core import Dense, Dropout, Activation from keras.optimizers import Adam from keras.utils.visualize_util import plot def build_multilayer_perceptron(): model = Sequential() model.add(Dense(512, input_shape=(784,))) model.add(Activation('relu')) model.add(Dropout(0.2)) model.add(Dense(512)) model.add(Activation('relu')) model.add(Dropout(0.2)) model.add(Dense(10)) model.add(Activation('softmax')) return model # 多層ニューラルネットワークモデルを構築 model = build_multilayer_perceptron() # モデルのサマリを表示 model.summary() plot(model, show_shapes=True, show_layer_names=True, to_file='model.png') # モデルをコンパイル model.compile(loss='categorical_crossentropy', optimizer=Adam(), metrics=['accuracy'])
隠れ層が2つある多層パーセプトロンを構築した。活性化関数には relu
。また、過学習を防止するテクニックである Dropout
を用いた。Dropoutも層として追加する。
model.summary()
を使うと以下のようなモデル形状のサマリが表示される。model
にadd()
で追加した順になっていることがわかる。Output ShapeのNone
はサンプル数を表している。dense_1
層のパラメータ数(重み行列のサイズのこと)は となる。512を足すのはバイアスも重みに含めているため。ユーザがバイアスの存在を気にする必要はないが、裏ではバイアスも考慮されていることがパラメータ数からわかる。同様に dense_2
層のパラメータ数は となる。同様に dense_3
層のパラメータ数は となる。
____________________________________________________________________________________________________ Layer (type) Output Shape Param # Connected to ==================================================================================================== dense_1 (Dense) (None, 512) 401920 dense_input_1[0][0] ____________________________________________________________________________________________________ activation_1 (Activation) (None, 512) 0 dense_1[0][0] ____________________________________________________________________________________________________ dropout_1 (Dropout) (None, 512) 0 activation_1[0][0] ____________________________________________________________________________________________________ dense_2 (Dense) (None, 512) 262656 dropout_1[0][0] ____________________________________________________________________________________________________ activation_2 (Activation) (None, 512) 0 dense_2[0][0] ____________________________________________________________________________________________________ dropout_2 (Dropout) (None, 512) 0 activation_2[0][0] ____________________________________________________________________________________________________ dense_3 (Dense) (None, 10) 5130 dropout_2[0][0] ____________________________________________________________________________________________________ activation_3 (Activation) (None, 10) 0 dense_3[0][0] ==================================================================================================== Total params: 669706
keras.utils.visualize_util
の plot()
を使うとモデルを画像として保存できる。今はまだ単純なモデルなので summary()
と同じでありがたみがないがもっと複雑なモデルだと図の方がわかりやすそう。
Early-stoppingによる収束判定
これまでの実験では、適当に nb_epoch
を決めて固定の回数だけ訓練ループを回していたが過学習の危険がある。たとえば、このMNISTタスクで100回回したときの loss
(訓練データの損失)と val_loss
(バリデーションセットの損失)をプロットすると下のようになる(次節のhistory
でプロット)。
この図からわかるように loss
はエポックが経つにつれてどんどん下がるが、逆に val_loss
が上がっていくことがわかる。これは、訓練データセットに過剰にフィットしてしまうために未知のデータセットに対する予測性能が下がってしまう過学習を起こしていることを意味する。機械学習の目的は未知のデータセットに対する予測性能を上げることなので過学習はダメ!
普通は訓練ループを回すほど性能が上がりそうだけど、先に見たように訓練ループを回せば回すほど性能が悪化する場合がある。そのため、予測性能が下がる前にループを打ち切りたい。val_loss
をプロットして目視でどこで打ち切るか判断することもできるが、それを自動で判断してくれるのがEarly-stoppingというアルゴリズム。
If the model’s performance ceases to improve sufficiently on the validation set, or even degrades with further optimization, then the heuristic implemented here gives up on much further optimization. http://deeplearning.net/tutorial/gettingstarted.html#early-stopping
Theanoでは自分で実装(2015/5/26)したが、Kerasではコールバック関数としてEarlyStopping
が実装されているためfit()
のcallbacks
オプションに設定するだけでよい(うまい実装だね~)。EarlyStopping
を使うには必ずバリデーションデータセットを用意する必要がある。fit()
のオプションでvalidation_data
を直接指定することもできるが、validation_split
を指定することで訓練データの一部をバリデーションデータセットとして使える。
Keras examplesもそうだが、テストデータセットをバリデーションデータセットとして使うのは本来ダメらしい。バリデーションデータセットとテストデータセットは分けたほうがよい。何となく直感的にダメそうというのはわかるのだがどうしてかは実はよく知らない。
# Early-stopping early_stopping = EarlyStopping(patience=0, verbose=1) # モデルの訓練 history = model.fit(X_train, Y_train, batch_size=batch_size, nb_epoch=nb_epoch, verbose=1, validation_split=0.1, callbacks=[early_stopping])
EarlyStopping
を導入するとわずか5エポックくらいで訓練が打ち切られる。確かにここら辺からval_loss
が上がってくるのでよいのかもしれないが少し早すぎかもしれない。EarlyStopping
にはpatience
というパラメータを指定できる。デフォルトでは0だが、patience
を上げていくと訓練を打ち切るのを様子見するようになる。通常はデフォルトでOKか?
verboseが機能していない?どこに表示されるのだろう?
学習履歴のプロット
fit()
の戻り値である history
に学習経過の履歴が格納されている。このオブジェクトを使えばいろいろな経過情報をプロットできる。デフォルトでは、loss
(訓練データセットの損失)だけだが、model
のmetrics
にaccuracy
を追加するとacc
(精度)が、バリデーションデータセットを使うとval_loss
(バリデーションデータセットの損失)やval_acc
(バリデーションデータセットの精度)が自動的に追加される。さっきの図はこのデータを使ってmatplotlibで書いた。ユーザ独自のメトリクスも定義できるようだ。
def plot_history(history): # print(history.history.keys()) # 精度の履歴をプロット plt.plot(history.history['acc']) plt.plot(history.history['val_acc']) plt.title('model accuracy') plt.xlabel('epoch') plt.ylabel('accuracy') plt.legend(['acc', 'val_acc'], loc='lower right') plt.show() # 損失の履歴をプロット plt.plot(history.history['loss']) plt.plot(history.history['val_loss']) plt.title('model loss') plt.xlabel('epoch') plt.ylabel('loss') plt.legend(['loss', 'val_loss'], loc='lower right') plt.show() # 学習履歴をプロット plot_history(history)
今回は、Kerasの便利なツールをいろいろ使ってみた。
参考
Kerasによる多クラス分類(Iris)
今回は、機械学習でよく使われるIrisデータセットを多層パーセプトロンで分類してみた(ありがち)。Irisデータセットのクラスラベルは3つ(setosa, versicolor, virginica)あるので前回までと違って多クラス分類になる。短いプログラムなので全部載せてポイントだけまとめておこう。
ソースコード
import numpy as np from sklearn import datasets from sklearn.model_selection import train_test_split from keras.models import Sequential from keras.layers.core import Dense, Activation from keras.utils import np_utils from sklearn import preprocessing def build_multilayer_perceptron(): """多層パーセプトロンモデルを構築""" model = Sequential() model.add(Dense(16, input_shape=(4, ))) model.add(Activation('relu')) model.add(Dense(3)) model.add(Activation('softmax')) return model if __name__ == "__main__": # Irisデータをロード iris = datasets.load_iris() X = iris.data Y = iris.target # データの標準化 X = preprocessing.scale(X) # ラベルをone-hot-encoding形式に変換 # 0 => [1, 0, 0] # 1 => [0, 1, 0] # 2 => [0, 0, 1] Y = np_utils.to_categorical(Y) # 訓練データとテストデータに分割 train_X, test_X, train_Y, test_Y = train_test_split(X, Y, train_size=0.8) print(train_X.shape, test_X.shape, train_Y.shape, test_Y.shape) # モデル構築 model = build_multilayer_perceptron() model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) # モデル訓練 model.fit(train_X, train_Y, nb_epoch=50, batch_size=1, verbose=1) # モデル評価 loss, accuracy = model.evaluate(test_X, test_Y, verbose=0) print("Accuracy = {:.2f}".format(accuracy))
ポイント
irisデータは
sklearn.datasets.load_iris()
でダウンロードできる。よく使う標準データセットはメソッドが用意されていて便利irisのラベルは文字列だがsklearnのデータセットでは、0, 1, 2のように数値ラベルに変換されている。これをニューラルネットで扱いやすいone-hotエンコーディング型式に変換する。上のコメントにも書いたようにone-hotエンコーディングは、特定のユニットのみ1でそれ以外は0のようなフォーマットのこと。この変換は、
keras.utils.np_utils
のto_categorical()
に実装されている訓練データとテストデータは、
sklearn.model_selection
のtrain_test_split()
を使う。訓練データが8割、テストデータが2割になるように分割した。少し古いscikit-learnだとsklearn.cross_validation
に実装されているけどこのモジュールはすでにdeprecated。モデル構築は
build_multilayer_perceptron()
という独自関数を用意した。モデルが複雑になると関数化したほうがわかりやすそう。入力層が4ユニット、隠れ層が16ユニット、出力層が3ユニットの多層パーセプトロンを構築した多クラス分類の場合は、損失関数に
categorical_crossentropy
を指定する
必要な前処理メソッドはたいていscikit-learnに実装されているので自分で実装する前に探してみたほうがよさそう。
5回くらい実行してテストデータの精度を求めると97%、93%、100%、93%、97%となった。どのサンプルがテストデータに選ばれるかによって精度が変わるようだ。分類境界あたりのサンプルがテストデータとして選ばれると予測が難しくなり精度が下がりそうなのは直感的にわかる。
こういう場合は、何度か実行して平均を求めるのがセオリーかな。でも巨大なデータセットの学習だと何日もかかるケースがありそうだし、そんなに何回もデータセットの分割を変えて実行はできないなあ。どうするのが一般的なんだろうか?