Kerasによるデータ拡張
今回は、画像認識の精度向上に有効な データ拡張(Data Augmentation) を実験してみた。データ拡張は、訓練データの画像に対して移動、回転、拡大・縮小など人工的な操作を加えることでデータ数を水増しするテクニック。画像の移動、回転、拡大・縮小に対してロバストになるため認識精度が向上するようだ。
音声認識でも訓練音声に人工的なノイズを上乗せしてデータを拡張するテクニックがあるらしいのでそれの画像版みたいなものだろう。
ソースコード
- test_datagen2.py
- test_datagen3.py
- ImageDataGeneratorの使い方 #3 - GithubのIssuesでTODOを管理し始めた
ImageDataGenerator
Kerasには画像データの拡張を簡単に行うImageDataGenerator
というクラスが用意されている。今回は、この使い方をまとめておきたい。ドキュメントを調べるとこのクラスにはパラメータが大量にあって目が回る。一気に理解するのは難しいので一つずつ検証しよう。
keras.preprocessing.image.ImageDataGenerator(featurewise_center=False, samplewise_center=False, featurewise_std_normalization=False, samplewise_std_normalization=False, zca_whitening=False, rotation_range=0., width_shift_range=0., height_shift_range=0., shear_range=0., zoom_range=0., channel_shift_range=0., fill_mode='nearest', cval=0., horizontal_flip=False, vertical_flip=False, rescale=None, dim_ordering=K.image_dim_ordering())
まずは、適当な画像を1枚だけ入力し、各パラメータを1つだけ指定してどのような画像が生成されるか確認してみた。
if __name__ == '__main__': # 画像をロード(PIL形式画像) img = load_img(IMAGE_FILE) # numpy arrayに変換(row, col, channel) x = img_to_array(img) # print(x.shape) # 4次元テンソルに変換(sample, row, col, channel) x = np.expand_dims(x, axis=0) # print(x.shape) # パラメータを一つだけ指定して残りはデフォルト datagen = ImageDataGenerator(rotation_range=90) # 生成した画像をファイルに保存 draw_images(datagen, x, "result_rotation.jpg")
draw_images()
は、ジェネレータオブジェクト、画像の4次元テンソル、出力ファイル名を指定すると生成した画像をファイルに描画する自作関数。
def draw_images(datagen, x, result_images): # 出力先ディレクトリを作成 temp_dir = "temp" os.mkdir(temp_dir) # generatorから9個の画像を生成 # xは1サンプルのみなのでbatch_sizeは1で固定 g = datagen.flow(x, batch_size=1, save_to_dir=temp_dir, save_prefix='img', save_format='jpg') for i in range(9): batch = g.next() # 生成した画像を3x3で描画 images = glob.glob(os.path.join(temp_dir, "*.jpg")) fig = plt.figure() gs = gridspec.GridSpec(3, 3) gs.update(wspace=0.1, hspace=0.1) for i in range(9): img = load_img(images[i]) plt.subplot(gs[i]) plt.imshow(img, aspect='auto') plt.axis("off") plt.savefig(result_images) # 出力先ディレクトリを削除 shutil.rmtree(temp_dir)
next()
を呼び出すたびにbatch_size
個の画像が生成される。今回は、入力画像が1枚の画像なのでbatch_size
は1にしている。生成した9枚の画像は3x3のグリッドに配置し、ファイルに保存する。
早速やってみよう。入力画像はなんでもいいけど下のを入れた。ぞうさんかわいいね。
rotation_range
画像を指定角度の範囲でランダムに回転する。目が回るゾウ。
datagen = ImageDataGenerator(rotation_range=90) draw_images(datagen, x, "result_rotation.jpg")
width_shift_range
画像を水平方向にランダムに移動する。
datagen = ImageDataGenerator(width_shift_range=0.2) draw_images(datagen, x, "result_width_shift.jpg")
height_shift_range
画像垂直方向にランダムに移動する。
datagen = ImageDataGenerator(height_shift_range=0.2) draw_images(datagen, x, "result_height_shift.jpg")
shear_range
シアー変換をかける。Wikipediaをざっと見たが斜めに引き延ばすような画像変換みたい。
datagen = ImageDataGenerator(shear_range=0.78) # pi/4 draw_images(datagen, x, "result_shear.jpg")
zoom_range
画像をランダムにズームする。
datagen = ImageDataGenerator(zoom_range=0.5) draw_images(datagen, x, "result_zoom.jpg")
channel_shift_range
画像のチャンネルをランダムに移動する。RGBの値をランダムに加えるのかな?要調査。
datagen = ImageDataGenerator(channel_shift_range=100) draw_images(datagen, x, "result_channel_shift.jpg")
horizontal_flip
画像を水平方向にランダムに反転する。
datagen = ImageDataGenerator(horizontal_flip=True) draw_images(datagen, x, "result_horizontal_flip.jpg")
vertical_flip
画像を垂直方向にランダムに反転する。
datagen = ImageDataGenerator(vertical_flip=True) draw_images(datagen, x, "result_vertical_flip.jpg")
samplewise_center
サンプル平均を0にする。平均の正規化なんだろうけど平均0にしてimshow()
しても大丈夫なのかな?imshow()
でとりあえず画像出たけどあとで仕様を確認。
datagen = ImageDataGenerator(samplewise_center=True) draw_images(datagen, x, "result_samplewise_center.jpg")
samplewise_std_normalization
サンプルを標準偏差で割る。画像はなんか真っ黒になっちゃった。上のと組み合わせると平均0、標準偏差1になる正規化をかけられるようだ。これもアルゴリズムを要確認。
datagen = ImageDataGenerator(samplewise_std_normalization=True) draw_images(datagen, x, "result_samplewise_std_normalization.jpg")
ZCA白色化(ZCA whitening)
ZCA白色化の詳細はまだよくわかっていない。この変換は一枚の画像に対して適用するのではなく、画像集合に対して適用するようだ。なので入力は一枚の画像x
ではなく、X_train
など画像集合を渡す必要がある。また、ZCA白色化を使うときはdatagen.fit()
を使ってあらかじめ統計量を計算しておく必要があるので注意。試しにCIFAR-10の訓練データ画像に対してZCA白色化を適用し、適用後の画像がどうなるか調べた。
img_rows, img_cols, img_channels = 32, 32, 3 batch_size = 16 nb_classes = 10 # CIFAR-10データをロード (X_train, y_train), (X_test, y_test) = cifar10.load_data() # 画素値を0-1に変換 X_train = X_train.astype('float32') X_train /= 255.0 draw(X_train[0:batch_size], 'zca_whitening_before.png') # データ拡張 datagen = ImageDataGenerator(zca_whitening=True) datagen.fit(X_train) g = datagen.flow(X_train, y_train, batch_size, shuffle=False) X_batch, y_batch = g.next() print(X_batch.shape) print(y_batch.shape) draw(X_batch, 'zca_whitening_after.png')
適用前:
適用後:
訓練データに加えてこれらのデータ拡張で生成した画像を畳み込みニューラルネットに食わせると精度向上が期待できるようだ。長くなったので畳み込みニューラルネットワークとデータ拡張を組み合わせた実験は次回にまわそう。
参考
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件) を見る