最近鄰居分類(k-nearest neighbors, k-NN)的概念相當簡單:一個資料點在空間中跟哪個類別的點最接近,就說它是屬於那一個類別。這個方法不需要訓練過程,或者說訓練就是把所有資料記起來;而在預測的階段,則是將待測點與其他資料點計算距離,找出最接近的 k 的點,並依據這 k 的點所屬的類別或值進行投票,來做為待測點的預測結果。

我們使用 scikit-learn 的 neighbors.KNeighborsClassifier 來幫我們執行 k-NN 演算法,使用的資料集是 Wine Data Set,該資料集在 scikit-learn 有內建一版,你可以直接透過 import 相關函式來使用,不必自己下載:

import numpy as np
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier

dataset = load_wine()
print('Data shapes:', dataset.data.shape, dataset.target.shape)

X_train, X_test, y_train, y_test = train_test_split(dataset.data, dataset.target)
print('Training data shapes:', X_train.shape, y_train.shape)
print('Test data shapes:', X_test.shape, y_test.shape)

model = KNeighborsClassifier(n_neighbors=1)
model.fit(X_train, y_train)
pred = model.predict(X_test)
print('Accuracy (sklearn): {:.2f}%'.format(100*np.mean(pred==y_test)))

pred = []
for p_te in X_test:
    dists = [np.sum((p_tr - p_te) ** 2) ** 0.5 for p_tr in X_train]
    pred.append(y_train[np.argmin(dists)])

pred = np.array(pred)
print('Accuracy (for loop): {:.2f}%'.format(100*np.mean(pred==y_test)))

在上述範例中,由於 train_test_split 是隨機進行,因此每次執行時,準確度可能會有所變化。此外,範例的下半部同時也示範了自行實作最近鄰居分類的參考方式,其中的 n_neighbors 與前半部同樣為 1,距離則是使用 2-norm。

下面對 KNeighborsClassifier 的部分參數進行一些說明,若需要更多細節,可以參考官方文件。

K-NN 在 k = 1 時,每個點的勢力範圍,會是一個 Voronoi Diagram,其中每一條邊界線,就是相鄰樣本的中垂線,範例如下:

import matplotlib.pyplot as plt
import numpy as np
from scipy.spatial import Voronoi, voronoi_plot_2d

points = np.random.rand(10, 2)
vor = Voronoi(points)
voronoi_plot_2d(vor)
plt.axis('equal')
plt.show()

K-NN 演算法在看待不同維度時是一視同仁的,亦即對各個特徵維度給予相同權重,並不會自動區分哪些維度具有判別力。因此若你的資料中,包含有很多雜訊維度的話,則很有可能讓 k-NN 演算法的準確度變差,稱為「高維詛咒」。以下範例是在固定只有兩個有效維度的狀況下,依次加入愈來愈多雜訊維度的範例:

import matplotlib.pyplot as plt
import numpy as np

from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier

dimensions = [2, 5, 10, 20, 50, 100]
accs_knn = []
accs_another = []

for d in dimensions:
	X, y = make_classification(
		n_samples=2000,
		n_features=d,
		n_informative=2,
		n_redundant=0,
		n_repeated=0,
		n_classes=2,
	)

	X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)

	knn = KNeighborsClassifier(n_neighbors=3)
	knn.fit(X_train, y_train)
	accs_knn.append(knn.score(X_test, y_test))

	clf = LogisticRegression(max_iter=1000)
	clf.fit(X_train, y_train)
	accs_another.append(clf.score(X_test, y_test))

plt.figure()
plt.plot(dimensions, accs_knn, label='KNN')
plt.plot(dimensions, accs_another, label='Another method')
plt.xlabel("# Features")
plt.ylabel("Accuracy")
plt.legend()
plt.show()

在上述範例中,make_classification 函式是用來產生範例資料的,範例中的設定是產生兩類分類問題的資料,有用的維度有兩個,其餘都是雜訊。此外,我們同時使用了另一個叫做 Logistic Regression 的分類器,於本篇中,各位只需要知道並不是每種分類方式都會受到雜訊維度的影響即可。