KerasでCIFAR-10の一般物体認識
今回は、畳み込みニューラルネットを使ってCIFAR-10(2015/10/14)の一般物体認識をやってみた。以前、Chainerでやった(2015/11/8)のをKerasで再実装した。
これもKerasの例題に含まれている。このスクリプトでは、データ拡張(Data Augmentation)も使っているがこれはまた別の回に取り上げよう。
ソースコード:cifar10.py
CIFAR-10
CIFAR-10は32x32ピクセル(ちっさ!)のカラー画像のデータセット。クラスラベルはairplane, automobile, bird, cat, deer, dog, frog, horse, ship, truckの10種類で訓練用データ5万枚、テスト用データ1万枚から成る。
まずは描画してみよう。
import numpy as np import matplotlib.pyplot as plt from scipy.misc import toimage from keras.datasets import cifar10 if __name__ == '__main__': # CIFAR-10データセットをロード (X_train, y_train), (X_test, y_test) = cifar10.load_data() print(X_train.shape, y_train.shape) print(X_test.shape, y_test.shape) # 画像を描画 nclasses = 10 pos = 1 for targetClass in range(nclasses): targetIdx = [] # クラスclassIDの画像のインデックスリストを取得 for i in range(len(y_train)): if y_train[i][0] == targetClass: targetIdx.append(i) # 各クラスからランダムに選んだ最初の10個の画像を描画 np.random.shuffle(targetIdx) for idx in targetIdx[:10]: img = toimage(X_train[idx]) plt.subplot(10, 10, pos) plt.imshow(img) plt.axis('off') pos += 1 plt.show()
以前は、CIFAR-10のホームページから直接ダウンロードしたが、Kerasではkeras.datasets.cifar10
モジュールを使えば勝手にダウンロードして使いやすい形で提供してくれる。
# 入力画像の次元 img_rows, img_cols = 32, 32 # チャネル数(RGBなので3) img_channels = 3 # CIFAR-10データをロード # (nb_samples, nb_rows, nb_cols, nb_channel) = tf (X_train, y_train), (X_test, y_test) = cifar10.load_data() # ランダムに画像をプロット plot_cifar10(X_train, y_train, result_dir) # 画素値を0-1に変換 X_train = X_train.astype('float32') X_test = X_test.astype('float32') X_train /= 255.0 X_test /= 255.0 # クラスラベル(0-9)をone-hotエンコーディング形式に変換 Y_train = np_utils.to_categorical(y_train, nb_classes) Y_test = np_utils.to_categorical(y_test, nb_classes)
X_train
は (50000, 32, 32, 3) の4次元テンソルで与えられる(image_dim_ordering
がtf
のとき)。画像が50000枚、行数が32、列数が32、チャンネルが3(RGB)であることを意味する。配列には0-255の画素値が入っているため255で割って0-1に正規化する。y_train
は (50000, 1) の配列で与えられる。クラスラベルは0-9の値が入っているのでNNで使いやすいようにone-hotエンコーディング形式に変換する。
CNNの構築
少し層が深いCNNを構成してみた。
INPUT -> ((CONV->RELU) * 2 -> POOL) * 2 -> FC
畳み込み層(CONV)、ReLU活性化関数(RELU)を2回繰り返してプーリング層(POOL)を1セットとしてそれを2セット繰り返した後に全結合層(FC)を通して分類するという構成。
# CNNを構築 model = Sequential() model.add(Convolution2D(32, 3, 3, border_mode='same', input_shape=X_train.shape[1:])) model.add(Activation('relu')) model.add(Convolution2D(32, 3, 3)) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.25)) model.add(Convolution2D(64, 3, 3, border_mode='same')) model.add(Activation('relu')) model.add(Convolution2D(64, 3, 3)) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.25)) model.add(Flatten()) model.add(Dense(512)) model.add(Activation('relu')) model.add(Dropout(0.5)) model.add(Dense(nb_classes)) model.add(Activation('softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) # モデルのサマリを表示 model.summary() plot(model, show_shapes=True, to_file=os.path.join(result_dir, 'model.png'))
モデルの訓練
今回は、Early-stoppingは使わずに固定で100エポック回した。Early-stoppingを使うとすぐに収束扱いされてしまったため。patienceを変更すればいいんだけど使いどころがちょっと難しいかも。
# 訓練 history = model.fit(X_train, Y_train, batch_size=batch_size, nb_epoch=nb_epoch, verbose=1, validation_split=0.1) # 学習履歴をプロット plot_history(history, result_dir)
損失と精度をプロットすると下のような感じ。validation dataによるテスト損失は30エポックくらいからほとんど変化ないのでもっと早く止めてもよかったかも。10エポックごとに学習途中のモデルを保存しておきたいところだけどどうやるのかな?callbackで実装できるのだろうか。
モデルの保存
CNNは学習にものすごい時間がかかる(GPUを使わないと特に)ので学習結果のモデルはファイルに保存するようにした。Kerasではモデルの形状(model.json)と学習した重み(model.h5)を別々に保存するようになっている。PythonなのにJSONを使うところがナウい。h5というのはHDF5 (Hierarchical Data Format)というバイナリフォーマットのようだ。ときどき見かけるけど使ったことがなかった。
# 学習したモデルと重みと履歴の保存 model_json = model.to_json() with open(os.path.join(result_dir, 'model.json'), 'w') as json_file: json_file.write(model_json) model.save_weights(os.path.join(result_dir, 'model.h5'))
ちなみにファイルからモデルを読み込むときは
model_file = os.path.join(result_dir, 'model.json') weight_file = os.path.join(result_dir, 'model.h5') with open(model_file, 'r') as fp: model = model_from_json(fp.read()) model.load_weights(weight_file) model.summary()
とすればよい。
モデルの評価
テストデータで評価すると 約80% の分類精度が得られた。
# モデルの評価 loss, acc = model.evaluate(X_test, Y_test, verbose=0) print('Test loss:', loss) print('Test acc:', acc)
次回は画像データの拡張についてまとめたい。
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の便利なツールをいろいろ使ってみた。