PyLearn Keras 03: モノクロ画像に対するCNNの学習

今回は、Mnistデータ(0〜9が手書きされた、9つの数字)と、畳込みニューラルネットワーク(Convolutional Neural Network; CNN)を利用して、数字当ての深層学習を作ってみます。本ページの後半に学習済みのモデルを、誰かが作った便利な関数を(ほとんど)使わずに直で判定するコードも置いていますので、便利な関数を使わずに深層学習を実装することに興味がある人はそちらも一読ください。

まず、準備として、jupyter を開く予定のターミナルで、

  • export PLAIDML_NATIVE_PATH=/usr/local/lib/libplaidml.dylib
  • export RUNFILES_DIR=/usr/local/share/plaidml

を実行します。その後、バックエンドをチェックします。plaidmlが返って来ればokです。

In [1]:
# kerasのバックエンドを plaidmlに変更
import plaidml.keras
plaidml.keras.install_backend()

import keras
print(keras.backend.backend())
plaidml

1. Mnist画像のロードと加工

まず、Mnistのデータロードを行います。

In [2]:
from keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

PyLearn Keras 01: Macbook/GPUによる深層学習 では、階層型ニューラルネットワークによって、

  • 1層目: dim_input_vec次元ベクトル(入力層、28*28画像なので、その積である784)
  • 2層目: 200次元ベクトル(中間層/隠れ層1)
  • 3層目: 100次元ベクトル(中間層/隠れ層2)
  • 4層目: num_class次元ベクトル(出力層)

という構造をしたモデルにて、Mnistの数字当てを行いました。上記のモデルでは、入力層がベクトル(まっすぐに長い数字の列)となっています。今回実施するCNNでは、入力としてベクトルを採用するのではなく、画像(つまり、行列成分を有するデータ)を採用する必要があります。したがいまして、 PyLearn Keras 01: Macbook/GPUによる深層学習 で実施した前処理の中で、画像をベクトルに変換する部分は、実施する必要がありません。すなわち前処理は、

  • 処理1: 0-255レンジを0-1レンジに変換
  • 処理2: クラスラベルをカテゴリカル変数に変換

となります。

In [3]:
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)
教師データサイズ:  60000
テストデータサイズ:  10000
In [5]:
import matplotlib.pyplot as plt
print("0枚目の画像に書かれている数字: ", y_train[0])
plt.imshow(x_train[0, :, :], cmap='gray')
plt.show()
0枚目の画像に書かれている数字:  [0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]

続いて、データの形を変換します。CNNは画像からの状態判定のために開発されたものです(工夫すれば、画像以外にも使えます)。それで、いわゆる画像というデータは、「縦サイズ、横サイズ、赤青緑の濃度」という3次元のデータ構造を持ちます。グレースケール画像の場合においても「縦サイズ、横サイズ」という形では不足があり、濃度の情報も添加した形に変換しなければなりません。すなわち、1枚の画像を表現する変数のサイズを、

  • 28, 28

から

  • 28, 28, 1

というように、少し変更しなければいけません。以下、これを実現するコードです。

In [6]:
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))
x_trainのshape:  (60000, 28, 28)
x_train_cnnのshape:  (60000, 28, 28, 1)

これで、サイズが正しく変化しました。

2. CNNの構築

続いて、CNNを構築していきます。今回は以下のモデルを組んでみます。


レイヤ1: 畳込みレイヤ: カーネルサイズ 5, 5

  • 28, 28サイズの画像を、28-5+1, 28+5+1サイズに畳込

レイヤ2: 畳込みレイヤ: カーネルサイズ 5, 5

  • 24, 24サイズのデータを、24-5+1, 24-5+1サイズに畳込

レイヤ3: 畳込みレイヤ: カーネルサイズ 5, 5

  • 20, 20サイズのデータを、20-5+1, 20-5+1サイズに畳込

レイヤ4: 全結合レイヤ(Flatten)

  • 16, 16サイズのデータを、16x16 = 256次元のベクトルに変換

レイヤ5: 全結合レイヤ(Dense, 30)

  • 256次元のベクトルを、30次元のベクトルに変換

レイヤ6: 全結合レイヤ(Classification, 10)

  • 30次元のベクトルを、10次元に変換
  • ここが判定層で、0〜9の数字の確信度をベクトルとして出力することを意味する。

