logo logo

February 08, 2019 03:26

ディープラーニングの旅③~cifar10をCNNで~

導入

前回まではcifar10データセットを元に、多層パーセプトロンで分類を行い、
テストデータの精度で50%という数字に到達しました。
今回は画像処理系でのブレークスルーである畳み込みニューラルネット(以下CNN)をkerasで簡易に実装します。

CNNの歴史概観

CNNは人間の視覚野中の受容野の神経系に関する、生物的な研究に端を発します。
1980年に福島邦彦のネオコグニトロンという形でモデル化されました。このモデル化は様々な派生を生みつつ、
工学的な応用への道を開くものでした。大まかに局所的な図形パターンに反応する畳み込み層と、
空間的な並進に対してロバストにするためのプーリング層という2層がセットにされ、画像信号に対して特徴的なパターンを
捉える仕組みとなっています。
ニューラルネットのトレーニング手法の一つである誤差逆伝搬法と出会い、ビッグデータ処理基盤やGPUによる高速処理、
2012年の画像分類コンテストでの鮮烈な活躍など、様々な追い風を受けて、今日のディープラーニングブームの火付け役となりました。
画像や動画の認識系タスクのみならず、時系列データやある種の自然言語処理でも有用なケースがあります。
CNNの理論的側面についてはいつか別にまとめたいと思っていますが、以下では使ってみる/実装してみるを優先します。

データ整形とCNNモデルの構成

CNNに入力する前のデータ整形から解説していきます。

cifar10データセットが既にある場合

これまでの記事(1, 2)で解説しているように、
既にcifar10の画像をDL済みの場合、それを土俵にのせるところからスタートしましょう。

import numpy as np
import matplotlib.pyplot as plt
import pickle

def unpickle(f):
    fo = open(f, 'rb')
    d = pickle.load(fo, encoding='latin1')
    fo.close()
    return d

# データの読み込み
b1 = unpickle("../Datasets/cifar-10-batches-py/data_batch_1")
b2 = unpickle("../Datasets/cifar-10-batches-py/data_batch_2")
b3 = unpickle("../Datasets/cifar-10-batches-py/data_batch_3")
b4 = unpickle("../Datasets/cifar-10-batches-py/data_batch_4")
b5 = unpickle("../Datasets/cifar-10-batches-py/data_batch_5")
test = unpickle("../Datasets/cifar-10-batches-py/test_batch") 

# 学習データと訓練データを入力X、正解ラベルyとして定義する
# np.concatenateでb1~b5を結合
X_train = np.concatenate((b1['data'], b2['data'], b3['data'], b4['data'], b5['data']), axis=0)
X_test  = np.array(test['data'])
y_train = np.concatenate((b1['labels'], b2['labels'], b3['labels'], b4['labels'], b5['labels']), axis=0)
y_test  = np.array(test['labels'])

多層パーセプトロンところでも同じ処理をしましたが、データを正規化します。これによってデータが0~1に収まるようになります。

from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Activation
from keras.optimizers import SGD
from keras.utils import np_utils
import matplotlib.pyplot as plt

np.random.seed(1)

X_train = X_train.astype('float32')
X_test = X_test.astype('float32')
X_train /= 255.0
X_test /= 255.0

y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)

現時点のX_trainのテンソル型は(50000, 3072)になっています。
これは32pix x 32pix x 3 channel(RGBの色強度それぞれに対応)のデータが1次元に引き伸ばされて
3072次元のデータが50000サンプルある状態です。
CNNにカラー画像を入力したい場合は(sample数, x方向画素, y方向画素, チャンネル数)にする必要があります。
以下の2行で実行できます。

X_train_ = np.array([x.reshape(3,32,32).transpose(1,2,0) for x in X_train])
X_test_  = np.array([x.reshape(3,32,32).transpose(1,2,0) for x in X_test])

以上でCNNに入力するためのデータ準備が完了しました。

kerasのデータセットを利用する

ここまでの処理はデータ整形の練習として行いましたが、kerasがインストールされていれば
簡単に行えます。

from keras.datasets import cifar10

(X_train_, y_train_), (X_test_, y_test) = cifar10.load_data()

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)

CNNモデル(ベース)の構築

モデルはこちらのものをベースにします。
KerasでCIFAR-10の一般物体認識

