项目标题里的“kNN算法”和“sklearn鸢尾花数据集实战”是我带新人时最常被点名要讲的一课。原因很实在kNNk-Nearest Neighborsk近邻几乎是把“机器学习”这四个字拆开摊在你面前的算法没有损失函数要推、没有梯度下降要调、没有迭代轮次要等理解成本极低但真拿sklearn在鸢尾花数据集上跑起来你会发现坑一个不少——标准化做不做、k取多少、交叉验证怎么切、准确率飙到1.0到底该高兴还是该警惕。这篇内容面向三类人刚接触机器学习想找个能跑的入门项目、写过一阵子代码但对kNN只有“调包印象”、以及需要给团队或课程准备一份可复现实战材料的人。我下面会从思路拆解、核心参数、完整代码到踩坑排查一条线走完代码都能直接复制运行参数选择过程和实测结果也一并交代清楚。1. 先想明白为什么第一个实战项目选kNN加鸢尾花1.1 kNN本质是“物以类聚”的投票机制我一般这么跟人解释kNN你搬进一个新小区想判断这栋楼里的人是早起型还是夜猫型最直接的办法不是去统计整座城市而是敲开离你最近的几户人家的门看看他们几点拉窗帘。近邻的多数意见就是你的标签。kNN干的事一模一样——一个新样本进来算法计算它到训练集里每一个样本的距离挑出距离最近的k个然后让这k个邻居“投票”决定它属于哪一类。这个类比里藏着三个关键量正好对应后面要调的三个东西。第一是“距离怎么量”决定了“离你近”到底是什么意思是直线距离还是沿街区走的距离这就是距离度量的选择。第二是“敲几户人家的门”也就是k值敲一户容易被噪音带偏敲二十户又把整栋楼的共性糊上来了。第三是“投票是否等权”离你三米的邻居和离你三十米的邻居意见分量显然不该一样这就是weights参数。理解了这三点你就会明白kNN的两个天然特征。它没有显式的训练过程所谓“训练”只是把数据存下来真正的计算全部发生在预测阶段所以业界管它叫懒惰学习lazy learning。好处是模型更新成本几乎为零坏处是预测时要跟全部训练样本算一遍距离数据量一大就非常吃资源和时间。这个特性决定了它在实际业务里的位置小数据、低维度、需要快速验证基线效果的场景它是把好用的“尺子”上了百万样本、几十维特征的场景就得请它退场了。1.2 鸢尾花数据集为什么是kNN的“天选陪练”鸢尾花数据集Iris在sklearn里是内置的一行代码就能加载共150条样本、4个特征花萼长度、花萼宽度、花瓣长度、花瓣宽度单位都是厘米标签是三个品种。这个数据集有几个特别适合入门的属性我在实战中体会很深。样本量小到能“看见”。150条数据你甚至可以手动排序看看分布画个散点图就能直观感受到类别之间的边界在哪里。不像动辄几十万行的业务数据跑完了你也不知道模型到底学到了什么。特征维度低。4维意味着不用做降维、不用怕维度灾难距离计算的意义也直白。类别可分性好但又不是完全线性可分。花瓣长度和花瓣宽度这两个特征几乎能靠一条直线把其中一个品种分出来剩下两个品种有轻微重叠——这个“轻微重叠”太重要了它让准确率不会永远停在1.0你能真实看到k值变化带来的波动。数据集干净。没有缺失值、没有异常到离谱的记录、类别均衡每类50条你不用在数据清洗上耗费精力可以专注在算法本身。我实测过很多次用鸢尾花做二分类划分时准确率常年稳定在1.0附近做三分类时合理区间在0.93到1.0之间浮动。这个区间正好既给你正反馈又留出观察参数影响的窗口。1.3 先用sklearn跑通再手写一版对照有人会觉得既然要学算法就该从零手写。我不反对但顺序要讲究。我的建议永远是先用sklearn把完整流程跑通——数据加载、划分、标准化、训练、预测、评估一口气走完先建立“一个机器学习项目长什么样”的肌肉记忆再回头手写核心部分去理解内部机制。理由很实际。手写kNN的代码量其实不大核心就是一个距离矩阵加一个排序取top-k但它会把你卡在实现的细枝末节上反而看不清整个流程的骨架。而sklearn提供的不只是一个KNeighborsClassifier类它背后是一整套设计范式统一的fit/predict/predict_proba接口、可组合的Pipeline、与交叉验证和网格搜索无缝衔接。这套范式你以后换成逻辑回归、随机森林、支持向量机用法完全一致迁移成本为零。先把范式吃透收益远大于先啃实现。后面的第5节我会给出手写版用向量化方式实现跑完你会发现两者结果能对上那时候你对kNN的理解才算闭环。2. 参数与距离度量把“近”这个字定义清楚2.1 距离度量的选择逻辑与计算过程sklearn的KNeighborsClassifier默认用闵可夫斯基距离minkowski而它的默认p值为2也就等价于欧氏距离。这个默认值对鸢尾花这种连续型、量纲统一都是厘米的特征是合适的但你需要知道背后发生了什么。欧氏距离的计算是各维度差值的平方和再开根号。拿两条鸢尾花样本举例样本A的花瓣长度为1.4、花瓣宽度为0.2样本B为4.5、1.5那么这两个特征维度上的距离贡献是根号下(4.5-1.4)² (1.5-0.2)² 根号下(9.61 1.69) 根号下11.3 ≈ 3.36。四个特征都这么算完再汇总。这个公式的几何含义是空间中两点的直线距离直观、可导、计算简单是绝大多数场景的默认答案。但有些场景要换。曼哈顿距离p1是各维度差值绝对值之和走的是“街区路线”对异常值的敏感度低于欧氏距离——因为平方会放大大的差值。如果你的数据里有少量离群点曼哈顿距离往往更稳。切比雪夫距离p无穷大取各维度差值的最大值适合“最差维度决定一切”的场景。这里有个容易被忽略的点闵可夫斯基距离公式里各维度是直接相加的。如果两个特征量纲差得远——比如一个特征是年收入几万到几十万另一个是年龄十几到几十那收入维度的差值会彻底压过年龄维度距离计算等于只看了收入。这就是为什么在算距离之前必须做标准化第3.2节会详细展开。距离类型metric参数p值计算方式适用场景欧氏距离minkowski2差值平方和开根号连续特征、量纲统一、默认首选曼哈顿距离minkowski1差值绝对值求和存在离群点、高维稀疏数据切比雪夫距离chebyshev—各维度差值取最大关注单维度最坏情况余弦距离cosine—向量夹角余弦文本、方向性特征、不关心绝对大小2.2 n_neighbors的取值k值的甜点区在哪k值是这个算法名字里唯一带参数的字母它的影响非常直观。k1时新样本的标签完全由最近那一个样本决定决策边界极度曲折训练集上准确率必然是1.0——因为每个训练样本的最近邻就是它自己用留一法评估时略有不同。这种模型对噪声毫无抵抗力一个标错的样本就能在它周围划出一块错误的领地。k取很大时比如k100在150条样本的训练集里每个预测都要看三分之二的样本投票结果趋近于全局类别分布模型退化成“多数类预测器”边界被抹平成一条线欠拟合。那怎么挑两条经验。第一条k取奇数在二分类场景下避免平票。三分类场景下平票的概率低一些但仍建议优先奇数。第二条k的经验初值取训练样本数的平方根。150条样本训练集按8:2划分后约120条根号120约等于11那就从k11附近开始试。这不是铁律但是个很合理的起点。我实测下来鸢尾花三分类任务里k从1到20扫一遍准确率通常在k3到k9之间达到最高往两边都会掉。掉得不剧烈因为数据本身干净但趋势能看出来。这个“扫一遍看趋势”的动作比背任何推荐值都有用代码我在3.4节会给出。还有一个细节样本量小时k增大会明显改变预测结果的稳定性。你可以用predict_proba看概率值k3时很多样本的类别概率是0.67两票对一票说明决策很勉强k9时概率会变得更平滑多数样本在0.8以上模型的“信心”更足。概率分布比单一标签包含的信息多得多实际业务里我会优先看这个。2.3 weights、algorithm、metric的实用取舍weights这个参数决定邻居的投票权重。默认是uniform所有邻居等权。改成distance时权重是距离的倒数越近的邻居话语权越大。什么时候用distance当你的数据分布密度不均、或者你相信“更近的样本更可信”这个假设成立时。鸢尾花上我试过distance相比uniform提升有限有时甚至因为过度相信最近那一个点而导致边界抖动。我的建议是默认用uniform只有在交叉验证结果明确显示distance更优时才切换不要凭感觉换。algorithm参数控制的是内部加速结构ball_tree、kd_tree、brute、auto。默认auto会自己选。这里的取舍逻辑是维度和数据量。kd树和球树属于空间划分结构通过在特征空间里建树来剪枝减少距离计算次数但高维情况下树的剪枝效率会急剧下降维度灾难一般维度超过20到30就不划算了此时brute暴力遍历全部样本反而更快。鸢尾花只有4维、150条样本任何算法都是毫秒级你根本感觉不到差别。但我还是建议你把这个参数当作一个“规模信号”如果你在真实项目里发现用brute比kd_tree快那说明两个事要么维度太高要么数据量大到树结构维护成本压过了收益这是一个很好的性能监控点。n_jobs是用来并行的预测阶段尤其有效因为kNN的计算可以完全并行。设成-1用满所有核。数据量小的时候设了也没感觉但如果你在做批量预测比如一次预测十万条n_jobs-1的提速非常明显我试过在八核机器上能接近线性加速。3. 完整跑通从加载数据到网格搜索调参3.1 环境准备与数据加载的第一手观察依赖就三样scikit-learn、numpy、matplotlib。numpy本身是sklearn的依赖装sklearn时会自动带上。matplotlib只用于画图不画图可以不装。pip install scikit-learn matplotlib加载数据只有一行但我要强调加载之后该做什么——别急着fit先看数据。from sklearn.datasets import load_iris import numpy as np iris load_iris() X iris.data # 形状 (150, 4) y iris.target # 形状 (150,) print(特征名:, iris.feature_names) print(类别名:, iris.target_names) print(数据形状:, X.shape) print(各类别样本数:, np.bincount(y)) print(各特征最小值:, X.min(axis0).round(2)) print(各特征最大值:, X.max(axis0).round(2)) print(各特征标准差:, X.std(axis0).round(3))跑出来你会看到特征名是四个花部尺寸类别是三种鸢尾花每类正好50条四个特征的取值范围大致在0.1到7.9之间但标准差差异不小——花瓣长度的标准差在1.76左右花萼宽度只有0.43左右。这个差异就是标准化必要性的伏笔花瓣长度这一维的波动幅度是花萼宽度的四倍不处理的话距离计算会被前者主导。这一步还有个隐藏价值确认数据没有缺失值sklearn内置数据集本来就没有确认标签是整数编码而不是字符串内置数据已经编码好了。如果换成你自己的数据这两件事必须手动检查否则后面fit的时候报错信息会让你一头雾水。3.2 标准化为什么这一步不能省以及怎么放对位置前面提过量纲问题这里展开说清楚。kNN是基于距离的算法距离计算把所有维度平等相加。如果某一维的取值范围特别大它在距离里就占绝对主导其他维度形同虚设。鸢尾花四个特征单位一致量级接近问题不算严重但标准化之后准确率仍会有可观测的提升更重要的是——你把标准化的习惯养成了以后遇到量纲差异大的数据才不会翻车。标准化StandardScaler做的事是对每一维减去均值、除以标准差把数据变成均值0、方差1的分布。这样每一维对距离的贡献就处在同一量级上了。另一种常见做法是归一化MinMaxScaler把数据压到0到1区间。两者的区别在于对异常值的态度标准化对异常值敏感均值和标准差会被离群点拉偏归一化更敏感极值直接决定缩放比例。kNN场景下我通常选标准化因为它在特征近似正态分布时表现更稳定。注意标准化器只能在训练集上fit然后用同一个标准化器去transform测试集。如果用全量数据fit标准化器再划分测试集的统计信息就泄漏进了训练过程评估结果会虚高。这个错误极其常见我在很多人的代码里都见过。正确的顺序是先划分再在训练集上fit_transform在测试集上只transform。如果嫌手动管理麻烦用Pipeline把标准化和模型打包交叉验证时会自动按折处理彻底杜绝泄漏。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.neighbors import KNeighborsClassifier X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy )这里三个参数都值得说。test_size0.2是八二开150条数据里测试集30条足够评估又不至于让训练集太小。random_state固定种子保证你每次跑出来的划分一致方便复现和对比。stratifyy最关键——它做分层抽样保证训练集和测试集里三个类别的比例都跟原始数据一致各占三分之一。150条数据做八二开如果不分层完全有可能某个类别在测试集里只剩七八条评估结果的方差会大得没法看。分层抽样在类别不均衡的数据上更是必须。3.3 训练、预测与一份能看懂的评估报告代码本身短我习惯把Pipeline和分类器一起建好后面调参直接复用。pipe Pipeline([ (scaler, StandardScaler()), (knn, KNeighborsClassifier(n_neighbors5, weightsuniform)) ]) pipe.fit(X_train, y_train) y_pred pipe.predict(X_test) from sklearn.metrics import classification_report, confusion_matrix, accuracy_score print(准确率:, accuracy_score(y_test, y_pred)) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, target_namesiris.target_names))固定随机种子42、k5的情况下我跑出来的准确率是1.030条测试样本全对。看到这个数字先别高兴第4.1节我会专门讲为什么kNN在鸢尾花上拿到1.0是常事以及什么时候该警惕。比准确率更有信息量的是混淆矩阵和分类报告。混淆矩阵是个3乘3的矩阵行是真实类别、列是预测类别对角线是分对的非对角线是分错的。分类报告里的precision精确率、recall召回率、f1-score要分开看精确率回答“我预测为A的样本里有多少真的是A”召回率回答“真实的A样本里我找出了多少”。在鸢尾花上这两个指标通常都在0.95以上但如果某个类别f1明显偏低说明这个类别的边界跟别的类别有重叠值得单独画图去看。还有个我常用的动作看predict_proba。proba pipe.predict_proba(X_test) print(proba[:5].round(3))每行三个数对应三个类别的概率加起来是1。k5时概率只会是0、0.2、0.4、0.6、0.8、1.0这几种取值因为5个邻居投票。如果某条样本的概率是0.6说明有2个邻居投了别的类这个预测其实挺勉强的。实际业务里我会对这类“低置信度预测”单独打标交给人工复核而不是直接采信。这一步是把模型从“能跑”推到“能用”的关键很多人调包调完就停了其实浪费了模型最有价值的那部分输出。3.4 交叉验证加网格搜索把调参交给流程而不是手感单次划分的评估结果方差大30条测试样本错了1条准确率就掉3.3个百分点这种波动盖住了参数带来的真实差异。所以要用交叉验证把训练集切成K折轮流用其中一折做验证、其余做训练最后取平均。5折在数据量不大时是常用选择折数太小方差大折数太多每折样本少、评估不稳。网格搜索是在这个基础上对参数组合做穷举。我把k值和weights一起扫同时用Pipeline保证每一折的标准化都只在训练折上fit。from sklearn.model_selection import GridSearchCV, cross_val_score param_grid { knn__n_neighbors: list(range(1, 31)), knn__weights: [uniform, distance], knn__p: [1, 2] } gs GridSearchCV( estimatorpipe, param_gridparam_grid, cv5, scoringaccuracy, n_jobs-1, return_train_scoreTrue ) gs.fit(X_train, y_train) print(最佳参数:, gs.best_params_) print(最佳交叉验证得分:, round(gs.best_score_, 4)) print(测试集得分:, round(gs.score(X_test, y_test), 4))注意参数名前缀knn__因为Pipeline里的分类器步骤命名是knn两层下划线是固定写法。这个组合一共30乘2乘2等于120种配置乘以5折等于600次训练加预测。鸢尾花数据小全部跑完也就一两秒n_jobs-1下更快。跑完之后我最关心的不是best_params本身而是趋势。我会把结果抽出来画一条k值与得分的关系曲线import matplotlib.pyplot as plt results gs.cv_results_ mask (results[param_knn__weights] uniform) (results[param_knn__p] 2) ks results[param_knn__n_neighbors][mask].astype(int) scores results[mean_test_score][mask] train_scores results[mean_train_score][mask] plt.figure(figsize(9, 5)) plt.plot(ks, train_scores, markero, label训练集交叉验证均分) plt.plot(ks, scores, markers, label验证集交叉验证均分) plt.xlabel(n_neighbors) plt.ylabel(accuracy) plt.legend() plt.grid(alpha0.3) plt.show()这条图能读出很多东西。k很小时训练分接近1.0而验证分略低这是过拟合的典型形态k变大后两条线逐渐靠拢并一起下降这是欠拟合的信号。两条线的间距就是泛化差距。我实测的结果是k在3到9之间验证分基本贴着1.0k超过15之后开始缓慢下滑到k29时降到0.95左右。这个形态很符合直觉——邻居越多少数派的意见越容易被淹没。提示如果你扫出来的曲线在k1时验证分也很高先别认定模型完美可能是数据太简单或者存在数据泄漏回到3.2节自查标准化的位置。顺便说一句scoring参数的选择。多分类任务上accuracy能用但如果类别不均衡我更倾向换成f1_macro它把每个类别的f1等权平均不会让多数类的好处掩盖少数类的失败。评估指标选错后面所有调参都是在优化一个错误的目标。4. 踩过的坑典型问题与排查思路实录4.1 准确率1.0该不该信三个自查点第一次在鸢尾花上跑到1.0我的第一反应是“代码写错了”。后来跑多了才明白这个数据集上三分类做到1.0是完全可能的尤其是k取3到7、做了标准化、测试集只有30条的时候。但“可能”不等于“可信”我形成了三个固定的自查动作。第一个看训练集和测试集的分数差距。如果训练集交叉验证分也是1.0测试集也是1.0两者一致那大概率是真本事。如果训练分1.0、测试分只有0.8那才是过拟合或者划分有问题。第二个换种子重跑。random_state从42换成0、1、7、2024各跑一遍。如果五次里有四次是1.0、一次是0.97说明模型确实强那0.97是划分带来的正常波动。如果五次结果在0.85到1.0之间大幅跳动说明模型不稳得回头看k值和标准化。第三个检查有没有泄漏。最常见的就是3.2节说的“先标准化全量再划分”。第二种隐蔽泄漏是用测试集参与了调参——比如你反复用测试集分数去挑k值挑到最后那个1.0其实是测试集被“看过”太多次的结果。正确做法是调参只看交叉验证分测试集只在最后用一次。我个人的判断标准是鸢尾花上准确率落在0.95到1.0之间都算正常低于0.93就要查问题。这个数字换个数据集完全不适用别把它当成通用标尺。4.2 预测慢、内存涨kNN的规模天花板在哪kNN的预测复杂度是O(n×d)n是训练样本数d是特征维度。150条4维数据毫无压力但你得知道这条线画在哪。我做过粗略测量训练集十万条、20维特征单条预测在毫秒级批量预测一万条就要几秒训练集涨到百万级单条预测开始上百毫秒批量预测基本不可接受。这种场景下的优化方向有几个。第一降低n也就是减少训练样本但kNN对样本量敏感砍样本会直接影响精度得用原型选择之类的办法挑代表性样本。第二降低d做特征选择或降维这对kNN收益特别大因为维度灾难会让“最近邻”这个概念本身失效——高维空间里所有点之间的距离趋于相等近邻不再“近”。第三换算法结构用近似最近邻检索的思路比如基于树或图的方法牺牲一点精度换速度。第四如果只是要做批量预测n_jobs-1加上把数据分批喂进去能缓解但不解决根本问题。还有一个纯内存的坑kNN的fit阶段只是把训练数据存起来看起来“训练零成本”但如果你训练集有几十GB模型对象本身就占几十GB内存保存成文件也是这个体积。这跟线性模型动辄几MB的模型文件完全不是一个量级。部署时忘了这点很容易在服务端内存上翻车。4.3 报错与异常结果速查下面这张表是我这些年被问到最多、以及自己踩过的kNN相关问题按现象倒推原因比从头读文档快得多。现象最可能的原因处理办法准确率明显偏低低于0.9未做标准化某维度主导距离加StandardScaler且只在训练集fit每次运行结果都不一样未固定random_state划分、模型、交叉验证都设种子测试集某类别样本极少甚至没有划分时未分层train_test_split加stratifyyk值增大到很大时准确率骤降邻居太多退化为多数类预测k控制在训练样本数的平方根附近训练分很高、验证分明显低过拟合k过小或存在噪声增大k检查标签是否有误标预测耗时随数据量爆炸式增长kNN预测复杂度与训练集规模成正比降维、减样本、或改用其他算法kNN(k1)时测试集准确率总是1.0用训练集自身评估未划分必须划分测试集或用交叉验证特征量纲相差悬殊时结果异常大尺度特征主导距离计算标准化或归一化出现平票导致预测不稳定k为偶数二分类场景优先取奇数k模型文件体积异常大kNN存储全部训练数据属正常现象部署时预留内存表格里最后一条特别值得强调因为它不是“错误”而是“特性”。很多人第一次把kNN模型存盘看到文件大小接近训练数据本身会以为保存方式有问题。其实这就是懒惰学习的代价模型的“参数”就是数据本身。4.4 一些不那么显眼但很影响结果的经验说几个文档里不会写、但我实际撞过的细节。第一个特征的尺度统一不等于特征的信息量统一。标准化只解决量纲问题不解决“某个特征其实是噪声”的问题。鸢尾花的四个特征里花萼宽度对分类的贡献明显弱于花瓣长度。如果你有一堆特征可以做一个简单的消融测试每次去掉一个特征重跑交叉验证看分数掉多少掉得少的说明贡献小可以考虑剔掉。对kNN来说去掉噪声特征往往比调k值带来的提升更大因为噪声维度会污染距离计算。第二个predict和predict_proba在大样本下的耗时不同。predict_proba要多算一步归一化还要返回完整的类别概率矩阵批量预测时内存占用会涨得快。如果只需要标签别顺手写predict_proba。第三个类别标签的编码方式会影响某些评估函数的行为。sklearn的多数指标默认按标签值排序处理如果你的标签是从1开始而不是从0开始或者标签是字符串混淆矩阵的行列顺序可能跟你想的不一样。用target_names明确指定显示名称能省掉一堆误读。第四个Pipeline里步骤命名不要用双下划线。网格搜索的参数名用双下划线连接步骤名和参数名如果步骤名本身带下划线你会在param_grid里数不清到底几个下划线很容易写错然后报一个很难懂的KeyError。5. 进阶对照手写一版kNN把黑盒拆开5.1 向量化实现的核心思路sklearn版跑顺之后我建议花二十分钟手写一版只做预测不做训练因为训练没什么可做的。核心是两个矩阵运算用numpy写出来只有几行但能让你彻底看清kNN在算什么。import numpy as np from collections import Counter class SimpleKNN: def __init__(self, k5, p2): self.k k self.p p def fit(self, X, y): self.X_train np.asarray(X, dtypefloat) self.y_train np.asarray(y) return self def _distances(self, x): diff self.X_train - x # 广播(n_train, d) if self.p 2: return np.sqrt((diff ** 2).sum(axis1)) return np.abs(diff).sum(axis1) def predict(self, X): X np.asarray(X, dtypefloat) preds [] for x in X: d self._distances(x) idx np.argsort(d)[: self.k] labels self.y_train[idx] preds.append(Counter(labels).most_common(1)[0][0]) return np.array(preds)关键点有三个。第一距离计算用了广播self.X_train是(n,d)x是(d,)相减自动扩展成(n,d)然后沿axis1求和一次算出这个样本到所有训练样本的距离避免了Python层的循环。第二argsort取前k个索引这是“找最近邻”的全部实现。第三Counter投票取出现次数最多的标签。这个实现没有处理平票如果两个标签票数相同Counter的most_common会按插入顺序返回结果不确定。工业级实现要引入距离加权或按距离排序打破平票这也是我在2.2节建议用奇数k的原因之一。5.2 两个版本结果对齐与性能对比把两个版本放在同一个划分上跑对比预测结果sk_model Pipeline([ (scaler, StandardScaler()), (knn, KNeighborsClassifier(n_neighbors5)) ]) sk_model.fit(X_train, y_train) pred_sk sk_model.predict(X_test) scaler StandardScaler().fit(X_train) X_train_s scaler.transform(X_train) X_test_s scaler.transform(X_test) my_model SimpleKNN(k5, p2).fit(X_train_s, y_train) pred_my my_model.predict(X_test_s) print(sklearn预测:, pred_sk) print(手写版预测:, pred_my) print(完全一致:, np.array_equal(pred_sk, pred_my))注意手写版必须喂标准化后的数据否则距离计算的量纲问题会让两个版本对不上这也是个很好的反面教材——你只要试一次“手写版直接用原始数据”就能直观看到准确率掉了多少比任何解释都管用。一致率通常在95%以上偶尔个别样本不一致多半是平票或者距离完全相等导致的顺序差异这属于正常现象说明两边的实现细节有细微差别。性能上sklearn版在150条数据上快一个数量级因为它用到了优化的底层实现Cython编译的距离计算。但在小数据上这个差距毫无意义手写版的价值在于可解释性。跑完这一轮我对kNN的完整认识才算建立起来它简单到能用二十行写明白但要在实际项目里用好标准化的顺序、k值的选择、评估方式的设计每一个环节都得想清楚。这也是我一直推荐拿它做第一个实战项目的原因——代码量小能暴露的问题却一样不少。最后分享一个我自己做题时的习惯第一次跑通之后故意把某一个环节改错比如不做标准化、把k设成100、把stratify去掉各跑一遍看看指标怎么变。错误是最好的老师你亲手制造过的偏差比读十遍文档记得都牢。这套方法我后来迁移到每个新算法上屡试不爽。
阅读完成 · 觉得有帮助?