第 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模型评估 →