kaggle の Digit Recognizer に挑戦する(1)

今回は kaggle の Digit Recognizer というコンペティションに挑戦したいと思います。このコンペティションは簡単に言うと画像認識で、0〜9の数字を手で書いた画像があるので、それが何の数字なのかを予測する、というものです。なお、開発は Jupyter Notebook を利用し、環境は Docker tensorflow/tensorflow:latest-jupyter イメージで構築しています。

MNIST のデータ

実は MNIST にも同じようなデータがあり、こちらの方が理解しやすいので、まずはこちらを説明します。手書きの数字画像は縦28×横28ピクセルの画像で、各ピクセルには色の濃淡を表す0〜255までの整数が入っています。この数値は、0だと真っ白で、255だと真っ黒を表します。MNIST のデータは解説しているサイトがたくさんありますので、例えばこちらのサイトを参照してもらえば、何を言っているのかすぐに理解してもらえると思います。

では、実際にダウンロードして確かめてみます。

import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.utils import to_categorical
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pandas as pd
(xm_train, ym_train), (xm_test, ym_test) = mnist.load_data()
print(xm_train.shape)
(60000, 28, 28)

60000 × 28 × 28 の numpy.ndarray であることが確認できます。縦28×横28ピクセルの画像データが60000枚あるということです。matplotlib の imshow() を使うと、画像で表示することができます。

plt.imshow(xm_train[0])

対応するラベル y_train を確認します。

print(ym_train[0])
5

手書き数字画像と、ラベルが一致することが確認できました。

kaggle のデータ

kaggle のデータはMNISTのデータとはちょっと違います。実際にデータをダウンロードし、データを読み込んで中身を確認します。

train = np.loadtxt('/root/practice/digit/csv/train.csv', skiprows=1, delimiter=',')
x_test = np.loadtxt('/root/practice/digit/csv/test.csv', skiprows=1, delimiter=',')
print(train.shape)
print(x_test.shape)
(42000, 785)
(28000, 784)

訓練データは 42000 × 785 になっています。42000は手書き数字画像の枚数です。785は、1列目はラベル、即ち手書き画像の数値が何かを表したもので、2〜785列目は縦28×横28ピクセルの画像データを横一列に並べたものものです。28 × 28 = 784 なので数も合いますね。テストデータは、訓練データと比較して、ラベルの列が無いものになります。

実際にデータを加工して確かめてみます。

# 1列目と2〜785列目を分ける
y_train, x_train = np.split(train, [1], 1)
print(x_train.shape)
print(y_train.shape)
(42000, 784)
(42000, 1)
# 1枚分を取り出し 28 * 28 の形にして表示させてみる
plt.imshow(x_train[0].reshape(28, 28))

# ラベル確認
print(y_train[0])
[1.]

画像が表示され、ラベルと一致することが確認できました。

データ準備

学習をする前にデータの下準備をしていきます。

# 0 - 1 の間の数値にする
x_train  = x_train.astype('float32')
x_test   = x_test.astype('float32')
x_train /= 255
x_test  /= 255
# one-hot 表現にする
y_train = to_categorical(y_train, 10)
print(y_train[0])
[0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]

ラベルの方を one-hot 表現にしています。訓練データの最初の数値は1でしたから、(0から数えて)1を表すところが1になっていて、後は全て0になっていることが確認できます。

学習

それでは、ニューラルネットワークを構築して学習していきます。

# ニューラルネットワーク構築
model = Sequential()
# [中間層] ユニット数:12, 活性化関数:relu, 入力次元数:784
model.add(Dense(units=12, activation='relu', input_dim=784))
# [出力層] ユニット数:10, 活性化関数:ソフトマックス
model.add(Dense(10, activation='softmax'))
# 損失関数:categorical_crossentropy, 最適化アルゴリズム:rmsprop
model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['accuracy'])
model.summary()
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 dense (Dense)               (None, 12)                9420      
                                                                 
 dense_1 (Dense)             (None, 10)                130       
                                                                 
=================================================================
Total params: 9,550
Trainable params: 9,550
Non-trainable params: 0
_________________________________________________________________

各層の設定はソース内のコメントとして書きましたので、参照してください。

それでは学習していきます。

# バッチサイズ:128, 反復数:20
history = model.fit(x_train, y_train, batch_size=128, epochs=20, verbose=1)

結果をグラフで表示します。

sns.lineplot(data=history.history["loss"])

sns.lineplot(data=history.history["accuracy"])

学習が進むにつれ、誤差が減少し、精度が増加していくことがわかります。

設定を変える

最適化アルゴリズムを adam に変えて試してみます。具体的には下記の optimizer 引数を adam に変更します。他の設定は全く同じです。

# 損失関数:categorical_crossentropy, 最適化アルゴリズム:adam
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

誤差、精度ともに少し改善したようです。

中間層のユニット数を128に増やしてみます。最適化アルゴリズムは adam を利用し、その他設定は同じです。具体的な修正箇所は下記です。

# [中間層] ユニット数:128, 活性化関数:relu, 入力次元数:784
model.add(Dense(units=128, activation='relu', input_dim=784))

誤差、精度ともに大幅に改善されたようです。 evaluate メソッドで具体的な数値も比較してみます。

loss, accuracy = model.evaluate(x_train, y_train, verbose=1)
loss accuracy
rmsprop 0.1851324439048767 0.9472380876541138
adam 0.1607203185558319 0.9545952677726746
adam + Unit数128 0.00736998999491334 0.9989285469055176

数値を見ても改善されているのが確認できます。

予測

では、 adam + 中間層ユニット数128 の設定で、テストデータを予測したいと思います。

predicts = model.predict(x_test)
# 0 - 9 の10列のうち、最も値の大きいものが予測された値なので、それを取り出す
predicts_label = np.argmax(predicts, axis=1)
# CSVを出力するためのデータを生成
df = pd.DataFrame({
    'imageId': list(range(1, len(predicts)+ 1)),
    'Label': predicts_label
})
print(df.tail())
imageId Label
27995 27996 9
27996 27997 7
27997 27998 3
27998 27999 9
27999 28000 2
# CSV出力
df.to_csv("/root/practice/digit/csv/predictions.csv", index=False)

無事にCSVが出力されたので、kaggle に提出します。

おおおおおおおおおおお!これも下から数えた方が早いですが、自分の名前が Leaderboard に刻まれました!!

参考にしたサイト