kaggleのDigit Recognizerに挑戦してみた

数字の認識に挑戦してみたので、たいしたことはやってないけどそのノウハウをまとめる。

詳細は以下のリンクにて
http://www.kaggle.com/c/digit-recognizer

利用したデータはtrain.csvで上記リンク先の”Get the Data”を辿るとみつかる。
train.csvには画像データ42000万毎分のデータがはいっており、始めの行に

label,pixel0,pixel1,pixel2,pixel3,pixel4,…,pixel782,pixel783

と書いてある。つまり、2行目以降の行ごとに1つ分の画像データが入っていて、その画像は28x28pixelのサイズだから全784pixelであることを示す。
一番最初のlabelには画像に書かれている数字が示しされている。以下のデータはある1行を実際に抜き出してきたものだが、一番初めの1はこの画像データが示している数字を表し、後の784つの数字は各pixelの画像データを表す。0ばかり並んでいるが、中略のところにちゃんと上限255で0じゃない数字も並んでいる。

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

train.csvには42000画像分のデータが入っているため、始めの各列の説明を示す行と合わせて42001行のデータとなっている。

まず自分が行ったのは、28×28の画像データからマージン部分を排除し、20×20の画像にリサイズする作業。

original

実際に上の画像を見てみるとわかるが、上下左右にマージンが存在する。マージンがあると画像内で数字が上下左右のどこかに偏る可能性があるので、それをなくす効果を期待した。
得られた画像が以下のもの。

sub

今回は16x20pixelの画像になったが、データによってマージンが異なる。つまり、マージンを除去すると画像ごとにデータ量が違うものになってしまう。これは後で認識する際に不便なので、20x20pixelにリサイズした。実際の画像が以下のもの。

original20x20

ちゃんと正方形の画像になっているのがわかると思う。この作業にはデータ量を揃える以外にもう一つ期待していることがある。それはサイズの統一化。42000枚の画像があるが、28x28pixelという狭いところに数字が1つ書かれているといっても恐らく書かれている数字の大きさに微妙なばらつきがあると思われる。マージンを排除して20x20pixelにリサイズすることで全ての数字のサイズを揃えている。

また、最後にデータごとに平均値で引いた後、標準偏差で割ることで分散を1に統一し、normalizeする。

今までの流れをソースコードにすると以下の通り。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import csv
import cv
import numpy as np
import sys

data = np.genfromtxt('train.csv', delimiter=',')

train_data = data[1:,1:]
label = data[1:,0]

#exclude margin
def trimArea(m):
    for j in range(28):
        for i in range(28):
            if 0 != m[j,i]:
                top_y = j
                break
        else:
            continue
        break

    for i in range(28):
        for j in range(28):
            if 0 != m[j,i]:
                top_x = i
                break
        else:
            continue
        break

    for j in range(28)[::-1]:
        for i in range(28):
            if 0 != m[j,i]:
                bottom_y = j
                break
        else:
            continue
        break

    for i in range(28)[::-1]:
        for j in range(28):
            if 0 != m[j,i]:
                bottom_x = i
                break
        else:
            continue
        break

    sub = cv.GetSubRect(m, (top_x, top_y, bottom_x - top_x + 1, bottom_y - top_y + 1))
    #cv.SaveImage("sub.png",sub)
    sub2 = cv.CloneMat(sub)
    square = cv.CreateMat(20, 20, cv.CV_8UC1)
    cv.Resize(sub2, square)
    return square

squares = []
count = 0
for d in train_data:
    count = count + 1
    tmp = cv.fromarray(d.reshape(28,28))
    dmat = cv.CreateMat(28,28,cv.CV_8UC1)
    cv.Convert(tmp,dmat)
    #cv.SaveImage("original.png", dmat)
    square = trimArea(dmat)
    squareArray = np.asarray(square)
    squares.append(squareArray.flatten())
    #cv.SaveImage('test.png', nonZeroArea)
    #sys.exit()

#convert list to ndarray
squares = np.array(squares)

#normalize
avg = np.mean(squares, axis=1)
sd = np.std(squares, axis=1)
squares = (squares - avg.reshape(len(squares),1)) / sd.reshape(len(squares),1)

np.savetxt("train_20x20_normalized.txt", squares)

次に、得られた20x20pixelの42000枚の画像を利用して、SVMに掛けてみた。コードは以下の通り。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import csv
import cv
import sys
import numpy as np

from sklearn import cross_validation
from sklearn import svm
from sklearn.grid_search import GridSearchCV
from sklearn import preprocessing

X = np.loadtxt("train_20x20_normalized.txt")
Y = np.loadtxt("label.txt")

min_max_scaler = preprocessing.MinMaxScaler(feature_range=(-1,1))
X_train_minmax = min_max_scaler.fit_transform(X)
np.savetxt("scale_normal_sq.txt", min_max_scaler.scale_)

tuned_parameters = [
        {'C': [1,10,100,1000], 'gamma': [0.1,0.01,0.001,0.0001], 'kernel': ['rbf']},
        ]
clf = GridSearchCV(svm.SVC(),tuned_parameters,n_jobs=-1)
clf.fit(X_train_minmax,Y,cv=5)
print clf.best_estimator_
print clf.best_score_
print clf.grid_scores_
sys.exit()

最初に-1〜1の値に収まるようにScalingを行っている。
cross validationでベストな数値は自分で探してみて下さい。良いパラメータを見つけることができれば98.1%くらいの精度がでる。
ちなみに上記のcross validationはThinkPadのX230(core4のSandyBridge)を使って丸1日掛かっていたりする。
また、GridSearchCVでjobs=-1を指定しているのでCPUをフルに使おうするので、適宜変更すると良い。

ソースはgithubにもおいておいた。
https://github.com/tishibas67/KaggleDigitRecognizer
結果的にOpenCVじゃなくてScipyとかでどうにかできそうなことしかやってないけど、今回Python入門したばかりなので勘弁して下さい。

良いSVMのパラメータが見つかったら、42000枚全ての画像を使ってモデルを作り、kaggleのDigit Recognizerのページから得られるtest.csvを使って、結果を出してkaggleに投げてみると良い。
モデルは20x20pixelのnormalize & Scalingされているデータを入力とするので、そこは自分でお願いします。
test.csvのデータ構造は最初の列にlabelが書かれていないものであることに注意すること。

ちなみに学習データの画像を時計回りに10度、半時計回りに10度回転させ、データ量を3倍にして学習してみたところ少し精度が向上した。
cross validationの段階で98.7%程度。

アルゴリズム然り、コードの書き方然り、もっとこうしたらよいのでは?というアドバイスがあればぜひお願いします。

tishibas on Githubtishibas on Instagramtishibas on Linkedintishibas on Twitter
tishibas
ソフトウェアエンジニア
1988年、富山生まれ
写真撮影が趣味
1児の父

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です