Scikit-learnで機械学習(画像識別)

Scikit-learn(サーキットラーン)は、個人や商用利用を問わず誰でも無料で利用可能な機械学習フレームワークです。分類、回帰やクラスタリングなどの機能が利用でき、初学者が人工知能を学ぶ際の導入として最適な機械学習フレームワークです。

Scikit-learnで機械学習(画像識別)

このトライアルでは、Scikit-learnのインストールや環境設定に関する説明は省略します。また、プログラムは、Pythonでコーディングしていきます。

学習データの準備

機械学習を行うためには、学習用のデータを準備する必要があります。

MNISTの手書き文字データは、ディープラーニングや機械学習で画像認識する際によく利用されるサンプルデータです。MNISTは、「0~9」の手書き文字の画像データと正解ラベルのデータが収録されています。このデータを利用して、scikit-learnによる機械学習を実践してみましょう。

MNISTの手書き数字のデータベースは、以下のサイトからダウンロードすることができます。

こちらのウェブページから、以下の4つのファイルをダウンロードします。

  • train-images-idx3-ubyte.gz
  • train-labels-idx1-ubyte.gz
  • t10k-images-idx3-ubyte.gz
  • t10k-labels-idx1-ubyte.gz


このデータはバイナリデータで、ビックエンディアンで記録されています。

以下のPythonコードを実行すると、バイナリデータから手書き文字データのCSVファイルが作成されます。

  • train.csv ⇒ 学習用に使用するデータ
  • t10k.csv ⇒ 検証(バリデーション)用データ
  • train-0-5.pgm ⇒ 学習用データの1番目のpgm画像データ
  • t10k-0-7.pgm ⇒ 検証用データの1番目のpgm画像データ
import struct

def to_csv(name, maxdata):
        lbl_f = open("./mnist/" + name + "-labels-idx1-ubyte", "rb")
        img_f = open("./mnist/" + name + "-images-idx3-ubyte", "rb")
        csv_f = open("./" + name + ".csv", "w", encoding="utf-8")
        mag, lbl_count = struct.unpack(">II", lbl_f.read(8))
        mag, img_count = struct.unpack(">II", img_f.read(8))
        row, cols = struct.unpack(">II", img_f.read(8))
        pixels = row * cols

        for idx in range(lbl_count):
                if idx > maxdata: break
                label = struct.unpack("B", lbl_f.read(1))[0]
                bdata = img_f.read(pixels)
                sdata = list(map(lambda n: str(n), bdata))
                csv_f.write(str(label) + ",")
                csv_f.write(",".join(sdata) + "\r\n")
                if idx < 1:
                        s = "P2 28 28 255\n"
                        s += " ".join(sdata)
                        iname = "./{0}-{1}-{2}.pgm".format(name,idx,label)
                        with open(iname, "w", encoding="utf-8") as f:
                                f.write(s)

        csv_f.close()
        lbl_f.close()
        img_f.close()

to_csv('train',1000)
to_csv('t10k',1000)

Pythonで ビッグエンディアン のバイナリデータを読み込むためには、以下のようにします。

mag, lbl_count = struct.unpack(">II", lbl_f.read(8))

struct.unpack()でパックされたバイナリデータとして読み込みます。「>」はビッグエンディアンで読み込むことを示しています。また「II」は、2つ連続したunsined int型(符号なし4バイト整数型)を読み込むことを意味しています。「read(8)」で8バイトづつの塊を読み込んで処理していきます。

文字バイトオーダサイズアライメント
@nativenativenative
=native標準なし
<リトルエンディアン標準なし
>ビッグエンディアン標準なし
!ネットワーク(=ビッグエンディアン)標準なし
バイトオーダ
フォーマットPythonの型サイズ
c長さ1の文字列1バイト
b整数型1バイト
B整数型1バイト
?bool型1バイト
h整数型2バイト
H整数型(符号なし)2バイト
i整数型4バイト
I整数型(符号なし)4バイト
f浮動小数型4バイト
d浮動小数型8バイト
主要なデータ型のフォーマット文字
for idx in range(lbl_count):
        if idx > maxdata: break
        label = struct.unpack("B", lbl_f.read(1))[0]
        bdata = img_f.read(pixels)
        sdata = list(map(lambda n: str(n), bdata))
        csv_f.write(str(label) + ",")
        csv_f.write(",".join(sdata) + "\r\n")

上記のコード部分は、ラベルのデータファイルと手書き文字のデータファイルから、それぞれデータを読み込み、CSVファイルに出力しています。手書き文字データの1文字は、28×28個の符号なし整数データで表現されていますので、pixelsに代入された784(28×28)バイトづつデータを読み込んでいます。

読み込まれた手書き文字の数値データ(0~255)は、map関数とlambda式(無名関数)を用いて1バイトづつ文字列に変換してlist関数で配列に格納しています。

CSVの出力時には、先頭1文字目にラベルデータの数字を出力し、次いでlist関数で配列に格納された手書き文字データの値をjoin関数でカンマ区切りにして出力しています。

MNISTから入手した手書き文字データが、実際どのような手書き文字になっているのか確認してみます。

if idx < 1:
        s = "P2 28 28 255\n"
        s += " ".join(sdata)
        iname = "./{0}-{1}-{2}.pgm".format(name,idx,label)
        with open(iname, "w", encoding="utf-8") as f:
                f.write(s)

上記のコード部分では、読み込んだ手書き文字データの先頭(一番目)をPGM(Portable GrayMap format)画像形式で出力しています。このPythonコードでは、学習用データの「train-0-5.pgm」と検証用データの「t10k-0-7.pgm」の2つのPGM画像ファイルが出力されますので、PGM画像を表示できるビューアで確認することができます。

IrfanView(フリーソフト)で画像を表示してみます。

