多層パーセプトロン(MLP)による手書き数字認識(Kerasでの実装)

多層パーセプトロン(MLP)で、MNISTの手書き数字を認識する。

最も単純なニューラルネットワークを使ったケースは、

で実装している。

MLPは、このケースより、中間層の数を増やしただけだ。2層フィードフォワードニューラルネットワークとは、MLPの2層の場合だと言える。

さて、これから実装するのは3層のMLPだ。つまり、中間層が2層と出力層が1層の構成となる。高い正解率を目指すため、ユニット数も多くする。ただし、過学習を防ぐために、何らかの正則化をする必要がある。

今回使う正則化テクニックは、ドロップアウトと呼ばれるものだ。まず、ドロップアウト率、すなわち、ユニット(ニューロン)が一時的に消去される確率pを定める。そして、パラメータの更新ごとに、各ユニットを確率pで一時的に消去する。

このようにすると、トレーニング時に使われるユニット数は、平均してキープ率(1 - p)倍になる。テスト時には全てのユニットを使うから、出力が 1 / (1 - p) 倍になってしまう。その分の調整として、出力に1 - pを掛ける。

調整の仕方は1つではない。個々の入力の重みにキープ率を掛けてもいいし、トレーニング時から出力をキープ率で割っておいてもいい。意味的に異なるが、どれもうまく機能するらしい。ただ、Kerasで実装するなら、自分で調整する必要はない。

なお、ドロップアウト率は0.5に設定されることが多い。

ドロップアウトを使うと、近隣のユニットや、ごく少数のユニットに依存したネットワークが形成されにくい。その結果、高い汎化能力が得られる。

また、ドロップアウトはアンサンブル学習と解釈することもできる。ドロップアウト可能なユニット数をNとすると、ネットワークは全部で2N通りある。N=100としても、2100=(210)10=102410≃(103)10=1030だから、同じネットワークが2回以上使われる確率は、ほとんどゼロだ。パラメータ更新が1万回だとすると、1万種類のニューラルネットワークが訓練される。最終的に得られるニューラルネットワークは、この1万種類のニューラルネットワークを平均化するアンサンブルということになる。

ドロップアウトに加えてearly stoppingも併用したいが、うまくいかない。ドロップアウトを使うと、一時的にニューラルネットワークの性能が下がることがあり、その時点で学習が打ち切られてしまうのかもしれない。ドロップアウトを使うなら、エポック数をある程度多くする必要がある。

以下に、結果を記載しておく。ドロップアウトは、2つの中間層にのみ適用している(上図は2層の場合で、入力層にもドロップアウトを適用していることに注意)。

Multi-Layer Perceptron (MLP)

In [1]:
import os
from time import time
from datetime import datetime, timedelta, timezone
import math
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.utils import np_utils
from keras.callbacks import EarlyStopping, TensorBoard
Using TensorFlow backend.
In [2]:
(X_train, y_train), (X_test, y_test) = mnist.load_data()
In [3]:
img_rows, img_cols = 28, 28
input_dim = img_rows * img_cols # = 784
In [4]:
X_train = X_train.reshape(-1, input_dim).astype('float32') / 255
X_test = X_test.reshape(-1, input_dim).astype('float32') / 255
In [5]:
X_train.shape
Out[5]:
(60000, 784)
In [6]:
output_dim = 10
In [7]:
Y_train = np_utils.to_categorical(y_train.astype('int32'), output_dim) 
Y_test = np_utils.to_categorical(y_test.astype('int32'), output_dim)
In [8]:
Y_train[0]
Out[8]:
array([0., 0., 0., 0., 0., 1., 0., 0., 0., 0.], dtype=float32)
In [9]:
UNITS_1 = 256
UNITS_2 = 512
In [10]:
ACTIVATION_1 = 'relu'
ACTIVATION_2 = 'relu'
In [11]:
DROPOUT_RATE_1 = 0.5
DROPOUT_RATE_2 = 0.5
In [12]:
OPTIMIZER = 'adam'
In [13]:
BATCH_SIZE = 128
In [14]:
EPOCHS = 50
In [15]:
VALIDATION_SPLIT = 0.1
In [16]:
model = Sequential()
model.add(Dense(UNITS_1, activation=ACTIVATION_1, input_dim=input_dim))
model.add(Dropout(DROPOUT_RATE_1))
model.add(Dense(UNITS_2, activation=ACTIVATION_2))
model.add(Dropout(DROPOUT_RATE_2))
model.add(Dense(output_dim, activation='softmax'))
model.summary()

model.compile(loss='categorical_crossentropy',
             optimizer=OPTIMIZER,
             metrics=['accuracy'])
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_1 (Dense)              (None, 256)               200960    
_________________________________________________________________
dropout_1 (Dropout)          (None, 256)               0         
_________________________________________________________________
dense_2 (Dense)              (None, 512)               131584    
_________________________________________________________________
dropout_2 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_3 (Dense)              (None, 10)                5130      
=================================================================
Total params: 337,674
Trainable params: 337,674
Non-trainable params: 0
_________________________________________________________________
In [17]:
VERBOSE = 0

