人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

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()

f:id:aidiary:20161127184014p:plain

以前は、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_orderingtfのとき)。画像が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で実装できるのだろうか。

f:id:aidiary:20161127184154p:plain:w320 f:id:aidiary:20161127184158p:plain:w320

モデルの保存

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.jsonth(Theano)またはtf(TensorFlow)のどちらかを指定できる。実際は、バックエンドとは無関係に指定でき、バックエンドにTensorFlowを使ってthを指定してもよいし、バックエンドにTheanoを使ってtfを指定してもよい。デフォルトではtfのようだ。入力画像集合の各次元は

  • th(Theano)では(サンプル数, チャネル数, 画像の行数, 画像の列数)
  • tf(TensorFlow)では(サンプル数, 画像の行数, 画像の列数, チャネル数)

の並び順になる。例えば、image_dim_orderingtfの場合、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)が出てくる。畳み込み層やプーリング層も「層」なのでmodeladd()で追加できる。最後に多層パーセプトロンに入力するときはデータをフラット化する必要がある。4次元配列を1次元配列に変換するにはFlatten()という層を追加するだけでOK。ユニット数などは自動的に計算してくれる。

モデルを図示してみると下のようになる。

f:id:aidiary:20161120200651p:plain:w150(クリックで拡大)

どうやらこの図の4次元テンソルはimage_dim_orderingtfにしていてもthと同じ(サンプル数, 行数, 列数, チャネル数) になるようだ・・・ちゃんと image_dim_orderingの設定を見るようにしてほしいところ。

Convolution2Dborder_modevalidにすると出力画像は入力画像より小さくなる(2015/6/26)。一方、sameにすると自動的にパディングして出力画像が入力画像と同じサイズになるよう調整される。出力画像のサイズは計算式があるが、Kerasでは自動的に計算してくれている。

畳み込みニューラルネットのパラメータ数はフィルタのパラメータ数になる。例えば、最初の畳み込み層のパラメータ数は、32 \times 1 \times 3 \times 3 + 32 = 320 となる。32を足すのは各フィルタにあるスカラーのバイアス項。二つ目の畳み込み層のパラメータ数は、32 \times 32 \times 3 \times 3 + 32 = 9248となる。

残りは前回の多層パーセプトロンとまったく同じなので省略。

実行すると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エポックくらい学習させた重み。

f:id:aidiary:20161120200855p:plain:w300 f:id:aidiary:20161120200900p:plain:w300

次回は、定番のCIFAR-10のデータセットで畳み込みニューラルネットを試してみたい。

参考

ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装

ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装

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()を使うと以下のようなモデル形状のサマリが表示される。modeladd()で追加した順になっていることがわかる。Output ShapeのNoneはサンプル数を表している。dense_1 層のパラメータ数(重み行列のサイズのこと)は 784 \times 512 + 512 = 401920 となる。512を足すのはバイアスも重みに含めているため。ユーザがバイアスの存在を気にする必要はないが、裏ではバイアスも考慮されていることがパラメータ数からわかる。同様に dense_2 層のパラメータ数は 512 \times 512 + 512 = 262656 となる。同様に dense_3 層のパラメータ数は  512 \times 10 + 10 = 5130 となる。

____________________________________________________________________________________________________
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_utilplot() を使うとモデルを画像として保存できる。今はまだ単純なモデルなので summary() と同じでありがたみがないがもっと複雑なモデルだと図の方がわかりやすそう。

f:id:aidiary:20161108215909p:plain:w300

Early-stoppingによる収束判定

これまでの実験では、適当に nb_epoch を決めて固定の回数だけ訓練ループを回していたが過学習の危険がある。たとえば、このMNISTタスクで100回回したときの loss(訓練データの損失)と val_loss(バリデーションセットの損失)をプロットすると下のようになる(次節のhistoryでプロット)。

f:id:aidiary:20161108215917p:plain

この図からわかるように 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(訓練データセットの損失)だけだが、modelmetricsaccuracyを追加すると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の便利なツールをいろいろ使ってみた。

参考