機械学習_k近傍法_pythonで実装

2017.12.19

概要

こんにちは、データインテグレーション部のyoshimです。
この記事は機械学習アドベントカレンダー19日目のものとなります。

本日は、先日ご紹介した「「k近傍法(k-NN)」を実際にPython(jupyter)で実装してみたのでご紹介します。
今回のゴールは「irisのデータセットを分類して、その結果を可視化する」です。

目次

最初に

今回のエントリでは、sklearnに用意されているデータセットから、各花がどの品種かを分類するモデルを作成します。
(何番煎じなのかわかりませんが...)

手順概要

今回の処理の流れは下記の通りです。
1.データセットの確認 2.前処理 3.モデル作成 4.評価 5.可視化

以下、その処理内容についてもう少し詳細に説明します。

1.データセットの確認

sklearnに用意されているirisのデータを取得し、中身を確認します。
データの詳細については、sklearnをご覧いただきたいのですが、ざっくり説明しますと「3品種の花それぞれに50サンプルずつ、4特徴量を保持した」データです。

2.前処理

標準化と次元削減をしています。
次元削減についてはそもそも4特徴量しかないので不要かとも思ったのですが、「最後に分析結果を可視化する時に2次元だとわかりやすそうだな」と思ったので次元削減をしただけです。

3.モデル作成

モデルを作成する際に、「近隣の何個のデータを参照するか」、「距離に基づく重み付け」について考慮しました。
結果としては、近隣20個のデータを参照、距離に基づく重み付けは無し、という形でモデルを作成しています。

4.評価

モデルの出来を評価します。 具体的には、「混同行列」、「検出率」、「F値」等の指標を計算しました。

5.可視化

せっかくなので分析結果を可視化しました。
正解のデータと分析結果を横に並べて比較してみたところ、「どういうデータを誤分類したのか」をなんとなく把握することができました。
(k-NNらしい結果が可視化できました。)  

コードと解説

では、実際にコードの解説をしていこうと思います。

まずは、「1.データセットの確認」からです。
今回は、sklearnに用意されているデータセットを使います。

'''1.データセットの確認
'''

# 1-1.データセットの取得
from sklearn.datasets import load_iris
iris = load_iris()


# 1-2.データの外観を確認
print(iris.data.shape) # (150, 4) 150個のサンプルが4つの特徴量を保持
print(iris.feature_names) # 特徴量の名前['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
print(iris.target_names) # 品種名['setosa' 'versicolor' 'virginica']
print(iris.data[:1])# どんな感じか確認

# 1-3.各変数ごとの分布を確認
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline

df = pd.DataFrame(data=iris.data, columns=iris.feature_names)
df.plot(kind="kde",subplots=True,layout=(2,2)) # 各特徴量ごとにグラフを分割して表示
plt.show()


# 1-4.4つある変数のうち2つを選んで相関を確認
df['target'] = iris.target # 標準化した後にラベルを付与
sns.pairplot(data=df, vars=iris.feature_names,kind='reg')

結果は下記の通りです。 データが150個、各々4つの特徴量を保持しています。
また、「petal length」、「petal width」は双峰性を確認できました。

また、4つある変数のうち2つを選んで散布図を作ってみたのですが、相関がありそうです。 これなら次元削減しても大丈夫そうですかね...。
(可視化しやすいので次元削減したい)

続いて前処理に移ります。 大枠だけ述べると、「標準化」したのちに「次元削減」で4つあった特徴量を2つに削減しています。

'''2.前処理
特徴量ごとに絶対値の大きさが異なります。単純に距離を求めるとなると、この絶対値の大きさの違いのせいで、うまくいかない可能性があります。
なので、あらかじめ各特徴量の値を「正規化」しておきます。
今回は一般的な正規化手法を使っています。
'''

# 2-1.標準化
from sklearn.preprocessing import StandardScaler 

sc = StandardScaler()
x_std = sc.fit_transform(iris.data)
print(x_std[:3])


# 2-2.次元削減(「lsa」を使って次元削減を行います)
from sklearn.decomposition import TruncatedSVD

# 2-2-1.パラメータの調整
list_n_comp = [1,2,3]
for i in list_n_comp:
    lsa = TruncatedSVD(n_components=i,random_state = 0)
    lsa.fit_transform(x_std) 
    print('次元削減後の特徴量が{0}の時の説明できる分散の割合合計は{1}です'.format(i,round((sum(lsa.explained_variance_ratio_)),2)))

'''
特徴量が4しかないので次元削減は不要な気もしますが、分析結果を二次元で可視化したいので、特徴量を2次元に削減します。
'''
# 2-2-2.次元削減(説明変数を2次元にしました)
lsa = TruncatedSVD(n_components=2,random_state = 0)
x_std = lsa.fit_transform(x_std) 