このトライアルでは、「5」と「7」の手書き文字が表示されました。このデータを使用してscikit-learnで機械学習を行います。

手書き文字画像を学習させる

今回作成した学習用データは、ラベルデータ(答え)と画像データがセットになっています。答えがわかっているものに対する学習のことを「教師あり学習」と言います。

from sklearn import svm, metrics
from sklearn.model_selection import train_test_split

def read_csv(flnm):
        labels = []
        images = []
        with open(flnm, "r") as f:
                for line in f:
                        cols = line.split(",")
                        labels.append(int(cols.pop(0)))
                        vals = list(map(lambda n: int(n) / 256, cols))
                        images.append(vals)
        return {"labels":labels, "images":images}

data = read_csv("./train.csv")
test = read_csv("./t10k.csv")

clf = svm.SVC()
clf.fit(data["images"], data["labels"])

predict = clf.predict(test["images"])

ac_score = metrics.accuracy_score(test["labels"], predict)
cl_report = metrics.classification_report(test["labels"], predict)

print("正解率=", ac_score)
print(cl_report)

上記のPythonコードで機械学習ができてしまいます。人工知能とか聞くと難しく感じてしまいがちですが、Pythonと人工知能フレームワークを利用することで、比較的簡単に実践することができるのです。

def read_csv(flnm):
        labels = []
        images = []
        with open(flnm, "r") as f:
                for line in f:
                        cols = line.split(",")
                        labels.append(int(cols.pop(0)))
                        vals = list(map(lambda n: int(n) / 256, cols))
                        images.append(vals)
        return {"labels":labels, "images":images}

上記のコード部分は、学習用とテスト用のCSVデータを読み込むための関数です。読み込んだCSVの先頭にラベルデータ(答え)が記録され、続いて手書き文字画像データを構成する784個(28×28個)のデータが記録されています。

このCSVデータを先頭から一行ずつ読み込み、ラベルデータと画像データに分離して、それぞれ配列変数に格納していきます。行の先頭にあるラベルデータは、pop()メソッドで取り出しています。また、lambda式(無名)で画像データの階調を256で割って規格化(または正規化)しています。

data = read_csv("./train.csv")
test = read_csv("./t10k.csv")

clf = svm.SVC(kernel='linear')
clf.fit(data["images"], data["labels"])

predict = clf.predict(test["images"])

ac_score = metrics.accuracy_score(test["labels"], predict)
cl_report = metrics.classification_report(test["labels"], predict)

print("正解率=", ac_score)
print(cl_report)

先に定義したread_csv()関数を呼び出してデータを読み込み、学習用データを「data」、テスト用データを「test」という名前の配列変数に格納しています。

分類器は、SVC(SVM Classification)を使用します。SVM(サポートベクターマシーン)は、2つのデータ間のマージン(離隔距離)を最大化するように分類化する分類器です。このトライアルでは、線形分類による手書き画像データの学習をすすめていきます。

fit()メソッドに手書き画像の学習データとラベルデータを与えて学習を行います。次に、predict()メソッドに手書き画像のテストデータを入力して、予測結果を出力しています。

accuracy_score()メソッドで予測したテストデータのラベルデータと予測結果を比較して、正解率を算出します。最後にclassification_report()メソッドで予測結果の分析値を出力しています。

機械学習の成果を確認する

$python3.6 mnist-train.py

正解率= 0.868263473054
             precision    recall  f1-score   support

          0       0.91      0.93      0.92        42
          1       0.97      1.00      0.99        67
          2       0.86      0.93      0.89        55
          3       0.84      0.80      0.82        46
          4       0.89      0.89      0.89        55
          5       0.72      0.84      0.78        50
          6       0.95      0.86      0.90        43
          7       0.80      0.84      0.82        49
          8       0.91      0.72      0.81        40
          9       0.84      0.80      0.82        54

avg / total       0.87      0.87      0.87       501

上記の結果は、1,000件の学習用データと500件のテストデータをによる実験結果です。それぞれ「0~9」のラベルデータの予測スコアと全体の平均的スコアが表示されています。

この時点で既に全体の正解率は87%に達しています。個別にみてみると「5」は、他のラベルに比べて予測がやや難しいことがみてとれます。

各カラムの意味は以下の通りです。

  • precision⇒精度(正解と予測したデータの内、実際に正解したものの割合)
  • recall⇒再現率(正解と予測されるべきものに対して、実際に正解したものの割合)
  • f1-score⇒F尺度(精度と再現率に調和平均)
  • support⇒対象の数

次に学習データ数を増やしてみましょう。10,000件の学習用データと5000件のテストデータにより実験してみます。学習データが増えたので、学習に多少時間が掛かります。

正解率= 0.868263473054

正解率= 0.891821635673
             precision    recall  f1-score   support

          0       0.92      0.97      0.94       460
          1       0.93      0.99      0.96       571
          2       0.88      0.89      0.88       530
          3       0.82      0.89      0.85       501
          4       0.88      0.92      0.90       500
          5       0.88      0.81      0.84       456
          6       0.93      0.91      0.92       462
          7       0.90      0.87      0.89       512
          8       0.88      0.82      0.85       489
          9       0.90      0.84      0.87       520

avg / total       0.89      0.89      0.89      5001

学習データを増やした結果、全体の正解率は89%に向上しました。また、「5」は72%から88%に正解率が向上したことが確認できます。このように学習データを増やすと正解率が向上することがわかりました。

ただし、学習データを増やし過ぎると逆に正解率が低下する場合もあります。これを過学習(オーバーフィッティング)と言います。

機械学習などの人工知能の精度を向上させるためには、たくさんのデータを学習させる必要がありますが、何でもよいというわけではなく、均質なデータを学習させることも重要なのです。