☰
纯Python从零手写KNN分类器:距离度量与K值优化全解析
2026/10/16 7:06:32 网站建设 项目流程

如果你让我给新手推荐一个最值得手写的机器学习算法,我会毫不犹豫地说KNN。这个判断听起来有点反直觉:KNN的调包版本只有几行代码,很多教程里甚至连原理都懒得细讲,直接调现成接口就出结果了。可正因为这样,大多数人对KNN的理解停留在"它能分类"这个层面,一旦被问到"预测一条新样本时,程序到底在机器内部做了什么",就开始含糊了。

这篇文章我就带你用纯Python从零手写一遍KNN分类器,不依赖任何现成的机器学习实现。我们会从最朴素的距离计算开始,一步步写出完整的分类代码,再用二维数据可视化看不同K值带来的边界变化,最后拿真实数据集检验手写版本的效果,并用交叉验证找出合适的K。整个过程不需要你提前掌握多少数学知识,有一点点Python基础就能跟上。等这套流程完整走下来,你对KNN的理解会比单纯调包深刻得多,后续学其他距离类算法也会顺很多。

1. 为什么还要手写KNN:它才是机器学习的起点

1.1 一行代码之外的盲区

KNN的全称是K-Nearest Neighbors,中文一般叫K近邻算法。它的核心思想特别朴素,八个字就能概括:近朱者赤,近墨者黑。一个未知样本的类别,由它在特征空间里距离最近的K个已知样本投票决定。

听起来这么简单,手写好像没什么必要。但如果你仔细观察那些只靠调包学KNN的人,往往会发现几个共同盲区:不知道距离是怎么算的,不知道K到底影响什么,不知道数据不归一化会发生什么,更不知道预测时为什么有的样本置信度高、有的样本模棱两可。这些知识在官方文档里不会明明白白写给你,只有在亲手实现一遍的过程中才会被迫面对。

我自己很早就吃过这个亏。刚开始用现成库的时候,我把K设成了10,跑出一个结果觉得很正常。后来面试被问到一句话:"K=10和二分类的平票问题你怎么处理?"我当时愣了一下。这个问题的答案其实就藏在投票机制里,但因为我一直在用封装好的接口,从未看过底层的计数逻辑,自然就说不清楚。从那天起,我开始坚持手写一切简单的算法,KNN就是第一个。

1.2 手写一次到底能收获什么

手写KNN的过程,实际上是在复刻一个完整分类器的生命周期,它带来的收获至少有三层:

第一层是理解预测的本质。你会发现KNN根本不存在"训练"阶段,fit只不过是把数据集存起来,真正的计算全部发生在predict那一刻。这和神经网络、线性回归这类"训练中调整参数"的模型有本质区别,理解这一点对后续选型非常重要。

第二层是排错能力的提升。当你亲手写过距离计算和投票逻辑,再遇到维度对不上、标签格式不对、特征量纲差异导致结果离谱这类问题,一眼就能看出根因在哪里,因为你亲眼见过数据在每一步是怎么流动的。

第三层是应用直觉。KNN不只是教学玩具,它在商品推荐、图像分类、异常检测里都有实际应用。推荐系统里找相似用户就是"找近邻"的思路,图像分类里比较像素向量的远近也是"距离度量"的思路,异常检测里判断一个点是否远离所有簇中心更是近邻思想的延伸。手写一遍之后,这些场景里的"距离"到底在算什么,你会有一个很清晰的画面。

2. KNN的工作机制:近朱者赤,也要看"多远算近"

2.1 算法执行流程的完整拆解

KNN的完整流程可以拆成四步,每一步都对应一个可以独立成函数的小任务:

  1. 接收训练数据。训练集里包含若干样本,每个样本有若干特征,每个样本都有一个已知标签。
  2. 计算待预测样本到所有训练样本的距离。这是KNN最核心的计算环节,特征空间里的"距离"就是相似度的反面,距离越小代表越相似。
  3. 对所有距离排序,取出最小的K个距离,这K个距离对应的样本就是待预测样本的K个近邻。
  4. 对这K个近邻的标签进行投票,票数最多的类别就是预测结果。如果做回归任务,则改成对K个近邻的数值结果取平均。

用一个很小的例子来说明:假设训练集只有5个样本,特征维度是2,待预测样本是(3, 4),K取3。第一步算出它到5个样本的距离,第二步排序后取前3个,第三步假设这3个近邻的标签是"A、A、B",那么多数票是A,于是预测结果是A。整个过程没有任何数学上的玄机,就是最朴素的"少数服从多数"。

