KNN 全名 K Nearest Neighbor,與 K-means 同有 K 字樣,然而它們具有不同的任務。K-means 用於一組沒有任何標記的資料,試圖以距離的概念對它們分為 K 群,可以直接用來分群、分析群聚趨勢、壓縮資訊等;KNN 是基於既有的資料,尋找對新進資料來說 K 個距離最近的資料,用鄰居資料來預測新資料的分類或值。
KNN 可以用來預測分類,例如,若有一組資料儲存在 height_waist2.csv,內容為:
171,110,-1
157,90,-1
164,115,-1
182,75,0
160,103,-1
199,68,1
152,103,-1
179,67,1
164,83,0
...
現在打算基於這組資料,看看某個身高、腰圍會被預測為過瘦(1)、適中(0)還是過胖(-1),若是採用 KNN,可以想成將既有資料的身高、腰圍畫為散佈圖,然後將想預測的身高放進去,找出距離該點最接近的 K 個鄰居,看看鄰居中有幾個過胖、幾個過瘦、幾個適中,看哪個票數多,就歸到該類,為了避免票數相同,通常 K 會選擇奇數。
可以這樣分類的原因在於,同樣身高下,腰圍在某個值以下就是過瘦,腰圍在某個值以下過胖,相對地,同樣腰圍下,身高在某個值以上就是過瘦,身高在某個值以下過胖,橫軸或縱軸都具有距離的概念。
從另一個比喻就更能理解了,如果資料具有居住地經緯度、有錢與否,那麼你住的地方附近有錢人多,你有很大的機會可能也是有錢人。
因此使用 KNN 來預測分類,也是代表著同意以距離概念來作為分類的方式,這點與 K-means 的假設是相同的,就作用而言,K-means 是沒有標記下試圖對既有資料分群,而採用 KNN 是有標記的情況下,試著預測新資料的分類。
另一個區別 K-means 與 KNN 的方式是,K-means 的資料會受到群心資料的影響,而 KNN 的資料是受到鄰居資料的影響;用另一種方式來比喻的話,K-means 中的人會受到群體的中心靈魂人物影響,KNN 中的人密集接觸者影響,所謂近朱者赤、近墨者黑。
sklearn 提供了 sklearn.neighbors.KNeighborsClassifier
,來看看怎麼使用:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
data = np.loadtxt('height_waist2.csv', delimiter=',')
height_waist = data[:,0:2]
label = data[:,2]
# 切分訓練與測試資料
height_waist_training, height_waist_test, lb_training, lb_test = train_test_split(
height_waist, label, stratify = label, random_state = 1
)
height = height_waist_training[:,0]
waist = height_waist_training[:,1]
normal_weight = lb_training == 0
overweight = lb_training == -1
rundown_weight = lb_training == 1
plt.xlabel('height')
plt.ylabel('waist')
plt.gca().set_aspect(1)
# 畫出訓練用資料
plt.scatter(height_waist_training[rundown_weight, 0], height_waist_training[rundown_weight, 1], marker = 'x')
plt.scatter(height_waist_training[normal_weight, 0], height_waist_training[normal_weight, 1], marker = 'o')
plt.scatter(height_waist_training[overweight, 0], height_waist_training[overweight, 1], marker = '^')
# KNN 預測分類
knn = KNeighborsClassifier(n_neighbors = 5)
knn.fit(height_waist_training, lb_training)
predicted = knn.predict(height_waist_test)
normal_weight = predicted == 0
overweight = predicted == -1
rundown_weight = predicted == 1
# 畫出測試用資料
plt.scatter(height_waist_test[rundown_weight, 0], height_waist_test[rundown_weight, 1], marker = 'x', c = 'red')
plt.scatter(height_waist_test[normal_weight, 0], height_waist_test[normal_weight, 1], marker = 'o', c = 'red')
plt.scatter(height_waist_test[overweight, 0], height_waist_test[overweight, 1], marker = '^', c = 'red')
# 評分
plt.text(150, 118,
'score: ' + str(knn.score(height_waist_test, lb_test)))
plt.show()
這會顯示以下的圖案,紅色散點表示預測的分類:
那麼 K 該怎麼選擇呢?通常透過交叉驗證的方式,看看不同的 K 值下,驗證的得分比較高。
不過 K 選得大,表示鄰居範圍大,太大的 K 值容易產生偏差,像是明明比較靠某幾個鄰居,偏偏那一類鄰居少,由於範圍大卻令遠的鄰居數投票多,被硬是歸到遠的鄰居那邊;K 值選得少,又可能發生過度擬合,邊界處會因為特定鄰居而使得分類複雜化了。
KNN 的概念也可以用於計算迴歸,在這個時候,最接近鄰居的計算方式,會使用原資料降一維,例如,原資料若為三維 (x, y, z) 形式,現在有一個新資料 (xn, yn),想以 KNN 來預測它的 z 值,方式可以是計算原資料中 (x, y) 與 (xn, yn) 的距離,找出 K 個距離最小的鄰居,求其 z 的平均值。
sklearn 提供了 sklearn.neighbors.KNeighborsRegressor
,來看看怎麼使用:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsRegressor
def points(start, end, step, noise, f):
n = (end - start) // step
x = np.arange(start, end, step) + np.random.rand(n) * noise
y = np.arange(start, end, step) + np.random.rand(n) * noise
z = f(x, y) + np.random.rand(n) * noise
return np.dstack((x, y, z))[0]
# 用來產生資料的平面函式
def f(x, y):
return 2 * x + y + 10
# 資料來源
data = points(0, 300, 1, 200, f)
xy = data[:,0:2] # 包含 [x, y] 的清單
z = data[:,2] # 包含 z 的清單
# 切分訓練與測試資料
xy_training, xy_test, z_training, z_test = train_test_split(
xy, z, random_state = 1
)
ax = plt.axes(projection='3d')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.set_box_aspect((1, 1, 1))
# 畫出訓練用資料
ax.scatter(xy_training[:,0], xy_training[:,1], z_training)
# KNN 迴歸
knn = KNeighborsRegressor(n_neighbors = 5)
knn.fit(xy_training, z_training)
predicted = knn.predict(xy_test)
# 畫出測試用資料
ax.scatter(xy_test[:,0], xy_test[:,1], predicted)
# 評分
ax.text(150, 118, 2000,
'score: ' + str(knn.score(xy_test, z_test)))
plt.show()
來看看執行結果,其中橘點是預測結果: