k近傍法(k-nearest neighbor algorithm, k-NN)の実装#

k-NNのScikit-Learn実装を使った演習#

パッケージの用意とデータの確認#

k-NNをsklearnを使って試してみましょう。

インポートするパッケージは以下の通りです。(matplotlibとplotlyをimportしていますが、どちらか得意な方を利用すれば問題ありません)

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import plotly.express as px
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
from sklearn.neighbors import KNeighborsClassifier as KNN

さて、まずはデータを読み込みましょう。

iris = load_iris()
type(iris)
sklearn.utils._bunch.Bunch

このsklearn.utils.Bunchはdict型に近いデータ構造を持ったクラスです。keyの一覧を見てみましょう。

iris.keys()
dict_keys(['data', 'target', 'frame', 'target_names', 'DESCR', 'feature_names', 'filename', 'data_module'])

この内、dataには説明変数に相当する特徴量がnp.ndarrayとして対応しています。
また、feature_namesは各特徴量の名前です。DataFrameにして表示してみます。

iris_df = pd.DataFrame(data=iris.data, columns=iris.feature_names)
iris_df["label"] = iris.target

iris_df.head(20)
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) label
0 5.1 3.5 1.4 0.2 0
1 4.9 3.0 1.4 0.2 0
2 4.7 3.2 1.3 0.2 0
3 4.6 3.1 1.5 0.2 0
4 5.0 3.6 1.4 0.2 0
5 5.4 3.9 1.7 0.4 0
6 4.6 3.4 1.4 0.3 0
7 5.0 3.4 1.5 0.2 0
8 4.4 2.9 1.4 0.2 0
9 4.9 3.1 1.5 0.1 0
10 5.4 3.7 1.5 0.2 0
11 4.8 3.4 1.6 0.2 0
12 4.8 3.0 1.4 0.1 0
13 4.3 3.0 1.1 0.1 0
14 5.8 4.0 1.2 0.2 0
15 5.7 4.4 1.5 0.4 0
16 5.4 3.9 1.3 0.4 0
17 5.1 3.5 1.4 0.3 0
18 5.7 3.8 1.7 0.3 0
19 5.1 3.8 1.5 0.3 0
iris_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 150 entries, 0 to 149
Data columns (total 5 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   sepal length (cm)  150 non-null    float64
 1   sepal width (cm)   150 non-null    float64
 2   petal length (cm)  150 non-null    float64
 3   petal width (cm)   150 non-null    float64
 4   label              150 non-null    int64  
dtypes: float64(4), int64(1)
memory usage: 6.0 KB
iris_df.describe()
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) label
count 150.000000 150.000000 150.000000 150.000000 150.000000
mean 5.843333 3.057333 3.758000 1.199333 1.000000
std 0.828066 0.435866 1.765298 0.762238 0.819232
min 4.300000 2.000000 1.000000 0.100000 0.000000
25% 5.100000 2.800000 1.600000 0.300000 0.000000
50% 5.800000 3.000000 4.350000 1.300000 1.000000
75% 6.400000 3.300000 5.100000 1.800000 2.000000
max 7.900000 4.400000 6.900000 2.500000 2.000000

このDataFrameを自分で操作して、データがどのような形なのかを確認してください。
データは全部で150個、3クラス。1クラス50個のデータがあります。

データセットの各特徴が何を表しているかについては,以下の画像を参考にしてください.

ここでは、全体の30%をテストデータとして利用します。また、教師データとテストデータで各クラスに偏りが無いように、stratifyという引数を利用していることに注意してください。 stratifyを使わない場合、完全なランダムサンプリングで教師とテストを分割します。
これに対してstratifyに正解ラベルを指定すると、クラスに属するデータの偏りを母集団の分布と同じようにサンプリングしてくれます。これを**層化抽出法(stratified sampling, 層化サンプリング)**と呼びます。

※ 関数の使い方が分からない場合は、コードセル上で

train_test_split?

のように?をつけて実行してみてください。docstringに書かれた説明が表示されるはずです。

X_train,X_test, y_train, y_test = train_test_split(iris.data, iris.target, # 分割したいデータを列挙
                                                   test_size=0.3, # テストデータの割合
                                                   stratify=iris.target, # 層化サンプリングの指針になるlabelを指定
                                                   random_state=2022 # 乱数シードの設定
                                                  )

上で登場した変数はそれぞれ以下のような意味になります。

  • X_train: 教師データ

  • X_test: テストデータ

  • y_train: 教師ラベル

  • y_test: テストラベル

これらはおそらく、一般的な名前の付け方だと思うので、覚えておくと良いでしょう。

sklearnによる実験#

それではsklearnのk-NNを利用してみましょう。

sklearnの教師あり学習モデルの使い方はほぼ一貫しています。

  1. インスタンスの生成

  2. 学習

  3. 予測 or スコアの算出

この流れにそって、実験を行います。

KNNクラスの使い方を知るために、?を使ってマニュアルを読んでみましょう。

classifier = KNN?

すると以下のような表示が出てきます。

Init signature:
KNN(
    n_neighbors=5,
    *,
    weights='uniform',
    algorithm='auto',
    leaf_size=30,
    p=2,
    metric='minkowski',
    metric_params=None,
    n_jobs=None,
)