2.2 距离度量:欧氏距离、曼哈顿距离与闵可夫斯基距离

既然KNN完全依赖"距离",那么距离怎么定义就成了算法的灵魂。最常用的是欧氏距离,也就是我们习惯理解的直线距离。二维平面里,点(x₁, y₁)到点(x₂, y₂)的欧氏距离是√((x₁-x₂)² + (y₁-y₂)²),推广到D维就是所有维度差值的平方和再开根号。

第二种常见的是曼哈顿距离,又叫街区距离。它计算的是在网格状街道里从一个路口走到另一个路口需要走过的水平与垂直路程之和,公式是|x₁-x₂| + |y₁-y₂|。曼哈顿距离对异常值没有那么敏感,在高维稀疏场景里有时候比欧氏距离更稳定。

更一般的形式叫闵可夫斯基距离,公式是(Σ|xᵢ-yᵢ|ᵖ)^(1/p)。当p=2时就是欧氏距离,当p=1时就是曼哈顿距离,p越大,距离数值越受"数值较大的那个维度"支配。

在绝大多数KNN入门场景里,特征都是连续数值,欧氏距离就是最合理也最好理解的选择,后面我的手写代码也以它作为默认实现。

2.3 K值选择和投票规则里的门道

K的取值直接决定了模型的敏感程度。K=1时,新样本的类别完全由最近的一个样本决定,这种模型对噪声极其敏感,边界会非常复杂,几乎是在把训练样本背下来。K增大到一定程度,决策边界越来越平滑,泛化能力看起来会变好,但如果K大到接近训练样本总数,投票结果就变成了整个数据集里多数类的"垄断",局部信息彻底丢失。

经验上K可以取√N附近的整数,N是训练样本数,但这只是起点,更可靠的做法是用交叉验证去选。关于交叉验证怎么操作,我在第7章会展开。投票规则也不是只有"一票一票"这种朴素多数表决,还有加权投票——距离更近的邻居权重更大,通常用距离的倒数作为权重,这样既保留了局部性,又缓解了平票问题。第5章我会专门讲到平票和加权投票的处理。

样本编号特征1特征2标签与(3,4)的距离
112A2.83
225A1.41
342B2.24
456B2.83
531A3.00

对(3,4)按距离排序后,前3个近邻是样本2(A)、样本3(B)、样本1(A),两票A一票B,预测结果是A。

3. 用纯Python手写KNN分类器:从零开始的一份代码

3.1 先写一个能跑通的最小实现

这一节我们完全不用第三方库,只用Python自带的标准库来写第一版实现。首先定义欧氏距离函数:

import math def euclidean_distance(x1, x2): return math.sqrt(sum((a - b) ** 2 for a, b in zip(x1, x2)))

这个函数接受两个长度相同的特征向量,逐个维度求差、平方、累加,最后开根号。这里故意用普通列表推导,是为了让你看清每一步的数学过程,等到第6章我们再做性能优化。

接着写找K个近邻的函数:

def get_k_neighbors(X_train, y_train, point, k): distances = [] for x, label in zip(X_train, y_train): dist = euclidean_distance(point, x) distances.append((dist, label)) distances.sort(key=lambda item: item[0]) return distances[:k]

这里把每个训练样本和它的标签打包在一起,按照距离排序后取前K个。注意排序用的是Python内置的sort方法,传入参数key=lambda item: item[0],表示只按元组里的距离值排序,这一点很关键,如果漏掉key参数,元组会先按距离、再按标签来比较,结果也没错,但在标签不可比较时就会抛出异常。

再写投票函数,完成分类预测:

from collections import Counter def predict_one(X_train, y_train, point, k): neighbors = get_k_neighbors(X_train, y_train, point, k) labels = [label for _, label in neighbors] counter = Counter(labels) return counter.most_common(1)[0][0]

Counter是Python标准库里的计数器,拿到标签列表后统计每个标签出现次数,most_common(1)返回出现次数最多的那个标签以及次数,[0][0]取出标签本身。到这里,一个最小的手写KNN核心流程已经完整了。

3.2 封装成类:掌握fit/predict的接口设计思想

能用函数实现之后,更好的习惯是把它封装成类。这样接口设计跟业界主流机器学习库保持一致的fit/predict风格,以后换成其他算法时心智模型是通用的。

