第22回および第23回では、「TensorFlow」のフレームワークを用いて、多項分類器と隠れ層が1層からなるニューラルネットワークの隠れ層の前段に畳み込みフィルタープーリング層を挿入した畳み込みニューラルネットワークをご紹介しました。

 

TensorFlowにより、Deep Learningの記述が簡単に行えることをご紹介しましたが、それでも、行列やベクトル(「テンソル」と呼びます)の演算をPythonにより、正確に記述する必要がありました。

 

そこで、今回は、TensorFlowなどをバックエンドにして、更にフロントエンドのコードの記述が行える「Keras」というフレームワークをご紹介したいと思います。Kerasでは、テンソルの演算をバックエンドのフレームワークが自動的に実行してくれるため、ユーザーはテンソルの演算を意識してコードの記述する必要が全く無くなります。また、フロントエンドのフレームワークであるため、コードを簡潔に記述でき、より少ない量の記述でプログラムを書くことができます。

 

Kerasは単独では実行することができず、必ずバックエンドのフレームワークを指定する必要があります。現在までのところ、Google社による「TensorFlow」, モントリオール大学のLISA/MILA Labによる「Theano」、およびMicrosoft社による「CNTK」をバックエンドのフレームワークとして使用することができます(図1参照)。Kerasでのコードの記述は共通に1つでよく、どのバックエンドのフレームワークでも、実行することができます(ただし、パフォーマンスには差があります)。

 

図1

 

前提条件

①Pythonと、そのパッケージ・ライブラリーの導入

まだ、導入されていない方は、第2回をご参照の上、導入してください。

 

②バックエンド・フレームワークの導入

②-1. TensorFlowの導入

TensorFlowは、Mac OS X、Windows、UbuntuのOS上で稼働します。まだ、導入されていない方は、第13回をご参照の上、導入してください。

②-2. Theanoの導入

Theanoは、Mac OS X、Windows、Ubuntu、CentOSのOS上で稼働します。

 

[1] Windows

DOSコマンドプロンプトから以下のコマンドを実行します(*1)

C:\> conda install mkl-service libpython
C:\> pip3 install --upgrade theano  
            

(*1)稼働確認ができていないため、非推奨です。

 

[2] Mac OS X

ターミナルから以下のコマンドを実行します。事前にg++の導入が必須です。

$ g++
$ sudo xcodebuild -license
$ pip3 install --upgrade theano  
		

 

[3] Ubuntu

Xterm から以下のコマンドを実行します。

 

$ sudo apt-get install g++
$ pip3 install --upgrade theano
          

その他、導入の詳細は、以下のサイトをご参照ください。

http://deeplearning.net/software/theano/install.html

正しく導入できたかの確認は、Pythonを対話モードで起動し、以下のコマンドで、エラーが表示されなければ成功です。

>>> import theano as th  
>>> 
          

 

②-3. CNTKの導入

CNTKは、Windows、UbuntuのOS上で稼働します。

 

[1] Windows

DOSコマンドプロンプトから以下のコマンドを実行します。

C:\> pip3 install <URL>  
            

<URL>の箇所に、以下のサイトから適切なリンクを選択して入力します。 https://docs.microsoft.com/en-us/cognitive-toolkit/setup-windows-python?tabs=cntkpy22

 

[2] Ubuntu

Xterm から以下のコマンドを実行します。(*2)

 

C:\> pip3 install <URL>  
            

<URL>の箇所に、以下のサイトから適切なリンクを選択して入力します。 https://docs.microsoft.com/en-us/cognitive-toolkit/setup-linux-python?tabs=cntkpy22

(*2)稼働確認できていないため、非推奨。

 

その他、導入の詳細は、以下のサイトをご参照ください。

https://docs.microsoft.com/en-us/cognitive-toolkit/setup-cntk-on-your-machine

正しく導入できたかの確認は、Pythonを対話モードで起動し、以下のコマンドで、エラーが表示されなければ成功です。

>>> import cntk  
>>> 
          

 

 

③Kerasの導入

 

[1] Windows

DOSコマンドプロンプトから以下のコマンドを実行します。

C:\> pip3 install --upgrade keras  
            

