CNNの仕組みを使ってMNISTのデータを機械学習にかける

import keras
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras.optimizers import RMSprop
from keras.datasets import mnist
import matplotlib.pyplot as plt

# 入力と出力を指定 --- (*1)
im_rows = 28 # 画像の縦ピクセルサイズ
im_cols = 28 # 画像の横ピクセルサイズ
im_color = 1 # 画像の色空間/グレイスケール
in_shape = (im_rows, im_cols, im_color)
out_size = 10

# MNISTのデータを読み込み
(X_train, y_train), (X_test, y_test) = mnist.load_data()
# 読み込んだデータをの三次元配列に変換 --- (*1a)
X_train = X_train.reshape(-1, im_rows, im_cols, im_color)
X_train = X_train.astype('float32') / 255
X_test = X_test.reshape(-1, im_rows, im_cols, im_color)
X_test = X_test.astype('float32') / 255
# ラベルデータをone-hotベクトルに直す
y_train = keras.utils.to_categorical(y_train.astype('int32'),10)
y_test = keras.utils.to_categorical(y_test.astype('int32'),10)

# CNNモデル構造を定義 --- (*2)
model = Sequential()
  model.add(Conv2D(32,
  kernel_size=(3, 3),
  activation='relu',
  input_shape=in_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(out_size, activation='softmax'))

# モデルをコンパイル --- (*3)
model.compile(
  loss='categorical_crossentropy',
  optimizer=RMSprop(),
  metrics=['accuracy'])

# 学習を実行 --- (*4)
hist = model.fit(X_train, y_train,
  batch_size=128,
  epochs=12,
  verbose=1,
  validation_data=(X_test, y_test))

# モデルを評価 --- (*5)
score = model.evaluate(X_test, y_test, verbose=1)
print('正解率=', score[1], 'loss=', score[0])

# 学習の様子をグラフへ描画 --- (*6)
# 正解率の推移をプロット
plt.plot(hist.history['accuracy'])
plt.plot(hist.history['val_accuracy'])
plt.title('Accuracy')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

# ロスの推移をプロット
plt.plot(hist.history['loss'])
plt.plot(hist.history['val_loss'])
plt.title('Loss')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

 

実行結果

f:id:hanamichi_sukusuku:20210208185252p:plain

f:id:hanamichi_sukusuku:20210208185358p:plain

 

このプログラムではCNN(畳み込みニューラルネットワークの仕組みを利用してMNISTのデータを機械学習にかけるプログラムです。MLPより高い精度を叩き出すことができた。

 

CNNや畳み込み層などの説明すごく分かりやすかった記事

https://yoshi-sun.com/web/python/1950/

 

MNISTデータ読み込み

(X_train, y_train), (X_test, y_test) = mnist.load_data()

 

読み込んだデータを三次元の配列に変換

im_rows = 28 # 画像の縦ピクセルサイズ
im_cols = 28 # 画像の横ピクセルサイズ
im_color = 1 # 画像の色空間/グレイスケール
in_shape = (im_rows, im_cols, im_color)
out_size = 10
 
X_train = X_train.reshape(-1, im_rows, im_cols, im_color)
X_train = X_train.astype('float32') / 255
X_test = X_test.reshape(-1, im_rows, im_cols, im_color)
X_test = X_test.astype('float32') / 255

 

CNNでは、畳み込み層を構成するために、画像の縦、横、色の三次元に変換する必要がある。

[[[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]]

[[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]]

[[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]]

.......

[[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]]]

このように1ピクセルは行数28,列数1の二次元配列を28個持った三次元の組み合わせによって表現しているから。

そのために、X_train.reshape(-1, im_rows, im_cols, im_color)ではデータを三次元の配列に変換している。ここでは、第一引数に-1を指定し、X_trainの中身を一次元の配列にして、第二引数の28、第三引数の28、第四引数の1を指定することで一次元の配列から28個、28行1列の配列を作り三次元の配列を作成している。

reshapeの引数は

例えば(3,3,2)なら

[[[0,0],[0,0],[0,0]],[[0,0],[0,0],[0,0]],[[0,0],[0,0],[0,0]]]こーなる。中身の0は気にしないで。これは言葉で言うと3個、3行2列の配列を持った配列を作ると言える。

(2,2)なら2行2列の配列に作り直すと行った具合に何行何列の配列を何個もった配列を作るのか考えると分かりやすいと思った。

 

X_train.astype('float32') / 255では255で要素全体を割ることで中身は色情報を持ち、0~255で表現されているので色情報を0.0~1.0で表現することができるからです。

 

ラベルデータをohe-hotベクトルに直す

y_train = keras.utils.to_categorical(y_train.astype('int32'),10)
y_test = keras.utils.to_categorical(y_test.astype('int32'),10)

 

keras.utils.to_catogorical()でラベルデータを渡し、10クラスで表現したものに変換。

今回は手書き数字の判定なので0~9の10種類のラベルデータが存在するので一つ一つのデータを10要素持つ配列で表現している。

 

 

CNNモデルの定義

model = Sequential()
model.add(Conv2D(32,
  kernel_size=(3, 3),
  activation='relu',
  input_shape=in_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(out_size, activation='softmax'))

畳み込みとは

入力画像のピクセル範囲に対してフィルターを適用していく作業。

カーネルサイズが3✖️3なら画像のピクセルデータの左上から順に3✖️3を1✖️1で表現するように変換していく。

f:id:hanamichi_sukusuku:20210208182631g:plain

このケースでは一つのフィルターしか使っていないが32や64をConv2Dの第一引数に指定することでフィルター数を指定できる。28✖️28のデータに3✖️3のフィルターをかけていくと26✖️26のデータに畳み込むことができる。

 

Conv2D()で畳み込み層の作成、MaxPooling2D()がプーリング層の作成。Flatten()では入力を平滑化(一次元の配列に変換)している。畳み込み層では、画像の特微量の畳み込みを行う。どう言うことかと言うと画像の各部分にある特徴を調べるということ。そして、プーリング層では、画像データの特徴を残しつつ圧縮する。圧縮することで、その後の処理がしやすくなる。画像解析のときのような流れで画像の特徴を捉えて圧縮、平滑化(ぼかし処理)を行い全結合層へとデータを流し込みイメージなのかな。

 

Conv2D(32, kernel_size=(3,3)では3✖️3のフィルターを32枚使うという意味。フィルタ数は、「16,32,64,128,256,512」枚などが使われる傾向にあるようだが複雑そうならフィルタ数を多めに、簡単そうならフィルタ数を少なめで試してみるといいらしい。

input_shapeで入力を指定。今回は28✖️28のグレースケールの画像なので(28, 28 ,1)を指定しているが例えば128✖️128のRGB画像であれば(128, 128, 3)と指定する。

 

MaxPooling2D()では平均値を取るものもあるが、今回は最大値を取るプーリングを行っている。pool_size=(2.2)を指定しており、これは2✖️2のプールサイズをとっていることになる。

f:id:hanamichi_sukusuku:20210208183501g:plain

 

 

こんな感じ。

 

Flatten()では各フィルタによって特徴が分割されたものを一つにまとめるため、一次元に変換する。

 

そして、Denseで全結合層へとデータを流し込んでいく。

 

モデルコンパイル

model.compile(
loss='categorical_crossentropy',
optimizer=RMSprop(),
metrics=['accuracy'])

 

学習を実行

hist = model.fit(X_train, y_train,
batch_size=128,
epochs=12,
verbose=1,
validation_data=(X_test, y_test))

 

validation_data=(X_test, y_test)を指定することで返り値としてhistoryオブジェクトにepoch毎での訓練データでの評価とvalidation_dataに指定したデータでの評価の結果を格納してくれる。

 

モデルを評価

score = model.evaluate(X_test, y_test, verbose=1)
print('正解率=', score[1], 'loss=', score[0])

 

学習の様子をグラフに描画

# 正解率の推移をプロット
plt.plot(hist.history['accuracy'])
plt.plot(hist.history['val_accuracy'])
plt.title('Accuracy')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

# ロスの推移をプロット
plt.plot(hist.history['loss'])
plt.plot(hist.history['val_loss'])
plt.title('Loss')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

 

hist.historyのキーで'val'とついているのがvalidation_dataのデータを評価した結果、ついてないのが訓練用データでの評価結果。