import math from collections import Counter class KNNClassifier: def __init__(self, k=5): self.k = k self.X_train = None self.y_train = None def fit(self, X, y): self.X_train = X self.y_train = y return self def _distance(self, x1, x2): return math.sqrt(sum((a - b) ** 2 for a, b in zip(x1, x2))) def _predict_one(self, x): distances = [] for train_x, label in zip(self.X_train, self.y_train): dist = self._distance(x, train_x) distances.append((dist, label)) distances.sort(key=lambda item: item[0]) k_neighbors = distances[:self.k] labels = [label for _, label in k_neighbors] counter = Counter(labels) return counter.most_common(1)[0][0] def predict(self, X): return [self._predict_one(x) for x in X]

细心的人会发现fit里没有任何"学习"动作,只是把训练数据保存下来。这正是KNN和参数化模型最大的区别,它属于基于实例的学习,真正的工作被推迟到了预测阶段。

predict方法内部是一个列表推导,对每条新样本调用_predict_one。这样的代码结构很清晰,以后想换成加权投票、换成曼哈顿距离,都只需要修改对应的小方法,不会牵一发而动全身。

3.3 增加predict_proba:让分类器告诉你"底气"有多少

分类器只输出一个类别标签往往不够用。实际业务里,我们还想知道这个预测有多可信。KNN天然可以给出一种置信度:K个近邻里,属于某个类别的样本占多大比例,这个比例就可以当作近似的概率。

def predict_proba(self, X): probas = [] for x in X: distances = [] for train_x, label in zip(self.X_train, self.y_train): dist = self._distance(x, train_x) distances.append((dist, label)) distances.sort(key=lambda item: item[0]) k_neighbors = distances[:self.k] labels = [label for _, label in k_neighbors] counter = Counter(labels) total = self.k probas.append({label: count / total for label, count in counter.items()}) return probas

比如K=5时预测结果可能是{0: 0.8, 1: 0.2},说明这条样本的邻居里有4个来自类别0,1个来自类别1,模型有八成的"把握"判为类别0。如果两条样本都被预测为0,但一条的概率是0.8、另一条是0.6,那么前者的判断其实更可信。这在业务里很有用,比如只对置信度超过0.7的预测结果采取自动操作,低于阈值则转人工处理。

4. 决策边界可视化:K值的选择直接影响模型表现

4.1 构造一份二维模拟数据

理论讲再多,不如看图片直观。我习惯用Matplotlib构造一份简单的二维模拟数据来观察决策边界。这里生成了两簇高斯分布的点,分别属于类别0和类别1:

import numpy as np import matplotlib.pyplot as plt np.random.seed(42) X0 = np.random.randn(80, 2) + np.array([-1.5, -1.5]) X1 = np.random.randn(80, 2) + np.array([1.5, 1.5]) X = np.vstack([X0, X1]) y = np.array([0] * 80 + [1] * 80) plt.scatter(X[y == 0][:, 0], X[y == 0][:, 1], c='steelblue', label='class 0') plt.scatter(X[y == 1][:, 0], X[y == 1][:, 1], c='orange', label='class 1') plt.legend() plt.show()

两簇点在平面上分布在左下和右上两个区域,肉眼就能看出大部分样本是线性可分的,但重叠区域存在少量交叉。这样的数据足够暴露K=1时边界复杂、K很大时边界过度平滑的问题。

4.2 绘制不同K值下的决策边界

绘制决策边界的思路是在画布上生成密集的网格点,让训练好的模型预测每个网格点的类别,然后把不同类别的区域用颜色填充。KNN不需要重新训练,直接对网格点批量预测即可:

def plot_decision_boundary(model, X, y, k): x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1 y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1 xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200), np.linspace(y_min, y_max, 200)) grid = np.c_[xx.ravel(), yy.ravel()] preds = np.array(model.predict(grid)) preds = preds.reshape(xx.shape) plt.contourf(xx, yy, preds, alpha=0.4, cmap=plt.cm.Paired) plt.scatter(X[y == 0][:, 0], X[y == 0][:, 1], c='steelblue', edgecolors='k') plt.scatter(X[y == 1][:, 0], X[y == 1][:, 1], c='orange', edgecolors='k') plt.title(f'K = {k}') plt.show()

然后分别创建K=1、K=5、K=20的模型,拟合数据并调用上面的绘图函数:

for k in [1, 5, 20]: model = KNNClassifier(k=k) model.fit(X.tolist(), y.tolist()) plot_decision_boundary(model, X, y, k)

4.3 从边界形状看懂过拟合与欠拟合

跑完三张图之后,现象非常直观。K=1时,分类边界蜿蜒曲折,几乎每个训练样本周围都有一个小领地,边界上布满锯齿状突起,看起来"很努力"但明显过度适应了训练数据——这正是过拟合的视觉形态。K=20时,边界被过度平滑,右侧区域里属于类别0的那一小块凸起直接被吞掉了,局部细节全丢,出现了欠拟合倾向。K=5时,边界顺滑度和细节保留达到一个不错的平衡,两类重叠区域的划分也比较符合直觉。

这个实验告诉我们:K实际上是模型复杂度的旋钮。K小,模型灵活但容易捕捉噪声;K大,模型稳健但容易忽略真实结构。实际项目中千万不要拍脑袋定K=5,要像这里一样通过可视化或者交叉验证来观察不同K对预测的影响。

5. 三个影响KNN效果的关键细节:特征缩放、距离选择、平票处理

5.1 特征缩放:为什么单位不统一会让KNN失效

KNN是距离类算法,对特征尺度极度敏感。举个例子,假设有两个特征分别是"年收入"和"年龄",年收入动辄几万到几十万,年龄只有几十。如果直接计算欧氏距离,年龄那个维度即便差异很大,在总距离里也几乎不占权重,相当于这个特征被无声地丢弃了。

解决方案是标准化。最常用的是z-score标准化:

def standardize(X_train, X_val): mean = np.mean(X_train, axis=0) std = np.std(X_train, axis=0) X_train_scaled = (X_train - mean) / std X_val_scaled = (X_val - mean) / std return X_train_scaled, X_val_scaled

这里有一个新手非常容易踩的坑:均值mean和标准差std必须只用训练集计算,然后用同一组统计量去转换验证集和测试集。如果先把训练集和测试集拼在一起算均值,或者用测试集参与了统计量的计算,就相当于让模型在"考试时看到了答案",属于数据泄露,得到的评估结果会虚高。

特征缩放不光让距离计算更合理,还让可视化、调试、模型之间的横向对比都变得更容易。在自己手写KNN的场景里,这是我最先检查的项目,很多"为什么效果这么差"的问题,根子都在这里。

5.2 距离度量要随数据形态而变

欧氏距离不是唯一的答案,它适合那些特征都是稠密连续数值、各个维度重要性相近的数据。但如果你的数据是高维稀疏向量,比如文本经过词袋模型处理后的形式,大量维度都是0,这时欧氏距离容易让"维度灾难"的影响被放大,曼哈顿距离通常更稳定。

还有一种非常常用的度量是余弦相似度,它衡量的是两个向量之间的夹角,而不是直线距离。在文本推荐、用户画像场景里,用户偏好向量往往取决于"方向"而不是"长度",余弦相似度就比欧氏距离更贴合业务语义。手写KNN时可以把距离函数抽出来作为参数传入,这样切换度量方式只需要改一行调用,不用动整个分类器逻辑:

class KNNClassifierFlexible: def __init__(self, k=5, distance_func=euclidean_distance): self.k = k self.distance_func = distance_func def _distance(self, x1, x2): return self.distance_func(x1, x2)

5.3 平票与样本不平衡的处理策略

当K是偶数、类别是二分类时,K个近邻可能出现比例完全对半的情况,这时候投票函数返回的往往是先被Counter遍历到的那个标签,结果带上了偶然性。

处理方案有几种:一是直接把K设为奇数,简单有效,能解决大部分二分类平票问题;二是改用距离加权投票,让离得近的样本拥有更大话语权,平票局面会被"近邻的倾向"打破;三是引入随机打破平票的机制,但随机意味着结果不可复现,我不太推荐。

加权投票的典型做法是用距离的倒数作为权重,距离为0时单独处理:

def _predict_one_weighted(self, x): distances = [] for train_x, label in zip(self.X_train, self.y_train): dist = self._distance(x, train_x) distances.append((dist, label)) distances.sort(key=lambda item: item[0]) k_neighbors = distances[:self.k] votes = {} for dist, label in k_neighbors: weight = 1.0 / (dist + 1e-10) votes[label] = votes.get(label, 0) + weight return max(votes, key=votes.get)