def make_tensorboard(log_dir_base):
    jst = timezone(timedelta(hours=+9), 'JST')
    jst_now = datetime.now(jst)
    log_dir = log_dir_base + '_' + jst_now.strftime('%Y%m%d%H%M%S')
    os.makedirs(log_dir, exist_ok=True)
    tensorboard = TensorBoard(log_dir=log_dir)
    return tensorboard

CALLBACKS = [
    # EarlyStopping(patience=0, verbose=1),
    make_tensorboard(log_dir_base='log/mlp_keras_mnist')
]
In [18]:
start_time = time()

history = model.fit(X_train,
                    Y_train,
                    batch_size=BATCH_SIZE,
                    epochs=EPOCHS,
                    verbose=VERBOSE,
                    validation_split=VALIDATION_SPLIT,
                    callbacks=CALLBACKS)

print('Executed in {0:.3f}s'.format(time() - start_time))
Executed in 210.021s
In [19]:
plt.figure(1, figsize=(10, 4))
plt.subplots_adjust(wspace=0.5)

plt.subplot(1, 2, 1)
plt.plot(history.history['loss'], label='train', color='black')
plt.plot(history.history['val_loss'], label='validation', color='dodgerblue')
plt.legend()
# plt.ylim(0, 10)
plt.grid()
plt.title('Loss')
plt.xlabel('epoch')
plt.ylabel('loss')

plt.subplot(1, 2, 2)
plt.plot(history.history['acc'], label='train', color='black')
plt.plot(history.history['val_acc'], label='validation', color='dodgerblue')
plt.legend(loc='lower right')
# plt.ylim(0, 1)
plt.grid()
plt.title('Accuracy')
plt.xlabel('epoch')
plt.ylabel('acc')

plt.show()
In [20]:
score = model.evaluate(X_test, Y_test, verbose=1)
print('Test loss: ', score[0])
print('Test accuracy: ', score[1])
10000/10000 [==============================] - 0s 39us/step
Test loss:  0.07334766941401122
Test accuracy:  0.9814
In [21]:
TEST_COUNT = 50

Y_predict = model.predict(X_test)

plt.figure(1, figsize=(5, 14))
plt.subplots_adjust(hspace=0.4)

for i in range(TEST_COUNT):
    plt.subplot(TEST_COUNT / 5, 5, i + 1)
    X = X_test[i, :].reshape(img_rows, img_cols)
    Y = Y_predict[i, :]
    prediction = np.argmax(Y)
    y = np.argmax(Y_test[i, :])
    if prediction == y:
        plt.title(prediction, pad=10)
    else:
        plt.title('{}({})'.format(prediction, y), pad=10,
                  backgroundcolor='red')
    plt.axis('off')
    plt.imshow(X, cmap='gray') 

plt.show()

ソースコード

https://github.com/aknd/machine-learning-samples

2層フィードフォワードニューラルネットワークによる手書き数字認識(Kerasでの実装)

最も単純なニューラルネットワークである、2層フィードフォワードニューラルネットワークで、MNISTの手書き数字を認識する。

MNISTについては、

を参照。

単純パーセプトロンの場合

との違いは、層が重なっていることと、活性化関数がステップ関数ではなくなっていることだ。これによって、手書き数字の認識(多クラス分類)のような複雑な問題も解くことができる。

層の構成についてだが、一般的に、入力層はニューラルネットワークの層に含めない。2層だと、中間層が1層と、出力層が1層となる。

また、フィードフォワード(順伝播)なので、データは一方向にのみ流れる。 {}

Handwritten Digit Recognition using Two-Layer Feedforward Neural Networks in TensorFlow with Keras

In [1]:
import os
from time import time
from datetime import datetime, timedelta, timezone
import math
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense
from keras.utils import np_utils
from keras.callbacks import EarlyStopping, TensorBoard
Using TensorFlow backend.
In [2]:
(X_train, y_train), (X_test, y_test) = mnist.load_data()

MNISTの手書き数字画像の特徴量は、28×28の2次元配列だ。その全ての行をつなげて1行にする。こうしてできた1次元配列(28×28=784次元ベクトル)を、ニューラルネットワークの各入力とする。

In [3]:
img_rows, img_cols = 28, 28
input_dim = img_rows * img_cols # = 784
In [4]:
X_train = X_train.reshape(-1, input_dim).astype('float32') / 255
X_test = X_test.reshape(-1, input_dim).astype('float32') / 255

入力の各要素(8ビットの非負整数値)を255(=2^8-1)で割ることで、0〜1に正規化している。

In [5]:
X_train.shape
Out[5]:
(60000, 784)
In [6]:
# X_train[0]

各出力は10次元ベクトルとする。そのベクトルの各次元は、分類されるクラスに対応している。今回は[0..9]の整数だ。