In [7]:
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() # 構造出力
INFO:plaidml:Opening device "opencl_intel_hd_graphics_615.0"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 24, 24, 1)         26        
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 20, 20, 1)         26        
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 16, 16, 1)         26        
_________________________________________________________________
dropout_1 (Dropout)          (None, 16, 16, 1)         0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 256)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 30)                7710      
_________________________________________________________________
dropout_2 (Dropout)          (None, 30)                0         
_________________________________________________________________
dense_2 (Dense)              (None, 10)                310       
=================================================================
Total params: 8,098
Trainable params: 8,098
Non-trainable params: 0
_________________________________________________________________

モデルが定義できたところで、学習に進みます。

In [8]:
# 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)) # <- ここに検証用のデータを入れると精度が上がる傾向
Epoch 1/30
60000/60000 [==============================] - 28s 472us/step - loss: 1.2036 - acc: 0.5979
Epoch 2/30
60000/60000 [==============================] - 8s 127us/step - loss: 0.8244 - acc: 0.7265
Epoch 3/30
60000/60000 [==============================] - 8s 127us/step - loss: 0.7403 - acc: 0.7551
Epoch 4/30
60000/60000 [==============================] - 7s 124us/step - loss: 0.7044 - acc: 0.7669
Epoch 5/30
60000/60000 [==============================] - 8s 126us/step - loss: 0.6752 - acc: 0.7751
Epoch 6/30
60000/60000 [==============================] - 7s 122us/step - loss: 0.6593 - acc: 0.7812
Epoch 7/30
60000/60000 [==============================] - 7s 124us/step - loss: 0.6436 - acc: 0.7863
Epoch 8/30
60000/60000 [==============================] - 8s 125us/step - loss: 0.6332 - acc: 0.7904
Epoch 9/30
60000/60000 [==============================] - 7s 122us/step - loss: 0.6281 - acc: 0.7920
Epoch 10/30
60000/60000 [==============================] - 8s 127us/step - loss: 0.6186 - acc: 0.7963
Epoch 11/30
60000/60000 [==============================] - 7s 118us/step - loss: 0.6144 - acc: 0.7969
Epoch 12/30
60000/60000 [==============================] - 10s 170us/step - loss: 0.6078 - acc: 0.7996
Epoch 13/30
60000/60000 [==============================] - 11s 177us/step - loss: 0.6044 - acc: 0.80030s - loss: 0.6062 -
Epoch 14/30
60000/60000 [==============================] - 8s 139us/step - loss: 0.6053 - acc: 0.7989
Epoch 15/30
60000/60000 [==============================] - 9s 146us/step - loss: 0.6020 - acc: 0.8006
Epoch 16/30
60000/60000 [==============================] - 11s 185us/step - loss: 0.6004 - acc: 0.8032
Epoch 17/30
60000/60000 [==============================] - 10s 167us/step - loss: 0.5948 - acc: 0.8019
Epoch 18/30
60000/60000 [==============================] - 9s 150us/step - loss: 0.5889 - acc: 0.8053
Epoch 19/30
60000/60000 [==============================] - 10s 171us/step - loss: 0.5943 - acc: 0.8027
Epoch 20/30
60000/60000 [==============================] - 8s 141us/step - loss: 0.5899 - acc: 0.8037
Epoch 21/30
60000/60000 [==============================] - 9s 157us/step - loss: 0.5913 - acc: 0.8042
Epoch 22/30
60000/60000 [==============================] - 8s 138us/step - loss: 0.5864 - acc: 0.8057
Epoch 23/30
60000/60000 [==============================] - 8s 141us/step - loss: 0.5836 - acc: 0.8065
Epoch 24/30
60000/60000 [==============================] - 9s 145us/step - loss: 0.5855 - acc: 0.8083
Epoch 25/30
60000/60000 [==============================] - 8s 132us/step - loss: 0.5771 - acc: 0.8067
Epoch 26/30
60000/60000 [==============================] - 10s 167us/step - loss: 0.5820 - acc: 0.8077
Epoch 27/30
60000/60000 [==============================] - 8s 126us/step - loss: 0.5769 - acc: 0.8074
Epoch 28/30
60000/60000 [==============================] - 9s 155us/step - loss: 0.5815 - acc: 0.8068
Epoch 29/30
60000/60000 [==============================] - 11s 190us/step - loss: 0.5736 - acc: 0.8110
Epoch 30/30
60000/60000 [==============================] - 9s 156us/step - loss: 0.5817 - acc: 0.8081

学習が終わりました。accが上がっていればokです。予期せぬエラーなどで学習済みモデルがなくなると勿体無いので、save & load しておきます。loadしたモデルの変数名は「load_mod」と名前が変わっているので、ご注意ください。

In [9]:
# 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()

学習&保存&読込が済んだところで、予測結果を出してみます。学習に使用しなかったテストデータを利用します。

