ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

KNN算法详解:从原理到Scikit-learn实战,分类回归一篇搞定

KNN算法详解:从原理到Scikit-learn实战,分类回归一篇搞定 KNN算法在机器学习里的地位挺特殊。很多人入门第一个模型不是它但绕来绕去都会回到它——它是少有的“不需要训练”的分类回归算法而且 Scikit-learn 对它的 API 封装非常完善几行代码就能同时跑通分类和回归任务。这篇文章是机器学习系列的第二篇咱们把 KNN 的原理、API 用法、调参技巧以及实际使用中容易翻车的几个点一次说清楚。看这篇文章的人我猜大概分三种第一种是刚入门的同学想搞明白 KNN 到底是什么、能解决什么问题第二种是期末复习的需要把算法原理吃透顺便看看 API 怎么用第三种是已经在项目里用 KNN 当 baseline但总被效果和性能搞得头疼的工程师。不管你对号入座哪一种这篇都能给你点能直接用的东西。强行概括一下KNN 的核心就是一句俗话“物以类聚人以群分”。一个新样本过来不需要先构建什么决策边界只需要找到离它最近的 K 个已知样本让这些邻居用投票分类或者取平均回归来替它做决定。听起来简单但这里面的细节比如距离怎么算、K 怎么选、特征要不要缩放每一个都能决定模型是“能用”还是“好用”。1. 先把KNN的原理揉碎了讲清楚1.1 深挖两个关键词惰性学习与非参数模型教科书爱给 KNN 贴两个标签惰性学习Lazy Learning和非参数模型Non-parametric Model。这两个词劝退过不少人我用大白话翻译一下。所谓惰性学习就是“不训练”。你调fit()的时候它什么都不干调用之后它也只是把样本原封不动地存下来。真正的工作是在predict()的时候才发生的——预测一个新样本它才开始遍历全部历史数据、计算距离。所以在 Sklearn 里KNN 的fit()几乎瞬间完成反倒是predict()可能很慢。这点和神经网络、线性回归那种“训练耗时很长预测飞快”的模式刚好反过来。我见过不少同学第一次训练 KNN 时以为卡死了其实是它在预测阶段才开始发力这个节奏要提前适应。所谓非参数不是说模型没有参数而是说模型的复杂度会跟着数据规模自动走。KNN 的“参数”就是存下来的全部样本本身样本越多模型越复杂它不会强行要求数据符合某个固定的数学形态比如直线、曲线这些。这种特性让 KNN 在数据形态不明确的时候特别好用。实际项目里如果你根本不知道特征和目标之间是什么关系拿 KNN 先跑一版看看通常不会太差。这是它当 baseline 的最大价值——能给你一个“这个任务至少能到多少分”的底线参考。1.2 决定“谁离我近”的三把尺子KNN 的全部判断都建立在“距离”上所以距离的定义方法很关键。实际里最常用的有三把尺子。第一把是欧氏距离也就是大多数人直觉里的“直线距离”。二维平面上两个点 (x1, y1)、(x2, y2) 的距离是 sqrt((x1-x2)^2 (y1-y2)^2)推广到高维就是把每个维度的差值平方再求和开根号。这是 KNN 默认的距离度量绝大多数场景下够用。第二把是曼哈顿距离。想象你在网格状的城市里穿行不能走斜线只能横平竖直地走最后走过的总路程就是曼哈顿距离公式是 |x1-x2| |y1-y2|。它的特点是受离群点的影响更小。因为平方运算会放大那些特别大的差值曼哈顿距离没有平方这一下所以个别特征的极端值不至于把整体距离完全带歪。第三把是闵可夫斯基距离可以看作前两把的统一形式公式是 (\sum \limits_{i} |x_i - y_i|^p)^{1/p}。p2 时是欧氏距离p1 时是曼哈顿距离p 越大大差值在总距离里的贡献权重越高。Sklearn 里 KNeighborsClassifier 的 p 参数管的就是这个。我的建议是常规数据用默认 p2如果特征里常见离群点、或者特征维度比较高试一下 p1 往往更稳。1.3 k值、投票规则与权重机制接下来是 KNN 的灵魂k 值和投票规则。分类场景里新样本的 K 个最近邻居逐一表态得票最多的类别获胜本质就是“民主投票”。但有个容易忽视的问题如果 K1模型完全看最近那一个样本的脸色噪声稍大一点就翻车如果 K 设成整个样本数那每个类别的得票就按总样本比例来模型退化成“哪类人多就预测谁”局部信息全丢。所以 k 值要取折中既要避免太小带来的过拟合也要避免太大把局部结构抹平。回归场景稍有变化不是投票而是求平均。找出 K 个邻居后把它们的真实目标值做平均或者加权平均作为预测结果。这个思路天然保证预测值落在训练集目标值的范围内不会给出离谱的极端值。还有种常用的升级玩法weights 参数。默认是 uniform所有邻居一视同仁改成 distance 后越近的邻居权重越大加权投票在数据分布不均匀、噪声较多的时候往往能救回不少精度。我想用一个生活类比帮大家记住这个机制你想判断一个人是不是篮球爱好者别管他自己怎么说去看平时跟他玩得最好的 5 个朋友——如果 4 个都常打球那大概率这人也是个球友。KNN 就是这种“看朋友圈”的逻辑。2. Scikit-learn里两个核心API分类与回归怎么选2.1 KNeighborsClassifier分类任务的入口先把概念捋清楚标题里的“API”不是网络接口而是 Sklearn 把 KNN 算法封装好的调用接口。分类场景对应的是sklearn.neighbors.KNeighborsClassifier。这个类的用法可以用“标准 Sklearn 三件套”来概括fit(X, y)负责把训练数据“记下来”predict(X)输出每个样本的类别标签score(X, y)直接算准确率。还有一个容易被忽略但很实用的方法predict_proba(X)它会输出每个样本属于各个类别的概率。这个概率在 KNN 里很好解释——最近 K 个邻居里每个类别的占比就是概率。比如最近 5 个邻居有 4 个属于类别 0那predict_proba就会给出接近 0.8 的概率值。Sklearn 最值得称赞的一点是模型 API 高度统一KNeighborsClassifier、LogisticRegression、RandomForestClassifier这些类的fit、predict、score用法完全一致。所以你今天学会 KNN明天换逻辑回归或者随机森林代码结构基本不用改只需要替换类名和参数。这也是为什么我建议新手先把 KNN 的代码流程吃透后面学其他模型会顺很多。2.2 KNeighborsRegressor预测连续值的用法回归任务对应的是sklearn.neighbors.KNeighborsRegressor。它和分类版一样有fit、predict、score区别主要在于没有predict_proba因为目标是连续值而不是类别。工作方式也很直观找到 K 个邻居后把邻居的目标值做平均或加权平均当作预测结果输出。这里有个重要特点要提醒大家KNN 回归天然不会给你超出训练数据范围的预测值。比如训练数据里房价最高 50 万那它预测出来的值基本不会超过 50 万。这在很多场景是优势不容易中途跑出特别离谱的值但如果你想做外推比如预测比历史数据更高的价格KNN 就无能为力了那种任务更适合线性回归或树模型。搞清楚这一点你才不会在错误的任务上硬套 KNN。2.3 主要参数横向对比表两个核心类的参数几乎完全一致放在一起对比更好记忆。参数默认值作用实操建议n_neighbors5邻居个数核心参数分类从 5 试起回归从 7 试起weightsuniform是否按距离加权数据噪声大时改 distancealgorithmauto寻找邻居的底层算法小数据 auto 够用大数据可指定leaf_size30树形算法的叶子节点大小默认即可一般不动p2距离范数默认欧氏距离高维可试 1metricminkowski距离度量方式默认即可特殊需求时自定义简单解释一下 algorithm。KNN 暴力搜索就是挨个算距离准确但数据一大就慢KD 树和球树是两种加速索引结构Sklearn 的 auto 会自动选一个合适的大多数时候不用操心。但如果你数据量到了几十万以上又清楚数据的分布形态可以手动指定实战里我很少动这个参数它属于“知道有这东西就行”的类型。3. 实操手把手跑通KNN分类与回归3.1 分类实战鸢尾花数据集拿经典的 iris 数据集做演示再合适不过。鸢尾花数据集包含三种花特征是花萼和花瓣的长宽任务是预测花的品种。数据干净、量小、类别均衡是新手验证分类模型的完美起点。from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import classification_report iris load_iris() X, y iris.data, iris.target X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) model KNeighborsClassifier(n_neighbors5, weightsdistance) model.fit(X_train_scaled, y_train) y_pred model.predict(X_test_scaled) print(classification_report(y_test, y_pred, target_namesiris.target_names))在我常用的随机划分下K5、distance 权重时测试集准确率通常能到 0.9667三个类别的 F1 值都接近 1.00。整套流程从读取数据到出报告不到 15 行已经算是“说人话”级别的 demo。有两个细节值得单独拿出来说。第一是stratifyy样本量少时按类别比例切分训练集和测试集能避免某一类全被切进测试集、导致训练集缺类的情况。对小数据集的分类任务这个参数直接写上去不用犹豫。第二是缩放器scaler要在训练集上fit_transform在测试集上只能transform千万不能用测试集的数据去重新拟合否则就是信息泄漏。3.2 回归实战加州房价预测回归这边我选用加州房价数据集。可能有人还在找经典的波士顿房价但要提醒一句Sklearn 1.2 之后把那个数据集移除了涉及数据本身的合规和隐私问题。现在常用的替代方案就是fetch_california_housing第一次运行需要联网下载之后走本地缓存。from sklearn.datasets import fetch_california_housing from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.neighbors import KNeighborsRegressor from sklearn.metrics import mean_squared_error, r2_score housing fetch_california_housing() X, y housing.data, housing.target X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) reg KNeighborsRegressor(n_neighbors7, weightsdistance) reg.fit(X_train_scaled, y_train) y_pred reg.predict(X_test_scaled) print(RMSE:, mean_squared_error(y_test, y_pred) ** 0.5) print(R2:, r2_score(y_test, y_pred))这个数据集样本约两万特征是收入中位数、房龄、房间数等 8 个维度目标值是房价单位是十万美元。实测下来如果忘记归一化R2 常常在 0.3 上下挣扎标准归一化之后能到 0.7 左右。差距就是这么大KNN 对数据预处理极度敏感这一点怎么说都不过分。3.3 数据归一化这一步别偷懒第一次踩 KNN 的坑几乎都栽在归一化上。因为距离计算依赖特征的实际数值大小如果某个特征动辄几万另一个特征只有 0 到 1那前者的数值天然主导了距离计算小数值特征的信息等于被无视了。这种情况不是模型的问题是数据形态的问题。归一化有两种主流做法StandardScaler标准化成均值 0、方差 1MinMaxScaler缩放到 [0,1] 区间。KNN 我习惯用StandardScaler因为在多个特征分布差异大的场景里它的表现更稳定。选哪种也好记如果数据分布近似正态用 StandardScaler如果数据本来就在有限区间里MinMaxScaler 也行。再强调一遍前面提过的坑fit_transform只能用在训练集测试集只用transform。很多人为了图省事把全量数据拿去做fit_transform严格来说这会让评估结果虚高。因为测试集的信息已经提前参与到了预处理参数的计算里相当于“开卷考试”。做机器学习数据处理这种细节最不能偷懒。4. 模型评估与参数调优4.1 k值到底选多少交叉验证与肘部法则k 值是 KNN 最核心的超参数怎么定网上常说的“肘部法则”在 KNN 里也能用画一条“k 值—错误率”曲线随着 k 增大错误率先降后升最低点附近就是比较合适的选择。不过“用眼睛看曲线”终归有点主观我更推荐直接上交叉验证让数据说话。交叉验证的思路很简单把训练集切成 5 份每轮拿 4 份训练、1 份验证轮完 5 次后把验证误差取平均。Sklearn 的cross_val_score几行就能跑完 k1 到 30 的完整评估在小数据集上完全不费劲。最终选平均误差最小的那个 k 值比肉眼看的肘部位置要可靠得多。4.2 用GridSearchCV做网格调参调参不用手写循环GridSearchCV把交叉验证和参数搜索封装在一起是处理这类问题的标准方案。from sklearn.model_selection import GridSearchCV from sklearn.neighbors import KNeighborsClassifier param_grid { n_neighbors: list(range(1, 21)), weights: [uniform, distance], p: [1, 2] } grid GridSearchCV( KNeighborsClassifier(), param_grid, cv5, scoringaccuracy, n_jobs-1 ) grid.fit(X_train_scaled, y_train) print(grid.best_params_) print(grid.best_score_)在刚才的 iris 数据上网格搜索给出的最优参数大概率是 n_neighbors3 到 5、weightsdistance、p2交叉验证得分在 0.97 上下。这种搜索的代价是训练时间乘以参数组合数。KNN 的“训练”其实只是存样本真正耗时在 predict 阶段会被交叉验证反复调用所以参数组合一多还是会慢。n_jobs-1开启多核并行能明显提速建议保留。有个原则要守住GridSearchCV默认用训练集内部做交叉验证测试集一定要留到搜索结束再碰。如果拿调参后的模型再回训练集上算分数那是自欺欺人。最好是一开始就把测试集切出去调参期间完全不看。4.3 分类与回归应该看哪些指标分类任务里准确率accuracy最直观但类别不平衡时容易被骗。比如 99% 样本都是负类模型全猜负类也有 99% 准确率。这时候必须搭配精确率Precision、召回率Recall和 F1。逻辑回归做信用评估时特别强调 F1KNN 分类项目也一样别只盯着 accuracy 一个数。回归任务则看 RMSE、MAE 和 R2。MAE 解释起来最亲民预测平均偏了多少单位跟目标值一致。RMSE 对大的偏差惩罚更重因为平方项放大了离群误差。R2 表示模型解释了目标变量多少方差0.7 可以粗略理解成“比单纯用均值预测好 70%”。评估回归模型时我习惯三个都打出来别只看 R2。还有个小经验KNN 回归评估时尽量多跑几个随机种子。回归目标经常有离群点单个种子的 RMSE 波动可能很大。多取几个种子平均一下结论才稳不然你可能因为一次划分的坏运气错过一个本来不错的模型。5. 常见问题与排查技巧实录5.1 特征尺度不同导致距离失真症状很好认模型在验证集上一塌糊涂代码逻辑检查下来却没毛病。十有八九是没做归一化。我自己在业务数据上遇到过类似情况特征里有“用户年龄”还有“账户余额”余额动辄几万距离计算几乎变成只看余额一个维度调参调了半天纹丝不动归一化之后立刻正常。排查技巧就一条先看各特征的均值、标准差和数值范围如果数量级差出几个零别想别的先缩放再说。缩放之后再跑一版模型结果常常天壤之别。这一步看起来平平无奇但踩过坑的人都知道它有多关键。5.2 样本量大、维度高时预测缓慢KNN 的软肋就是预测慢。训练当然瞬间完成但每次预测都要遍历一遍历史样本。你给它 50 万条训练数据线上单条预测可能就会到几十毫秒并发一上来立刻顶不住。解决办法大概四条路。第一是限制样本量对训练集做随机抽样再建模。第二是降维先压缩特征空间再跑 KNN。第三是用 KD 树或球树加速对应algorithm参数。第四如果对速度要求极高可以考虑类似 nmslib、Pynndescent 这类近似最近邻方案精度略有损失但速度能提升好几个数量级。图像这类高维数据实战里几乎不用 KNN直接上深度学习或专门的视觉模型更靠谱这也是计算机视觉和机器学习常规用法差异挺大的原因之一。5.3 类别不平衡、缺失值与重复样本类别不平衡时KNN 的投票机制天然偏向多数类。两个急救方案一个是用weightsdistance让少数类样本即使数量少也能靠“靠得近”拿到话语权另一个是适当增大 k 值让更多邻居参与投票避免极端情况下少数类邻居完全被淹没。样本量再大一点还可以对少数类做采样但小数据集上还是先改参数比较稳妥。缺失值处理没什么捷径KNN 本身不支持缺失值你必须在建模前处理好。常见做法是删除、均值填充或中位数填充。但要注意填充发生在交叉验证之前还是之后会影响评估的公正性。重复样本也会带来麻烦完全相同的样本如果正好落在邻居圈里等于投票时一方多了好几票。处理上可以先做去重但缺点是可能丢信息需要权衡。5.4 误把库API当成网络API401报错咋排查最后说一个特别常见、也特别容易混淆的点。经常有同学看到关键词里写着“API”就跑来问为什么我调用 KNN API 时返回 401 错误这里必须分清楚Sklearn 里的KNeighborsClassifier、KNeighborsRegressor是本地算法接口在你自己机器上运行根本不经过网络也不会返回 HTTP 状态码。401 这类报错通常是调用外部机器学习平台、模型服务或数据接口时才出现的报错里常见一句incorrect api key provided直白翻译就是“提供的 API 密钥不对”。遇到这种报错我一般的排查顺序如下先确认 key 有没有复制完整。很多 key 带固定前缀、字符串较长复制时容易漏掉后半段。再确认请求头里有没有正确带上认证信息有些平台要求自定义 headerSDK 路径不同写起来也不一样。最后查 key 的权限范围是不是只开放给了某个服务、某个 IP或者额度已经用完还不自知。实在不行重新生成一个 key 再试一次。另外提醒一个安全习惯key 别硬编码在代码里放到环境变量用完注意清理这在任何项目里都成立。最后分享一点我的个人习惯。现在接到新的分类或回归需求我依然会先用 KNN 跑一版 baseline哪怕最终方案大概率是梯度提升树或者别的更重的模型。原因是 KNN 几乎不做假设结果稳定跑出来的分数能给我一个“这个任务大概能到多少”的底线。后续模型如果连这个底线都打不过那说明问题出在数据而不是模型。踩坑多了之后还有一个体会KNN 的调参顺序一定是先归一化、再调 k、后考虑权重顺序反了容易白忙活一场。如果你做的是小样本、低维度的任务它甚至能直接成为最终方案数据量上来以后就把它当基准再换更快的模型。这样安排我基本没吃过亏。
返回列表