(Day 8) K-近鄰 (K-Nearest Neighbors)
K-近鄰 (K-Nearest Neighbors; KNN) 是一種很直學的機器學習演算法。它沒有模型參數、沒有訓練過程,卻可以在某些任務上有不錯的效果。它的核心理念只有一句話: 「你是誰,由你周圍最像你的人決定。」
K-近鄰的預測邏輯其實就是投票機制。當一筆新資料進來時,K-近鄰會計算它與訓練集中每一筆資料的距離,選出最近的 K 筆,根據這些鄰居的標籤來進行分類或回歸。
舉個例子,如果你住進一個新的社區,而這個社區 5 戶人家中有 4 戶都是教師,那麼你很可能也被視為教師。這就是K-近鄰的基本邏輯:用「距離」定義相似度,用「投票」進行預測。
- 無需訓練、實作簡單
- 可處理多類別分類問題
- 非常適合 baseline 模型或少量資料的場景
模型介紹
模型邏輯與核心概念
運作原理
- 定義距離度量: 最常見的是歐幾里得距離。
- 標準化資料: 避免不同特徵尺度影響距離計算。
- 選擇 K 值: K 值太小容易過擬合,太大容易欠擬合。
- 查找最近鄰: 找出距離最近的 K 筆資料。
- 分類或回歸: 分類就多數決,回歸就取平均。
模型評估指標
- Accuracy: 整體正確率
- Precision / Recall / F1-score: 評估正例預測品質與召回
適用情境
• 資料量不大、特徵數量低的任務 • 資料本身具備明顯群聚性質 • 需要快速做出初步 baseline 的時候
限制條件
• 計算成本高 (尤其資料量大時) • 對資料標準化非常敏感 • 高維度下效果會大幅下降 (維度災難)
模型實作
這個 K-近鄰的案例,我們來聊聊簡單的操參數實驗,我們先準備一組資料,這個過程就不過多敘述。
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, classification_report
from sklearn.model_selection import cross_val_score
# 資料產生
X, y = make_classification(
n_samples=1000, n_features=2, n_informative=2, n_redundant=0,
n_clusters_per_class=1, class_sep=1.2, random_state=42
)
# 資料分割與標準化
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
scaler = StandardScaler()
X_train_std = scaler.fit_transform(X_train)
在建模的部分就跟之前不一樣,而是在外層寫了一個迴圈,因為 K-近鄰的 K 值,沒有人知道要用多少,K=1 表示我只抓最近的一個來比,完全就沒有那種投票的概念,所以 k 不應該選 1,再來是怕有平票的問題所以 k 會以奇數為主,而且 k 如果太小會有個問題,容易過擬合,越小越準,那怎麼辦? 所以這邊搭配了 Cross Validation 做設計,可以避免這個問題 (Cross Validation 請讀者自行找資源學習)。
for k in range(1, 21):
knn = KNeighborsClassifier(n_neighbors=k)
scores = cross_val_score(knn, X_train_std, y_train, cv=5)
print(f"K={k}, Mean Accuracy={scores.mean():.3f}")
我們來看看結果,我們發現 0.971 是最高分,分別有 [k=3, k=4, k=8, k=9],這邊就把 [k=3, k=4] 直接排除,因為如前面所述,k 越小容易過擬合,這篇的重點在於 [k=8, k=9] 怎麼選? 這個沒有一定的答案,提供以下思考方式:
- 選較小的 k (bias-variance trade-off): 在 KNN 中,k 較小會有較低的 bias (更貼近真實分布),k 較大雖穩定,但會造成過度平滑、損失局部結構。
- 選較大的 k (較穩定、抗雜訊): 若你懷疑資料有噪音或小樣本異常,較大的 k 能抵消這些點的干擾。
我個人會選 k=8,因為我認為模型終就是要被沒看過的資料所考驗,他的泛化度會比較高,根據實驗的流程,我們不能先用 k=8 跑跑看 test data,來比較哪個好,這邊就決定用 k=8 為例。
K=1, Mean Accuracy=0.946
K=2, Mean Accuracy=0.958
K=3, Mean Accuracy=0.971
K=4, Mean Accuracy=0.971
K=5, Mean Accuracy=0.970
K=6, Mean Accuracy=0.969
K=7, Mean Accuracy=0.964
K=8, Mean Accuracy=0.971
K=9, Mean Accuracy=0.971
K=10, Mean Accuracy=0.963
K=11, Mean Accuracy=0.961
K=12, Mean Accuracy=0.964
K=13, Mean Accuracy=0.961
K=14, Mean Accuracy=0.960
K=15, Mean Accuracy=0.960
K=16, Mean Accuracy=0.961
K=17, Mean Accuracy=0.965
K=18, Mean Accuracy=0.963
K=19, Mean Accuracy=0.964
K=20, Mean Accuracy=0.963
接下來就是實際建模。
# 建模
knn = KNeighborsClassifier(n_neighbors=8)
knn.fit(X_train_std, y_train)
# 預測與評估
X_test_std = scaler.transform(X_test)
y_pred = knn.predict(X_test_std)
print(f"[Accuracy] {accuracy_score(y_test, y_pred):.2f}")
print(classification_report(y_test, y_pred))
程式實例
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, classification_report
from sklearn.model_selection import cross_val_score
# 資料產生
X, y = make_classification(
n_samples=1000, n_features=2, n_informative=2, n_redundant=0,
n_clusters_per_class=1, class_sep=1.2, random_state=42
)
# 資料分割與標準化
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
scaler = StandardScaler()
X_train_std = scaler.fit_transform(X_train)
# for k in range(1, 21):
# knn = KNeighborsClassifier(n_neighbors=k)
# scores = cross_val_score(knn, X_train_std, y_train, cv=5)
# print(f"K={k}, Mean Accuracy={scores.mean():.3f}")
# 建模
knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train_std, y_train)
# 預測與評估
X_test_std = scaler.transform(X_test)
y_pred = knn.predict(X_test_std)
print(f"[Accuracy] {accuracy_score(y_test, y_pred):.2f}")
print(classification_report(y_test, y_pred))
# 建立 mesh grid
h = .02
x_min, x_max = X_train_std[:, 0].min() - 1, X_train_std[:, 0].max() + 1
y_min, y_max = X_train_std[:, 1].min() - 1, X_train_std[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))
Z = knn.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# 畫圖
plt.figure(figsize=(10, 6))
plt.contourf(xx, yy, Z, alpha=0.4)
sns.scatterplot(x=X_train_std[:, 0], y=X_train_std[:, 1], hue=y_train, palette="Set1", edgecolor="k")
plt.title("KNN Decision Boundary (K=3)")
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.show()
執行結果
[Accuracy] 0.96
precision recall f1-score support
0 0.95 0.98 0.97 104
1 0.98 0.95 0.96 96
accuracy 0.96 200
macro avg 0.97 0.96 0.96 200
weighted avg 0.97 0.96 0.96 200
結果對比
這個例我們來看看視覺化的結果,直接對比 k=3 與 k=8
分析面向 | K = 3 決策邊界圖 | K = 8 決策邊界圖 |
---|---|---|
邊界形狀 | 明顯更鋸齒、局部扭曲 | 相對平滑、整體輪廓自然 |
邊界敏感度 | 對個別樣本非常敏感 (high variance) | 對雜訊較不敏感 (variance 較低) |
分類細節 | 可以辨識右下角少數藍點區塊 (但可能是雜訊) | 將右下角混合區一併歸類為紅色 (更保守) |
過擬合風險 | 高,容易過度依賴局部特徵 | 低,較能捕捉整體結構 |
模型泛化能力 | 較差,容易被 noise 誘導 | 較好,能穩定分類未見樣本 |
# 建立 mesh grid
h = .02
x_min, x_max = X_train_std[:, 0].min() - 1, X_train_std[:, 0].max() + 1
y_min, y_max = X_train_std[:, 1].min() - 1, X_train_std[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))
Z = knn.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# 畫圖
plt.figure(figsize=(10, 6))
plt.contourf(xx, yy, Z, alpha=0.4)
sns.scatterplot(x=X_train_std[:, 0], y=X_train_std[:, 1], hue=y_train, palette="Set1", edgecolor="k")
plt.title("KNN Decision Boundary (K=8)")
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.show()
結語
K-近鄰是一個極度直覺的模型,它不試圖去「學習」任何東西,而是直接在推論時靠近鄰居來做決策。這種「懶惰學習」(Lazy Learning) 雖然簡單,但在某些場景中卻效果不俗。但 K-近鄰不適合大規模資料,也不適合高維度任務,因為這個算法不聰明,但在很多情況下,是種很務實的解法,所以實務上多用作 baseline 模型或特定類型問題的輔助工具。