KDOG Notebook

機械学習とか、頭の中を整理するために

多層パーセプトロンによる手書き文字の分類

はじめに

これまで、もっとも単純なパーセプトロンによる2値分類と3クラス分類への拡張を行ってきた。今回はパーセプトロンの層を重ねて多クラス分類を行うネットワーク、多層パーセプトロン(Multi-Layer Perceptron : MLP)を実装する。今回用いたデータセットはMNISTの手書き数字である。kNNによる文字分類とは異なり、トレーニングデータを用いて手書き文字の特徴量とそのクラスラベルを結ぶ重み関数を最適化する学習を行った。また、最適化に伴う過学習の状態をk分割交差検証により確認した。

理論

a) MLP

多層パーセプトロンの模式図を下に示す。 入力されたベクトルは総入力関数にわたされ、続いて活性化関数 \phiに渡される。その出力されたベクトル( z)が再び入力ベクトルとして次のネットワークの層に渡される。この手続きを層の続く限り繰り返す。最終層では活性化関数の代わりにソフトマックス関数に引き渡され、その出力結果( y_i)と教師ラベル( t_i)をもとに誤差関数の値を求める。多層パーセプトロンの学習は各層における重み {\bf W}およびバイアス {\bf b}を最適化することである。

f:id:kdog08:20170518030223p:plain:w700

b) 誤差逆伝搬法

各層の重みおよびバイアスの最適化は各要素について誤差関数を微分し、その傾きをもとに更新をすることで得られる。しかし、次元や層が増えるにつれてその計算は煩雑になる。そこで、誤差逆伝搬法を用いて関数の勾配を求める。交差エントロピー誤差とソフトマックス関数の計算グラフを下図に示す。

f:id:kdog08:20170518154459p:plain:w600

f:id:kdog08:20170519022114p:plain:w600

c) k分割交差検証

普通、手元にあるデータセットは学習データとテストデータに分割すると学習は学習データを用いて行い、その後テストデータで正解率を求めてその精度を確認する。しかし、良いモデルを作る上で過学習と学習不足を検証することは欠かせない手続きである。k分割交差検証では学習データをさらにk個に分割すると、k個のサブセットからk-1個を学習に用いて、残り1個のデータセットで学習の検証を行う。この検証をk種類の学習および検証データセットの組み合わせについてそれぞれ行う(下図参照)。k種類のモデルから得られたスコア(正解率や誤分類率)の平均が検証スコアとなる(valid score)。テストデータについても同様にk種類のモデルからその平均値を得ることができる(test score)。valid scoreとtest scoreの値に大きな乖離がなければ学習モデルとして信頼できるということになる。

f:id:kdog08:20170521234632p:plain:w600

実装

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であった。

f:id:kdog08:20170521234732p:plain:w600

b) k分割交差検証を用いたエポック数のチューニング

エポック数が1, 10, 30, 50および100の場合についてモデルの検証をF1値を比較することで実施した。 下図に結果を示す。青は検証データを用いたスコア、緑はテストデータを用いたスコアである。すべてのエポック数でほぼ同じスコアを示しており、モデルの明らかな過学習または学習不足は確認できなかった。エポック数が50と100の場合でわずかにテストデータの精度が検証データを上回っており、過学習の傾向があると言える。また、エポック数が1の場合はわずかにテストデータの精度が検証データを下回っており、学習不足の傾向があると言える。

f:id:kdog08:20170521234756p:plain:w600

顕著な過学習や学習不足が確認できなかった理由としては用いたデータセットが十分大きい(サンプル数が多い)ことが挙げられる。

参考文献

Sebastian Raschka 「Python機械学習プログラミング」第6章, インプレス, 2016.

斎藤 康毅 「ゼロから作るDeep Learning」第5章, O'Reilly Japan, 2016.