kaggle初挑戦 手書き文字認識

概要

kaggleコンテスト(チュートリアル)に初参加した。 手書きの文字認識を4層のNeural Networkでやってみると、ベンチマーク(ランダムフォレスト)は超えられた。 これからちょくちょくやっていきたい。

はじめに

研修で別の業務をやっているので、退屈している、プログラムが書きたい、機械学習したい。 ということで、データサイエンス的なコンテストであるkaggleに参加することにした。 kaggleは初挑戦かつ色々にわかなので、適当ですが、許してほしい。

kaggleとは

公式サイトはこちら

Wikipediaによると

Kaggleは企業や研究者がデータを投稿し、世界中の統計家やデータ分析家がその最適モデルを競い合う、予測モデリング及び分析手法関連プラットフォーム及びその運営会社である。

要するに、企業とかがデータを提供して、世界中の人でそれを解くコンテストである。 今やってるコンテストを見ると、例えば、降水量の予測とか売上の予想とか色々あって面白そう。 本番のコンテストは賞金がついてるものもあって、出題者が本当に課題を解決したいんだなぁという感じがある。

いきなり本番のコンテストに参加するのは色々ハードルが高いので、とりあえず、チュートリアル的なコンテストをやることにする。 チュートリアル的なコンテストは4つ。

とりあえず、一番簡単そうな手書き数字認識をやることにした。

手書き数字認識(mnist)

今回認識するデータはmnistという大規模手書き数字データ。 画像の大きさは28x28ピクセルで白黒画像である。 今回のコンテストでは42000枚のラベル付きの画像で学習を行い、28000枚のラベルなしの画像を認識させる。 このコンテストはチュートリアルなので、ベンチマークアルゴリズム(ランダムフォレスト)、コード、結果(正答率93.514%)が公開されていた。 とりあえずこれを超える結果を出すこと今回の目標とした。

データ

とりあえず、生データをみる。 たまに人間でもなかなか厳しそうなやつがある。もうちょっと丁寧に書いてほしい。 f:id:ksknw:20151108163057p:plain

まずは何も考えずに、画素データを単にベクトルと思って、PCAしてプロットしてみる。

from sklearn.decomposition import PCA
pca = PCA(n_components=2)
pcaed_data = pca.fit_transform(data[:,1:]/255.0)
pcaed_data = zip(*pcaed_data)

plt.scatter(pcaed_data[0], pcaed_data[1], marker=".", c=data[:,0], linewidths=0, alpha=0.7)
f=plt.gcf()
f.set_size_inches(20.0,20.0)
plt.savefig("pca.png")

f:id:ksknw:20151108163045p:plain この時点ですでに、いくつか分かれているものもあるけど、まあ正直よくわからない。 真面目にやるならもうちょっとデータを眺めているのもいいと思うけど、そこまでやる気はないので適当にささっと判別する。

アルゴリズム選択

教師あり学習、画像判別ということで、パッと思いつくアルゴリズムは以下。

  • ランダムフォレスト
  • SIFTやHOG、HLACなどを計算した後、SVMとか
  • Convolutional Neural Network
  • 多層パーセプトロン

最近CNNを使った際、かなり性能が良かったこともあり、これらの中で、直感的に性能が出そうなものはCNNと思う。 ただ、数字だけの認識なのでCNNまでいかなくても、多層パーセプトロンで何とかなるだろうと思った。 そこで、今回はまず多層パーセプトロンで実装してみて、だめなら、もうちょっと強そうなアルゴリズムを使うことにした。

mnistは有名なデータセットなので、少し調べるだけで、多層パーセプトロンで分類をやっている人はたくさんいた。 これらの記事をありがたく参考にさせてもらいつつ、実装を行う。

こちらのブログ で4層のパーセプトロンで認識を行っていたので、とりあえず、4層のネットワークを組むことにした。

実装

今後の勉強も兼ねて、chainerで実装を行う。 もし4層でイマイチな性能だったときに簡単に層数を増やせそうというのもある。

中間層の数は1000ずつ、活性化関数はReLUにして、Dropoutもすることにした。 chainer使うとこのへんの処理がとても簡単に書けるので良い。

学習方法について、最近こういうの を見て、確率的勾配法いまいちなのではと思っている。 今回は、詳細を知らないけど強そうなAdamを使うことにした。

エポック数は20に設定した。 本当は汎化誤差とか見ながら打ち切りするといいと思う。 4層程度ならgpuはいらないかなと思ってcpuで動くコードにした(あとで後悔した)。