畳み込み層→プーリング層→畳み込み層→プーリング層→全結合層→全結合層(出力層)
という形でCNNの火付けに一役買ったLeCunのLeNet-5よりも少しだけ複雑なモデルかと思われます。
ベースラインにするにもお手本にするにもちょうどよいと思いました。
CNNといえでも使って見る分には、多層パーセプトロンのときと同じように層を積み重ねていけばよいだけなので恐れる必要はありません。
記事元ではDropout層が挿入されていますが、まずはこれを知らないものとして、ナイーブに以下のような構成としました。

from keras.layers.convolutional import Conv2D
from keras.layers.pooling import MaxPool2D
from keras.layers.core import Dense,Activation,Dropout,Flatten

model = Sequential()
model.add(Conv2D(32,(3,3),padding='same',input_shape=(32,32,3)))
model.add(Activation('relu'))
model.add(Conv2D(32,(3,3),padding='same'))
model.add(Activation('relu'))
model.add(MaxPool2D(pool_size=(2,2)))

model.add(Conv2D(64,(3,3),padding='same'))
model.add(Activation('relu'))
model.add(Conv2D(64,(3,3),padding='same'))
model.add(Activation('relu'))
model.add(MaxPool2D(pool_size=(2,2)))

model.add(Flatten())
model.add(Dense(512))
model.add(Activation('relu'))
model.add(Dense(10,activation='softmax'))

model.compile(loss='categorical_crossentropy', optimizer=SGD(lr=0.1), metrics=["accuracy"])

# 構成したモデルの概要を出力
model.summary()

history = model.fit(X_train_, y_train, validation_data=(X_test_,y_test), batch_size=100, epochs=40, verbose=True)

結果をプロットするための関数を定義しておきます。
実際の解析でも、モデルを複数構築したりチューニングしたりするので、結果を表示するための関数を
作っておくと重宝します。今回は損失関数(loss)と精度(accuracy)を訓練データとテストデータそれぞれについて
エポック毎にプロットしていきます。

def plot_history(history):
    fig = plt.figure(figsize=(10, 4))
    plt.subplot(1, 2, 1)
    plt.plot(history.history['loss'], label="train", marker="o", markersize=4)
    plt.plot(history.history['val_loss'], label="test", marker="o", markersize=4)
    plt.xlabel('Epochs')
    plt.ylabel('Loss')
    plt.legend()
    plt.grid()
    plt.subplot(1, 2, 2)
    plt.plot(history.history['acc'], label="train", marker="o", markersize=4)
    plt.plot(history.history['val_acc'], label="test", marker="o", markersize=4)
    plt.xlabel('Epochs')
    plt.ylabel('Accuracy')
    plt.legend()
    plt.grid()
    plt.show()

そしてモデルの学習履歴であるところのhistoryを引数に、
plot_history(history)で結果をプロットしてみます。

損失関数と精度

テストデータでは約70%の精度となりました。
前回の多層パーセプトロンの精度から一気に20%ほどアップしました。
一方、気になるのは訓練データとテストデータの成績の乖離です。
精度に関しては、訓練データはほぼ100%に到達しています。
これがいわゆる過学習という状態で、このCNNモデルは訓練データにオーバーフィッティング
しており、初見のテストデータに上手く汎化できていない状態です。
このような状況は色々な要因が複合的に効いており、また対処法も多岐に渡ります。
教科書的な有名処だと、L1罰則L2罰則といった、モデルの自由度に制限をかける方法や、
ネットワークに対するドロップアウトなどがありますが、日進月歩の世界で様々な手法が提案されています。
ここではドロップアウトを試して、比較検討してみたいと思います。

過学習を抑制するためのドロップアウト

ドロップアウト

ニューラルネットワークの過学習対策は多岐にわたりますが、ドロップアウトはその中でも効果が得やすく、
また実装も用意なために頻繁に使用されています(2019年現在)。
最近ではBatch Normalizationがドロップアウトにとってかわる手法として流行していますので、
後々そちらも比較実験してみたいと思っています。
ドロップアウトでは、学習過程で上図のようにネットワークの一部のニューロンを学習途中でランダムな確率pで残し、
それ以外をなかったかのように消してしまいます。
本稿でも行っているミニバッチ学習の場合であれば、各バッチ毎にランダムでニューロンを消してしまいます。
そして推論時には残した割合pを乗じて順方向の計算を行います。cf. 深層学習