[2] Mac OS X および Ubuntu

$ sudo pip3 install --upgrade keras  
		

 

 

Kerasによる畳み込みニューラルネットワークを使用した機械学習の流れは、概ね以下になります。なお、Pythonのバージョンは3.5以上を想定しています。

  1. データを入力する。
  2. バックエンドの選択と入力チャネル形式の選択。
  3. イメージデータ形式の設定
  4. 変量(image)の正規化とラベル(0〜9)の変換(one-hot vector representation)
  5. 畳み込みニューラルネットワークの構成を定義する。
  6. Kerasにより損失関数と最適化関数を定義する。
  7. 確率的勾配降下法(ミニバッチ)による機械学習を実行する。
  8. テストデータを用いて、損失と精度を計算する。
  9. 損失と精度をグラフに表示する。

では、各ステップを詳しく見ていきましょう。

 

①データを入力する。

ここで使用するデータは、MNIST(Mixed National Institute of Standards an Technology)と呼ばれるデータで、60,000個のトレーニングデータと10,000個のテストデータからなります。トレーニングデータは、250名(半分は高校生、半分は国税調査局の職員)による0〜9までの手書き数字のラベルと画像データからなります。また、テストデータについても、同じ割合で(トレーニングデータとは)別の人の手書きデータからなります。それぞれの手書き数字は、0〜9までのラベルと28x28ピクセルのグレースケールの画像データで、0〜255の整数値で各ピクセルの濃度が与えられています。

 

まず、対話形式ではなく、Pythonのスクリプトをファイルで用意します。Pythonスクリプトの先頭に以下の2行を添付しておくことをお勧めします。

#!/usr/bin/env python
# -*- coding:utf-8 -*-            
            

また、機械学習の途中経過を表示するために、以下の1行をスクリプトの先頭に追記します。

from __future__ import print_function
            

 

TensorFlowには、Webで公開されているMNISTのデータセットをダウンロードして、arrayオブジェクトとして格納する関数があらかじめ用意されています。以下のように記述します。

import keras
from keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
            

※上記関数をWindow上で実行すると、以下のようなエラーが発生する場合があります。

It was caused by updating to latest version of pandas (0.20.2). Updating dask package to the latest version (0.14.3) can resolve this issue.
            

 

その場合は、DOSコマンドプロンプトから以下のコマンドを実行してください。

 

$ conda update dask
            

②バックエンドの選択と入力チャネル形式の選択

まず、バックエンドを選択する前に、以下のコマンドをPython対話モードで実行します。

>>> import keras
Using TensorFlow backend.
>>> 
          

 

そうすると、Mac OS X および Ubuntuの場合は、$HOME/.keras というディレクトリに、Windowsの場合は、%USERPROFILE%¥.kerasフォルダーに、keras.jsonという名前のファイルが生成されます。 keras.jsonの内容は、JSON形式で以下の内容になっています。

{
    "floatx": "float32",
    "epsilon": 1e-07,
    "backend": "tensorflow",
    "image_data_format": "channels_last"
}
          

"backend"の項目に使用するバックエンドのフレームワークを指定します。TensorFlowであれば"tensorflow"を(デフォルト)、Theanoであれば"theano"を、CNTKであれば"cntk"と指定します。これによりバックエンドのフレームワークが切り替わります。また、"image_data_format"の項目には、イメージデータ形式として、入力チャネル数をピクセル数の前に指定するか、後に指定するかを決めます(スクリプトの具体的な記述方法は以下を参照)。前に指定する場合は"channels_fist"を、後に指定する場合は"channels_last"(デフォルト)を指定します。どちらを指定しても問題ありませんが、筆者の実験では、バックエンドがTensorFlowの場合は"channels_last"を、バックエンドがTheanoとCNTKの場合は、"channels_fist"を指定すると、よりパフォーマンスが向上する結果が得られています。

 

③イメージデータ形式の設定

