人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

多層パーセプトロンでMNISTの手書き数字認識

多層パーセプトロンで手書き数字認識(2014/2/1)の続き。今回は、簡易版のdigitsデータではなく、MNISTのより大規模な手書き数字データを使って学習してみます。

MNISTデータ

MNISTは、28x28ピクセル、70000サンプルの数字の手書き画像データです。各ピクセルは0から255の値を取ります。まずは、digitsデータの時と同様にMNISTのデータを描画してどのようなデータなのか確認してみます。MNISTのデータは上記サイトからダウンロードしなくてもscikit-learnのfetch_mldata()関数でWebから取得できます。取得するのは初回実行時だけで二回目以降は第二引数のdata_homeに指定した場所に保存されます。

#coding: utf-8
import numpy as np
import pylab
from sklearn.datasets import fetch_mldata

# mnistの手書き数字データをロード
# カレントディレクトリ(.)にない場合は、
# Webから自動的にダウンロードされる(時間がかかるので注意!)
# 70000サンプル、28x28ピクセル
mnist = fetch_mldata('MNIST original', data_home=".")

# ランダムに25サンプルを描画
# digits.images[i] : i番目の画像データ(8x8ピクセル)
# digits.target[i] : i番目の画像データのクラス(数字なので0-9)
p = np.random.random_integers(0, len(mnist.data), 25)
for index, (data, label) in enumerate(np.array(zip(mnist.data, mnist.target))[p]):
    pylab.subplot(5, 5, index + 1)
    pylab.axis('off')
    pylab.imshow(data.reshape(28, 28), cmap=pylab.cm.gray_r, interpolation='nearest')
    pylab.title('%i' % label)
pylab.show()

実行結果の画像。digitsに比べると解像度が高いので比較的わかりやすくなっている。

f:id:aidiary:20140205205555p:plain

多層パーセプトロン

多層パーセプトロンの実装は前回とほぼ同じですが、誤差関数に交差エントロピー誤差関数を利用するように修正しています。この誤差関数を使う結果、出力ユニットの誤差は、

    delta2 = y - t[i]

のように微分が入らない簡単な式になります。また、ソフトマックス関数も実装しました。

def softmax(x):
    temp = np.exp(x)
    return temp / np.sum(temp)

ソフトマックス関数を使うと出力層の各ユニットの和が1になります。つまり、出力値が各クラスである確率と見なせるようになります。ソースコードは前回とほとんど同じ上に長いので省略。
https://github.com/sylvan5/PRML/blob/master/ch5/mlp.py
を参照してください。

MNISTの手書き数字データ認識

入力層784ユニット(28x28ピクセル)、隠れ層100ユニット、出力層10ユニットのニューラルネットワークを構成しました。隠れ層の活性化関数はtanh、出力層の活性化関数はsoftmaxです。入力は画像データの各ピクセルの値を0〜1に正規化していますが、特徴抽出などせずにピクセルの値をそのまま入力しています。また、教師信号は1-of-K表記に変換しています。学習率は0.01とかなり低めにしています(0.2くらいだと認識精度が低くなる)。学習データから100000個のサンプルをランダムに選択して、重みを更新する逐次学習方式を使っています。

マシンの性能にもよりますが学習には数分かかります。

#coding:utf-8
import numpy as np
from mlp import MultiLayerPerceptron
from sklearn.datasets import fetch_mldata
from sklearn.cross_validation import train_test_split
from sklearn.preprocessing import LabelBinarizer
from sklearn.metrics import confusion_matrix, classification_report

"""
MNISTの手書き数字データの認識
scikit-learnのインストールが必要
http://scikit-learn.org/
"""

if __name__ == "__main__":
    # MNISTの数字データ
    # 70000サンプル, 28x28ピクセル
    # カレントディレクトリ(.)にmnistデータがない場合は
    # Webから自動的にダウンロードされる(時間がかかる)
    mnist = fetch_mldata('MNIST original', data_home=".")

    # 訓練データを作成
    X = mnist.data
    y = mnist.target

    # ピクセルの値を0.0-1.0に正規化
    X = X.astype(np.float64)
    X /= X.max()

    # 多層パーセプトロンを構築
    mlp = MultiLayerPerceptron(28*28, 100, 10, act1="tanh", act2="softmax")

    # 訓練データとテストデータに分解
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1)

    # 教師信号の数字を1-of-K表記に変換
    labels_train = LabelBinarizer().fit_transform(y_train)
    labels_test = LabelBinarizer().fit_transform(y_test)

    # 訓練データを用いてニューラルネットの重みを学習
    mlp.fit(X_train, labels_train, learning_rate=0.01, epochs=100000)

    # テストデータを用いて予測精度を計算
    predictions = []
    for i in range(X_test.shape[0]):
        o = mlp.predict(X_test[i])
        predictions.append(np.argmax(o))
    print confusion_matrix(y_test, predictions)
    print classification_report(y_test, predictions)

実行結果の混同行列と精度レポートです。

[[622   0   5   1   0   7   4   1   7   4]
 [  0 795   3   0   0   1   0   6   3   0]
 [  4   4 606  10   8   5   6  13   8   6]
 [  2   4  17 626   1  13   2  11  18   8]
 [  1   3   4   1 643   1   5   7   9  28]
 [  6   2   7  13   4 548  14   2  10  11]
 [  4   0   8   1   6   8 697   1   3   0]
 [  3   0  10   4  12   2   0 724   2  11]
 [  1   5   8  10   4   8   4   3 608  11]
 [  2   2   1   8  29   5   1  24   7 613]]

             precision    recall  f1-score   support

        0.0       0.96      0.96      0.96       651
        1.0       0.98      0.98      0.98       808
        2.0       0.91      0.90      0.91       670
        3.0       0.93      0.89      0.91       702
        4.0       0.91      0.92      0.91       702
        5.0       0.92      0.89      0.90       617
        6.0       0.95      0.96      0.95       728
        7.0       0.91      0.94      0.93       768
        8.0       0.90      0.92      0.91       662
        9.0       0.89      0.89      0.89       692

avg / total       0.93      0.93      0.93      7000

数字によって認識率は違いますが、平均すると93%(エラー率7%)という結果でした。何もチューニングしないわりにはかなり高いので驚き。

MNISTのサイトには様々な学習器のエラー率と論文へのリンクが掲載されています。これを見ると2層(入力、隠れ、出力のことを2層と呼んでいる?)のニューラルネットで隠れユニット数1000の場合のエラー率は4.5%となっています*1。試しに隠れ層のユニット数1000で試してみたところ

             precision    recall  f1-score   support

        0.0       0.99      0.92      0.95       719
        1.0       0.98      0.98      0.98       794
        2.0       0.84      0.98      0.91       704
        3.0       0.95      0.92      0.94       702
        4.0       0.91      0.97      0.94       662
        5.0       0.89      0.97      0.93       602
        6.0       0.97      0.97      0.97       680
        7.0       0.94      0.97      0.95       752
        8.0       0.95      0.88      0.91       671
        9.0       0.99      0.82      0.90       714

avg / total       0.94      0.94      0.94      7000

重み行列のサイズが大きくなるため学習には数十分くらいかかりました。大部分の数字の認識精度は向上してますが、「2」と「5」が足を引っ張って平均値が下がっている感じです。もしかしたら学習回数が10万回じゃ少なくて偏りができてる?

*1:よく見るとk-NNでも認識精度95%くらい出るのね・・・けっこう簡単なタスクなのかな?