MNISTをKerasでやってみる(普通のNNとCNN)

はじめに

前回、ただ動かしたレベルですがKerasを少し覚えたので、このままMNISTをKerasでやってみます. そういえば、もうクリスマスですね(遠い目).

前回の記事:
唐突にKerasを始める - 他力本願で生き抜く(本気)


今回の目標など

  • 目標:Kerasで普通のニューラルネットとCNNを構築し、MNISTの画像識別を行う
  • 使用するデータ:MNIST(今回はKerasコマンドから利用)
  • 環境:Google Colaboratory


普通のニューラルネットという言い方が適切かは無視していただいて、要は畳み込みのあるなしで結果の比較をしてみたかったというだけです.


ほぼ同じことをネットワーク構造を変えてやるだけなので、クラスとかを使いこなせればもっと美しく書けるんだとは思うのですが、今回はこれで勘弁してください.

実装

今回は、コピペよりは自力で公式のコードを横目に見ながらやった部分が多いのでコード多めです(CNNをメインに載せています).
noteはこちら

必要なライブラリのインポート

まず、必要となるライブラリをインポートします.

# 必要なライブラリのインポート
from matplotlib import pyplot as plt
from sklearn.model_selection import train_test_split
import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPooling2D

MNISTデータのダウンロード

KerasではMNISTのデータの利用も次のコードで簡単にできます.

# データをダウンロードして、訓練とテストに分ける
(x_train, y_train), (x_test, y_test) = mnist.load_data()

画像データを学習で使える型に変換

公式のやり方に従い、 MNISTの画像データ(グレイスケール(一次元)、サイズは28ピクセル×28ピクセル)を、ニューラルネットワークで使えるように行列に変換していきます.


また、画像の各ピクセルのカラーは0~255の階調で表現されていますので、最大値が1となるように255で除しています(正規化といいます).


CNNに入力するデータは行列型でよいのですが、単純なニューラルネットの方に入力するデータは一次元配列である必要があるため、正規化済のデータを一次元に変換しています.


最後に、教師ラベル(0~9)をOne-hotベクトルに変換しています.

この変換によって

  • 0は[ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
  • 4は[ 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]
  • 9は[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]

のように0と1のベクトルに変換されます.


ここまでで、私用するデータの準備が終了.
引き続き、ネットワークの構築、学習と進めていきます.

# インプットデータの型を変換する
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1)
x_test = x_test.reshape(x_test.shape[0], 28, 28, 1)

# pixelの値を0〜1の小数に変換
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255

# 通常のニューラルネット用に、一次元化したデータも作っておきます
x_train_nn = x_train.reshape(x_train.shape[0], 784)
x_test_nn = x_test.reshape(x_test.shape[0], 784)

# One-Hot ベクトル形式に変換する
y_train = keras.utils.to_categorical(y_train, 10)
y_test = keras.utils.to_categorical(y_test, 10)

ネットワークの構築

これまでのわずかな経験とコードを活用しながら、ネットワークを構築していきます.わずかな経験でできてしまうのがKerasのすごいところです.
通常のニューラルネットワーク、畳み込みニューラルネットワークの順に構築していきます.

通常のニューラルネットワーク

  • model_nn
    • あまり考えずに入力1層、中間層2層、出力層の前回とほぼ同じ構成としました.
    • 活性化関数はreluを採用
    • 過学習を防ぐため中間層ではDropoutを入れています.
    • 最後は分類問題では定番のsoftmax関数で10クラスに分類させます


畳み込みニューラルネットワーク

次にCNNを構築します.Kerasでは畳み込み層をConv2D()という言葉を知っていれば定義できてしまいます.同じく、プーリング層についても、MaxPooling2D()AveragePoolin2D()を使えば簡単に定義できます.
Conv2D()で指定している最初の値filtersはフィルタの枚数になります.kernel_sizeがフィルタのサイズになります.filtersとkernel_sizeは必ず指定する必要があり、その他の項目はなしでも動くようデフォルト値が設定されています.ただし、activationに関しては入力がなければ”活性化関数なし”の扱いになるため、線形結合となります.あえて活性化させないのでなければ何かを指定するべきです.
MaxPooling()では、フィルターの大きさ(〇×〇)を定義すればよいです.

詳しいドキュメントは以下になります
Convolutionalレイヤー - Keras Documentation

Pooling Layers - Keras Documentation

# 通常のニューラルネットワーク(model_nn)
省略
:
# 畳み込みニューラルネットワーク用モデルの構築(model_cnn)
model_cnn = Sequential()
model_cnn.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=(28, 28, 1)))
model_cnn.add(Conv2D(64, (3, 3), activation='relu'))
model_cnn.add(MaxPooling2D(pool_size=(2, 2)))
model_cnn.add(Dropout(0.25))
model_cnn.add(Flatten())
model_cnn.add(Dense(128, activation='relu'))
model_cnn.add(Dropout(0.5))
model_cnn.add(Dense(10, activation='softmax'))