上記 keras.json ファイルの"image_data_format"の項目で指定した値は、Kerasをimportすると、keras.backend.image_data_format()関数で参照することができます。これを利用して、変量(image)の配列を指定した順序で整形します。チャネル数(ここでは、channels ) グレイスケールの場合は1、RGBの場合は3を指定します。MNISTのイメージはグレイスケールのため、1を指定します。また、整形した配列のサイズを input_shapeに 保存します。

from keras import backend as K
img_rows, img_cols = 28, 28
channels = 1

if K.image_data_format() == 'channels_first':
    x_train = x_train.reshape(x_train.shape[0], channels, img_rows, img_cols)
    x_test = x_test.reshape(x_test.shape[0], channels, img_rows, img_cols)
    input_shape = (channels, img_rows, img_cols)
else:
    x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, channels)
    x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, channels)
    input_shape = (img_rows, img_cols, channels)
            

 

④変量(image)の正規化とラベル(0〜9)の変換(one-hot vector representation)

一般に、変量をx、変量の最大値と最小値をそれぞれ、x_max, x_min   とすると、変量xの正規化の値は、(x - x_min)/(x_max - x_min)で与えられます。ここで、変量(image)データの各要素の最大値は255, 最小値は0 であるため、結果として、各要素を255で割ったものが正規化された値となります。なお、正規化の前に変量xの型をfloat32に指定します。

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

次にラベル y_trainとy_testを多項分類器により分類できるように以下のように変換します。このようなベクトルを one-hot vectorと呼びます。

"0" -> [1,0,0,0,0,0,0,0,0,0]

"1" -> [0,1,0,0,0,0,0,0,0,0]

"2" -> [0,0,1,0,0,0,0,0,0,0]

"3" -> [0,0,0,1,0,0,0,0,0,0]

"4" -> [0,0,0,0,1,0,0,0,0,0]

"5" -> [0,0,0,0,0,1,0,0,0,0]

"6" -> [0,0,0,0,0,0,1,0,0,0]

"7" -> [0,0,0,0,0,0,0,1,0,0]

"8" -> [0,0,0,0,0,0,0,0,1,0]

"9" -> [0,0,0,0,0,0,0,0,0,1]

 

num_classes = 10

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

⑤畳み込みニューラルネットワークの構成を定義する。

Kerasを用いると、畳み込みニューラルネットワークの構成を簡単に定義できます。

ここでは、図2にあります畳み込みフィルターとプーリング層を2段に重ねた畳み込みニューラルネットワークをKerasにより構成します。

図2

 

まず、model = Sequential() でニューラルネットワークの開始を宣言します。その後、addメソッドで、順次、各層の構成を定義して行きます。最初の層のConv2D関数は、2次元畳み込みフィルターで、第一引数の32は、使用するフィルターの個数です。フィルターの個数は任意ですが、ここでは、32を指定します。第2引数のkernel_size=(3, 3)は、畳み込みフィルターのサイズを3x3ピクセルに指定しています。第3引数のactivation='relu'は、活性化関数にReLUを使用することを指定しています。活性化関数については、第16回をご参照ください。第4引数のinput_shapeには、③イメージデータ形式の設定した入力チャネルのサイズと順序(28x28x1または1x28x28)を指定します。第1層に必ず指定する必要があります。

 

畳み込みフィルターの出力(28x28x1x32または1x28x28x32)は、次のプーリング層に入力されます。畳み込みフィルターの出力を入力として、MaxPooling関数により、画像の解像度を縮小するためのプーリング層の処理を定義します。pool_sizeで指定されたサイズのブロック(ここでは2x2)をブロック内におけるピクセルの最大値で置き換えて行きます。これにより、28x28ピクセルだったオリジナル画像がプーリング層で14x14ピクセルの解像度に置き替わります。また、プーリング層から出力されるデータの総数は「14x14xフィルター数」となり、次の3層目の畳み込みフィルターへ出力されます。

 

3層目の畳み込みフィルターでは、1段目の畳み込みフィルターと同等の処理を行いますが、フィルター数を64に変更しています。この出力が第4層目のプーリング層に入力され、これにより、14x14ピクセルだった画像がこのプーリング層で7x7ピクセルの解像度に置き替わります。

 