ここでn_neighborskに相当します。最低限、これだけを指定すれば動作します。
また、n_jobsは並列計算をさせたい時に、cpuのコア数を指定するオプションです。Noneは並列計算をしない設定です。特にこだわりがなく、並列化したい場合は-1を指定しましょう。使えるコアをすべて使ってくれるはずです。

最後に、 p=2metric='minkowski'はそれぞれ、距離計算に利用する距離自体を指定するオプションです。デフォルトのまま利用すれば、ユークリッド距離を利用してくれます。※ ミンコフスキー距離(minkowski)

k=4で実験をしてみましょう。

# 1. インスタンスの生成
classifier = KNN(n_neighbors=4)

次に、学習ステップです。fitメソッドを利用します。

classifier.fit?
# 2. 学習

classifier.fit(X_train,y_train)
KNeighborsClassifier(n_neighbors=4)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

最後に予測性能をテストデータで評価します。scoreメソッドを利用します。

classifier.score?
classifier.score(X_test, y_test)
0.8666666666666667

0.86… の正答率が確認できれば、これで完了です。

k-NNのNumpyを利用した実装#

このセクションではk-NNをNumPyを使って実装してもらいます。

k-NNクラスのヒント#

k-NNのクラスの雛形を示します。


import numpy as np
import scipy.stats as stats

class KNearestNeighbor():
    def __init__(self, k):
        self.k = k
        self.is_fitted = False
        
    def fit(self, X, y):
        ...
        # 教師データのXとyをこのインスタンスの変数として保存して、predictで使えるようにする
        return self
    
    def predict(self, X)->np.ndarray:
        ...
        # 1. 以下の2,3を全てのXに対して行う
        # 2. x(x \in X)と教師データの距離を計算する
        # 3. 距離の近いself.k個のデータのラベルの中で、最も出現したものをxのラベルとする
        return pred_y
    
    def score(self, X,y)->float:
        ...
        # Xに対しての予測ラベルpred_yと正解ラベルyとの比較をして、等しいラベルを持っていた数を数える
        # score = 等しいラベルの数 / データの総数
        return score
    
    def __repr__(self):
        return ...

predictの所をもう少し細かく書くと以下のようになります。

pred_y = []
for x in テストデータ:
    xと教師データとの距離を計算し距離が近い順にk個のラベルを得る
    k個のラベルの中で最も出現頻度の高いラベルをxのラベルに採用する
    pred_y.append(xのラベル)
pred_yを教師ラベルと同じ型にキャスト変換してreturnする

これらを参考に、以下の実装課題をやってみてください。

[基礎]以下の要件を満たす用に、KNearestNeighborクラスを修正してk-nnを実装してください。#

  1. __init__メソッドにおいて、kの値が1以上でない場合にエラーを出して下さい。

  2. __init__メソッドの引数kにint型のtype hintを追加し、デフォルト引数として3が与えられているようにしてください。

  3. fitメソッドにおいて、与えられたXyをそれぞれインスタンス変数self._X, self._yとして保存してください。

  4. fitメソッドが呼び出されたらself.is_fittedをTrueにして下さい。

  5. predictメソッドに、このメソッドに与えられたテストデータXのラベルを予測するコードを追加してください。

  6. predictメソッドにおいて、返り値として、Xに対する予測ラベルをnp.ndarray型で返してください。このとき、X.shape[0] == pred_y.sizeになります。

  7. predictメソッドにおいて、引数Xself._Xと同じ特徴量の次元数を持っているかを確認してください。また、異なっていた場合はエラーを出してください。

  8. predictメソッドが呼び出された際に、事前にfitが実行されていないならエラーを出すようにしてください。

  9. scoreメソッドに正答率を計算するコードを追加してください。正答率は0から1の範囲で値を取ります。

  10. __repr__メソッドを実装し、eval(repr(ここで実装したクラスのインスタンス))とした際に、インスタンスを再構成できるようにしてください。

  11. __repr__を除く全てのメソッドにdocstringを追加し、docstringから以下が分かるようにして下さい。

    1. なんのためのメソッドなのか(何を実行するメソッドなのか)

    2. どんな引数を受け取るのか

    3. どんな返り値を返すのか

※k-nnクラスは以下のセルにまとめて実装してください。

# クラスを実装するセル

[発展]KNearestNeighborクラスを修正し、距離尺度としてユークリッド距離(euclid)、コサイン距離(cos)を選択できるようにしなさい。また、動作することを確認してください。#

  • __init__の引数に metricを追加します。

  • metricにeuclid, cosなどの文字列が与えられた場合、それぞれを距離尺度として利用します。

  • metricに上記2つ以外の文字列が与えられた場合は、NotImplementedErrorを返してください。

# クラスを実装するセル

実験#

[基礎]sklearnを使った実験で用いたX_train,X_test,y_train, y_testを使って、k=4の際の正答率を表示する実験を行ってください。#

# 正答率を計算するセル

[基礎]Kの値を1~10の範囲で変化させて、正答率を折れ線グラフにしなさい。#

# kと正答率の関係をプロットするセル

[発展]kの値と正答率の間には、どのような関係がありますか?#


(解答欄)


[終わり]