モデルのコンパイル

ネットワークの構成を設定したら、コンパイルを行います. ドキュメントにならい、必要な項目は以下のように設定しています.

  • 損失関数はcategorilca_crossentropy
  • 最適化手法はAdadelta
  • 評価関数はaccuracy


コンパイル後には、summary()で、モデルの概要を表示することができます.

# mode_nn(通常のNN)のコンパイル
省略
:
# mode_cnn(畳み込みNN)のコンパイル
model_cnn.compile(loss="categorical_crossentropy",
              optimizer=keras.optimizers.Adadelta(),
              metrics=['accuracy'])
print("CNN model")
print(model_cnn.summary())

履歴を描画するグラフの定義

学習結果として、ロス関数と精度の変動をグラフで表示させるようにします.
kerasのfit関数は引数に学習中の記録を返してくれるので、
log_cnn = model.fit()として得られるログデータを、そのまま以下のグラフ描画関数に渡しています.
最後にグラフを描画して、ダウンロードするようにしています.

実はちょっと問題があって、エポック数が5であればダウンロードまで問題なくできるのですが、エポック数を50とかにするとダウンロードの際にFailed to Fetchエラーが生じます. 描画および保存はできているので、その後ダウンロードはできるのですがなぜエポック数の違いでエラーが生じるのかはよくわかっていません.

def log2graph(log1, log2, name1, name2):
    import google.colab

    # Accuracyの履歴
    plt.figure()
    # log1
    plt.plot(log1.history["acc"], label="Accuracy_"+ name1, marker=".", color = "green")
    plt.plot(log1.history["val_acc"], label="Val_Accuracy_"+ name1, marker=".", color = "blue")
    # log2
    plt.plot(log2.history["acc"], label="Accuracy_"+ name2, marker=".", color = "magenta")
    plt.plot(log2.history["val_acc"], label="Val_Accuracy_"+ name2, marker=".", color = "skyblue")
    
    
    plt.grid()
    plt.legend(loc="best")
    plt.title("Accuracy")
    file_name_acc = "graph_accuracy.png"
    plt.savefig(file_name_acc)
    plt.show()
    
    # Loss関数の履歴
    省略
    :
     
    google.colab.files.download(file_name_acc)    
    google.colab.files.download(file_name_loss)

学習(fit)および結果

モデルのコンパイルが終われば後は、fitで学習するだけです.ほとんどの設定値は公式のまんまです.

validation_splitでインプットする訓練データのうち何割を検証用に使うかが指定できるので、あらかじめ検証用(validation)のデータを作っておく必要がありません.便利ですねー.

また、verboseの値を0にすると、進捗状況をリアルタイムで出力することをやめることができます.結構たくさんの情報が出力されるので嫌な人は0にしてしまえばいいと思います.私は勝手に動いている画面の方が、なんか仕事してるっぽいので好んで使っています.


訓練データと検証用データによるAccuracyとLossのログを示したグラフを以下に示します.またテストデータによる分類精度は、ノーマルNNで95.7%、CNNで99.3%となりました.
それぞれかなりの高精度です.ただグラフを見ると、CNNの方が圧倒的に効率が良いことが分かります.

f:id:shirakonotempura:20181225025202p:plain
Accuracyの履歴

f:id:shirakonotempura:20181225025205p:plain
Lossの履歴

# エポック数の定義(NN、CNN共通)
epochs = 100

# NNの学習
省略
:
# CNNの学習
log_cnn = model_cnn.fit(x_train, y_train,
                    batch_size=128,
                    epochs=epochs,
                    verbose=1,
                    validation_split = 0.2)

## テストデータによる結果表示
score_cnn = model_cnn.evaluate(x_test, y_test, verbose=1, batch_size = 128)
print('Test loss:', score_cnn[0])
print('Test accuracy:', score_cnn[1])


# グラフの描画(3番目と4番目の引数はグラフに追加する名前)
log2graph(log_nn, log_cnn, "nn", "cnn")


感想

Kerasを使えばMNISTの画像識別もわずかなコードで実装することができました. この手軽さは使わないと損ですね.ブラックボックスになりがちとか言われるようですが、それを差し引いても、この便利さは使わないと時間の無駄な気がします.Kerasの方がどんなネットワーク構造を作ろうとしているかを理解しながら構築できるので、少なくとも私にはこれで十分です.


一方、簡単に99%の分類精度が出たとは言え、データセットがMNISTなんであまり嬉しくはないですね.やはり自分が用意したデータなり、もっと現実的なデータを使ってやってみたいです.なので、次は、画像データの準備と学習に使えるデータへの変換なんかを勉強してみたいと思います.