In [7]:
output_dim = 10

ラベルは、あらかじめ10次元ベクトルに変換しておく。該当クラスの要素だけ1で、他の要素は0にする。この表現方法はone-hotエンコーディング(OHE)と呼ばれる。

In [8]:
Y_train = np_utils.to_categorical(y_train.astype('int32'), output_dim) 
Y_test = np_utils.to_categorical(y_test.astype('int32'), output_dim)
In [9]:
Y_train[0]
Out[9]:
array([0., 0., 0., 0., 0., 1., 0., 0., 0., 0.], dtype=float32)

中間層(隠れ層)のユニット数(ニューロン数)を16とする。ユニット数を増やせば正解率(accuracy)が上がるが、学習に時間がかかる。また、パラメータの数が増えて過学習が起きやすくなる。

In [10]:
UNITS = 16
# UNITS = 128
# UNITS = 512

中間層の活性化関数にはSigmoidを使う。これをReLUに変えるだけでも、正解率が上がることを確認しておく。ReLUの方が学習にかかる時間も短くなる。

In [11]:
ACTIVATION = 'sigmoid'
# ACTIVATION = 'relu'

最適化アルゴリズムにはAdamを使う。

In [12]:
# OPTIMIZER = 'sgd'
# OPTIMIZER = 'rmsprop'
OPTIMIZER = 'adam'

また、ミニバッチ学習を行う。つまり、ランダムに選択したトレーニングデータの一部(ミニバッチ)を用いた学習(パラメータ更新)を繰り返す。今回、ミニバッチのサイズ(batch_size)を128とする。

In [13]:
BATCH_SIZE = 128

トレーニングデータが60,000個だとすると、60,000 / 128 = 468.75なので、468回のパラメータ更新で、データを全て使ったとみなすことができる。この1サイクルのことをエポックという。

今回のように、トレーニングデータ数がバッチサイズで割り切れない場合、実際には、使われずに残るデータがある。さらに、ランダムに選択するがゆえに選択されずに残るデータもある。それでも、(トレーニングデータ数 / バッチサイズ)の小数点以下を切り捨てた整数を、1エポックのイテレーション数とする。

今回、エポック数は300とする。

In [14]:
EPOCHS = 300

最後までいけば、468 × 300 = 140,400回のパラメータ更新が行われる。

しかし、学習すればするほど正解率が上がる訳ではない。トレーニングデータに適合し過ぎて、未知データに適合できなくなることがある。これは、オーバーフィッティング(過学習)と呼ばれる。

最適なエポック数を試行錯誤で見つけてもいいが、early stoppingを使うことにする。つまり、クロスエントロピー誤差がほとんど変わらなくなった時点で、学習を終了させる。

この検証時に使われるデータは、バリデーションデータと呼ばれる。バリデーションデータを用意するために、Kerasでは、model.fitの引数validation_splitを渡す。

今回は、

In [15]:
VALIDATION_SPLIT = 0.1

としておく。すると、自動的にトレーニングデータの10%をバリデーションデータとして使ってくれる。バリデーションデータをよけておくので、トレーニングデータは、実際には60,000個より少なくなる。

バリデーションデータは、自分でトレーニングデータから一部をよけておいてもいい。この場合、model.fitの引数validation_dataに、そのデータを渡すことになる。

validation_dataにテストデータを渡しているソースコードもあるが、本当は良くない。テストの時に、実際よりも高い正答率が出てしまう。テストデータを使ってチューニングをするのだから、当然そうなる。テストデータは、最後にパフォーマンスを見るためだけにとっておく。

出力層の活性化関数にはsoftmax関数を使う。

\begin{equation} \require{color} \color{black} y = \frac{\exp(x)}{\sum^n_{k=1}\exp(x_k)} \nonumber \end{equation}

すると、出力は、全てのユニットの出力の和が1であるような正数となる。このことが、確率的解釈を可能とする。つまり、出力層の各ユニットから出力される値は、そのユニットに対応するクラスに分類される確率だと考えることができるようになる。

損失関数、つまり誤差としては、カテゴリカルクロスエントロピーを使う。

\begin{equation} \require{color} \color{black} E = -\sum_kt_k\log(y_k) \nonumber \end{equation}

 {\require{color}\color{black}y_k}: 出力層のk番目のユニットの出力

 {\require{color}\color{black}t_k}: 出力層のk番目のユニットのターゲット出力(正解値)

ラベルはOHEで表現されているので、 {\require{color}\color{black}t_k}は0か1だ。このことから、損失関数は、

\begin{equation} \require{color} \color{black} E = -\log(y_\hat{k})|\tiny{\hat{k}\mbox{は、正解のクラスに対応するユニットのインデックス}} \nonumber \end{equation}

ということになる。

この関数は単調減少で、 {\require{color}\color{black}y_\hat{k}}が1に近づくほど0に近づく。

