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の教師あり学習モデルの使い方はほぼ一貫しています。
インスタンスの生成
学習
予測 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_neighbors
がk
に相当します。最低限、これだけを指定すれば動作します。
また、n_jobs
は並列計算をさせたい時に、cpuのコア数を指定するオプションです。None
は並列計算をしない設定です。特にこだわりがなく、並列化したい場合は-1
を指定しましょう。使えるコアをすべて使ってくれるはずです。
最後に、
p=2
とmetric='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.
KNeighborsClassifier(n_neighbors=4)
最後に予測性能をテストデータで評価します。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を実装してください。#
__init__
メソッドにおいて、k
の値が1以上でない場合にエラーを出して下さい。__init__
メソッドの引数k
にint型のtype hintを追加し、デフォルト引数として3が与えられているようにしてください。fit
メソッドにおいて、与えられたX
とy
をそれぞれインスタンス変数self._X
,self._y
として保存してください。fit
メソッドが呼び出されたらself.is_fittedをTrueにして下さい。predict
メソッドに、このメソッドに与えられたテストデータX
のラベルを予測するコードを追加してください。predict
メソッドにおいて、返り値として、X
に対する予測ラベルをnp.ndarray
型で返してください。このとき、X.shape[0] == pred_y.size
になります。predict
メソッドにおいて、引数X
がself._X
と同じ特徴量の次元数を持っているかを確認してください。また、異なっていた場合はエラーを出してください。predict
メソッドが呼び出された際に、事前にfitが実行されていないならエラーを出すようにしてください。score
メソッドに正答率を計算するコードを追加してください。正答率は0から1の範囲で値を取ります。__repr__
メソッドを実装し、eval(repr(ここで実装したクラスのインスタンス))
とした際に、インスタンスを再構成できるようにしてください。__repr__
を除く全てのメソッドにdocstringを追加し、docstringから以下が分かるようにして下さい。なんのためのメソッドなのか(何を実行するメソッドなのか)
どんな引数を受け取るのか
どんな返り値を返すのか
※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の値と正答率の間には、どのような関係がありますか?#
(解答欄)
[終わり]