2019-04-19

ゼロから作るDeep Learningとともに学ぶフレームワーク(3層ニューラルネットワーク & 手書き数字認識編)

はじめに

このシリーズでは、深層学習の入門書として有名な、「ゼロから作るDeep Learning」(以下、ゼロから〜)と同時並行で、フレームワークを学習し、その定着を目指します。

前回の第一弾: パーセプトロン編に目を通していない方は、先に目を通しておくことをおすすめします。

それでは、今回は3層ニューラルネットワークをKerasで実装し、実際に手書き数字認識(MNISTデータセットの分類)をしていきましょう!

(3層ニューラルネットワーク)参考: ゼロから〜のP58〜65
(MNIST)参考: ゼロから〜のP72〜81

1. 3層ニューラルネットワークの実装

1.1 プロジェクトの作成

まず、3層ニューラルネットワーク実装用のプロジェクトを作成してください。
以下本章では、three_nn.pyを作成するものとして進めていきます。

1.2 モデルの作成

  • 今回の実装からは全てFunctional APIを用いたモデル実装を行います。
    Sequentialモデルによる実装は行いませんので、あらかじめご了承ください。

  • これから実装する3層ニューラルネットワークの概念図を以下に示します。
    実装の際の参考にしてください。

3層ニューラルネットワークの概念図

1.2.1 レイヤー & NumPyを読み込む

まず、前回と同じように、実装に必要なレイヤーとNumPyを読み込みます。

from keras import Model
from keras.layers import Input, Dense

import numpy as np

1.2.2 各種レイヤーを定義する

  • 3層のニューラルネットワークなので、それと同数の3つのDenseレイヤーのインスタンスを生成します。

  • unitsは各層におけるニューロンの数を表しています。

  • 活性化関数については、ゼロから〜ではsigmoid関数が使用されているので、ここでもsigmoidを活用します。

  • 下の例では、簡単のため入れ子式に活性化関数を定義しましたが、活性化関数をレイヤーインスタンスとして定義することもできます。

<活性化関数を入れ子式に定義する例>
_input = Input(shape=(2, ))
_layer1 = Dense(units=3, activation='sigmoid')(_input)
_layer2 = Dense(units=2, activation='sigmoid')(_layer1)
_output = Dense(units=2)(_layer2)
<活性化関数をインスタンスとして定義する例>
from keras.layers import Activation
_input = Input(shape=(2, ))
_layer1 = Dense(units=3)(_input)
_activ1 = Activation('sigmoid')(_layer1)
_layer2 = Dense(units=2)(_activ1)
_activ2 = Activation('sigmoid')(_layer2)
_output = Dense(units=2)(_activ2)

参考: https://keras.io/ja/activations/

1.2.3 モデルを定義する

  • 前回と同様に、入力のテンソルと出力のテンソルをModelに渡すことで、モデルを定義します。

  • model.summary()でモデルの状態を確認することができます。

model = Model(inputs=_input, outputs=_output)
model.summary() # モデルの状態をみる

Output:

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
input_1 (InputLayer)         (None, 2)                 0
_________________________________________________________________
dense_1 (Dense)              (None, 3)                 9
_________________________________________________________________
dense_2 (Dense)              (None, 2)                 8
_________________________________________________________________
dense_3 (Dense)              (None, 2)                 6
=================================================================
Total params: 23
Trainable params: 23
Non-trainable params: 0
_________________________________________________________________

1.2.4 重みを設定する

  • ゼロから〜では重みの初期化を定義しているので、本実装でも事前に重みを設定します。

  • Kerasでは、重みは入力層から順にNumPyの配列で保持されています。
    したがって、重みを設定する際には、順当に重みの配列をNumPy配列で定義して、そのリストを渡せばよいです。

  • 重みの設定には、model.set_weights()を使用します。
    引数は重みの入ったリストです。

w1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
b1 = np.array([0.1, 0.2, 0.3])
w2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
b2 = np.array([0.1, 0.2])
w3 = np.array([[0.1, 0.3], [0.2, 0.4]])
b3 = np.array([0.1, 0.2])
weight_array = [w1, b1, w2, b2, w3, b3]

model.set_weights(weight_array)
print(model.get_weights())

参照: ゼロから〜 P65

Output:

[array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]], dtype=float32), 
    array([0.1, 0.2, 0.3], dtype=float32), 
    array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]], dtype=float32), 
    array([0.1, 0.2], dtype=float32), 
    array([[0.1, 0.3], [0.2, 0.4]], dtype=float32), 
    array([0.1, 0.2], dtype=float32)]

1.3 テストする

それでは、作成したモデルをテストしましょう。
作成したモデルのテストには、前回と同様に、model.predict()を使用します。

テストデータは、ゼロから〜 P65に示されているものと同一のものを使用します。

X = np.array([[1.0, 0.5]])
Y = model.predict(X)

print(Y)

Output:

[[0.3168271 0.6962791]]

見事、ゼロから〜 P65に示されている出力値と同一の値を得ることができました!

2. 手書き数字認識(MNIST)

ここからは、今実装した3層ニューラルネットワークを活用して、手書き数字を実際に分類していきます。

参考: ゼロから〜のP72〜81

2.1 MNISTデータセットの読み込み

Kerasでは、MNISTデータセットは簡単にダウンロード&呼び出しできるようになっています。