では、Kerasで2層のニューラルネットワークを作る。中間層が1層と出力層が1層の構成だ。入力層は、実質的には中間層の1層目に対する入力なので、実装上は出現しない。

In [16]:
model = Sequential()
model.add(Dense(UNITS, activation=ACTIVATION, input_dim=input_dim))
model.add(Dense(output_dim, activation='softmax'))

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

ログを出すには、verbose=1とする。TensorBoardを作成するためのコールバックも用意しておく。

In [17]:
VERBOSE = 1
# VERBOSE = 0

def make_tensorboard(log_dir_base):
    jst = timezone(timedelta(hours=+9), 'JST')
    jst_now = datetime.now(jst)
    log_dir = log_dir_base + '_' + jst_now.strftime('%Y%m%d%H%M%S')
    os.makedirs(log_dir, exist_ok=True)
    tensorboard = TensorBoard(log_dir=log_dir)
    return tensorboard

CALLBACKS = [
    EarlyStopping(patience=0, verbose=1),
    make_tensorboard(log_dir_base='log/tlffnn_keras_mnist')
]

実行時間を計測しながら、学習させる。

In [18]:
start_time = time()

history = model.fit(X_train,
                    Y_train,
                    batch_size=BATCH_SIZE,
                    epochs=EPOCHS,
                    verbose=VERBOSE,
                    validation_split=VALIDATION_SPLIT,
                    callbacks=CALLBACKS)

print('Executed in {0:.3f}s'.format(time() - start_time))
Train on 54000 samples, validate on 6000 samples
Epoch 1/300
54000/54000 [==============================] - 1s 18us/step - loss: 1.2513 - acc: 0.7514 - val_loss: 0.7045 - val_acc: 0.8877
Epoch 2/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.5960 - acc: 0.8830 - val_loss: 0.4252 - val_acc: 0.9170
Epoch 3/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.4251 - acc: 0.9020 - val_loss: 0.3320 - val_acc: 0.9237
Epoch 4/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.3533 - acc: 0.9104 - val_loss: 0.2832 - val_acc: 0.9290
Epoch 5/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.3137 - acc: 0.9166 - val_loss: 0.2550 - val_acc: 0.9337
Epoch 6/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.2882 - acc: 0.9213 - val_loss: 0.2368 - val_acc: 0.9372
Epoch 7/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.2692 - acc: 0.9251 - val_loss: 0.2237 - val_acc: 0.9405
Epoch 8/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.2547 - acc: 0.9283 - val_loss: 0.2126 - val_acc: 0.9432
Epoch 9/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.2432 - acc: 0.9316 - val_loss: 0.2043 - val_acc: 0.9438
Epoch 10/300
54000/54000 [==============================] - 1s 16us/step - loss: 0.2332 - acc: 0.9342 - val_loss: 0.1978 - val_acc: 0.9468
Epoch 11/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.2251 - acc: 0.9367 - val_loss: 0.1919 - val_acc: 0.9482
Epoch 12/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.2178 - acc: 0.9381 - val_loss: 0.1887 - val_acc: 0.9507
Epoch 13/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.2112 - acc: 0.9396 - val_loss: 0.1838 - val_acc: 0.9512
Epoch 14/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.2055 - acc: 0.9411 - val_loss: 0.1805 - val_acc: 0.9497
Epoch 15/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.2005 - acc: 0.9427 - val_loss: 0.1781 - val_acc: 0.9502
Epoch 16/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.1958 - acc: 0.9439 - val_loss: 0.1761 - val_acc: 0.9518
Epoch 17/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.1914 - acc: 0.9450 - val_loss: 0.1746 - val_acc: 0.9515
Epoch 18/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.1874 - acc: 0.9466 - val_loss: 0.1724 - val_acc: 0.9525
Epoch 19/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.1840 - acc: 0.9470 - val_loss: 0.1713 - val_acc: 0.9527
Epoch 20/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.1805 - acc: 0.9480 - val_loss: 0.1705 - val_acc: 0.9523
Epoch 21/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.1776 - acc: 0.9491 - val_loss: 0.1671 - val_acc: 0.9540
Epoch 22/300
54000/54000 [==============================] - 1s 16us/step - loss: 0.1748 - acc: 0.9500 - val_loss: 0.1657 - val_acc: 0.9533
Epoch 23/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.1718 - acc: 0.9506 - val_loss: 0.1648 - val_acc: 0.9528
Epoch 24/300
54000/54000 [==============================] - 1s 15us/step - loss: 0.1692 - acc: 0.9513 - val_loss: 0.1635 - val_acc: 0.9533
Epoch 25/300
54000/54000 [==============================] - 1s 16us/step - loss: 0.1669 - acc: 0.9523 - val_loss: 0.1638 - val_acc: 0.9540
Epoch 00025: early stopping
Executed in 21.327s

学習曲線を見て、オーバーフィッティングが起きていないか確認する。

