Pythonで自分用のAIを作ろう10-教師あり学習SVM | グラ山ギターのブログ

グラ山ギターのブログ

毎週健康のために山登りをしています。低い山ばかりですが播磨の山々は岩山が多く景観がすばらしい所が多いです。山頂などで演奏しています。 井上陽水のカバーが多いです。
暑い時期は山登りは控えています。

機械学習の中のクラス分類の問題では、SVM(Suport Vector Machine)というアルゴリズムがある。ニューラルネットやファジイが非線形な問題を解くのに対して、yi= Σwj xji - hという線形認識システムを非線形問題が扱えるように拡張したのが、1992年のBernhard E.Boser,etcの論文である。
 
1992_A Training Algorithm for Optimal Margin Classiers_Bernhard E.Boser,etc
ここでは、xの代わりに非線形関数D(x)でyi= Σwj xD(x)ji - hに置き換えても、学習ができることを示した。またD(x)をカーネル関数 K(x1,x2)に置き換えて、数種類のK(x1,x2)に対して線形の学習システムより圧倒的に高い認識率のシステムが作れることを示した。
現在においては、このSVMはPythonを用いて外部ライブラリのscikit-learnで簡単に実装することができる。今日はscikit-learnのライブラリを使って手書き文字認識をしてみよう。
まず一般に公開されている手書き文字のデータをhttp://yann.lecun.com/exdb/mnist/
サイトから下記の4つの gzファイル をダウンロードしてくる。
train-images-idx3-ubyte.gz (training set images (9912422 bytes))
train-labels-idx1-ubyte.gz (training set labels (28881 bytes))
t10k-images-idx3-ubyte.gz (test set images (1648877 bytes))
t10k-labels-idx1-ubyte.gz (test set labels (4542 bytes))
gzファイルはwin10で解凍するには一般に入手できるLhazなどのファイル圧縮・解凍ツールで展開する。
解凍すると、gz拡張子が無い、下記の4つのファイルが取り出せる
train-images-idx3-ubyte
train-labels-idx1-ubyte
t10k-images-idx3-ubyte
t10k-labels-idx1-ubyte
trainは学習用の画像とそのラベルで、60000枚あります。t10kは認識率テスト用で10000枚あります。
 
Windows Powershellを立ち上げて、ファイルを解凍したディレクトリにcdコマンドで移動し、
> Format-Hex train-images-idx3-ubyte | Select-Object -First (256/16)
とコマンドを入力します。すると、ファイルの先頭から256バイトを16バイト毎に16進表示してくれます。
画像ファイルは2番目の4byteで0000EA60(Hex)=60000(Dec)なので60000枚の画像データがあることが和k利ます。3番目の4byteは0000001C(Hex)=28(Dec)で画像の行数(縦サイズ)、4番目の4byteは0000001C(Hex)=28(Dec)で画像のカラム数(横サイズ)を意味しています。
 
The labels values are 0 to 9.
TRAINING SET IMAGE FILE (train-images-idx3-ubyte):
[offset] [type]          [value]          [description]
0000     32 bit integer  0x00000803(2051) magic number
0004     32 bit integer  60000            number of images
0008     32 bit integer  28               number of rows
0012     32 bit integer  28               number of columns
0016     unsigned byte   ??               pixel
0017     unsigned byte   ??               pixel
........
xxxx     unsigned byte   ??               pixel
同様に、
> Format-Hex trin-labels-idx3-ubyte | Select-Object -First (256/16)
とコマンド入力すると、ファイルの中身が16進数で表示され、2番目の4byteが0000EA60(Hex)=60000(Dec)なので60000個の対応するラベル(カテゴリー)が含まれていることが判ります。0008以降が1byteが1ラベルを表しています。この例で行くと1枚目の画像は05=5,2枚目の画像は00=0、3枚目の画像は04=4、というように読み取れます。
TRAINING SET LABEL FILE (train-labels-idx1-ubyte):
[offset] [type]          [value]          [description]
0000     32 bit integer  0x00000801(2049) magic number (MSB first)
0004     32 bit integer  60000            number of items
0008     unsigned byte   ??               label
0009     unsigned byte   ??               label
........
xxxx     unsigned byte   ??               label
 
これらのファイルを使って、scikit-learnという機械学習のライブラリを使って、手書き認識を行ってみる。
 
まずは、これらバイナリデータをcsv形式(,で区切られたtxt形式)に変換する。
trainかt10kをしていすると、maxdataだけ先頭からデータを切り出し、csvにしてファイル保存させる
###############
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("./mnist/"+name+".csv", "w", encoding="utf-8")
    # ヘッダ情報を読む --- (※1)
    mag, lbl_count = struct.unpack(">II", lbl_f.read(8))
    mag, img_count = struct.unpack(">II", img_f.read(8))
    rows, cols = struct.unpack(">II", img_f.read(8))
    pixels = rows * cols
    # 画像データを読んでCSVで保存 --- (※2)
    res = []
    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")
        # うまく取り出せたかどうかPGMで保存してテスト -- (*3)
        if idx < 10:
            s = "P2 28 28 255\n"
            s += " ".join(sdata)
            iname = "./mnist/{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()