import chainer
import chainer.functions as F
from chainer import optimizers
import numpy as np


class Perceptron(object):

    def __init__(self):
        self.batchsize = 100
        self.num_epoch = 20
        self.num_hiddens = 1000
        self.model = chainer.FunctionSet(
            l1=F.Linear(784, self.num_hiddens),
            l2=F.Linear(self.num_hiddens, self.num_hiddens),
            l3=F.Linear(self.num_hiddens, 10)
        )
        self.optimizer = optimizers.Adam()
        self.optimizer.setup(self.model)

    def forward(self, x_data, y_data=None, train=True):
        x = chainer.Variable(x_data)
        if train == True:
            t = chainer.Variable(y_data)
        h1 = F.dropout(F.relu(self.model.l1(x)), train=train)
        h2 = F.dropout(F.relu(self.model.l2(h1)), train=train)
        y = self.model.l3(h2)
        if train:
            loss = F.softmax_cross_entropy(y, t)
            return loss
        else:
            return y.data

    def train(self, inputs, labels, tests):
        N = len(inputs)
        inputs = np.array(inputs).astype(np.float32)
        labels = np.array(labels).astype(np.int32)
        tests = np.array(tests).astype(np.float32)

        for epoch in range(self.num_epoch):
            print "epoch: %d" % epoch

            perm = np.random.permutation(N)
            sum_loss = 0
            for i in range(0, N, self.batchsize):
                x_batch = np.asarray(inputs[perm[i:i + self.batchsize]])
                y_batch = np.asarray(labels[perm[i:i + self.batchsize]])

                self.optimizer.zero_grads()
                loss = self.forward(x_batch, y_batch)
                loss.backward()
                self.optimizer.update()
                sum_loss += float(loss.data) * len(y_batch)
            print "train mean loss: %f" % (sum_loss / N)
        np.asarray(tests)
        y = self.forward(tests, train=False)
        return y

これをipython notebookから読んで使う。

import pandas as pd
import numpy as np

data = pd.read_csv("train.csv")
data = data.as_matrix()
test_data = pd.read_csv("test.csv")

import nn
perceptron = nn.Perceptron()
test_results = perceptron.train(data[:,1:]/255.0, data[:,0], test_data[:]/255.0)
epoch: 0
train mean loss: 0.314591
epoch: 1
train mean loss: 0.149317
epoch: 2
train mean loss: 0.115880
epoch: 3
train mean loss: 0.101776
epoch: 4
train mean loss: 0.082577
epoch: 5
train mean loss: 0.082045
epoch: 6
train mean loss: 0.074332
epoch: 7
train mean loss: 0.064091
epoch: 8
train mean loss: 0.059199
epoch: 9
train mean loss: 0.061192
epoch: 10
train mean loss: 0.053155
epoch: 11
train mean loss: 0.054124
epoch: 12
train mean loss: 0.050729
epoch: 13
train mean loss: 0.050139
epoch: 14
train mean loss: 0.048307
epoch: 15
train mean loss: 0.045948
epoch: 16
train mean loss: 0.044958
epoch: 17
train mean loss: 0.046211
epoch: 18
train mean loss: 0.040627
epoch: 19
train mean loss: 0.041885

ちゃんと損失が下がっている。 あとは提出用のフォーマットを作成する。

results =  np.argmax(test_results, axis = 1)
import csv
f = open("results.csv", "w")
f.write(('"ImageId","Label"\n'))
for i, result in enumerate(results):
    f.write(('%d,"%d"\n'%(i+1,result)))
f.close()

結果

結果をkaggleのサイトに送信すると、正答率とランキングを教えてくれる。 今回の正答率は98%で、順位は205/830位でした。 f:id:ksknw:20151108162603p:plain

間違ったデータを確認してアルゴリズムを改良したいところだけど、テストデータはラベルがないのでどれ間違ったのかわからない。 ちゃんとやるなら、学習データ分割してvalidationしないとだめそう。

今後

パラメータもデータの正規化も、アルゴリズムもかなり適当だったので、色々もうちょっとちゃんとすれば性能上がると思う。 とはいえ、mnist自体はやり尽くされている感じで、あまりおもしろい感じではないので、次はタイタニックのやつをやってみようと思う。

参考

Kaggle: The Home of Data Science

MNIST handwritten digit database, Yann LeCun, Corinna Cortes and Chris Burges

Deniz Yuret's Homepage: Alec Radford's animations for optimization algorithms

Chainerによる多層パーセプトロンの実装 - 人工知能に関する断創録