In [19]:
plt.figure(1, figsize=(10, 4))
plt.subplots_adjust(wspace=0.5)

plt.subplot(1, 2, 1)
plt.plot(history.history['loss'], label='train', color='black')
plt.plot(history.history['val_loss'], label='validation', color='dodgerblue')
plt.legend()
# plt.ylim(0, 10)
plt.grid()
plt.title('Loss')
plt.xlabel('epoch')
plt.ylabel('loss')

plt.subplot(1, 2, 2)
plt.plot(history.history['acc'], label='train', color='black')
plt.plot(history.history['val_acc'], label='validation', color='dodgerblue')
plt.legend(loc='lower right')
# plt.ylim(0, 1)
plt.grid()
plt.title('Accuracy')
plt.xlabel('epoch')
plt.ylabel('acc')

plt.show()

テストデータを使って結果を確認する。

In [20]:
score = model.evaluate(X_test, Y_test, verbose=1)
print('Test loss: ', score[0])
print('Test accuracy: ', score[1])
10000/10000 [==============================] - 0s 18us/step
Test loss:  0.19434047244638206
Test accuracy:  0.941

正解率を出すだけではなく、最初の50個の具体的な結果も出す。画像の上に予測を表示し、間違えている場合は括弧つきで正解も表示する。背景色を赤にすることで、間違いを見つけやすくしておく。

In [21]:
TEST_COUNT = 50

Y_predict = model.predict(X_test)

plt.figure(1, figsize=(5, 14))
plt.subplots_adjust(hspace=0.4)

for i in range(TEST_COUNT):
    plt.subplot(TEST_COUNT / 5, 5, i + 1)
    X = X_test[i, :].reshape(img_rows, img_cols)
    Y = Y_predict[i, :]
    prediction = np.argmax(Y)
    y = np.argmax(Y_test[i, :])
    if prediction == y:
        plt.title(prediction, pad=10)
    else:
        plt.title('{}({})'.format(prediction, y), pad=10,
                  backgroundcolor='red')
    plt.axis('off')
    plt.imshow(X, cmap='gray') 

plt.show()

最後に、パラメータ(weights)を視覚化しておくのも面白い。

In [22]:
weights = model.layers[0].get_weights()[0]

columns = 4
rows = math.ceil(UNITS / columns)
plt.figure(1, figsize=(columns * 1.5, rows * 1.5))
plt.subplots_adjust(wspace=0.25, hspace=0.25)

for i in range(weights.shape[-1]):
    plt.subplot(rows, columns, i + 1)
    w = weights[:, i].reshape(img_rows, img_cols)
    plt.axis('off')
    plt.imshow(w, cmap='gray')
 
plt.show()

白い部分に文字の一部があると、そのユニットは活性し、黒い部分だと抑制する。いくつかは、数字の"2"や"3"の形が浮かび上がっているように見える。それらの数字を認識するのに役立っているのかもしれない。

ソースコード

https://github.com/aknd/machine-learning-samples

Kerasに含まれるMNISTのデータ構造

機械学習の例として、手書き数字の判定をさせることがある。サンプルデータとしては、MNISTがよく使われる。Kerasのサンプルデータセットにも、MNISTが含まれている。

今回は、そのデータ構造を確認する。

MNIST Dataset

In [1]:
from keras.datasets import mnist
import matplotlib.pyplot as plt
%matplotlib inline
import math
Using TensorFlow backend.

MNISTのデータを読み込む。

In [2]:
(X_train, y_train), (X_test, y_test) = mnist.load_data()
In [3]:
X_train.shape
Out[3]:
(60000, 28, 28)
In [4]:
y_train.shape
Out[4]:
(60000,)
In [5]:
X_test.shape
Out[5]:
(10000, 28, 28)
In [6]:
y_test.shape
Out[6]:
(10000,)

60,000個のトレーニングデータ(X_train, y_train)と、10,000個のテストデータ(X_test, y_test)で構成されている。

X_train, X_testの要素は、28×28の2次元配列だ。これは、28×28ピクセルの画像と対応している。

つまり、2次元配列の要素は[0..255]の整数値で、グレイスケール画像の1画素の濃淡の階調を表現している。8ビットで表現しており、0が黒、255(=2^8-1)が白だ。

一方、y_train, y_testには、画像に対応した[0..9]のラベルが格納されている。

試しに、X_trainの最初の要素を取り出す。

In [7]:
some_digit = X_train[0]
some_digit.shape
Out[7]:
(28, 28)

画素の並び順と一致するように、この2次元配列の数値をprintする。ただし、視覚的に分かりやすいように、値が正であれば全て1に変換しておく。

In [8]:
digit_binary = [[math.ceil(eight_bit_int / 255) for eight_bit_int in row] for row in some_digit]

for row in digit_binary:
    print(row)
[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]
[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, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 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, 1, 1, 1, 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, 1, 1, 1, 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, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 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, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 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]