# 出力件数を指定 --- (※4)
to_csv("train", 1000)
to_csv("t10k", 500)
###############
プログラム中の参考説明
mag, lbl_count = struct.unpack(">II",lbl_f.read(8))
の">"は最初のbyteが上の位、順次下の位に並んでいるということを意味しています。
"I"はunsigedの4byteという意味です。
4byteを2回読み、8byteデータを読み取るということです。
最初の4byteはmagに、次の4byteはlbl_countに値が返されます。
mag, img_count = struct.unpack(">II", img_f.read(8))
rows, cols = struct.unpack(">II", img_f.read(8))
は最初の4byteはmagへ、2番目の4byteはimg_countへ、3番目の4byteはrowへ、4番目の4byteはcolsへ返されます。
pixels = rows * cols =28x28
で1枚の画像の総画素数を表します。
label = struct.unpack("B", lbl_f.read(1))[0]
"B"はunsigned charを意味します。1byteだけ読み込んで、unsigned charとしてlabelに代入します。
bdata = img_f.read(pixels)
img_fからpixels bytesだけデータを読み取りbdataに代入します。
sdata = list(map(lambda n: str(n), bdata))
リストbdataの要素をstr()変換(文字に変換)してsdataのリストに変換します。
str(label)+","
label(unsigned char)を文字に変換します
",".join(sdata)+"\r\n"
,sdata+CR/LF、CR/LFはWindowsOSで改行を意味します(0Dh 0Ah)
プログラムの中身は、最初に関数to_csv(name, maxdata)を定義し、最後の2行が実際に実行されるプログラムです。
to_csv("train", 1000)
to_csv("t10k", 500)
./mnist/tarin-labels-idx1-ubyte を読出し、1000個のデータを文字列に変換して、ラベルと画像を対にして./mnist/train.csvに保存する。
./mnist/t10k-labels-idx1-ubyte を読出し、500個のデータを文字列に変換して、ラベルと画像を対にして./mnist/t10k.csvに保存する。
です。
if idx < 10:
    s = "P2 28 28 255\n"
    s += " ".join(sdata)
    iname = "./mnist/{0}-{1}-{2}.pgm".format(name,idx,label)
    with open(iname, "w", encoding="utf-8") as f:
        f.write(s)
10件までは"name-idx-label.pgm"という画像ファイルに書き込まれます。

pgmファイルはwindowsではIrfanviewというフリーのソフトで画像を表示することができます。
(http://www.irfanview.com よりダウンロード、インストール)
これで学習用の画像とラベル1000枚分、テスト用の画像とラベルがcsvファイルにまとめられました。
 

このcsvファイルを使って、SVMで文字認識します。
手書き文字認識をSVMを使って学習するプログラム
###############
from sklearn import svm, metrics
from sklearn.metrics import accuracy_score
# CSVファイルを読んで学習用データに整形 --- (※1)
def load_csv(fname):
    labels = []
    images = []
    with open(fname, "r") as f:
        for line in f:
            cols = line.split(",")
            if len(cols) < 2: continue
            labels.append(int(cols.pop(0)))
            vals = list(map(lambda n: int(n) / 256, cols))
            images.append(vals)
    return {"labels":labels, "images":images}
data = load_csv("./mnist/train.csv")
test = load_csv("./mnist/t10k.csv")
# 学習 --- (※2)
clf = svm.SVC(gamma ='auto')
clf.fit(data["images"], data["labels"])
# 予測 --- (※3)
predict = clf.predict(test["images"])
# 結果がどの程度合っていたか確認 --- (※4)
ac_score = accuracy_score(test["labels"], predict)
#cl_report = classification_report(test["labels"], predict)
print("正解率=", ac_score)
#print("レポート=")
#print(cl_report)
###############
SVM(サポートベクターマシーン)をするためには、scikit-learnをインポートします。
def以下は先ほどデータを変換してセーブしたcsvからデータを読み出します。
読み出した訓練データは
data["images"]とすると画像データが、data["labels"]とすると対応するラベルデータが指定できます。
テストデータも同様
test["images"]とすると画像データが、test["labels"]とすると対応するラベルデータが指定できます。
学習はたった2行です。
clf = svm.SVC(gamma ='auto')
clf.fit(data["images"], data["labels"])
clf.fitでデータを学習させます。データdata["images"]とそれに対応するラベルdata["labels"]をセットすれば学習されます。
predict = clf.predict(test["images"])
テストデータの画像を入力して学習結果の予測ラベルpredictを計算します。
プログラム実行結果は
  正解率= 0.7884231536926147
となりました。SVMでは手書き文字の位置ズレに関して考慮されていなので、この例ではあまり認識率が上がらないのだと思います。CNNを用いたディープラーニングでは、位置ズレの問題をmaxプーリングという手法で吸収できるようにしてあります。
ac_score = accuracy_score(test["labels"], predict)
真のラベルと予測ラベルを比較して認識精度を求めます。
 
どんなアルゴリズムでどの程度の誤認識率になったのかの詳細は、
 http://yann.lecun.com/exdb/mnist/
に詳しく比較してあるので参考にしてください。
 
参考:SVMの理論的解説(1992論文より)