今回は、Mnistデータ(0〜9が手書きされた、9つの数字)と、畳込みニューラルネットワーク(Convolutional Neural Network; CNN)を利用して、数字当ての深層学習を作ってみます。本ページの後半に学習済みのモデルを、誰かが作った便利な関数を(ほとんど)使わずに直で判定するコードも置いていますので、便利な関数を使わずに深層学習を実装することに興味がある人はそちらも一読ください。
まず、準備として、jupyter を開く予定のターミナルで、
を実行します。その後、バックエンドをチェックします。plaidmlが返って来ればokです。
# kerasのバックエンドを plaidmlに変更
import plaidml.keras
plaidml.keras.install_backend()
import keras
print(keras.backend.backend())
まず、Mnistのデータロードを行います。
from keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
PyLearn Keras 01: Macbook/GPUによる深層学習 では、階層型ニューラルネットワークによって、
という構造をしたモデルにて、Mnistの数字当てを行いました。上記のモデルでは、入力層がベクトル(まっすぐに長い数字の列)となっています。今回実施するCNNでは、入力としてベクトルを採用するのではなく、画像(つまり、行列成分を有するデータ)を採用する必要があります。したがいまして、 PyLearn Keras 01: Macbook/GPUによる深層学習 で実施した前処理の中で、画像をベクトルに変換する部分は、実施する必要がありません。すなわち前処理は、
となります。
import keras
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.optimizers import RMSprop
# 各種サイズ取得
num_train = len(y_train) # 教師データ数
num_test = len(y_test) # テストデータ数
print("教師データサイズ: ", num_train)
print("テストデータサイズ: ", num_test)
size_0 = x_train.shape[1] # 横サイズ
size_1 = x_train.shape[2] # 縦サイズ
dim_input_vec = size_0 * size_1
# 規格化
# グレースケール画像の場合、0が黒、255が白となっている。
# ニューラルは値のレンジが0〜1が望ましいので、そのように規格化
x_train = x_train / 255
x_test = x_test / 255
# クラス分類問題なので、クラスラベルをカテゴリカル変数に変換
num_class = 10
y_train = keras.utils.to_categorical(y_train, num_class)
y_test = keras.utils.to_categorical(y_test, num_class)
import matplotlib.pyplot as plt
print("0枚目の画像に書かれている数字: ", y_train[0])
plt.imshow(x_train[0, :, :], cmap='gray')
plt.show()
続いて、データの形を変換します。CNNは画像からの状態判定のために開発されたものです(工夫すれば、画像以外にも使えます)。それで、いわゆる画像というデータは、「縦サイズ、横サイズ、赤青緑の濃度」という3次元のデータ構造を持ちます。グレースケール画像の場合においても「縦サイズ、横サイズ」という形では不足があり、濃度の情報も添加した形に変換しなければなりません。すなわち、1枚の画像を表現する変数のサイズを、
から
というように、少し変更しなければいけません。以下、これを実現するコードです。
import numpy as np
num_train = len(x_train[:, 0,0]) # 教師データの数
num_test = len(x_test[:, 0,0]) # テストデータの数
size_rows = len(x_train[0, :,0]) # 画像、行サイズ
size_cols = len(x_train[0, 0, :]) # 画像、列サイズ
print("x_trainのshape: ", np.shape(x_train))
x_train_cnn = np.reshape(x_train, [num_train, size_rows, size_cols, 1])
x_test_cnn = np.reshape(x_test, [num_test, size_rows, size_cols, 1])
print("x_train_cnnのshape: ", np.shape(x_train_cnn))
これで、サイズが正しく変化しました。
続いて、CNNを構築していきます。今回は以下のモデルを組んでみます。
レイヤ1: 畳込みレイヤ: カーネルサイズ 5, 5
レイヤ2: 畳込みレイヤ: カーネルサイズ 5, 5
レイヤ3: 畳込みレイヤ: カーネルサイズ 5, 5
レイヤ4: 全結合レイヤ(Flatten)
レイヤ5: 全結合レイヤ(Dense, 30)
レイヤ6: 全結合レイヤ(Classification, 10)
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D
model = Sequential()
model.add(Conv2D(filters=1, kernel_size=(5, 5), activation='relu', input_shape=(28, 28, 1)))
model.add(Conv2D(filters=1, kernel_size=(5, 5), activation='relu'))
model.add(Conv2D(filters=1, kernel_size=(5, 5), activation='relu'))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(30, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(10, activation='softmax'))
# コンパイル
model.compile(loss='categorical_crossentropy', optimizer=RMSprop(), metrics=['accuracy'])
model.summary() # 構造出力
モデルが定義できたところで、学習に進みます。
# plaidmlをバックエンドにする場合は、バッチサイズを「2^n+1」にすること
# それ以外だと、lossがnanになる模様
batch_size = 129
epochs = 30 # 学習回数
learning_process = model.fit(x_train_cnn, y_train, # 画像とラベルデータ
batch_size=batch_size, # バッチサイズ
epochs=epochs, # エポック数
verbose=1)#, # 学習過程を表示する1, しない0
#validation_data=(x_test_cnn, y_test)) # <- ここに検証用のデータを入れると精度が上がる傾向
学習が終わりました。accが上がっていればokです。予期せぬエラーなどで学習済みモデルがなくなると勿体無いので、save & load しておきます。loadしたモデルの変数名は「load_mod」と名前が変わっているので、ご注意ください。
# save
filename = "mnist_cnn.h5" # 保存名
model_json_str = model.to_json()
open('mnist_model.json', 'w').write(model_json_str)
model.save_weights(filename)
# load
from keras.models import model_from_json
load_mod = model_from_json(open('mnist_model.json').read())
load_mod.load_weights(filename)
load_mod.compile(loss='categorical_crossentropy', optimizer=RMSprop(), metrics=['accuracy']) # compileしておかないとエラーが出る場合がある
#load_mod.summary()
学習&保存&読込が済んだところで、予測結果を出してみます。学習に使用しなかったテストデータを利用します。
# 純粋なCNNの推定値を、全テストデータ分、取得
test_pred = load_mod.predict(x_test_cnn, batch_size=32+1) # バッチサイズには2^n+1
# predict関数だけだと、0〜9の数字の最もらしさしか出てこないので、argmaxをとる。
test_pred_list = []
test_true_list = []
for i in range(len(test_pred)):
test_pred_list.append(np.argmax(test_pred[i])) #予測の結果
test_true_list.append(np.argmax(y_test[i])) # 本当の結果
# 結果の閲覧(たくさん出るので注意)
#print(test_pred_list) # 予測
#print(test_true_list) # 実測
これで個別の推定結果が出ました。テストデータが1万枚あるので、1万の判定結果が出てきます。
続いて、正答率を出してみます。推定結果と実測が一致している枚数をカウントし、データの総数で割ればいいので、以下の通りになります。
# 正解枚数のカウント
NumCorrect = 0
for i in range(len(test_pred_list)):
if test_pred_list[i] == test_true_list[i]:
NumCorrect = NumCorrect + 1
# 正答率の算出
AccRate = NumCorrect / len(test_pred_list)
print("手作業で求めた正答率: ", AccRate)
こんな感じで素朴に正答率を出してみましたが、kerasにはこれを一発で出す関数が用意されています。
score = load_mod.evaluate(x_test_cnn, y_test, verbose=1, batch_size=32+1) # バッチサイズには2^n+1
print('Kerasの関数による正答率:', np.round(score[1], 4))
自分で求めた正答率と、Keras の正答率を算出する関数の出力が一致しました。
状況によって、pythonが使えないだとか、kerasが使えないだとか、そういうこともあります。そのためここでは、学習済みのCNNを、数学的に一からすべて正しく、丁寧に実装する方法を説明します。
はじめに、学習済みパラメータの保存を行います。モデルではなく、モデルが有する変数のことです。いわゆる人工知能の正体とも言えます。今回構築したCNNを数学的に実装するには、学習された、以下のパラメータが必要です。
こんな感じで、今回開発したCNNの正体は、だいたい8000個の数字の集まりであることがわかります。入力された28x28サイズの画像が、上述した8000個の数字で掛け算したり、足し算したり、指数関数に通したりして、手書き数字の判定結果が得られるわけです。では、8000個の数字を保存していきます。
###############################
# 学習済みのレイヤ1の畳込みフィルタの取得
temp = load_mod.layers[0].get_weights()[0]
KernelFilter1 = np.zeros([5, 5])
for i in range(5):
for j in range(5):
KernelFilter1[i, j] = temp[i][j][0][0]
print("KernelFilter1: \n", KernelFilter1)
print("****************")
np.savetxt("Lay1_Ker.csv", KernelFilter1, delimiter=",")
###############################
###############################
# 学習済みのレイヤ2の畳込みフィルタの取得
temp = load_mod.layers[1].get_weights()[0]
KernelFilter2 = np.zeros([5, 5])
for i in range(5):
for j in range(5):
KernelFilter2[i, j] = temp[i][j][0][0]
print("KernelFilter2: \n", KernelFilter2)
print("****************")
np.savetxt("Lay2_Ker.csv", KernelFilter2, delimiter=",")
###############################
###############################
# 学習済みのレイヤ3の畳込みフィルタの取得
temp = load_mod.layers[2].get_weights()[0]
KernelFilter3 = np.zeros([5, 5])
for i in range(5):
for j in range(5):
KernelFilter3[i, j] = temp[i][j][0][0]
print("KernelFilter3: \n", KernelFilter3)
print("****************")
np.savetxt("Lay3_Ker.csv", KernelFilter3, delimiter=",")
###############################
# レイヤ4はいらない
###############################
# 学習済みのレイヤ5のweight, biasの取得
P_w5 = load_mod.layers[5].get_weights()[0]
P_b5 = load_mod.layers[5].get_weights()[1]
print("Weight_Lay5:", np.shape(P_w5), "サイズ\n")
print(P_w5)
print("\n Bias_Lay5:", np.shape(P_b5), "サイズ\n")
print(P_b5)
print("****************")
np.savetxt("Lay5_w.csv", P_w5, delimiter=",")
np.savetxt("Lay5_b.csv", P_b5, delimiter=",")
###############################
###############################
# 学習済みのレイヤ6のweight, biasの取得
P_w6 = load_mod.layers[7].get_weights()[0]
P_b6 = load_mod.layers[7].get_weights()[1]
print("Weight_Lay6:", np.shape(P_w6), "サイズ\n")
# print(P_w6) # 多いのでコメントアウト
print("多いので省略")
print("\n Bias_Lay6:", np.shape(P_b6), "サイズ\n")
print(P_b6)
np.savetxt("Lay6_w.csv", P_w6, delimiter=",")
np.savetxt("Lay6_b.csv", P_b6, delimiter=",")
###############################
これで保存が完了しました。どんな数字か見たいからは、csvファイルを覗くと良いでしょう。
さて、これ以降、別の環境で実施していることを前提にします。別の環境でコーディングする場合、保存したcsvファイルをロードするところから始まります。ここからは、便利な関数などはなるべく使用せず、どんな言語でも実装できるようにコーディングしていきます。でも、データを読み込むnp.loadtxt関数、指数関数を求めるmath.exp関数、for文の範囲指定であるrange関数だけは使用します。これはどんな言語でもあると思うので…。
そういうわけで、ロードに進みます。
KernelFilter1 = np.loadtxt("Lay1_Ker.csv", delimiter=",")
KernelFilter2 = np.loadtxt("Lay2_Ker.csv", delimiter=",")
KernelFilter3 = np.loadtxt("Lay3_Ker.csv", delimiter=",")
P_w5 = np.loadtxt("Lay5_w.csv", delimiter=",")
P_b5 = np.loadtxt("Lay5_b.csv", delimiter=",")
P_w6 = np.loadtxt("Lay6_w.csv", delimiter=",")
P_b6 = np.loadtxt("Lay6_b.csv", delimiter=",")
使用する画像以下のものにしてみます。ついでに、Kerasを使用した場合の出力値も取得してみます。
# 試しに入力する画像 (28*28)
img_id = 1
input_dat = x_test[img_id, :, :]
pred_cnn = load_mod.predict(x_test_cnn[img_id:img_id+1, :, :, :])
print("CNNの予測値: ", np.argmax(pred_cnn))
plt.imshow(input_dat, cmap='gray')
plt.title("Input img")
plt.show()
plt.plot(pred_cnn[0])
plt.title("CNN output value")
plt.show()
2が映る画像で、Kerasで構築したモデルも、2に対応する箇所が高いことがわかりました。続いて、Kerasを使わずに組んでみます。
まず、画像を入力したら、レイヤ1によりそれを畳み込む箇所です。これは、
という構造の変換を意味します。CNNの畳み込みがどんな演算か、絵でも良いので、一度見て、理解した後にコードを読み解くと理解できると思います。あとは、ActivationをReLuにした人は、ReLu関数も忘れずに実装します。
# 入力画像のサイズ
Size_Input_f1_0 = 28
Size_Input_f1_1 = 28
# カーネルフィルタのサイズ
# モデル作成時に自分で決めた数字
Size_Ker_f1_0 = 5
Size_Ker_f1_1 = 5
# 出力サイズ
Size_KerOut_f1_0 = Size_Input_f1_0 - Size_Ker_f1_0 + 1
Size_KerOut_f1_1 = Size_Input_f1_1 - Size_Ker_f1_1 + 1
# 出力用の零行列を用意 24*24サイズ
dat_f1_t2 = np.zeros([Size_KerOut_f1_0, Size_KerOut_f1_1])
# 畳込み演算
for k in range(0, Size_KerOut_f1_0):
for t in range(0, Size_KerOut_f1_1):
sub_dat = input_dat[k:k+Size_Ker_f1_0, t:t+Size_Ker_f1_1]
for i in range(Size_Ker_f1_0):
for j in range(Size_Ker_f1_1):
dat_f1_t2[k, t] = dat_f1_t2[k, t] + sub_dat[i, j] * KernelFilter1[i, j]
# ReLu演算
for i in range(Size_KerOut_f1_0):
for j in range(Size_KerOut_f1_1):
if dat_f1_t2[i, j] <= 0:
dat_f1_t2[i, j] = 0
# データ表示
plt.imshow(dat_f1_t2, cmap='gray')
plt.title("from 1 to 2: img")
続いて、2層目です。こちらも畳込みとなります。
# 2層目に入力されるデータのサイズ
Size_Input_f2_0 = Size_KerOut_f1_0
Size_Input_f2_1 = Size_KerOut_f1_1
# カーネルフィルタのサイズ
# モデル作成時に自分で決めた数字
Size_Ker_f2_0 = 5
Size_Ker_f2_1 = 5
# 出力サイズ
Size_KerOut_f2_0 = Size_Input_f2_0 - Size_Ker_f2_0 + 1
Size_KerOut_f2_1 = Size_Input_f2_1 - Size_Ker_f2_1 + 1
# 出力用の零行列を用意 20*20サイズ
dat_f2_t3 = np.zeros([Size_KerOut_f2_0, Size_KerOut_f2_1])
# 畳込み演算
for k in range(0, Size_KerOut_f2_0):
for t in range(0, Size_KerOut_f2_1):
sub_dat = dat_f1_t2[k:k+Size_Ker_f2_0, t:t+Size_Ker_f2_1]
for i in range(Size_Ker_f2_0):
for j in range(Size_Ker_f2_1):
dat_f2_t3[k, t] = dat_f2_t3[k, t] + sub_dat[i, j] * KernelFilter2[i, j]
# ReLu演算
for i in range(Size_KerOut_f2_0):
for j in range(Size_KerOut_f2_1):
if dat_f2_t3[i, j] <= 0:
dat_f2_t3[i, j] = 0
# データ表示
plt.imshow(dat_f2_t3, cmap='gray')
plt.title("from 2 to 3: img")
続いて、3層目です。こちらも畳込みとなります。
# 3層目に入力されるデータのサイズ
Size_Input_f3_0 = Size_KerOut_f2_0
Size_Input_f3_1 = Size_KerOut_f2_1
# カーネルフィルタのサイズ
# モデル作成時に自分で決めた数字
Size_Ker_f3_0 = 5
Size_Ker_f3_1 = 5
# 出力サイズ
Size_KerOut_f3_0 = Size_Input_f3_0 - Size_Ker_f3_0 + 1
Size_KerOut_f3_1 = Size_Input_f3_1 - Size_Ker_f3_1 + 1
# 出力用の零行列を用意 16*16サイズ
dat_f3_t4 = np.zeros([Size_KerOut_f3_0, Size_KerOut_f3_1])
# 畳込み演算
for k in range(0, Size_KerOut_f3_0):
for t in range(0, Size_KerOut_f3_1):
sub_dat = dat_f2_t3[k:k+Size_Ker_f3_0, t:t+Size_Ker_f3_1]
for i in range(Size_Ker_f3_0):
for j in range(Size_Ker_f3_1):
dat_f3_t4[k, t] = dat_f3_t4[k, t] + sub_dat[i, j] * KernelFilter3[i, j]
# ReLu演算
for i in range(Size_KerOut_f3_0):
for j in range(Size_KerOut_f3_1):
if dat_f3_t4[i, j] <= 0:
dat_f3_t4[i, j] = 0
# データ表示
plt.imshow(dat_f3_t4, cmap='gray')
plt.title("from 3 to 4: img")
これで畳込みが終わりました。続いて、全結合層に行きます。 これは、以下の変換を表します。単純に、26x26をまっすぐに並べ替えただけです。
# 5層目の出力サイズ
Size_Out_f4 = Size_KerOut_f3_0 * Size_KerOut_f3_1
dat_f4_t5 = np.zeros(Size_Out_f4)
k=0
for i in range(Size_KerOut_f3_0):
for j in range(Size_KerOut_f3_1):
dat_f4_t5[k] = dat_f3_t4[i, j]
k = k + 1
# データ表示
plt.plot(dat_f4_t5)
plt.title("from 4 to 5: flat")
続いて、全結合層から全結合層への変換です。これは、以下の変換構造を持ちます。
「出力 = P_w5 * 入力 + P_b5」というベクトルと行列の演算を意味します。ReLu関数を通すのも忘れずに行います。
# 出力サイズ
# 30はモデル構築時に自分で決めた数
Size_Out_f5 = 30
dat_f5_t6 = np.zeros(Size_Out_f5)
# Weight, Bias演算
k=0
for j in range(Size_Out_f5):
for i in range(Size_Out_f4):
dat_f5_t6[k] = dat_f5_t6[k] + dat_f4_t5[i] * P_w5[i, j]
dat_f5_t6[k] = dat_f5_t6[k] + P_b5[j]
k=k+1
# ReLu演算
for i in range(Size_Out_f5):
if dat_f5_t6[i] < 0:
dat_f5_t6[i] = 0
# データ表示
plt.plot(dat_f5_t6)
plt.title("from 5 to 6: flat")
最後も、全結合層から全結合層への変換です。これは、以下の変換構造を持ちます。
「出力 = P_w6 * 入力 + P_b6」というベクトルと行列の演算を意味します。モデルの構造を見てもらうとわかると思いますが、最後はReLuではなくSoftmaxがActivationになっています。softmaxは、出力された10次元の値、個別にexpを取り、その後、各値のexpを総和で割るという演算になります。
# 出力サイズ
# 10はモデル構築時に自分で決めた数
# 0〜9の数字を判定したいので、10となる。
Size_Out_f6 = 10
dat_f6_t7 = np.zeros(Size_Out_f6)
# Weight, Bias演算
k=0
for j in range(Size_Out_f6):
for i in range(Size_Out_f5):
dat_f6_t7[k] = dat_f6_t7[k] + dat_f5_t6[i] * P_w6[i, j]
dat_f6_t7[k] = dat_f6_t7[k] + P_b6[j]
k=k+1
# softmax演算(最後はReLuではなく、Softmax)
import math
sumval = 0
for i in range(Size_Out_f6):
sumval = sumval + math.exp(dat_f6_t7[i])
for i in range(Size_Out_f6):
dat_f6_t7[i] = math.exp(dat_f6_t7[i]) / sumval
# データ表示
plt.plot(dat_f6_t7)
plt.title("from 6 to 7: flat")
plt.show()
これが、学習済みのCNNを手作業で組んで行った際の、出力値です。dat_f6_t7に10個の数字が格納されています。今回は2番目の値が高いことがわかりますので、「数字の2」と判定されることになります。ところでこれ、当然のことながら、Kerasのpredict関数で求めた形とまったく一緒です。したがって、Kerasの中に組み込まれているpredict関数と、今回ここで書いたコードは、意味的にまったく一緒であることを意味します。ですから、別の環境でここのコードをうまく移植すれば、どんな環境でも学習済みのCNNを使用できるようになります。