加一个极小值1e-10是为了防止点与某个训练样本完全重合时除数为零。如果你准备把这个分类器用到生产环境,我建议默认就开加权投票,它几乎不会带来额外开销,却能明显缓解平票带来的随机性。

样本不平衡是另一个问题。假设类别0有1000条样本、类别1只有50条,即使新样本周围实际更接近类别1,类别0也可能因为数量多而"霸占"近邻名额。KNN在不平衡数据上天然处于劣势,简单的缓解方式是适当地降采样多数类、或者根据类别的频率对投票做加权,但都没有彻底根治,换用树模型往往效果更好,这一点要心里有数。

6. 手写KNN的效率问题与两种优化思路

6.1 朴素实现的复杂度到底有多高

我们前面写的朴素版本,每预测一条新样本都要计算它到全部N个训练样本的距离。一次距离计算的开销是维度D,预测一条样本的时间复杂度是O(ND),预测M条样本就是O(MND)。

当训练集只有几百条样本时,这个复杂度完全无所谓。可一旦数据量到几十万、维度到几百,朴素实现会慢到让你怀疑人生。我在某次处理用户行为特征时就吃过这个亏,训练数据十几万条,手写KNN预测一轮跑了几分钟,当时真想直接换调包版。所以,手写归手写,到了性能敏感的环节必须知道怎么优化。

6.2 用NumPy向量化改写距离计算

第一种优化思路是向量化。Python的for循环有比较大的解释开销,而NumPy把循环下沉到底层编译好的C代码里,速度能快一个数量级以上。对单个待预测样本,向量化写法如下:

import numpy as np def knn_predict_numpy(X_train, y_train, x_test, k): X_train = np.asarray(X_train) x_test = np.asarray(x_test) diff = X_train - x_test sq_diffs = diff ** 2 distances = np.sqrt(np.sum(sq_diffs, axis=1)) k_idx = np.argsort(distances)[:k] k_labels = y_train[k_idx] counts = np.bincount(k_labels) return np.argmax(counts)

这里的关键是NumPy的广播机制。X_train的形状是(N, D),x_test形状是(D,),相减时NumPy会自动把x_test在每一行上展开,一次性得到(N, D)的差矩阵,随后逐维度平方、求和、开根号得到(N,)的距离向量。np.argsort返回距离从小到大排序后的索引,取前K个对应的标签,再用np.bincount配合np.argmax完成多数投票,前提是标签是整数且从0开始。

如果一次要预测很多条样本,还可以用二维广播一口气算出所有样本两两之间的距离矩阵,不过这样内存消耗会变成O(N·M),样本量大时容易爆内存,实际使用时要根据数据规模权衡。

6.3 KD树是怎么回事:从建树到剪枝搜索

向量化解决的是"常数速度"问题,没有改变"要和所有样本比一遍"的本质。真正从算法层面把复杂度降下来的是空间索引结构,KD树是其中最经典的一种。

KD树做的是一件事:把训练样本按空间位置组织成一棵二叉树。建树时选择方差最大的维度作为切分轴,取该维度上的中位数作为切分点,比中位数小的样本进左子树、大的进右子树,递归建下去。这样整个空间被切分成一块块小区域,搜索一个点的近邻时,只需要沿着树找到目标点所在的叶子区域,再回溯检查兄弟分支是否有更近的邻点。

KD树搜索的关键在于剪枝:回溯过程中,如果当前已经找到的最近距离已经小于"目标点到某个兄弟分支边界面的距离",那么这个分支里不可能存在更近的点了,可以整个跳过。这个剪枝条件让KD树在低维数据下把查询复杂度从O(N)降到O(log N)量级。

但KD树不是银弹。当维度升高后,样本点在空间里变得稀疏,切分空间时会产生大量空区域,剪枝效果急剧退化,最终几乎退化回全量暴力搜索。所以高维场景通常配合近似最近邻搜索来处理,而不是硬上KD树。如果只是教学或者小规模数据,向量化后的暴力搜索就够用了,不一定非要实现一棵KD树。

7. 用真实数据集检验手写KNN:交叉验证选K

7.1 鸢尾花分类:三维特征下的手写KNN表现

模拟数据验证了KNN的基本行为,接下来我觉得有必要跑一个标准数据集来检验手写代码的通用性。这里选用的是机器学习里被用烂了的鸢尾花数据集,150条样本,4个数值特征(花萼长度、花萼宽度、花瓣长度、花瓣宽度),一共3个类别,每个类别50条样本。这个数据本身比较友好,类别之间几乎是线性分离的,KNN在它上面的表现通常都非常好。

