【AI初心者向け】mnist_cnn.pyを1行ずつ解説していく(KerasでMNISTを学習させる)


はじめに

この記事は全3回予定の第2回の記事です。
この記事はmnist_cnn.pyを1行ずつ解説していくだけの記事です。
前回と重なる部分もありますが、記事単体で読みやすくするため、重複して書いている内容もありますのでご了承ください。

AIに興味があるけどまだ触ったことはない人などが対象です。これを読めばディープラーニングの基本的な学習の流れが理解できるはず、と思って書いていきます。(もともとは社内で研修用に使おうと思って作成していた内容です)

  1. 【AI初心者向け】mnist_mlp.pyを1行ずつ解説していく(KerasでMNISTを学習させる)
  2. 【AI初心者向け】mnist_cnn.pyを1行ずつ解説していく(KerasでMNISTを学習させる)
  3. 【AI初心者向け】mnist_transfer_cnn.pyを1行ずつ解説していく(KerasでMNISTを学習させる)

動作確認方法について

MNISTは画像なので、このコードを動かすにはGPUがあったほうがいいです(CPUだとちょっと辛いです)。
おすすめの方法はGoogle Colaboratoryを使う方法です。

やることは2つだけ。
・Python3の新しいノートブックを開く
・ランタイムからGPUを有効にする
これでGPUが使えるようになりました。
セルにコードを貼り付けて実行(ショートカットはCTRL+ENTER)するだけで動きます。

mnistについて

手書き文字画像のデータセットで、機械学習のチュートリアルでよく使用されます。
内容:0~9の手書き文字
画像サイズ:28px*28px
カラー:白黒
データサイズ:7万枚(訓練データ6万、テストデータ1万の画像とラベルが用意されています)

cnnとは

Convolutional Neural Network、畳み込みニューラルネットワークのことです。
この記事はあくまでコードの解説を目的としているので、畳み込み処理についての詳しい説明は省略します。
ものすごく簡単に言ってしまうと、畳み込み処理をすることによって画像の特徴を抽出することができます。

mnist_cnn.pyについて

mnistの手書き文字の判定を行うモデルをKerasとTensorFlowを使って作成するコードです。
0~9の10種類の手書き文字を入力として受け取り、0~9のいずれであるか10種類に分類するモデルを作成します。

機能としては、第1回のmnist_mlp.pyと変わりませんが、モデル内の処理が違います。

また、mlpでは、28*28の二次元データ(こんなデータ→[[0, 0, ..., 0, 0], ... [0, 0, ..., 0, 0]])を784の1次元
データ(こんなデータ→[0, 0, ... 0, 0])に変換して入力値としていました。
今回のcnnでは、28*28の二次元データをそのまま入力値とします。

コードの解説

準備

'''Trains a simple convnet on the MNIST dataset.
Gets to 99.25% test accuracy after 12 epochs
(there is still a lot of margin for parameter tuning).
16 seconds per epoch on a GRID K520 GPU.
'''

# 特に必要ないコードです(Pythonのバージョンが3だが、コードがPython2で書かれている場合に必要になる)
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

# 定数
batch_size = 128  # バッチサイズ。1度に学習するデータサイズ
num_classes = 10  # 分類するラベル数。今回は手書き画像を0~9の10種類に分類する
epochs = 12       # エポック数。全データを何回学習するか(前回記事のmlpではここが20でした)
img_rows, img_cols = 28, 28  # 入力画像の次元数

Dense:全結合層
Dropout:Dropoutという処理があり、一定の割合(確率)で出力を無効(0)にします
Flatten:データを1次元に変換
Conv2D:畳み込み層
MaxPooling2D:プーリング層

はじめに定義した定数のうち、batch_sizeとepochsは人間が調整する必要がある、ハイパーパラメータというものです。ここを変更することによってモデルの性能が変わってきます。

簡単に説明しておくと、batch_sizeが大きいほうが学習は安定しますがその分メモリに余裕が必要です。
epochsは学習させる回数です。たくさん勉強させたほうが頭がよくなる気がしますが、大きすぎると過学習という状態に陥り、汎化性能が下がります。(=学習に使用したデータなら判断ができるが、未知のデータに対応できなくなります。)もちろん少なくても学習不足で頭は悪いままです。

データの前処理