In [10]:
# 純粋な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万の判定結果が出てきます。

  • test_pred_list: CNNの予測値
  • test_true_list: 実測値(正解)

続いて、正答率を出してみます。推定結果と実測が一致している枚数をカウントし、データの総数で割ればいいので、以下の通りになります。

In [11]:
# 正解枚数のカウント
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)
手作業で求めた正答率:  0.932

こんな感じで素朴に正答率を出してみましたが、kerasにはこれを一発で出す関数が用意されています。

In [12]:
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))
10000/10000 [==============================] - 7s 665us/step
Kerasの関数による正答率: 0.932

自分で求めた正答率と、Keras の正答率を算出する関数の出力が一致しました。

3. Kerasが使えない環境で学習済みCNNを使用したい場合

状況によって、pythonが使えないだとか、kerasが使えないだとか、そういうこともあります。そのためここでは、学習済みのCNNを、数学的に一からすべて正しく、丁寧に実装する方法を説明します。

学習済みパラメータの保存

はじめに、学習済みパラメータの保存を行います。モデルではなく、モデルが有する変数のことです。いわゆる人工知能の正体とも言えます。今回構築したCNNを数学的に実装するには、学習された、以下のパラメータが必要です。

  • レイヤ1のカーネルフィルタ(5x5サイズ、25個の数字)
  • レイヤ2のカーネルフィルタ(5x5サイズ、25個の数字)
  • レイヤ3のカーネルフィルタ(5x5サイズ、25個の数字)
  • レイヤ4はただ単に数字を並べ替えるだけなので不要
  • レイヤ5のウェイト、バイアス(256x30サイズ=7680個の数字と、30個の数字)
  • レイヤ6のウェイト、バイアス(30x10サイズ=300個の数字と、10個の数字)

こんな感じで、今回開発したCNNの正体は、だいたい8000個の数字の集まりであることがわかります。入力された28x28サイズの画像が、上述した8000個の数字で掛け算したり、足し算したり、指数関数に通したりして、手書き数字の判定結果が得られるわけです。では、8000個の数字を保存していきます。

In [13]:
###############################
# 学習済みのレイヤ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=",")
###############################
KernelFilter1: 
 [[ 0.0725766  -0.18512093  0.04283413 -0.06226759  0.00127519]
 [ 0.20374194 -0.03090063 -0.01663686  0.13214253 -0.23772195]
 [-0.02643861  0.14215739  0.0905127   0.09652374  0.10931395]
 [ 0.1637312  -0.17387666  0.00426118 -0.22242694  0.44430688]
 [ 0.18959969  0.02203576  0.20840842  0.01867931 -0.16926104]]
****************
KernelFilter2: 
 [[ 0.02799382  0.30246016 -0.13923211 -0.21329568 -0.28167289]
 [ 0.26355261 -0.33342406 -0.16267103 -0.14554363 -0.22101289]
 [ 0.26220968  0.12371004 -0.39613914  0.20716415  0.45005083]
 [-0.16169567  0.25502664  0.26859719 -0.19691962  0.19438994]
 [ 0.26613805 -0.03518784 -0.24255306 -0.04587831  0.12922178]]
****************
KernelFilter3: 
 [[-0.12840924  0.12985058  0.07387821  0.20329417  0.07975445]
 [-0.03778166  0.11307009  0.27098066  0.22765625  0.09761964]
 [ 0.06358442  0.22036733  0.15844107  0.16178069  0.03562485]
 [ 0.08985481  0.08192424  0.15038738 -0.016541    0.09781121]
 [-0.06663102  0.03053563 -0.02995876  0.07302466  0.03631626]]
****************
Weight_Lay5: (256, 30) サイズ

[[-0.0024117  -0.25764716 -0.49851346 ...  0.13412568 -0.4925198
  -0.16419801]
 [ 0.02425079 -0.13938682 -0.3868336  ...  0.19046925 -0.50889105
  -0.25494564]
 [ 0.04187455 -0.10685716 -0.26434058 ...  0.10026919 -0.35287583
  -0.24744828]
 ...
 [ 0.06160944  0.16325003  0.08945863 ... -0.04720748  0.11336673
  -0.09262132]
 [ 0.0081775   0.09767069  0.12844846 ...  0.03042425  0.11422297
  -0.13963044]
 [ 0.05673273 -0.00285138  0.1618923  ...  0.03791244  0.08343229
  -0.35024673]]

 Bias_Lay5: (30,) サイズ