画像データの分類のアルゴリズムですが、2段のプーリング層を通過することにより、7x7ピクセルの画像になります。各ピクセルの濃度の数値を一列に並べてしまえば、7x7=49次元のベクトルになります。言い換えると49次元空間の1つの点に対応します。すなわち、MNISTの画像データは、49次元空間上に配置された多数の点の集合とみなすことができます。この時、同じ数字に対応する画像は、49次元空間上で互いに近い場所に集まっていると期待することができます。そして、49次元空間を10個の領域に分割することで、その領域に対応する数字が決まることになります。この一列に並べる処理をFlatten()関数により行います。このFlatten()関数は、全結合層の前に配置する必要があります。

 

Dense関数で、全結合層を定義します。第一引数の128は、全結合層のノードの数を指定しています。

 

更に、全結合層と出力層の間にドロップアウト層を挿入します。Dropout(0.5)により、全結合層からの入力の半分をカットします。ドロップアウト層を挿入する目的は、オーバーフィッティングを防ぐために行います。オーバーフィッティングとは、トレーニングデータに対して過剰に最適化が行われると、テストデータに対する正解率(精度)が低下する状態を言います。

 

最後に、出力層を定義します。第一引数にnum_classes(数字の0〜9までの10種類)を指定し、多項分類器のソフトマックス関数を指定します。

 

from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Dense, Dropout, Flatten

num_classes = 10

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

 

⑥Kerasにより損失関数と最適化関数を定義する。

model.compileで、損失関数と最適化関数を指定します。 ここでは、損失関数として、keras.losses.categorical_crossentropyを指定しています。Kerasでは、その他、多数の損失関数が用意されています。詳細は以下リンクをご参照ください。

https://keras.io/losses/

 

またこの例では、最適化関数として、keras.optimizers.Adadelta()を指定しましたが、こちらも、多数の最適化関数が用意されています。詳細は以下リンクをご参照ください。

https://keras.io/optimizers/

 

model.compile(loss=keras.losses.categorical_crossentropy,
              optimizer=keras.optimizers.Adadelta(),
              metrics=['accuracy'])
            

 

⑦確率的勾配降下法(ミニバッチ)による機械学習を実行する。

トレーニングデータとテストデータを全て使うのではなく、batch_sizeで指定された回数(ここでは128回)繰り返すごとに、トレーニングデータとテストデータが最初の要素から順次、取り出され変更する仕組みになっています。この手法は「ミニバッチ」もしくは、「確率的勾配降下法」と呼ばれています。利点は、トレーニングデータが大量にある場合、1回あたりのデータ量を減らして、その代わり最適化の処理を何度も繰り返すことで、全体の計算時間を短くすることができます。また、損失関数の値が極小値になるのを避けて、真の最小値に達することができます。これは、勾配ベクトルが正確に計算されないため、損失関数の値はジグサグに移動して行きます。このため、極小値の付近にやってきた場合でも、パラメーターの修正処理を何度も繰り返せば、偶然に極小値の谷から出て、本来の最小値の方へ向かうことができるのです。epocks=12を指定することによりミニバッチを12回繰り返します。 このミニバッチは、model.fitで実行され、戻り値として、各エポックにおけるトレーニングデータとテストデータの損失値と精度の値をhistory に格納します。

batch_size = 128
epochs = 12

history = model.fit(x_train, y_train,
          batch_size=batch_size,
          epochs=epochs,
          verbose=1,
          validation_data=(x_test, y_test))
            

 

⑧テストデータを用いて、損失と精度を計算する。

model.evaluateで、機械学習の結果として最終的に得られた重みとバイアスを用いてテストデータの損失と精度を計算します。

 

score = model.evaluate(x_test, y_test, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])
            

 ⑨損失と精度をグラフに表示する。

エポックの回数をx軸に、y軸としてトレーニングデータの精度(history['acc'])およびテストデータの精度(history['val_acc'])をプロットします。 同様に、エポックの回数をx軸に、y軸としてトレーニングデータの損失(history['loss'])およびテストデータの損失(history['val_loss'])をプロットします。

 

import matplotlib.pyplot as plt
# list all data in history
print(history.history.keys())
# summarize history for accuracy
plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
# summarize history for loss
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
            

 