実行結果は下記の通りです。

標準化することで、それっぽい数字になっていることもなんとなく確認しました。
また、2次元に削減してもそれなりに説明力は保持できそうなので、安心して次元を2次元に落としました。
(あくまでも今回は分析結果の可視化をしたいがために2次元にしていますが、この程度のデータ量なら次元削減する必要もないと思います)

ここまでで、やっとデータの準備ができました。
続いて、「k-NN」のモデルを作成していこうと思います。
「k-NN」モデル作成にあたって今回指定したパラメータは下記の2つです。
・近くの何個のデータを参考にするか
・類似度による重み付け

具体的には、「tuned_parameters」で指定していてる「weights」が類似度による重み付け、「n_neighbors」が近くの何個のデータを参考にするか、です。とりあえずいくつかのパターンを組み合わせた状態で交差検証し、精度を確認しました。

'''3.モデル作成
パラメータ調整
今回調整するパラメータは「重み付けの基準」、「近くの何個のデータを参考にするか」、の2つである。
「重み付けの基準」については「weigths」で、「近くの何個のデータを参考にするか」については「n_neighbors」で指定している。
'''

# 3-1.調整するパラメータの指定
tuned_parameters ={ 'weights': ['uniform','distance'], 'n_neighbors': [3,5,10,20,30,50] }

# 3-2.分類器作成(パラメータ調整用)
from sklearn.grid_search import GridSearchCV
from sklearn.neighbors import KNeighborsClassifier

clf = GridSearchCV(KNeighborsClassifier(), tuned_parameters, scoring="accuracy",cv=5, n_jobs=-1)# 上記で用意したパラメーターごとに交差検証を実施
clf.fit(x_std, iris.target ) # 学習
for params, mean_score, all_scores in clf.grid_scores_:
        print ("{0},精度:{1} ,標準誤差=(+/- {2}) ".format(params, round((mean_score),3), round((all_scores.std() / 2),3))) # 各パラメータごとの精度を確認

# 3-3.モデル作成
clf  = KNeighborsClassifier(n_neighbors = 20, weights ='uniform') #n_neighbors = 20, weights ='uniform' がよさそうです。

確認した結果、「類似度による重み付けはなし」、「近くの20個のデータを参考にする」というパラメータを指定することとしました。

続いて、このモデルの「評価」に移ろうと思います。 評価の指標も色々ありますが、今回はこちらを参考にいくつか集計してみました。

'''4.評価
評価の基準についてはこちらを参考にしました。  
https://pythondatascience.plavox.info/scikit-learn/%E5%88%86%E9%A1%9E%E7%B5%90%E6%9E%9C%E3%81%AE%E3%83%A2%E3%83%87%E3%83%AB%E8%A9%95%E4%BE%A1
'''


# 4-1.混同行列
from sklearn.metrics import confusion_matrix
y_pred = clf.predict(x_std)
print('--混同行列--')
print(confusion_matrix(y_pred, iris.target))


# 4-2.色々
from sklearn.metrics import classification_report
print('--精度、検出率、F値、支持度--')
print(classification_report(iris.target, y_pred, target_names=['setosa' ,'versicolor', 'virginica']))

上記コードの実行結果がこちらです。

この結果の見方についてはこちらを参考にしていただきたいのですが、「setosa」のデータは完璧に分類できているというのがすごいですね。

最後に、折角なので分析結果を可視化してみようと思います。

'''5.可視化
せっかくだし、ちょっと可視化したい
'''
import matplotlib.pyplot as plt
%matplotlib inline

plt.figure(figsize=(10,8))

# 予測
plt.subplot(2,2,1)
plt.scatter(x_std[:,0], x_std[:,1],c = y_pred, s=50)
plt.xlabel('pc1')
plt.xlabel('pc2')
plt.title('predict')

# 正解
plt.subplot(2,2,2)
plt.scatter(x_std[:,0], x_std[:,1],c = iris.target, s=50)
plt.xlabel('pc1')
plt.xlabel('pc2')
plt.title('teacher')


plt.show()

実行結果は下記の通りです。

モデルによる分類と、元のデータに次元削減しただけのものとで比較してみました。 「k-NNの特徴」とも言えるのですが、クラスの境界付近のデータを誤分類しているみたいですね。 また、「setosa」が総て正しく分類されている理由も、この結果を見ると納得できます。

まとめ

今回のエントリーは以上です。
今回はクラス分類をご紹介しましたが、回帰への応用も可能です。

明日は、14日目のエントリーでちょろっと出てきました「lsa(潜在意味解析)」なるものについてご説明する予定です。