一条新评论,听起来很像 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 运行时,可能需要几秒钟。
练习本课
12 道题,作答后立即看到解释。