(実行結果)


Using TensorFlow backend.
x_train shape: (60000, 28, 28, 1)
60000 train samples
10000 test samples
Train on 60000 samples, validate on 10000 samples
60000/60000 [=========] - 71s - loss: 0.3661 - acc: 0.8909 - val_loss: 0.0867 - val_acc: 0.9720
Epoch 2/12
60000/60000 [=========] - 68s - loss: 0.1213 - acc: 0.9640 - val_loss: 0.0571 - val_acc: 0.9816
Epoch 3/12
60000/60000 [=========] - 66s - loss: 0.0944 - acc: 0.9721 - val_loss: 0.0490 - val_acc: 0.9836
Epoch 4/12
60000/60000 [=========] - 66s - loss: 0.0791 - acc: 0.9765 - val_loss: 0.0421 - val_acc: 0.9856
Epoch 5/12
60000/60000 [=========] - 65s - loss: 0.0693 - acc: 0.9794 - val_loss: 0.0384 - val_acc: 0.9863
Epoch 6/12
60000/60000 [=========] - 66s - loss: 0.0628 - acc: 0.9813 - val_loss: 0.0357 - val_acc: 0.9882
Epoch 7/12
60000/60000 [=========] - 65s - loss: 0.0572 - acc: 0.9829 - val_loss: 0.0333 - val_acc: 0.9888
Epoch 8/12
60000/60000 [=========] - 65s - loss: 0.0513 - acc: 0.9854 - val_loss: 0.0321 - val_acc: 0.9897
Epoch 9/12
60000/60000 [=========] - 65s - loss: 0.0472 - acc: 0.9859 - val_loss: 0.0300 - val_acc: 0.9899
Epoch 10/12
60000/60000 [=========] - 65s - loss: 0.0439 - acc: 0.9865 - val_loss: 0.0304 - val_acc: 0.9896
Epoch 11/12
60000/60000 [=========] - 214s - loss: 0.0424 - acc: 0.9871 - val_loss: 0.0277 - val_acc: 0.9900
Epoch 12/12
60000/60000 [=========] - 68s - loss: 0.0392 - acc: 0.9883 - val_loss: 0.0274 - val_acc: 0.9907
Test loss: 0.0274138969503
Test accuracy: 0.9907
dict_keys(['val_loss', 'val_acc', 'loss', 'acc'])

 

 

図3 

図4

 

損失(Loss)の値が減少すると共に、トレーニングデータとテストデータの精度(Accuracy)の値が共に上昇していることがわかります。最終的にテストデータの精度が99.07%となりました。また、常にテストデータの精度がトレーニングデータの精度を上回り、オーバーフィッティングが生じていないことがわかります。

 

 

なお、今回の内容は、以下の書籍の3章を参考に執筆いたしました。ご興味のある方は、ご参考いただければと思います。

 

「Deep Learning with Keras」 

Antonio Gulli, Sujit Pal

出版社: Packt Publishing Ltd.

 

全体を通してのコードは以下のようになります。なお、本コードの稼働環境は、Python3.5以上を想定しています。

#!/usr/bin/env python
# -*- coding: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

channels = 1
batch_size = 128
num_classes = 10
epochs = 12

# input image dimensions
img_rows, img_cols = 28, 28

# the data, shuffled and split between train and test sets
(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], channels, img_rows, img_cols)
    x_test = x_test.reshape(x_test.shape[0], channels, img_rows, img_cols)
    input_shape = (channels, img_rows, img_cols)
else:
    x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, channels)
    x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, channels)
    input_shape = (img_rows, img_cols, channels)

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

# convert class vectors to binary class matrices
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(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))

model.compile(loss=keras.losses.categorical_crossentropy,
              optimizer=keras.optimizers.Adadelta(),
              metrics=['accuracy'])

history = model.fit(x_train, y_train,
          batch_size=batch_size,
          epochs=epochs,
          verbose=1,
          validation_data=(x_test, y_test))
score = model.evaluate(x_test, y_test, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])

import matplotlib.pyplot as plt
# list all data in history
print(history.history.keys())
# summarize history for accuracy
plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
# summarize history for loss
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()