なんとなく"5"であることが分かる。

In [9]:
some_digit_label = y_train[0]
some_digit_label
Out[9]:
5

ラベルを確認すると、実際に"5"だ。

確認のため、X_trainの最初の15個を画像で表示し、対応するy_trainの値をタイトル(画像上)につけておく。

In [10]:
for i in range(15):
    plt.subplot(3, 5, i + 1)
    plt.axis('off')
    plt.title(y_train[i])
    plt.imshow(X_train[i], cmap='gray')
    
plt.show()

黒背景に白文字で書かれていることが分かる。

ソースコード

https://github.com/aknd/machine-learning-samples

パーセプトロンの学習アルゴリズム

分類問題が線形分離可能であれば、単純パーセプトロンで解決できる。例として、パーセプトロンで論理回路を作るというものがある。それについては

を参照。

論理回路の場合、座標上の4点(0, 0), (0, 1), (1, 0), (1, 1)を考える。各点は、論理回路の2つの入力の組に対応する。信号を流す(真)が1、信号を流さない(偽)が0だ。これらの点を、対応する論理回路の出力が0か1かで分離するような直線を見つければいい。それが、パーセプトロンの重み(パラメータ)を設定することに相当する。ただし、適切な重みの設定をするのは人間だ。

人間が設定しなくても、適切な重みを自動で学習できることが望ましい。そこで、scikit-learnに含まれるiris(アヤメの品種データ)を使って、パーセプトロンの学習アルゴリズムを試してみる。

まず、irisのデータの中身を確認する。確認するには、視覚化してみるのが分かりやすい。視覚化にはseabornを使うことにする。ちょうどseabornにもサンプルデータとしてirisが入っている。データの意味を知りたいだけだから、ひとまずこちらを使う。

視覚化してみると、アヤメの各個体のデータであることが分かる。特徴量として、がく片の長さ(sepal length)、がく片の幅(sepal width)、花弁の長さ(petal length)、花弁の幅(petal width)が入っている。そして、そのアヤメがどの品種かを示すラベル(species)も入っている。“setosa”, “versicolor”, “virginica”の3種類だ。

ただし、scikit-learnに含まれるデータでは、品種は0, 1, 2とダミー変数になっている。また、特徴量はdataというキーでアクセスし、ラベルはtargetというキーでアクセスするようになっている。

ここで、問題を単純に設定して、“setosa”, “versicolor”の2種類を分離することにする。特徴量は、sepal widthとpetal lengthのみ使用する。

さて、どのような規則でパーセプトロンの重みを更新していくかだ。一般的な式を書いてしまうとこうなる。

 {
\begin{align}
  \require{color}
  \color{black}
  w^{(k + 1)}_{ji} = w^{(k)}_{ji} +\eta(y_j - \hat{y}_j)x_i
\end{align}
}
  •  \require{color}\color{black}w^{(k)}_{ji}は、i番目の入力ニューロンとj番目の出力ニューロンの接続の重み(k回目の更新後)
  •  \require{color}\color{black}x_iは、i番目の入力
  •  \require{color}\color{black}\hat{y}_jは、j番目の出力
  •  \require{color}\color{black}y_jは、j番目のターゲット出力(ラベル)
  •  \require{color}\color{black}\etaは、学習率

今回は出力が1つだから、jは無視していい。

 {
\begin{align}
  \require{color}
  \color{black}
  w^{(k + 1)}_{i} = w^{(k)}_{i} +\eta(y - \hat{y})x_i
\end{align}
}

誤差が小さくなるように、(学習率を掛けて)少しずつ重みを更新している。更新量は、誤差と入力の大きさに比例する(デルタ則)。

データが線形分離可能であれば、このアルゴリズムは解に収束する。パーセプトロンの収束定理と呼ばれるものだ。ただし、解は一意ではない。無限にある。

実装して動かしてみると、あっという間に収束することが分かる。データを全て見ないうちに。問題が簡単過ぎるのかもしれない。

In [1]:
import numpy as np
import pandas as pd
from sklearn import datasets, model_selection
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
In [2]:
df = sns.load_dataset('iris')
df.head()
Out[2]:
sepal_length sepal_width petal_length petal_width species
0 5.1 3.5 1.4 0.2 setosa
1 4.9 3.0 1.4 0.2 setosa
2 4.7 3.2 1.3 0.2 setosa
3 4.6 3.1 1.5 0.2 setosa
4 5.0 3.6 1.4 0.2 setosa
In [3]:
sns.set(style='ticks')

sns.pairplot(df,
             hue='species',
             markers=["o", "s", "x"])\
    .savefig('iris.png')
In [4]:
iris = datasets.load_iris()

