2026/10/26 5:53:45

KNN算法从原理到实战:鸢尾花数据集全流程调参与优化

KNN算法从原理到实战:鸢尾花数据集全流程调参与优化 1. 先把KNN这件事想明白一个不训练的算法凭什么上台面K近邻算法K-Nearest Neighbors简称KNN在机器学习里是个特别有意思的存在。你把训练集喂给它它几乎什么都不干——不拟合参数、不更新权重、不建梯度、不迭代。fit()一调用时间基本花在把数据存进内存上。真正干活是在predict()那一刻来一个新样本它把训练集里所有点和这个新样本逐个算一遍距离找出最近的K个邻居看这些邻居大多属于哪一类就把新样本判成哪一类。这套逻辑朴素得像小区里问路——你不知道某个地方在哪就问身边最近的几个人多数人说往东你大概就往东走。我在带新人的时候常讲一句话KNN的全部智能都藏在距离和K这两个词里。它没有学到特征和标签之间的映射函数它学到的其实是整个训练集本身就是模型。这个特性决定了它的优缺点都极其鲜明——实现简单、无需训练、天然支持多分类、对非线性边界有一定适应力但同时内存占用随数据线性增长、预测慢、对特征尺度极其敏感、在高维空间里几乎失效。这篇文章讲的是鸢尾花Iris数据集上的KNN完整实现。这个数据集小得可爱150条样本4个特征花萼长度、花萼宽度、花瓣长度、花瓣宽度3个类别三个不同品种每类50条。很多人嫌它太简单一行代码准确率就96%但我偏偏喜欢拿它做教学载体因为它干净到可以把每一个环节单独拎出来反复折腾——你怎么选K、怎么缩放、怎么切分、怎么验证、怎么解释结果全都看得清清楚楚不会有一堆脏数据干扰你的判断。适合谁看写过一点Python、听过KNN但没真正跑通过全流程的人调包能出结果但说不清为什么准确率是96%而不是99%的人想手写一遍KNN理解底层的人。如果你已经能熟练做网格搜索和特征工程这篇可能偏基础但里面关于标准化顺序、维度灾难、距离度量选择的那些坑大概率还是能帮你省点时间。1.1 距离即投票KNN的核心假设到底是什么任何算法背后都有一套对世界的假设KNN的假设直白得不像话特征空间里挨得近的样本标签大概率相同。这句话听起来像废话但它成立是需要条件的。第一个条件特征必须能构成一个有意义的度量空间。也就是说两个样本之间的距离要真的能反映它们的相似程度。如果你的特征里混了用户ID这种纯编号ID1001和ID1002看起来距离是1但它们之间没有任何语义关联这种距离就是假的会把整个模型带偏。第二个条件各特征的量纲要可比。花萼长度单位是厘米取值范围4.3到7.9如果我还加入一个花瓣面积单位是平方厘米取值可能到20以上。那么在欧氏距离的平方和里面积这个维度会完全压制长度维度长度特征等于白给。这就是为什么KNN几乎必须配标准化不是建议是必须。第三个条件样本密度要足够。KNN是纯局部方法它只看邻居。如果某个区域训练样本极其稀疏那最近的K个邻居可能其实离得很远投票结果的可靠性就大打折扣。所以KNN在样本分布均匀、类别边界清晰的问题上表现好在长尾、稀疏、类别极不平衡的问题上容易翻车。想清楚这三点你就明白为什么很多人拿KNN跑出来的结果忽高忽低——不是算法玄学是这三个前提被破坏了。我见过有人把KNN直接用在几千维的文本TF-IDF向量上然后抱怨KNN效果太差其实问题不在KNN在于高维空间里最近这个概念已经失真了这个后面会专门讲。1.2 它和线性模型、树模型的根本差别在哪拿KNN和逻辑回归对比差别不只在效果更在模型的形态。逻辑回归训练完得到的是一组权重模型被压缩成几个数字你甚至可以把它写在纸上KNN训练完得到的是一整个训练集模型体积等于数据体积。这是参数化模型和非参数化模型的分水岭。和决策树对比更有意思。决策树是在特征空间上切格子切完之后每个格子给一个标签切法由训练数据决定但一旦确定就固定了KNN不切格子它的决策边界是随着查询点移动的每个新样本都会临时划出一片邻域。所以KNN的决策边界可以非常曲折、非常非线性代价是它没法给你一个可解释的规则表。这个差别带来的直接影响是预测成本。逻辑回归预测是几次乘加微秒级决策树预测是几次比较也是微秒级KNN预测要遍历训练集算距离10万条样本就是10万次距离计算。线上服务里如果QPS高、延迟敏感KNN基本不适合直接怼在请求链路上得配近似最近邻索引或者做预计算。1.3 哪些场景该用它哪些场景趁早换我自己的判断标准很简单看三条数据量中等偏小万级到十万级特征维度不高几十维以内且特征都是数值型、量纲可以统一——用KNN很合适甚至经常能打赢一些复杂模型。样本量极大或者维度极高几百上千维——先别急着上KNN除非你愿意上近似检索。需要给出为什么这么判的业务解释——KNN能给出因为和这5个历史样本最像这种解释在某些风控、推荐场景反而比权重系数更好用。实际落地里KNN出现频率最高的地方往往不是主模型而是推荐系统的召回层找相似物品/相似用户、图像检索的相似度匹配、异常检测离所有邻居都远的点判为异常、缺失值填补用邻居的均值填、以及小样本冷启动场景。这些场景有一个共同点——相似本身就是业务语义不需要额外解释。2. 核心参数与原理细节K值、距离度量、决策规则2.1 K值怎么选偏差和方差的那杆秤K是KNN里唯一一个真正意义上的超参数它的作用是把局部的尺度调大调小。K1的时候模型完全信任最近的那一个点。训练集里只要有一个噪声点或者标错的样本它周围就会被划出一小块错误的领地。这叫过拟合方差极大换一批训练数据决策边界抖得厉害。准确率在训练集上永远是100%因为最近的邻居就是它自己测试集上往往明显掉一档。K取很大比如K100那就等于每次预测都看一大片区域。极端情况KN所有样本都参与投票预测结果对所有输入都一样永远是多数类。这叫欠拟合偏差极大。所以K的选择本质就是在偏差和方差之间找平衡点。经验做法有这么几条从K sqrt(N)开始试N是训练样本数。150条样本开方约12所以鸢尾花上可以先试K11或13这类奇数。二分类问题尽量取奇数避免平票。多分类问题平票的可能性低但sklearn内部会按距离加权或取索引小的类行为不一定符合你的预期能避开就避开。不要用训练集选K。必须用验证集或者交叉验证否则你会选出一个过拟合的极小K。K通常不会取到样本量的很大比例一般不超过样本数的十分之一到五分之一。在鸢尾花上做5折交叉验证扫一遍K你会看到一个很典型的U形曲线K从1涨到5左右准确率快速上升5到15之间平台区很平超过25之后开始缓慢下滑。平台的平坦程度说明这个数据集类别边界很干净实际上K取3到15结果都差不多在0.95以上。这时候真正的决策依据不是哪个准确率高0.01而是哪个更稳——我一般会选平台区中间偏小的值兼顾稳定性和局部性。2.2 距离度量不是只有欧氏距离默认的欧氏距离是L2范数公式是各维度差值的平方和再开根号。它在特征维度独立、量纲统一、分布比较团状的时候最合适。曼哈顿距离是L1范数各维度差值绝对值求和。它对异常值更不敏感因为误差不被平方放大。如果某一维偶尔出现离谱的值L1会比L2稳。余弦相似度衡量的是方向而不是长度。文本向量、embedding向量这种长度不重要、方向才重要的场景必须用余弦。你在高维稀疏向量上算欧氏距离结果基本被向量模长主导完全没有意义。闵可夫斯基距离是把上面这些统一起来的通式参数p决定具体形态p1是曼哈顿p2是欧氏p→∞是切比雪夫距离取各维度差值的最大值。sklearn里metricminkowski, p2就是欧氏距离。还有几个值得知道但少用的马氏距离考虑了特征之间的协方差能消除特征相关性影响但对小样本估计协方差矩阵不稳汉明距离专门给离散/二值特征用Jaccard用于集合相似度。选距离度量的判断顺序我一般是这样的特征有明确物理量纲且相互独立→欧氏存在离群点或分布偏斜→曼哈顿向量方向比大小重要→余弦特征间强相关→考虑马氏或先做PCA去相关。2.3 投票规则与距离加权标准做法是等权投票K个邻居一人一票。但这里有个隐含问题——K5的时候最近的邻居距离可能是1.0最远的那个可能是3.0它们的话语权完全一样这显然不太合理。weightsdistance就是来解决这个的每个邻居的票权按距离的倒数分配。距离越近权重越大距离趋近于0时权重趋于无穷大。实现上sklearn用的是1/d而不是严格的1/d²避免数值爆炸。效果上距离加权在小K时通常能提升一点准确率尤其在类别边界附近但在噪声多的数据上反而可能放大单个异常点的影响。我的实测经验是数据干净、边界清晰比如鸢尾花等权和距离加权差别在1个百分点以内数据噪声大、密度不均等权反而更稳。回归任务里KNN就是取邻居标签的均值或加权均值逻辑完全对应。分类投票、回归平均这是同一套思想的两种输出形式。2.4 维度灾难KNN在高维空间为什么会失灵这一条很关键但很多教程一笔带过。高维空间里距离这个概念会钝化。设想在一维区间[0,1]里随机撒点最近邻的距离大概在1/N量级在100维的单位超立方体里撒点任意两点之间的距离会高度集中在一个很窄的范围内最近邻和最远邻的距离比值趋近于1。这意味着什么意味着最近的那个邻居并不比随便挑一个点近多少KNN的局部性假设直接崩塌。有个粗略的经验说法KNN在维度超过20左右就要小心了超过50基本只能靠降维或者换算法。这不是硬阈值跟样本量有关——样本量随维度指数增长才够用所以维度和样本量的关系比单纯的维度数字更重要。对策很实在先做特征选择把明显无关的维度砍掉。用PCA或者别的降维手段压到低维再跑KNN很多时候比直接堆全特征效果还好。如果维度确实高比如embedding检索不要用暴力搜索改用近似最近邻HNSW、IVF之类的索引结构现代向量检索基本都是把这个思路工程化。2.5 特征缩放不做这一步后面全是白干标准化公式很基础但值得写清楚因为很多细节在为什么这么算上$$z \frac{x - \mu}{\sigma}$$其中 μ 是训练集上该特征的均值σ 是训练集上该特征的标准差。变换之后每个特征的均值是0标准差是1。标准化不改变分布形状只改尺度和位置。这里有两个必须记住的操作禁忌计算 μ 和 σ 只能用训练集然后把这个 μ、σ 应用到验证集和测试集上。如果用全体数据算均值和方差再切分测试集的信息就泄漏进训练过程了评估结果会虚高。正确做法是scaler.fit(X_train)后scaler.transform(X_test)或者用Pipeline把缩放和模型绑在一起。StandardScaler 对常数列标准差为0处理时会把该列置0不会报错但那一维就废了。如果某个特征在训练集里只有一个值请提前识别出来删掉。除了StandardScaler还有几种常见选择MinMaxScaler把值压到[0,1]适合需要固定范围或者稀疏数据RobustScaler用中位数和四分位距对离群点更稳Normalizer是行归一化改的是样本向量的模长而不是列的分布别和标准化搞混。在鸢尾花上标准化的影响其实不大因为四个特征本来就在同一量级厘米欧氏距离不会严重失衡。但为了养成习惯也因为这是通用流程的一部分正式代码里我还是会加上。这个习惯在你自己接手的真实数据上价值巨大——我踩过的最隐蔽的一次坑就是一个以元为单位的价格特征和一个以百分比为单位的比率特征混在一起KNN的准确率一直在60%左右徘徊标准化之后直接跳到88%。3. 鸢尾花数据集实操从加载到评估的完整流程3.1 加载数据与第一轮观察先用sklearn自带的版本省去下载和清洗的功夫。import numpy as np import pandas as pd from sklearn.datasets import load_iris iris load_iris() X pd.DataFrame(iris.data, columnsiris.feature_names) y pd.Series(iris.target, nametarget) print(X.shape) # (150, 4) print(y.value_counts()) # 每类各50 print(X.describe().round(2))跑出来你会看到各特征的范围花萼长度4.3到7.9花萼宽度2.0到4.4花瓣长度1.0到6.9花瓣宽度0.1到2.5。四个特征量纲一致但分布宽度差了一倍多花瓣宽度的标准差比花萼宽度小不少说明花瓣相关的特征区分度可能更高。再看一下类别分布三个类各50条完全均衡。这一点很重要——均衡数据上准确率这个指标才可信如果是9:1的不均衡数据全猜多数类也有90%准确率那时候必须看混淆矩阵、看每类的precision和recall。还有一个必做的动作看特征之间的相关性。print(X.corr().round(2))在鸢尾花上花瓣长度和花瓣宽度的相关系数大约0.96花萼长度和花瓣长度大约0.87。高度相关意味着信息冗余对KNN来说不致命它不像线性回归那样有共线性问题但确实意味着你在重复计权同一个维度。这也是后面能不能做PCA降维的依据之一。我一般还会画一下散点图用花瓣长度和花瓣宽度做二维投影肉眼就能看出三类基本是分开的其中一类setosa和另外两类完全线性可分另外两类之间只有少量交叠。这解释了为什么这个数据集的准确率天花板在96%到98%之间——那2%到4%的错误几乎全部来自后两类的边界样本无论你怎么调K都是这个数。3.2 数据划分与标准化的正确姿势划分训练测试集固定随机种子保证可复现from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy )stratifyy这个参数一定要加。它保证训练集和测试集里的类别比例与原数据一致。150条数据、3个类如果不分层随机切出30条测试集有概率某一类只分到6条甚至更少评估结果波动会很大。分层之后每类在测试集里稳定出现10条。然后是标准化注意顺序from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) # fit和transform都在训练集 X_test_scaled scaler.transform(X_test) # 只用transform这里有个细节fit_transform(X_train)之后scaler.mean_和scaler.scale_就固定了transform(X_test)用的是训练集的统计量。如果你写成scaler.fit_transform(X_test)那就是彻头彻尾的数据泄漏测试集的均值方差参与了变换评估结果会偏乐观。这个坑极其常见我在代码review里见过不止一次。更稳妥的写法是直接用Pipeline把缩放和模型串成一体交叉验证的时候sklearn会自动保证每一折的缩放只在该折的训练部分拟合from sklearn.pipeline import Pipeline from sklearn.neighbors import KNeighborsClassifier pipe Pipeline([ (scaler, StandardScaler()), (knn, KNeighborsClassifier(n_neighbors5)) ])用Pipeline最大的好处不是代码短是从结构上消灭了忘记顺序的可能性。我现在的习惯是只要涉及任何需要fit的预处理一律走Pipeline不给自己犯错的机会。3.3 建模、预测与评估from sklearn.metrics import accuracy_score, classification_report, confusion_matrix pipe.fit(X_train, y_train) y_pred pipe.predict(X_test) print(f准确率: {accuracy_score(y_test, y_pred):.4f}) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, target_namesiris.target_names))默认K5跑出来测试集准确率通常在0.93到0.97之间浮动取决于random_state。混淆矩阵会显示错误集中在那两个容易混的类之间。光看一个测试集的结果不够30条样本错一条就是3.3个百分点波动太大。养成习惯用交叉验证from sklearn.model_selection import cross_val_score scores cross_val_score(pipe, X, y, cv5, scoringaccuracy) print(f5折CV: {scores.mean():.4f} ± {scores.std():.4f})5折交叉验证用的是全部150条数据每一折都有120条训练、30条验证最后取5次平均。这个数字比单次train_test_split稳定得多也是我用来判断模型好坏的主要依据。注意这里的cross_val_score传入的是完整的X和y内部会自己切分——Pipeline保证了每一折的缩放只在训练折上拟合所以没有泄漏。还有一个指标值得看一眼predict_proba。proba pipe.predict_proba(X_test[:5]) print(proba.round(3))KNN输出的概率就是K个邻居里各类的占比。K5的话概率只可能是0、0.2、0.4、0.6、0.8、1.0这几档粒度非常粗。如果你需要比较细的概率估计比如做排序或者阈值调整KNN不是好选择它的概率没经过校准。可以用CalibratedClassifierCV包一层或者干脆换逻辑回归。3.4 手写一遍KNN把黑箱拆开调包调通了不代表懂了。我强烈建议每个人手写一遍代码不超过40行import numpy as np from collections import Counter class SimpleKNN: def __init__(self, k3, 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 _dist(self, a, b): if self.p 1: return np.abs(a - b).sum(axis1) if self.p 2: return np.sqrt(((a - b) ** 2).sum(axis1)) return np.max(np.abs(a - b), axis1) def predict(self, X): X np.asarray(X, dtypefloat) preds [] for x in X: d self._dist(x, self.X_train) idx np.argsort(d)[:self.k] votes Counter(self.y_train[idx]) preds.append(votes.most_common(1)[0][0]) return np.array(preds)写完自己手动标准化一下再跑mu, sigma X_train.mean(0), X_train.std(0) Xtr (X_train - mu) / sigma Xte (X_test - mu) / sigma knn SimpleKNN(k5).fit(Xtr.values, y_train.values) pred knn.predict(Xte.values) print((pred y_test.values).mean())结果应该和sklearn基本一致。手写的价值在于你会亲眼看到预测这一步是O(N×D)的计算N是训练样本数D是维度。这解释了为什么KNN预测慢、为什么大数据量下必须上索引结构。你也会看到argsort是整个流程的瓶颈——实际上只需要前K个用argpartition会快很多工业实现里还会用堆或者树结构。顺便说一个优化点很多人不知道sklearn的KNN可以预计算距离矩阵在特征不变、只反复调K的场景下能省一半时间还有一个更实用的技巧是设置algorithmauto之外显式指定kd_tree或ball_tree在低维中等数据量下比暴力搜索快一个数量级。4. 调参与性能优化把准确率从96%推到98%4.1 用网格搜索找K和权重手工试K效率太低直接上GridSearchCV。注意要搜的参数在Pipeline里带前缀from sklearn.model_selection import GridSearchCV param_grid { knn__n_neighbors: list(range(1, 31, 2)), knn__weights: [uniform, distance], knn__p: [1, 2] } grid GridSearchCV(pipe, param_grid, cv5, scoringaccuracy, n_jobs-1) grid.fit(X, y) print(grid.best_params_) print(f{grid.best_score_:.4f})参数空间是15×2×260组每组5折一共300次训练加预测在鸢尾花这种规模上几秒钟就跑完了。一个观察跑完之后你大概率会发现最优的K落在3到11之间weights是uniform还是distance差距很小p是1还是2差距也很小。这说明在这个数据集上这些超参并不敏感。这时候别再纠结了挑一个泛化误差曲线平台区靠中间的值定下来把精力放到别的地方去。但有一种情况需要注意GridSearchCV用的是同一份数据做交叉验证选参数然后再用同一个best_score_报告结果这个数字会略微偏乐观因为参数是在这同一批数据上挑出来的最优值。严格的评估应该留一个从不参与调参的测试集最后单独跑一次。我一般这么切先分出20%测试集锁起来剩下80%做交叉验证调参最后用那20%报最终结果且只报一次。4.2 缩放方式对结果的对比实验这个实验值得亲手做一遍体会一下预处理选择会直接影响指标。from sklearn.preprocessing import MinMaxScaler, RobustScaler from sklearn.model_selection import cross_val_score for name, sc in [(none, None), (standard, StandardScaler()), (minmax, MinMaxScaler()), (robust, RobustScaler())]: steps [(knn, KNeighborsClassifier(n_neighbors5))] if sc: steps.insert(0, (scaler, sc)) p Pipeline(steps) s cross_val_score(p, X, y, cv5) print(f{name:9s} {s.mean():.4f} ± {s.std():.4f})在鸢尾花上你会看到不缩放的结果其实只差一点点因为四个特征量纲本来就接近。但如果我把花萼宽度乘以100模拟量纲差异不缩放的准确率会立刻掉到0.6以下而标准化版本纹丝不动。这个对比实验我每次带人做都会做冲击感比讲十遍原理都强。顺便提一个边界情况如果数据里有明显的极端离群点StandardScaler会被拉偏因为均值和标准差都受离群值影响这时候RobustScaler通常更稳。判断方法很简单画个箱线图看看就清楚了。4.3 大数据量下的加速思路鸢尾花只有150条谈性能优化有点奢侈。但既然讲到这把思路说清楚对将来有用。暴力搜索的复杂度预测单个样本是O(N×D)N条查询就是O(N×M×D)。N到十万级、D到百级单次预测就要上千万次运算这是不可接受的。第一条路是空间索引。KD树在低维经验上D20能显著加速本质是把空间递归二分查询时剪枝掉不可能包含近邻的分支。Ball Tree对高维稍好一些用超球体代替超矩形切分。sklearn里algorithm参数支持这两种设置auto会让它自动选。但注意维度一高这些树的剪枝效率急剧下降最后退化成接近暴力搜索。第二条路是近似最近邻ANN。放弃精确找到最近的K个转而以很高概率找到足够近的K个换来数量级的速度提升。常见思路有基于图的把样本连成近邻图查询时在图上贪心游走、基于量化的把向量空间分桶只在附近桶里找、基于哈希的。这些在检索场景已经成为标配文本、图像、向量数据库底层基本都是这套东西。第三条路是降维。PCA降到几十维再跑既缓解维度灾难又直接降低每次距离计算的成本双赢。前提是降维后保留的方差足够一般看累计解释方差到95%以上。第四条路是工程手段预计算距离矩阵、批量向量化用矩阵运算代替循环、把训练集常驻内存避免重复加载、用更紧凑的数据类型float32代替float64内存减半、速度提升。这些都是实现层面的小优化累加起来也很可观。5. 常见问题与排查实录5.1 准确率异常低按这个顺序排查我整理了一个排查顺序从最常见的开始第一检查是否做了缩放。这是头号原因占比超过一半。特征量纲差异大而没缩放KNN基本等于只在一个特征上做判断。第二检查缩放是不是做在了划分之前。如果先对全量数据fit_transform再切分虽然准确率会虚高而不是变低但另一个常见错误是把fit_transform用在了测试集上这会导致训练和测试的变换标准不一致结果可能变得很奇怪。第三检查有没有把ID、时间戳之类的列喂进去。这类特征的距离没有语义会严重稀释真实特征的作用。特征列一定要手动点一遍名。第四检查类别是否极度不平衡。不平衡时KNN会倾向于预测多数类整体准确率看起来还行但少数类的召回率惨不忍睹。这时候要看每类的指标并考虑用weightsdistance或者对多数类下采样。第五检查K值是不是太大。K接近样本数时模型退化所有预测趋同。第六检查是否存在重复样本或近重复样本导致的泄漏。如果同一条数据或几乎相同的样本同时出现在训练集和测试集KNN会直接背答案准确率高得离谱。这在做过数据增强或者按时间切分不当的场景里很常见。5.2 问题速查表现象最可能的原因处理方式准确率明显低于预期未做特征缩放加StandardScaler走Pipeline准确率虚高接近100%数据泄漏或重复样本检查切分逻辑去重缩放只在训练集fit训练集100%、测试集很差K过小过拟合增大K用交叉验证选值所有样本预测成同一类K过大或类别极不平衡减小K处理不平衡调参后提升很小超参不敏感或特征信息不足转去做特征工程别死磕超参维度很高时效果崩塌维度灾难特征选择或降维换ANN索引预测速度很慢暴力搜索大样本KD树/Ball树或近似检索概率输出只有几档KNN概率粒度等于1/K增大K或用概率校准5.3 几个我踩过的坑写下来省你时间坑一把predict和predict_proba的结果混用。前者返回类别标签后者返回各类占比。有次我在做阈值筛选直接把predict的结果当概率去比较大小逻辑全错排查了半天才发现类型不对。坑二忽略特征的相关性做特征越多越好的判断。加了一堆高度相关的特征KNN的距离计算里同一个信息被反复计权效果反而下降。加特征之前先看相关性矩阵。坑三在时间序列数据上随机切分。KNN天然会把未来的信息带到过去因为相似的样本在时间上往往相邻。时间序列必须按时间切分否则评估结果毫无意义。坑四以为n_neighbors是唯一要调的。实际上metric的选择在文本、embedding类任务上比K的影响大得多很多人压根没试过换度量。坑五在小样本上过度相信单次评估结果。150条数据测试集30条多对一个少对一个就是3.3个百分点。一定要用交叉验证并且报出均值±标准差而不是报一个孤零零的数字。坑六忘记固定随机种子。每次跑结果都不一样调试的时候根本不知道变化是来自你的改动还是来自切分。random_state是必备参数不是可选项。我在实际使用中的一个体会是KNN看起来是最简单的算法之一但它对数据质量的敏感度在所有常见模型里排前列。线性模型还能靠权重把噪声特征压下去树模型还能靠分裂点忽略无用维度KNN不行——你给它什么特征它就老老实实按这些特征算距离一维都不能糊弄。所以用好KNN的关键从来不在算法本身而在数据准备那几步特征选得准不准、量纲统一了没有、有没有混进伪特征。把这几步做扎实KNN在很多中小规模问题上依然是个又便宜又稳的基线值得在项目早期就先跑一遍看看天花板在哪。