多層パーセプトロンによる手書き文字の分類
はじめに
これまで、もっとも単純なパーセプトロンによる2値分類と3クラス分類への拡張を行ってきた。今回はパーセプトロンの層を重ねて多クラス分類を行うネットワーク、多層パーセプトロン(Multi-Layer Perceptron : MLP)を実装する。今回用いたデータセットはMNISTの手書き数字である。kNNによる文字分類とは異なり、トレーニングデータを用いて手書き文字の特徴量とそのクラスラベルを結ぶ重み関数を最適化する学習を行った。また、最適化に伴う過学習の状態をk分割交差検証により確認した。
理論
a) MLP
多層パーセプトロンの模式図を下に示す。 入力されたベクトルは総入力関数にわたされ、続いて活性化関数に渡される。その出力されたベクトル()が再び入力ベクトルとして次のネットワークの層に渡される。この手続きを層の続く限り繰り返す。最終層では活性化関数の代わりにソフトマックス関数に引き渡され、その出力結果()と教師ラベル()をもとに誤差関数の値を求める。多層パーセプトロンの学習は各層における重みおよびバイアスを最適化することである。
b) 誤差逆伝搬法
各層の重みおよびバイアスの最適化は各要素について誤差関数を微分し、その傾きをもとに更新をすることで得られる。しかし、次元や層が増えるにつれてその計算は煩雑になる。そこで、誤差逆伝搬法を用いて関数の勾配を求める。交差エントロピー誤差とソフトマックス関数の計算グラフを下図に示す。
c) k分割交差検証
普通、手元にあるデータセットは学習データとテストデータに分割すると学習は学習データを用いて行い、その後テストデータで正解率を求めてその精度を確認する。しかし、良いモデルを作る上で過学習と学習不足を検証することは欠かせない手続きである。k分割交差検証では学習データをさらにk個に分割すると、k個のサブセットからk-1個を学習に用いて、残り1個のデータセットで学習の検証を行う。この検証をk種類の学習および検証データセットの組み合わせについてそれぞれ行う(下図参照)。k種類のモデルから得られたスコア(正解率や誤分類率)の平均が検証スコアとなる(valid score)。テストデータについても同様にk種類のモデルからその平均値を得ることができる(test score)。valid scoreとtest scoreの値に大きな乖離がなければ学習モデルとして信頼できるということになる。
実装
a) MLPによる分類
利用したモジュール類
import numpy as np import matplotlib.pyplot as plt from sklearn.utils import shuffle from sklearn.metrics import f1_score from sklearn.datasets import fetch_mldata from sklearn.model_selection import train_test_split
多層パーセプトロン(隠れ層が1層の場合)、クラス
class MLP_MNIST: def __init__(self, inp_dim, hid_dim): self.W1 = np.random.uniform(low=-0.08, high=0.08, size=(inp_dim, hid_dim)).astype('float32') self.b1 = np.zeros(hid_dim).astype('float32') self.W2 = np.random.uniform(low=-0.08, high=0.08, size=(hid_dim, 10)).astype('float32') self.b2 = np.zeros(10).astype('float32') def train(self, x, t, eps): # forward_prop u1 = np.dot(x, self.W1) + self.b1 z1 = sigmoid(u1) u2 = np.dot(z1, self.W2) + self.b2 z2 = softmax(u2) y = z2 ti = (t == 1) cost = -np.sum(np.log(y[ti])) # backward_prop delta_2 = y - t delta_1 = deriv_sigmoid(u1) * np.dot(delta_2, self.W2.T) # update dW1 = np.dot(x.T, delta_1) db1 = np.dot(np.ones(len(x)),delta_1) dW2 = np.dot(z1.T, delta_2) db2 = np.dot(np.ones(len(z1)),delta_2) self.W1 -= eps*dW1 self.b1 -= eps*db1 self.W2 -= eps*dW2 self.b2 -= eps*db2 return cost def test(self, x): u1 = np.dot(x, self.W1) + self.b1 z1 = sigmoid(u1) u2 = np.dot(z1, self.W2) + self.b2 z2 = softmax(u2) pred_y = np.argmax(z2, axis=1) return pred_y
def sigmoid(x): return 1.0 / (1.0 + np.exp(-x)) def deriv_sigmoid(x): return sigmoid(x) * (1.0 - sigmoid(x)) def softmax(x): x = x - np.max(x, axis=1, keepdims=True) y = np.exp(x) / np.sum(np.exp(x), axis=1, keepdims=True) return y
データセットの読み込み、データの整形
def load_mnist(): mnist = fetch_mldata('MNIST original', data_home=".") mnist_X, mnist_y = shuffle(mnist.data.astype('float32'), mnist.target.astype('int32'), random_state=42) mnist_X = mnist_X / 255.0 return train_test_split(mnist_X, mnist_y, test_size=0.2, random_state=42) train_X, test_X, train_y, test_y = load_mnist() # standardize x norm_train_X = np.linalg.norm(train_X, ord=2, axis=1) train_X = train_X / norm_train_X[:, np.newaxis] norm_test_X = np.linalg.norm(test_X, ord=2, axis=1) test_X = test_X / norm_test_X[:, np.newaxis] # one-hot vector temp = np.zeros((train_y.size,10)) for i, indx in enumerate(train_y): temp[i, indx] = 1.0 train_y = temp
モデル作成、実行
mlp = MLP_MNIST(train_X.shape[1], 100) n_epochs = 100 batch_size = 100 n_batches = train_X.shape[0] // batch_size cost_history = [] for epoch in range(n_epochs): train_X, train_y = shuffle(train_X, train_y) cost = 0 for i in range(n_batches): start = i * batch_size end = start + batch_size cost += mlp.train(train_X[start:end], train_y[start:end], eps=0.01) cost_history.append(cost/(n_batches*batch_size)) pred_y = mlp.test(test_X) print(f1_score(test_y, pred_y, average='macro'))
b) k分割交差検証 (エポック数のチューニングの場合)
train_X, test_X, train_y, test_y = load_mnist() # standardize X norm_train_X = np.linalg.norm(train_X, ord=2, axis=1) train_X = train_X / norm_train_X[:, np.newaxis] norm_test_X = np.linalg.norm(test_X, ord=2, axis=1) test_X = test_X / norm_test_X[:, np.newaxis] def split_train_data_set(tX, ty, start, end): indx = np.arange(start, end) vX = tX[indx] vy = ty[indx] tX = np.delete(tX,indx,axis=0) ty = np.delete(ty,indx,axis=0) temp = np.zeros((ty.size,10)) for i, indx in enumerate(ty): temp[i, indx] = 1.0 ty = temp return tX, ty, vX, vy k = 10 subset = train_X.shape[0] // k temp_X = train_X temp_y = train_y n_epochs_list = np.array([5, 10, 20, 50]) batch_size = 100 valid_score = np.zeros((len(n_epochs_list),k)) test_score = np.zeros((len(n_epochs_list),k)) for l in range(len(n_epochs_list)): n_epochs = n_epochs_list[l] for j in range(k): start = j * subset end = start + subset train_X, train_y, valid_X, valid_y = split_train_data_set(temp_X, temp_y, start, end) n_batches = train_X.shape[0] // batch_size mlp = MLP_MNIST(train_X.shape[1], 50) for epoch in range(n_epochs): train_X, train_y = shuffle(train_X, train_y) sum_cost = 0 for i in range(n_batches): start = i * batch_size end = start + batch_size sum_cost += mlp.train(train_X[start:end], train_y[start:end], eps=0.01) cost = sum_cost / (n_batches*batch_size) pred_y = mlp.test(valid_X) jscore = f1_score(valid_y, pred_y, average='macro') valid_score[l,j] = jscore pred_y = mlp.test(test_X) jscore = f1_score(test_y, pred_y, average='macro') test_score[l,j] = jscore
結果
a) MLPによる分類
隠れ層を1層含むMLPによる分類の結果を示す。下図はエポック数を100とした場合の誤差関数(交差エントロピー誤差)の変化を示している。エポック数が5程度で誤差関数が大きく減少している。エポック数が40以上でも減少し続けているが変化は小さい。また、100回の繰り返し学習を終えたところでF1値は0.9711であった。
b) k分割交差検証を用いたエポック数のチューニング
エポック数が1, 10, 30, 50および100の場合についてモデルの検証をF1値を比較することで実施した。 下図に結果を示す。青は検証データを用いたスコア、緑はテストデータを用いたスコアである。すべてのエポック数でほぼ同じスコアを示しており、モデルの明らかな過学習または学習不足は確認できなかった。エポック数が50と100の場合でわずかにテストデータの精度が検証データを上回っており、過学習の傾向があると言える。また、エポック数が1の場合はわずかにテストデータの精度が検証データを下回っており、学習不足の傾向があると言える。
顕著な過学習や学習不足が確認できなかった理由としては用いたデータセットが十分大きい(サンプル数が多い)ことが挙げられる。
参考文献
Sebastian Raschka 「Python機械学習プログラミング」第6章, インプレス, 2016.
斎藤 康毅 「ゼロから作るDeep Learning」第5章, O'Reilly Japan, 2016.