mnist_nn.pyを作成して、以下を入力し動作させてみてください。

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

print(x_train.shape)
print(y_train.shape)

参照: https://keras.io/ja/datasets/#mnist

Output:

(60000, 28, 28)
(60000,)

(注意) Kerasのmnist.load_data()は、「正規化されていない & 2次元配列」の状態のリストが返ってきます。ゼロから〜のP73の挙動とは異なるので、注意してください。

ここで、データセットがtraintestの二つに分割されていることに気がつくと思います。これは、機械学習においてはモデルの汎化性能を測定するために、通常データセットを訓練データとテストデータの二つ(もしくはそれ以上)に分割するためです。詳しくは、ゼロから〜の第4章を見てください。

2.2 データセットの前処理

モデルの推論処理においてMNISTデータセットを活用するので、「正規化 & 一次元配列化」の処理を行っておきます。

  • 配列の形状変換は.reshapeメソッドで行えます。
    引数は変換後の配列形状です。

  • 正規化処理のために、リストをfloat型に変換します。
    型変換は、.astypeメソッドで行えます。

# 一次元配列にする
x_train = x_train.reshape(60000, 784)
x_test = x_test.reshape(10000, 784)

# 正規化処理
x_train = x_train.astype('float')
x_test = x_test.astype('float')
x_train /= 255.
x_test /= 255.

print(x_train.shape)

Output:

(60000, 784)

きちんと一次元配列に変換されていることが確認できました。
正規化されているかどうかは、配列の要素のうち一つを表示させて確認してみてください。

2.3 モデルの改良

three_nn.pyでMNISTを分類できるように、以下の2点を改良します。
改良したものは、mnist_nn.pyに追記してください。

  • ゼロから〜のP76と合わせて、一層目の全結合層のunit数を50、二層目を100とし、出力層は10とします。

  • MNISTデータセットの分類は「分類問題」であるため、出力層にsoftmax層を追加します。

from keras import Model
from keras.layers import Input, Dense, Activation
import numpy as np

_input = Input(shape=(784, ))
_hidden = Dense(units=50, activation='sigmoid')(_input)
_hidden = Dense(units=100, activation='sigmoid')(_hidden)
_hidden = Dense(units=10)(_hidden)
_output = Activation('softmax')(_hidden)

model = Model(inputs=_input, outputs=_output)
model.summary() # モデルの状態をみる

Output:

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
input_1 (InputLayer)         (None, 784)               0
_________________________________________________________________
dense_1 (Dense)              (None, 50)                39250
_________________________________________________________________
dense_2 (Dense)              (None, 100)               5100
_________________________________________________________________
dense_3 (Dense)              (None, 10)                1010
_________________________________________________________________
activation_1 (Activation)    (None, 10)                0
=================================================================
Total params: 45,360
Trainable params: 45,360
Non-trainable params: 0

きちんと望みのモデルが実装されていることが確認できました。

ここで、中間層の定義方法について、three_nn.pyと書き方が変わっていると気づいた方もいるでしょう。

Functional APIを用いたモデル定義では、前回紹介したように、入力と出力のテンソルを個別に保持しておけば良いルールになっています。

したがって、中間のベクトルに関しては特段利用したいケースがない限りは、同一の変数を利用した方が、コードが煩雑にならないのでおすすめです。

2.4 重みファイルの読み込み

重みファイルは、ゼロから〜のものと同一のファイルを使用します。

  • 公式レポジトリから、sample_weight.pklをダウンロードしてください。

  • mnist_nn.pyと同一ディレクトリ内にsample_weight.pklを配置してください。

  • 以下が、重みファイルを読み込むためのスクリプトになります。
    なお、今回は「推論処理のみ行う & できる限りゼロから〜に即したものにする」ために、このスクリプトを使用しますが、このような手間のかかる初期化処理は通常行いません。

import pickle
def load_weight():
    with open('sample_weight.pkl', 'rb') as f:
        weights = pickle.load(f)
        weight_array = [weights['W1'], weights['b1'], 
                        weights['W2'], weights['b2'],
                        weights['W3'], weights['b3']]
        
        return weight_array

model.set_weights(load_weight())

参考: Kerasでモデルの保存/ロードを行う

2.5 テストする

それでは、ゼロから〜のP77と同様に実装したモデルをテストしていきましょう。

  • np.argmaxで軸(axis)指定をしていますが、これはリストが2次元配列以上のときに、「どの軸方向に対して演算を行うか」を指定するために活用します。
    軸指定の詳細については、このページを参照するとわかりやすいです。
_y_test = model.predict(x_test)
_y_test = np.argmax(_y_test, axis=1)

print(f'Accuracy: {np.sum(y_test == _y_test) / len(y_test)}')

Output:

Accuracy: 0.9352

めでたく、ゼロから〜のP77に示されている分類精度: 0.9352を得ることができました!

まとめ

今回はゼロから〜の3章に対応する部分である、3層ニューラルネットとMNISTデータセットの分類をKerasで実装しました。 深層学習フレームワークを活用することで、たった数行でゼロから〜にあるものと同一のモデルを実装できることを実感したと思います。

次回からはゼロから〜の4章以降に対応する、ニューラルネットワークの学習に入ります!

ソースコード

ソースコードは、GitHubにて公開しています。