AlphaNyamo製作記 #02 MNIST with CNN

畳み込みニューラルネットワークってなんなのん?

畳み込みというのは簡単に言うとフィルタリング。

先鋭化やラプラシアンや平滑化を行うことによって画像の特徴を抽出することができる。つまり、画像全体からまんべんなく学習するよりもより効率的に学習されることが期待できる。

その反面、画像に対してフィルタを掛けなければいけないので単純な多層パーセプトロンよりは重くなる。

また、多層パーセプトロンでは二次元配列の画像を一次元行列に変換するという処理を行ったが、畳み込みニューラルネットワークでは画像にフィルタリングを行うため、このような変換は行わない。また、二次元情報を用いることにより、画像の縦横の座標という情報を持った上で学習することができるのも強みである。

CNNの実装

畳み込みニューラルネットワークでは一般的に畳み込み層とプーリング層と呼ばれるものを交互に重ねて実装する。※過学習を防ぐためにDropout層が加えられることもある。

それぞれがやることというのは、まず畳み込み層が画像にフィルタをかける。かけられたフィルタが一つだけであればそれに対して活性化関数をかければ良いのだが、実際に実装する上ではいくつものフィルタをかけるということが行われる。

仮に三つのフィルタをかけたとすると一枚の画像からフィルタリング後の画像が三枚生成されることになる。すると「結局どの画像に対して活性化関数をかければええのん…」って具合になってしまう。

そこで使われるのがプーリング層と言われるもので、簡単に言えばフィルタリングされた画像郡に対してダウンサンプリングを行ったものと考えて良い。ダウンサンプリング手法にはいろいろなものがあるけれど、最も有名なのはMaxPooingと言われる手法。これは近傍の最大値をとるというものです。

やりたいことの流れ

やりたいことをまとめるとこんな感じ。

元画像からフィルタリング済みの画像を複数生成(ここでは仮にN枚としました)し、そこからプーリングで元画像と同じサイズの画像を作ります。

最後にそれをFlatten()したものを入力として利用します。
なんてわかりやすい!!

MNISTのコード

https://raw.githubusercontent.com/keras-team/keras/master/examples/mnist_cnn.py

前回は多層パーセプトロンでネットワークを構成しましたが、今回は畳み込みニューラルネットワークのコードについて考えていきたいと思います。

# encoding: utf-8
from __future__ import print_function
import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras import backend as K

多層パーセプトロンのときと違う箇所は後半の三行ですね。前回はoptimizerとしてRMSpropをインポートしていましたが、今回はしていません。

また、Conv2DとMaxPooling2Dをインポートしています。
https://keras.io/ja/layers/convolutional/#conv2d

Conv2Dとは2次元入力をフィルターする畳み込み層のことです。今回は畳み込みニューラルネットワークを作るので、これは必ずインポートする必要があります。

Poolingレイヤー - Keras Documentation

MaxPooling2Dとは空間データのマックスプーリング演算のことらしいです。よくわからないのでここではとりあえず無視します(あ)

また、今回はFlattenというレイヤーもインポートしています。

Coreレイヤー - Keras Documentation

これは入力を平滑化するレイヤーです。

backend Kというのは畳み込みの部分を行うバックエンドのライブラリのことです。特に何もしていなければTensorflowが指定されています。

バッチサイズの定義

エポックの回数だけが減っています。CNNだと12回で十分ということなのでしょうか?

batch_size = 128
num_classes = 10
epochs = 12

入力データの定義

前回は行わなかった入力データ定義を行います。

img_rows, img_cols = 28, 28

28×28の画像でデータが与えられますよ、ということをプログラムに認識させます。

データを読み込む

(x_train, y_train), (x_test, y_test) = mnist.load_data()

ここは前回と全く同じようにデータを読み込むだけです。

データの整形

前回と同じようにデータを整形するだけかと思えば、少し異なった動作をします。

if K.image_data_format() == 'channels_first':
  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)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255
print('x_train shape:', x_train.shape)
print(x_train.shape[0], 'train samples')
print(x_test.shape[0], 'test samples')

最初の謎の条件節なのですが、これはKerasの設定によって異なるようです。keras.jsonの中に以下の設定が書き込まれています。

  • image_data_format
    「channels_last」または「channels_first」。
  • epsilon
    0除算を防ぐための0に近い定数。
  • floatx
    浮動小数点の精度。float16かfloat32のどっちか。
  • backend
    Kerasのバックエンドで動くライブラリ。黙っていればtensorflowが指定されているはず。

実際にコードを動かす際にはimage_data_formatにどちらが指定されているかわからないので、設定ファイルを読み込んでその内容によって分岐するということですね!

データの変換

データの変換を行います。

y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)

コード自体は前回と全く同じです。

ネットワーク定義

多層パーセプトロンでは入力層・出力層の他に一層だけ中間層を組み込みましたが、畳み込みニューラルネットワークでは一気に層が増えます、注意しましょう。

