(Day 8) K-近鄰 (K-Nearest Neighbors)

(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 誘導 較好,能穩定分類未見樣本

Image_2025-08-06_20-55-05.png

Image_2025-08-06_20-57-09.png

# 建立 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 模型或特定類型問題的輔助工具。

備註