流程分三步走:先把数据随机打乱切出训练集和测试集,比如80%训练、20%测试;然后用我们手写的KNNClassifier训练一个K=5模型;最后在测试集上计算准确率。由于这个数据集量纲差异不算巨大,可以先用不缩放版本看看效果,但我强烈建议在真实场景里除了判断特征量纲外一律先做标准化,再对比一下结果。大多数情况下,标准化之后的分类边界更稳定,测试集的准确率也更高。

7.2 手写数字识别:特征必须归一化

第二个数据集是手写数字识别,每条样本是一张小尺寸的灰度手写数字图片,训练时要先把二维像素矩阵展平成一维特征向量。这类数据对KNN来说有两个需要认真对待的地方。

第一是特征数量比较大。比如8x8的图片展平后是64维,稍大一点的28x28图片就是784维。维度上升后,距离计算的耗时显著增加,这时候正好可以用来实践第6章的NumPy向量化优化。

第二是必须做标准化或归一化。虽然像素灰度值本身都在0到255之间,各维度量纲看起来一致,但如果不做归一化,高亮度背景区域那些数值恒定为255的维度,会在欧氏距离里占据极大比重,导致图像前景的形状差异被稀释。常见做法是把每个像素除以255,缩放到[0, 1]区间,或者把整条样本减去均值除以标准差。实践下来,简单除以255就能看到明显的准确率提升。

手写数字数据示例中,K=3或K=5通常能拿到不错的识别率,但用K=1时会出现很多因为笔画粗细差异产生的误判。这类数据天然的"近邻"结构,让KNN能发挥出接近更复杂模型的效果,非常直观地说明了算法本身虽简单,却完全有实战价值。

7.3 用验证集找出最优K:交叉验证的完整流程

最后也是最关键的一步:不要凭空猜K,用验证集去选。做法是把训练集再切出一部分作为验证集,对一组候选K值逐一训练并评估,画出K与验证准确率的曲线,选出最高点对应的K,再拿这个K去测试集上做最终评估。

import random from collections import Counter def evaluate_with_k(X_train, y_train, X_val, y_val, k): model = KNNClassifier(k=k) model.fit(X_train, y_train) predictions = model.predict(X_val) correct = sum(1 for pred, true in zip(predictions, y_val) if pred == true) return correct / len(y_val) k_candidates = [1, 3, 5, 7, 9, 11, 15, 21, 31] val_scores = [] for k in k_candidates: score = evaluate_with_k(X_train_scaled, y_train, X_val_scaled, y_val, k) val_scores.append(score) print(f'K = {k:2d}, validation accuracy = {score:.4f}')

这样选出来的K通常比拍脑袋定的K靠谱很多。如果你希望结果更稳定,可以不用单次切分,改用K折交叉验证:把训练集分成K份,每份轮流做一次验证集,其余做训练集,取平均准确率。这个过程不复杂,但逻辑上需要多写几层循环,我建议先跑通上面的简单版本,再升级成K折交叉验证。

在手写数字识别任务上做过一轮完整实验之后,你会发现一个有趣的现象:K过大的时候验证准确率明显下滑,K=1的时候训练集准确率接近100%但验证集波动很大。这其实和第4章的决策边界可视化完全对应,K在控制模型偏移与方差之间的权衡,这个思路对整个机器学习学习曲线都有帮助。

写在最后的个人体会

手写代码这件事,很多教程都呼吁过,但真正动手的人还是少数。我自己完整写完一遍KNN之后,最大的变化不是"我会写这个算法了",而是再遇到任何距离相关的模型时,都会下意识地去想"这个距离度量对当前业务到底意味着什么"。如果连近邻之间的"近"都没有定义清楚,后面的参数调得再精细也是白搭。

如果你跟着这篇步骤从头到尾把代码敲了一遍,建议把它留在手上多玩几天。可以试着给KNNClassifier增加一个"返回被判为各别的概率"的方法,观察哪些样本的预测置信度特别低,这些样本往往位于类别交界处,是最容易出错的区域;也可以把距离函数换成曼哈顿距离,在相同数据上对比准确率变化;甚至可以挑战自己写一个简单的加权投票版本。把这些小改动都做过一遍之后,你对KNN的理解就真的扎实了,之后去碰更复杂的模型也会顺手很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询