第 43 課 · 實戰 · 19 分鐘

Python 最近鄰分類器

看一個點最像哪些鄰居,再用鄰居的標籤分類。

k-NN距離多數投票特徵縮放

一條新評論,聽起來很像 Maya 已經標過的三條舊評論。k-NN 讓最近的鄰居投票。幾個鄰居才算合適,距離又該在什麼尺度上量?

最近鄰分類器通過找出離新點最近的訓練樣本,並對它們的標籤投票來預測。它沒有學習到的權重,卻仍然涉及核心問題:怎樣度量距離、用幾個鄰居,以及為什麼特徵尺度很重要。 你還會比較距離度量,並用留一驗證選擇 k。

度量點之間的距離

歐氏距離是各坐標差平方和的開方。zip 把坐標配對,這個表達式可以處理任意維數。函式要短,距離規則才容易檢查和替換。

def distance(a, b):
    return sum((x - y) ** 2 for x, y in zip(a, b)) ** 0.5

print(distance((1, 1), (4, 5)))

兩個維度,一個公式

給訓練點排序

為每個訓練點計算距離,再按距離排序。儲存 (距離, 標籤) 對可以讓標籤跟隨點移動。小資料集這樣就夠了;大規模集合在生產環境通常使用更快的索引結構。

def distance(a, b):
    return sum((x - y) ** 2 for x, y in zip(a, b)) ** 0.5

points = [((1, 1), "a"), ((4, 4), "b"), ((1, 2), "a")]
target = (1, 1.5)
ranked = sorted(points, key=lambda item: distance(target, item[0]))
print([label for _, label in ranked])

最近的標籤排在前面

進行多數投票

取 k 個鄰居,統計標籤並選擇出現最多的。Counter 讓投票過程更易讀。平票需要明確規則,例如選擇距離最近的平票鄰居,避免結果隨字典順序變化。

from collections import Counter

labels = ["a", "a", "b"]
winner = Counter(labels).most_common(1)[0][0]
print(winner)

出現最多的標籤

度量前先縮放特徵

以千為單位的特徵會壓倒個位數特徵。計算距離前,應對每個特徵做標準化或歸一化。縮放參數只能在訓練資料上配適,再用同樣的變換處理新點。

def distance(a, b):
    return sum((x - y) ** 2 for x, y in zip(a, b)) ** 0.5

print(round(distance((30, 30000), (31, 30000)), 1))
print(round(distance((30, 30000), (30, 31000)), 1))

原始單位不可直接比較

距離度量會改變排序

歐氏距離對坐標差取平方;曼哈頓距離取絕對值之和。它們代表不同的「接近」含義,因此同樣一組點可能得到不同的鄰居順序。選擇與資料比對的度量,並用驗證結果判斷,而不是假定任何單一度量都放之四海皆準。

def euclidean(a, b):
    return sum((x - y) ** 2 for x, y in zip(a, b)) ** 0.5

def manhattan(a, b):
    return sum(abs(x - y) for x, y in zip(a, b))

points = [((0, 0), "a"), ((3, 3), "b"), ((1, 4), "a")]
target = (0, 2)
for name, metric in [("euclidean", euclidean), ("manhattan", manhattan)]:
    ranked = sorted((metric(point, target), label) for point, label in points)
    print(name, [(round(d, 2), label) for d, label in ranked])

同一組點,不同的鄰居

用留一驗證選擇 k

較小的 k 容易追隨雜訊;較大的 k 可能抹平局部結構。留一法每次取出一個訓練點,用其餘點預測它,再重複。先在驗證訊號上比較 k,再接觸最終測試集。

def distance(a, b):
    return sum((x - y) ** 2 for x, y in zip(a, b)) ** 0.5

points = [((0, 0), "a"), ((1, 1), "a"), ((5, 5), "b"), ((6, 5), "b")]

def predict(train, target, k):
    ranked = sorted((distance(point, target), label) for point, label in train)
    labels = [label for _, label in ranked[:k]]
    return max(sorted(set(labels)), key=labels.count)

for k in [1, 3]:
    correct = 0
    for i, (point, label) in enumerate(points):
        train = points[:i] + points[i + 1:]
        correct += predict(train, point, k) == label
    print(k, correct, "/", len(points))

在最終測試前驗證 k

本課要點

  • 最近鄰預測依賴到訓練樣本的距離。
  • 儲存 (距離, 標籤) 對,預測才不會和點分離。
  • k-NN 中的 k 決定參與投票的鄰居數量。
  • 平票需要明確且確定的處理規則。
  • 度量距離前要縮放特徵,否則大單位會佔主導。
  • 距離度量包含假設;k 應通過驗證選擇,而不是只憑直覺。

常見錯誤

  • 特徵單位不同卻不縮放,就直接計算距離。
  • 讓平票結果依賴任意的字典或集合順序。
  • 用訓練集本身評估分類器,卻當成未見資料。
  • 在測試集上調 k,又把同一個分數當作無偏結果報告。
輪到你

給定三個帶標籤的點和目標點,按距離排序,取最近標籤並列印。

動手試試

改改變數、先預測輸出,再執行程式碼。第一次點擊「執行程式碼」時會下載 Python 執行時,可能需要幾秒鐘。

瀏覽器內 Python本機執行 · 無需帳號
首次執行會從 CDN 下載 Pyodide。修改後點擊「執行程式碼」。

練習本課

12 道題,作答後立即看到解釋。

查看 12 道題答案開始練習
← Softmax模型評估 →