[-0.8165357   0.16318463  0.22123438  0.14572375 -0.38451263 -0.37518784
  0.51936305  0.4113314  -0.5398655  -0.05322728 -0.5360835  -0.4035481
  0.03102839 -0.25546816 -0.039253   -0.18972811  0.14655104 -0.2103144
  0.3357091  -0.09973868 -0.05288942  0.11735659  0.70584255 -0.04982376
  0.25156215  0.6513233   0.41894385 -0.0332986   0.42300007 -0.02351983]
****************
Weight_Lay6: (30, 10) サイズ

多いので省略

 Bias_Lay6: (10,) サイズ

[-0.61536837  0.5968966  -0.655686   -0.12696537  0.2053403   0.43974125
  0.4945732  -0.5749861  -0.15180406 -0.32692066]

これで保存が完了しました。どんな数字か見たいからは、csvファイルを覗くと良いでしょう。

データのロード & 便利な関数を使わないCNNの実装

さて、これ以降、別の環境で実施していることを前提にします。別の環境でコーディングする場合、保存したcsvファイルをロードするところから始まります。ここからは、便利な関数などはなるべく使用せず、どんな言語でも実装できるようにコーディングしていきます。でも、データを読み込むnp.loadtxt関数、指数関数を求めるmath.exp関数、for文の範囲指定であるrange関数だけは使用します。これはどんな言語でもあると思うので…。

そういうわけで、ロードに進みます。

In [14]:
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を使用した場合の出力値も取得してみます。

In [15]:
# 試しに入力する画像 (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()
CNNの予測値:  2

2が映る画像で、Kerasで構築したモデルも、2に対応する箇所が高いことがわかりました。続いて、Kerasを使わずに組んでみます。

第1層から第2層への変換(畳込みレイヤ)

まず、画像を入力したら、レイヤ1によりそれを畳み込む箇所です。これは、

  • 入力: 28x28サイズの行列
  • 使用するパラメータ: KernelFilter1, 5x5サイズ
  • 出力: 24x24サイズの行列

という構造の変換を意味します。CNNの畳み込みがどんな演算か、絵でも良いので、一度見て、理解した後にコードを読み解くと理解できると思います。あとは、ActivationをReLuにした人は、ReLu関数も忘れずに実装します。

In [16]:
# 入力画像のサイズ
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")
Out[16]:
Text(0.5, 1.0, 'from 1 to 2: img')

第2層から第3層への変換(畳込みレイヤ)

続いて、2層目です。こちらも畳込みとなります。

  • 入力: 24x24サイズの行列
  • 使用するパラメータ: KernelFilter2, 5x5サイズ
  • 出力: 20x20サイズの行列
In [17]:
# 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")
Out[17]:
Text(0.5, 1.0, 'from 2 to 3: img')

第3層から第4層への変換(畳込みレイヤ)

続いて、3層目です。こちらも畳込みとなります。

  • 入力: 20x20サイズの行列
  • 使用するパラメータ: KernelFilter3, 5x5サイズ
  • 出力: 16x16サイズの行列
In [18]:
# 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")
Out[18]:
Text(0.5, 1.0, 'from 3 to 4: img')

第4層から第5層への変換(全結合層)

これで畳込みが終わりました。続いて、全結合層に行きます。 これは、以下の変換を表します。単純に、26x26をまっすぐに並べ替えただけです。

  • 入力: 16x16サイズの行列
  • 使用するパラメータ: なし
  • 出力: 256次元のベクトル
In [19]:
# 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")
Out[19]:
Text(0.5, 1.0, 'from 4 to 5: flat')

第5層から第6層への変換(全結合層)

続いて、全結合層から全結合層への変換です。これは、以下の変換構造を持ちます。

  • 入力: 256次元のベクトル
  • 使用するパラメータ: P_w5(256x30サイズの行列), P_b5(30次元のベクトル)
  • 出力: 30次元のベクトル

「出力 = P_w5 * 入力 + P_b5」というベクトルと行列の演算を意味します。ReLu関数を通すのも忘れずに行います。

In [20]:
# 出力サイズ
# 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")
Out[20]:
Text(0.5, 1.0, 'from 5 to 6: flat')

第6層から第7層への変換(全結合層)

最後も、全結合層から全結合層への変換です。これは、以下の変換構造を持ちます。

  • 入力: 30次元のベクトル
  • 使用するパラメータ: P_w6(30x10サイズの行列), P_b6(10次元のベクトル)
  • 出力: 10次元のベクトル

「出力 = P_w6 * 入力 + P_b6」というベクトルと行列の演算を意味します。モデルの構造を見てもらうとわかると思いますが、最後はReLuではなくSoftmaxがActivationになっています。softmaxは、出力された10次元の値、個別にexpを取り、その後、各値のexpを総和で割るという演算になります。

In [21]:
# 出力サイズ
# 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を使用できるようになります。