model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3),
 activation='relu',
 input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))

前回は入力層+ドロップアウト層+中間層+ドロップアウト層+出力層の五層構造でした。今回は三層増えて八層構造になります。

というか、このままじゃ何もわからないままなので一行ずつ読み解いていきましょう。

一層目
model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=input_shape))

Conv2Dが使われているので、そのドキュメントに目を通してみました。また、一層目なのでinput_shapeを用いて入力層のサイズを与える必要があります。

Conv2D
keras.layers.convolutional.Conv2D(filters, kernel_size, strides=(1, 1), padding=’valid’, data_format=None, dilation_rate=(1, 1), activation=None, use_bias=True, kernel_initializer=’glorot_uniform’, bias_initializer=’zeros’, kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None)
定義がゲロ長くてお茶吹きました。でも実際に使っているのは3つだけでした。
  • filters: 使用するカーネルの数。
  • kernel_size: 畳み込みカーネルの幅と高さを指定します. タプル/リストでカーネルの幅と高さをそれぞれ指定でき,整数の場合は正方形のカーネルになります.
  • strides: カーネルのストライドを指定します. 2つの整数からなるタプル/リストで縦と横のストライドをそれぞれ指定でき,整数の場合は幅と高さが同様のストライドになります.
  • padding: validかsameのどちらかを指定します.
  • data_format: 何も変更していなければkeras.jsonの値を使います。
  • dilation_rate: 膨張率。なんのことかさっぱりわからない。
  • activation: 使用する活性化関数の名前。何も指定しなければ線形.
  • use_bias: 真理値で,バイアスベクトルを加えるかどうかを指定します。
  • kernel_initializer: カーネルの重み行列の初期値を指定します。
  • bias_initializer: バイアスベクトルの初期値を指定します。
  • kernel_regularizer: カーネルの重み行列に適用させるRegularizerを指定します。
  • bias_regularizer: バイアスベクトルに適用させるRegularizerを指定します。
  • activity_regularizer: 出力テンソルに適用させるRegularizerを指定します。
  • kernel_constraint: カーネルの行列に適用させるConstraintを指定します。
  • bias_constraint: バイアスベクトルに適用させるConstraintを指定します。
二層目
model.add(Conv2D(64, (3, 3), activation='relu'))

やはりConv2Dが使用されています。これだけ見るとrelu(relu(input))みたいな状態になっていそうな気がするのですが、大丈夫なんでしょうか…
あと、フィルタの数が変わっています。なんでなんでしょう?

三層目
model.add(MaxPooling2D(pool_size=(2, 2)))

二層目のデータをダウンサンプリングする。

MaxPooling2D
keras.layers.pooling.MaxPooling2D(pool_size=(2, 2), strides=None, padding=’valid’, data_format=None)
  • pool_size: ダウンスケールする係数を決めます。(2, 2)を指定すると(垂直, 水平)に対してそれぞれ半分にします。
  • strides: ストライド値.2つの整数からなるタプル,もしくはNoneで指定します。
  • padding: validかsameのいずれか。
  • data_format: 何も変更していなければkeras.jsonの値を使用します。

さて、思ってしまったのだが次元を半分にするだけで足りるのだろうか?一層目でカーネルを32使い、二層目でカーネルを64使っているからデータの次元もものすごい増えていそうな気がするのだけれど。

よくわからないのが、カーネルの数を指定するっていやつ。カーネルの数って何?number of kernelなの?

四層目
model.add(Dropout(0.25))

単純なドロップアウト層なので省略。全シナプスの25%をランダムに出力を0にする。

五層目
model.add(Flatten())

平滑化フィルタをかけるだけ。

六層目
model.add(Dense(128, activation='relu'))

活性化関数を適応。

七層目
model.add(Dropout(0.5))

ドロップアウト層。

八層目
model.add(Dense(num_classes, activation='softmax'))

softmaxを使って10通りのデータを出力。

訓練・テスト

Epoch 12/12
60000/60000 [==============================] - 7s 110us/step - loss: 0.0283 - acc: 0.9912 - val_loss: 0.0298 - val_acc: 0.9912
Test loss: 0.02975640546698114
Test accuracy: 0.9912

CPUだと時間がかかりすぎるのでGPUで実行してみました1epochあたり7秒くらいで終わったので、全部完了するのに大体1分半程度でしょうか。正解率は99.12%とMPLの正解率を上回ってきました。といってもかけた時間の割には正解率が伸びていないかなーという印象もあります。

ちなみにGPU版のTensorflowのセットアップ方法は以下の記事をご覧ください。

Tensorflow-GPU #セットアップ
なんでいまさらGPUなのん? CNNをサーバで実行しようと思ったら重すぎたから(1epochで大体3分くらいかかる)。 ならやっぱり...

シェアする