# mnistのデータを読み込み、訓練データ(6万件)とテストデータ(1万件)に分割する
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# データの形を変換する
'''
KerasのバックエンドがTheano(channels_first)か tensorflow(channels_last)かで画像の書式が異なる
K.image_data_format()は"channels_last" か "channels_first" のいずれかを返すので、これに従って場合分けする
白黒の場合はチャンネル数は1になる。RGBのカラー画像であればチャンネル数は3になる
今回はx_train:(60000, 28, 28)->(60000, 28, 28, 1)になる
'''
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)

# 画像データは0~255の値をとるので255で割ることでデータを標準化する
# .astype('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)    # x_train shape: (60000, 28, 28, 1)
print(x_train.shape[0], 'train samples')  # 60000 train samples
print(x_test.shape[0], 'test samples')    # 10000 test samples

# ラベルデータをone-hot-vector化する
'''one-hot-vectorのイメージはこんな感じ
label  0 1 2 3 4 5 6 7 8 9
0:    [1,0,0,0,0,0,0,0,0,0]
8:    [0,0,0,0,0,0,0,0,1,0]'''
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)

標準化について:画像の各ピクセルの値は0~255になっています。これを0~1に変換するイメージです。画像で機械学習するときは大体この255で割る処理をして、値を標準化します。

one-hot-vectorについて:今回、ラベルは0~9の10種類があり、それぞれ0~9の数字で表しています。しかし、10種類に分類したいだけなので、ラベルの数字自体には意味がありません。そこで、one-hot-vectorすることにより0と1のみでどのラベルなのかを表せるように変換します。

モデルの定義

# Sequentialクラスをインスタンス化
model = Sequential()

# 中間層
# 畳み込み層(フィルター:32枚、フィルターサイズ:(3, 3)、活性化関数:Relu、受け取る入力サイズ:( 28, 28, 1))を追加
model.add(Conv2D(32, kernel_size=(3, 3),
                 activation='relu',
                 input_shape=input_shape))
# 畳み込み層(フィルター:64枚、フィルターサイズ:(3, 3)、活性化関数:Relu、受け取る入力サイズは自動で判断)を追加
model.add(Conv2D(64, (3, 3), activation='relu'))
# プーリング層
model.add(MaxPooling2D(pool_size=(2, 2)))
# 0.25の確率でドロップアウト
model.add(Dropout(0.25))
# データを1次元に変換
model.add(Flatten())
# 全結合層(128ユニット、活性化関数:relu、受け取る入力サイズは自動で判断)を追加
model.add(Dense(128, activation='relu'))
# 0.5の確率でドロップアウト
model.add(Dropout(0.5))

# 出力層
# 全結合層(10ユニット、活性化関数:SoftMax、受け取る入力サイズは自動で判断)を追加
model.add(Dense(num_classes, activation='softmax'))

SequentialモデルはDNNの層を積み重ねて作るモデルです。一番最初の層にだけ、input_shapeを指定してやる必要があります。
出力層の活性化関数は、今回は多値分類するモデルなのでsoftmaxを使います。

Dense(全結合層)は、1次元のデータでないと受け取ることができません。なので、Denseの前にFlatten()でデータを1次元に変換してやる必要があります。

学習

# 学習プロセスを設定する

model.compile(
              # 損失関数を設定。今回は分類なのでcategorical_crossentropy
              loss=keras.losses.categorical_crossentropy,
              # 最適化アルゴリズムをAdadeltaにしている(mlpとの違い)
              optimizer=keras.optimizers.Adadelta(),
              # 評価関数を指定
              metrics=['accuracy'])

# 学習させる
model.fit(x_train, y_train,      # 学習データ、ラベル
          batch_size=batch_size, # バッチサイズ(128)
          epochs=epochs,         # エポック数(12)
          verbose=1,             # # 学習の進捗をリアルタムに棒グラフで表示(0で非表示)
          validation_data=(x_test, y_test))  # テストデータ(エポックごとにテストを行い誤差を計算するため)

モデルの定義が終わったら、損失関数や最適化アルゴリズムを指定してコンパイルします。その後、モデルにデータを渡して学習させます。

ここで指定している、最適化アルゴリズムのAdadeltaもハイパーパラメータの一種です。よく使われるのはAdamというアルゴリズムだと思いますが、絶対これがいいというものはないので試行錯誤する必要があります。

評価

# テストデータを渡す(verbose=0で進行状況メッセージを出さない)
score = model.evaluate(x_test, y_test, verbose=0)
# 汎化誤差を出力
print('Test loss:', score[0])
# 汎化性能を出力
print('Test accuracy:', score[1])

学習が終わったら、テストデータを使ってどの程度の性能になったのかを評価します。
lossが低く、accuracyが高いほど良いモデルといえます。