コンテンツにスキップ

第4章 手書き文字の認識

1 データの読み込み

ここでは手書きで描いた数字の画像データを読み込んでそれが何という数字を書いたかを認識(予測)します。

機械学習の練習で使う有名なデータセットがいろいろありますが、実はscikit-learn本体にそのようなデータセットがいくつか内蔵されています。そのうちの一つがこの数字の手書き画像です。

さっそく、読み込んでみましょう。今までと異なり、csvファイルからではなく、scikit-learnのdatasetsから読み込むことが出来ます。 ファイル digits.ipynb に以下を記述します。

from sklearn import datasets

digits = datasets.load_digits()

読み込んだ変数 digitsの images にデータが格納されています。 いくつあるかを表示してみましょう。

len(digits.images)

最初のデータを表示してみます。

digits.images[0]

すると二次元配列になっており、0~15までの数が入っています。これが手書き文字の画像です。データは縦8ドット×横8ドットで、数字が大きいほど色が濃いドットを表しています。0以外のところを見るとなんとなく数字の0を表しているように見えます。

この数字が何を表しているかは、digits.target に答えが入っています。

digits.target[0]

2 画像の表示

しかし、数字ではわかりにくいので画像で表示してみましょう。 matplotlibでimshow関数にこのデータを渡すと画像として表示されます。

import matplotlib.pyplot as plt

plt.imshow(digits.images[0], cmap="gray")

引数にcmap="gray"を付けて灰色にして表示しています。

for文を使って最初の15個を表示してみましょう。 matplotlibのsubplot機能を使って3行×5列の複数の画像を表示してみます。

for i in range(15):
    plt.subplot(3, 5, i+1)
    plt.axis("off")
    plt.title( str(digits.target[i]) )
    plt.imshow( digits.images[i], cmap="gray" )

3 モデルの構築と学習

では、これを教師データとして、特徴量 x と正解ラベル y に入れましょう。

x = digits.images
y = digits.target

ただし、特徴量 x は一次元のリストである必要があります。今の段階ではリストのリストになっていますので、これを一次元に変換します。

# 二次元を一次元に変換
x = x.reshape((-1, 64)) 

次に学習用データとテスト用データに分割します。

from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(x, y,  test_size=0.2, random_state=0)

最後の学習を行います。今回も決定木を使用してみましょう。

# 学習
from sklearn import tree
model = tree.DecisionTreeClassifier(random_state=0)
model.fit(x_train, y_train)

4 評価の表示

では、どれぐらい正解率があるかを出してみましょう

model.score(x_test,y_test)

80%台は出たのでは無いかと思います。まずまずの結果です。

どの文字が間違いが多かったでしょうか。予測した結果と正解とを比較してみます。

# 予測した結果を predicted に入れる
predicted = model.predict(x_test)

from sklearn import metrics
# y_test と比較した結果の表示
metrics.confusion_matrix(predicted, y_test)

結果は一番上の行が「0が何と予測されたか」の件数が入っています。左から0と予測されたもの、1と予測されたもの、2と予測されたものです。

実際にどの画像が何と予測されたか、画像と照らしあわせて最初の15個を表示してみます。

for i in range(15):
    # テストデータ(8x8の2次元に戻す)
    image = x_test[i].reshape(8,8)

    seikai = y_test[i]
    yosoku = predicted[i]

    plt.subplot(3, 5, i + 1)
    plt.axis('off')
    plt.title(f'{seikai}:{yosoku}')
    plt.imshow(image, cmap="gray")

5 過学習

再び、決定木の階層を表示してみましょう。

import matplotlib.pyplot as plt
import seaborn as sns
sns.set(font=["Meiryo", "Yu Gothic"])

tree.plot_tree(model, filled=True)
plt.show()

このとき、木の階層が いくつも伸びていることが分かります。 この階層の最大値は、モデル作成時にmax_depthで指定することが出来ます。

model = tree.DecisionTreeClassifier(max_depth=3)

max_depthを大きくすると階層が深くなり、それだけたくさんの判断を重ねることができます。しかし、深すぎても良い結果は得られなくなります。 なぜなら過学習を起こすからです、

過学習とはあまりにも学習データに最適化しすぎて未知のデータの判断を誤ってしまうことをいいます。学習しすぎてもいけないということです。

それを確認してみましょう。max_depthを1から初めて14まで増やしてみます。 結果をDataFrameに格納し表示してみます。

import pandas as pd
dfk = pd.DataFrame(columns=['学習用', 'テスト用'])

for n in range(1,15):
    model = tree.DecisionTreeClassifier(max_depth=n, random_state=0)
    model.fit(x_train, y_train)

    score_train = model.score(x_train,y_train)
    score_test = model.score(x_test,y_test)

    dfk.loc[n] = [score_train, score_test]
dfk

max_depthを増やしていくと学習用もテスト用も順調に正解率が伸びることが分かります。しかし、max_depthが12ぐらいになると学習用データについては正解率は伸びますが、テスト用データでは逆に下がっています。

モデルが学習用データに最適化しすぎて、テスト用データで正しく判断できなくなっていることが分かります。

過学習を起こさないようにすることが汎用的に正解率を上げるポイントです。

6 ランダムフォレスト

決定木では80%台の正解率でした。これをもっと上げたいところです。 そこで、より強力なアルゴリズムを使用します。それがランダムフォレストです。ランダムフォレストは決定木を複数生成します。そして、その決定木で出た結果で最も多いものを予測結果とします。

初期設定では決定木を100個作成します。もちろん、同じ判断基準では同じ結果になってしまいますので、違う判断が出来るようにランダムで判定基準を作成します。

では、使用してみましょう。

from sklearn.ensemble import RandomForestClassifier
# ランダムフォレスト
model = RandomForestClassifier(random_state=0)

model.fit(x_train, y_train)
model.score(x_test,y_test)

ランダムフォレストでは、90%以上の結果が出るはずです。 なお、ランダムフォレストでも、max_depthを指定できます。また、n_estimatorsで決定木の数を指定できます。

それぞれの決定木を可視化したい場合、model.estimators_ に決定木のリストが入っています。

plt.subplots(figsize=(16, 12))
tree.plot_tree(model.estimators_[0], filled=True)
plt.show()