なぜドロップアウトは有効なのか?

直感的になぜこの手法が有効なのか考えてみます。
そもそも過学習がなぜ起こるかということを振り返ると、パラメータの自由度が高すぎて、個々の訓練データに
過剰に合わせこんでしまうことが原因でした。そこで敢えてパラメータの自由度を制限することが考えられます。
パラメータの自由度とは、動ける範囲やその数ですが、ドロップアウトはL1罰則やL2罰則で知られる変数の範囲というよりは、数を制限してしまう方策です。最終的にはそれらを合算して使っているので、パラメータ数が多く、モデルの表現力が高いという利点ともうまくバランスさせています。

学校の生徒全員で、無人島で宝探しを行うゲームを考えてみましょう。
無人島には無数の財宝が散らばっていますが、それらはある場所は多く、
ある場所は少ない傾向にあります。事前にその傾向はわかりません。
また1時間は練習時間で、生徒は自由に動きまれ、本番では練習時間で最終的に個々人が決めた持ち場を中心に穴掘りをします。
あなたは学校の校長先生で、学生の探索方策をある程度制御でき、時間内で学校全体の財宝発掘数を最大化したいとします。
さて、何も制御しない状態ではスタートの合図から誰かが宝を見つけるたびに、学生はその場所に集まっていきます。
宝はある箇所に集中しているので、これはリーズナブルな方策ですが、たまたま少ないところを掘り当てた場合は悲惨です。
運悪く財宝の少ない箇所に多くの学生が集まってしまいます。小学生の団子サッカー状態です。
また集中しすぎれば制限時間内で探索しない箇所も増えてしまいます。これがまさに過学習の状態です。
単純な方策は、生徒の持ち場を面積レベルで決めて、その中を動くように制限することです。
誰かが宝を発見したときは、持ち場の範囲内で、その方向に少し近寄るイメージです。
これはL1罰則やL2罰則のコンセプトに近い方策と言えます。個々人の自由な行き来は制限されますが、
無人島をくまなくカバーできる可能性が高まります。
一方でドロップアウト法はもう少し複雑です。生徒を6グループに分け、各グループ10分ずつの練習時間を割きます。
各10分では、人数が少ない分、各々が財宝を発見できる可能性がたかく、どこかに集中せず分散する効果が期待できます。
これを全グループで行います。最終的な本番では各グループ、各自がそれぞれの練習時間内で財宝が眠っていそうな
箇所を中心に、配置されます。これによって個々人単位の練習時間は減ってしまうものの、濃密な10分間で練習を終え、
本番時には数の利点も活かすことができます。

ドロップアウトの比較実験

論より実験ということで、これまでのモデルにドロップアウトの仕組みを実装してあげましょう。
kerasの場合、これまた簡単です。

model = Sequential()

model.add(Conv2D(32, 3, 3, border_mode='same', input_shape=X_train_.shape[1:]))
model.add(Activation('relu'))
model.add(Conv2D(32, 3, 3))
model.add(Activation('relu'))
model.add(MaxPool2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Conv2D(64, 3, 3, border_mode='same'))
model.add(Activation('relu'))
model.add(Conv2D(64, 3, 3))
model.add(Activation('relu'))
model.add(MaxPool2D(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(10))
model.add(Activation('softmax'))

model.compile(loss='categorical_crossentropy', optimizer=SGD(lr=0.1), metrics=["accuracy"])

# 構成したモデルの概要を出力
model.summary()

history = model.fit(X_train_, y_train, validation_data=(X_test_,y_test), batch_size=100, epochs=40, verbose=True)

plot_history(history)

結果は次の通りです。

cnn and dropout

たったこれだけの改良で精度は80%ラインに近づきました。
また学習曲線は完璧とはいえないまでも、ドロップアウトがないときと比べれば訓練データとテストデータの乖離が縮まっています。
過学習の抑制効果が効いている証拠です。

まとめ

  • CNNを含むネットワークによって、cifar10の分類精度は70%程度に到達
  • さらに過学習を抑制するドロップアウト法を導入し、80%に到達
  • ドロップアウトやその他のテクニックは、いつも上手く行く方法ではありません。問題の背後にあるデータ分布の性質、設計したモデルやそのパラメータ数によっては、ここまで劇的な改善は見込めないことの方が普通ですが、過学習が確認できる場合には有効な選択肢といえそうです。