type(iris)
Out[4]:
sklearn.utils.Bunch
In [5]:
iris.keys()
Out[5]:
dict_keys(['filename', 'target_names', 'data', 'target', 'feature_names', 'DESCR'])
In [6]:
iris_data = pd.DataFrame(data=iris.data, columns=iris.feature_names)
iris_data.head()
Out[6]:
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 5.1 3.5 1.4 0.2
1 4.9 3.0 1.4 0.2
2 4.7 3.2 1.3 0.2
3 4.6 3.1 1.5 0.2
4 5.0 3.6 1.4 0.2
In [7]:
iris.target
Out[7]:
array([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, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
In [8]:
iris_df = iris_data.copy()
iris_df['species'] = iris.target
iris_df.head()
Out[8]:
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) species
0 5.1 3.5 1.4 0.2 0
1 4.9 3.0 1.4 0.2 0
2 4.7 3.2 1.3 0.2 0
3 4.6 3.1 1.5 0.2 0
4 5.0 3.6 1.4 0.2 0
In [9]:
iris_df = iris_df.iloc[:, [1, 2, 4]]
iris_df = iris_df[iris_df['species'].isin([0, 1])]
iris_df.head()
Out[9]:
sepal width (cm) petal length (cm) species
0 3.5 1.4 0
1 3.0 1.4 0
2 3.2 1.3 0
3 3.1 1.5 0
4 3.6 1.4 0
In [10]:
X = iris_df.iloc[:, :2].values
Y = iris_df.species.values
In [11]:
x_train, x_test, y_train, y_test = model_selection.train_test_split(X, Y)
In [12]:
class Perceptron:
    def  __init__(self, n_iter=10, eta=0.01):
        self.n_iter = n_iter
        self.eta = eta
        
    def output(self, input):
        weighted_sum = np.dot(input, self.__weights[1:]) + self.__weights[0]
        return self.__activate(weighted_sum)
    
    def fit(self, X, Y):
        self.__weights = np.zeros(X.shape[1] + 1)
        
        for i in range(self.n_iter):
            for j, (x, y) in enumerate(zip(X, Y)):
                y_output = self.output(x)
                diff = y - y_output
                if diff != 0:
                    print('iter: {}, y_index: {}, diff: {}'.format(i, j, diff))
                self.__weights += self.eta * diff * np.hstack((1, x))
    
    def __activate(self, weighted_sum):
        return self.__heaviside_step(weighted_sum)
    
    def __heaviside_step(self, z):
        return np.where(z < 0, 0, 1)
In [13]:
perceptron = Perceptron()
perceptron.fit(x_train, y_train)
iter: 0, y_index: 0, diff: -1
iter: 0, y_index: 1, diff: 1
iter: 0, y_index: 2, diff: -1
iter: 0, y_index: 3, diff: 1
iter: 0, y_index: 13, diff: -1
In [14]:
perceptron.output(x_test)
Out[14]:
array([1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0,
       0, 0, 0])
In [15]:
y_test
Out[15]:
array([1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0,
       0, 0, 0])

ソースコード

https://github.com/aknd/machine-learning-samples

単純パーセプトロンによる論理回路(2)

単純パーセプトロンで、ANDゲート・NANDゲート・ORゲートを作ることができる。また、これらを組み合わせることで、XORゲートも作ることができる。

このことについては、

を参照。

実は、NANDゲートだけで、他の全ての論理回路を実現できる。論理回路どころか、あらゆる回路を実現できるが、今回は論理回路に話を限定する。

まずはNOTゲートだ。

NANDゲートはすでに作成しているものとする。NANDゲートの特性から、

[0, 0] -> 1
[1, 1] -> 0

のように入力を変換する。

そこで、同じ値をNANDゲート2つの入力に通すような回路を作れば、NOTゲートになる。

次にANDゲートを作る。

ANDゲートは、NANDゲートの出力を反転させたものだ。出力を反転させるにはNOTゲートを通せばいい。

さらに、ORゲートを作る。

ORゲートは、[0, 0]のみ0に変換して、その他は1に変換する。一方、NANDゲートは、[1, 1]のみ0に変換して、その他は1に変換する。このことから、NANDゲートの入力を反転してやれば、ORゲートになることが分かる。

最後に、XORゲートだ。少し複雑なので、先に完成した配線を見よう。

NANDゲートは、入力のどちらか一方でも0であれば、必ず1を出力することに注目する。

入力が[0, 0]であれば、真ん中の2つのNANDゲートには共に0の入力がある。0の入力があるから、共に1を出力する。その2値を入力とする右端のNANDゲートは0を出力する。

入力が[1, 1]であれば、左端のNANDゲートは0を出力する。この場合も、真ん中のNANDゲートには共に0の入力がある。あとは入力が[0, 0]の場合と同じで、最後に0を出力する。

入力が[0, 1]であれば、左端のNANDゲートの出力は1になる。真ん中の下側のNANDゲートの入力は[1, 1]だから、その出力は0になる。すると、右端のNANDゲートには0の入力があるから、1を出力する。

対称性から、入力が[1, 0]の場合も同様に1を出力する。

これで、XORゲートも実現していることが分かった。

論理回路には、他にもNORゲート・XNORゲートがあるが、それぞれORゲート・XORゲートの出力を反転させればいい。つまり、最後にNOTゲートをつなげば実現する。

単純パーセプトロンによる論理回路(1)

単純パーセプトロンで、ANDゲート・NANDゲート・ORゲート・XORゲートを実装する。ただし、XORゲートは単層では実装できない。非線形領域は単純パーセプトロンで分離できないからだ。そこで、他の3種を組み合わせることで、XORゲートも実装する。

入力([x1, x2])は、

import numpy as np

X = np.array([
    [0, 0],
    [1, 0],
    [0, 1],
    [1, 1]
])

の4通りで、1が真(信号を流す)、0が偽(信号を流さない)に対応している。

出力(y)は、ANDゲートであれば、[x1, x2] = [1, 1]の場合のみ1で、その他は0になる。つまり、

Y = np.array([
    0,
    0,
    0,
    1
])

となる。

NANDゲートであれば、[1, 1]の場合のみ0で、その他は1になる。

Y = np.array([
    1,
    1,
    1,
    0
])

ORゲートであれば、[0, 0]の場合のみ0で、その他は1になる。

Y = np.array([
    0,
    1,
    1,
    1
])

XORゲートであれば、[1, 0], [0, 1]の場合に1で、その他は0になる。

Y = np.array([
    0,
    1,
    1,
    0
])

さて、単純パーセプトロンは、入力の加重総和にヘヴィサイドステップ関数を適用して出力する。

一般的には、出力層以外の層にバイアスニューロンをつけ加える。バイアスニューロンは、常に1を出力する特別なタイプのニューロンだ。これと次の層の接続部に与えられる重みのことをバイアスという。バイアス(b)は、ニューロンの発火のしやすさをコントロールし、その他の重み(w1, w2)は、各信号の重要性をコントロールする。

パラメータ(バイアスと重み)を適切に設定することで、AND・NAND・ORは実現できる。適切なパラメータの組み合わせは無限にある。

そして、XORゲートは、以下のような配線で実現できる。

Logic Gates by Perceptron

In [1]:
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
In [2]:
def heaviside_step(z):
    return np.where(z < 0, 0, 1)
In [3]:
x = np.arange(-5.0, 5.0, 0.1)
y = heaviside_step(x)
In [4]:
plt.plot(x, y)
plt.ylim(-0.1, 1.1)
plt.show()
In [5]:
class Perceptron:
    def  __init__(self, weights):
        self.weights = weights
        
    def output(self, input):
        return self.__activate(np.dot(input, self.weights[1:]) + self.weights[0])
    
    def __activate(self, weighted_sum):
        return self.__heaviside_step(weighted_sum)
    
    def __heaviside_step(self, z):
        return np.where(z < 0, 0, 1)
In [6]:
X = np.array([
    [0, 0],
    [1, 0],
    [0, 1],
    [1, 1]
])
In [7]:
and_gate = Perceptron(np.array([-0.7, 0.5, 0.5]))
out = and_gate.output(X)
out
Out[7]:
array([0, 0, 0, 1])
In [8]:
nand_gate = Perceptron(np.array([0.7, -0.5, -0.5]))
out = nand_gate.output(X)
out
Out[8]:
array([1, 1, 1, 0])
In [9]:
or_gate = Perceptron(np.array([-0.3, 0.5, 0.5]))
out = or_gate.output(X)
out
Out[9]:
array([0, 1, 1, 1])
In [10]:
class XOR:
    def __init__(self):
        self.and_gate = Perceptron(np.array([-0.7, 0.5, 0.5]))
        self.nand_gate = Perceptron(np.array([0.7, -0.5, -0.5]))
        self.or_gate = Perceptron(np.array([-0.3, 0.5, 0.5]))
        
    def output(self, input):
        nand_output = self.nand_gate.output(input)
        or_output = self.or_gate.output(input)
        and_input = np.vstack([nand_output, or_output]).T
        return self.and_gate.output(and_input)
In [11]:
xor_gate = XOR()
out = xor_gate.output(X)
out
Out[11]:
array([0, 1, 1, 0])

ソースコード

https://github.com/aknd/machine-learning-samples

FX, EURUSD(ユーロドル), 2018/05/18

【メモ】

4時間足の下降トレンドが一旦終了して、大きく戻してからのもう一段の下げ途中だった。

4時間足の短期移動平均線(1時間足の中期移動平均線)への戻しからの戻り売り。

基本的に週持ち越しはしたくない(本日は金曜日)し、既にお酒を飲んでいたこともあり、節目到達で利益確定した。

節目を抜けかけていた(1時間足で見た感じは抜けてる)ので、この後ニューヨーク時間にかけて大きく下がるかもしれないが、利食い千人力ということで。

欲張らずにまた来週。

下手したら、節目を抜けると見せかけて反発し、ダブルボトムを作って上昇という可能性もあるし。