【機械学習 no.1】Pythonで最近傍法を実装してみた

Pythonで機械学習のプログラムを組むのであれば,scikit-learnを使えば一発じゃねーの?と思うかもしれませんが,あえて「中身」を理解する意味も含めてPythonで1から実装してみました.

・・・・と言ってますが,最近傍法は機械学習の手法の中でも最も簡単で実装が容易な手法ですけどね.今後少しずつテクニカルな手法を実装していきたいと思いますが,先ずは簡単なものから.最初は,NumPyとPandasをインポートします.データはirisを使いましょうか.サンプルで使用されるデータとして最も一般的なので.

>>> import numpy as np
>>> import pandas as pd
>>> df = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data', header=None)
>>> df.tail()
       0    1    2    3               4
145  6.7  3.0  5.2  2.3  Iris-virginica
146  6.3  2.5  5.0  1.9  Iris-virginica
147  6.5  3.0  5.2  2.0  Iris-virginica
148  6.2  3.4  5.4  2.3  Iris-virginica
149  5.9  3.0  5.1  1.8  Iris-virginica
>>>

という具合に,データはUCIから直接ダウンロードします.irisは,4次元の特徴量を有する三種類のアヤメ(Iris-setosa, Iris-versicolor, Iris-verginica)をそれぞれ50個ずつ集めたデータセット.したがって,行方向はデータの番号,列方向は特徴量で最後はIrisの種類の名前を示します.

先ずはデータを特徴とクラスラベルに切り分けます.


x = df.iloc[:,0:4].values
label = df.iloc[:,4].values

データ数は150個あって(\(n=0,1,2,…,149\)),次元は4次元(\(d=0,1,2,3\))なので,たとえば,\(n=2\) のデータ\(\mathbf{x}(n)\) とクラスラベル \(y(n)\) は

>>> x[2]
array([ 4.7,  3.2,  1.3,  0.2])
>>> label[2]
'Iris-setosa'
>>>

となります.ここまでがデータの前処理.で,関数knn()を作成したいと思います.Rの関数knn()との対比のため引数の名前は同じにしてます,train, test, clを入力として,


def knn(train, test, cl):
    dist = np.sum((test-train)**2, axis=1)  
    argmin = np.argsort(dist)[0]
    return cl[argmin]

へ?たったこれだけ?と思っちゃうかもしれませんね.これだけです(笑).NumPyの関数について,np.sum()は行方向と列方向の総和を求めることが可能で,axis=0とaxis=1で設定.たとえば,実行結果例を以下に.axis=0で行方向の総和(最初の要素は,5.1+4.9+4.7=14.7),axis=1で列方向の総和(最初の要素は,5.1+3.5+1.4+0.2=10.2)となります.

>>> data = x[0:3]
>>> data
array([[ 5.1,  3.5,  1.4,  0.2],
       [ 4.9,  3. ,  1.4,  0.2],
       [ 4.7,  3.2,  1.3,  0.2]])
>>> np.sum(data, axis=0)
array([ 14.7,   9.7,   4.1,   0.6])
>>> np.sum(data, axis=1)
array([ 10.2,   9.5,   9.4])
>>> 

np.argsort()は昇べき順にソートし,インデクスを出力する関数(Rのorder()に相当).たとえば,実行結果例を以下に.元のデータの3番目が-3.3で最も小さく,次は2番目の1.8が小さく,… ,最後は5番目の8.6になるので,結果は,3,2,4,0,1,5となります.

>>> data = np.array([3.2,5.0,1.8,-3.3,2.2,8.6])
>>> data
array([ 3.2,  5. ,  1.8, -3.3,  2.2,  8.6])
>>> np.argsort(data)
array([3, 2, 4, 0, 1, 5])
>>> 

で,全体のコードは以下のようになります.最初に \(n=0\) のデータをテストデータ,残りを学習データとして用い,次は \(n=1\) をテストデータ,残りを学習データとして用い,以下,それを \(n=149\) まで繰り返します(Leave-One-Out法).

import numpy as np
import pandas as pd

def knn(train, test, cl):
    dist = np.sum((test-train)**2, axis=1)  
    argmin = np.argsort(dist)[0]
    return cl[argmin]

df = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data', header=None)

x = df.iloc[:,0:4].values
label = df.iloc[:,4].values

correct = 0;
for n in range(150):
	ind = np.ones(150, dtype=bool)
	ind[n] = False
	
	train = x[ind]
	cl = label[ind]
	test = x[n]
	
	r = knn(train, test, cl)
	if r==label[n]:
		correct += 1

print('correct classification: {}%'.format(100*correct/150))

\(n\) 番目のデータだけ学習データから除くというところが,Python的にはあまりスマートに出来ないようです.ネットで調べてたら見つけた方法を採用.私のオリジナルではありませんが,以下のようなやり方で.

ind = np.ones(150, dtype=bool)
ind[n] = False

先ず,データ数と同じ配列を作成し,その要素をすべてTrueにします.そして,\(n\) 番目だけFalseを代入する.これで

train = x[ind]

とすることで,xのTrueの要素のみを抽出してtrainに代入します(つまり,trainにx[n]を含まない).

さて,このプログラムをファイル名knn.pyとして保存し,コマンドプロンプトから実行すると.

$ python knn.py
correct classification:96.0%
$ 

となり,最近傍法では96%が正しく識別出来ました.次回はK最近傍法に改良したいと思います.