尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Sklearn实现KNN分类:从特征缩放到调参评估的完整实战指南

Sklearn实现KNN分类:从特征缩放到调参评估的完整实战指南 如果让我给刚接触机器学习的人推荐第一个分类算法我大概率会选 KNN。原因不是它效果最好而是它足够笨笨到你能一眼看穿它在做什么。这个被称作懒惰学习的算法在 sklearn 里跑一次分类任务只需要三行核心代码但它牵扯出的问题——特征缩放、距离度量、超参数选择、数据划分——恰好是每一个监督学习项目都要面对的基础问题。这篇文章不打算把 KNN 讲得玄乎而是用做项目的方式带你完整走一遍用 Sklearn 实现 KNN 分类的流程从数据准备到调参评估顺带把那些文档里不会明说但实际项目中一定会踩的坑都翻出来。无论是课程作业、比赛入门还是工作里的第一个快速验证方案这套流程都能直接照搬。1. 分类任务里KNN 为什么值得第一个学1.1 KNN 的分类逻辑离谁近就归谁KNN 的全称是 K-Nearest Neighbors中文叫 K 近邻。它的核心思想用一句话就能说清楚一个新样本的类别由离它最近的 K 个训练样本投票决定。这里没有训练模型这个概念。线性回归要拟合一条直线决策树要递归划分特征空间神经网络要反向传播更新权重——KNN 统统不干这些事。你喂给它带标签的数据它只是原样保存下来等到你要预测新样本时它才开始真正干活算出新样本到所有已保存样本的距离挑出最近的 K 个让它们投票票数最多的类别就是预测结果。这就是为什么它被称为懒惰学习lazy learning。它不是懒惰到不干活而是把计算压力推迟到了预测阶段。你可以把它想象成一个新搬到小区的人想知道小区住户什么职业最多于是挨家挨户问了附近 K 户人家少数服从多数得出了一个大概率是程序员小区的结论。这个直觉逻辑带来两个直接好处一是实现简单调参维度少适合初学者建立完整的建模流程概念二是天然支持多分类不用像二分类模型那样做 OvR 或 OvO 转换直接输出所有类别中的得票冠军。1.2 KNN 的适用边界与典型误区我见过不少新手一上来就把 KNN 当成万能分类器哪个任务都先跑一遍 KNN然后迷惑为什么线上效果这么差。不是 KNN 不行是它有自己的脾气——它适合的场景有几个明确特征。第一数据量不能太大。KNN 预测时需要计算新样本与所有训练样本的距离样本量从一万涨到十万预测耗时接近线性增长。如果你做的是用户实时推荐这种高并发场景KNN 裸跑基本不可用。第二特征维度不能太高。维度升高后样本间距离会趋向于看起来都差不多这就是常说的维度灾难。KNN 的整个决策依据就是距离远近一旦距离稀薄化投票结果近似随机。经验上几十维以内的稠密特征问题不大文本 TF-IDF、图像像素这种上千维稀疏场景KNN 很难出效果。第三特征必须做缩放。这一点太容易被忽略我放在下一章专门说它是 KNN 实战中影响最大但很多人最后才发现的问题。顺便说一句网上常有人把 KNN 和 KMeans 混在一起聊这俩名字看着像实际一个是分类一个是聚类本质完全不同。我留到后面专门展开讲。2. 数据预处理缩放这一步不做KNN 就白做了2.1 一个栗子看懂欧氏距离如何被大数值带偏先看一个具体例子。假设你要预测一个用户是否购买某商品特征有两个年龄20 到 60 岁和年收入3 万到 100 万。KNN 最常用的距离是欧氏距离d sqrt((x1_age - x2_age)^2 (x1_salary - x2_salary)^2)一个 25 岁、年收入 5 万的用户 A和一个 26 岁、年收入 8 万的用户 B两者收入差的平方是 (50000 - 80000)^2 900000000而年龄差的平方只有 1。在这个距离公式里年龄相差 35 岁带来的影响还不如收入差 3000 块的零头大。结果就是KNN 的最近邻几乎完全由收入决定年龄这个特征形同虚设。这不是数学错误而是数据尺度带来的本能偏差。欧氏距离默认所有特征的单位长度是等价的但现实中不同特征的量纲完全不同——有的特征在 0 到 1 之间有的动辄上万。KNN 对特征尺度极度敏感这是它在所有分类算法中最突出的特点之一。同样是做分类树模型就不太在乎这个因为它们做的是特征划分按阈值切分单个特征的尺度不影响分裂准则。这也是很多从决策树入门的同学直接用 KNN 时翻车的原因。2.2 StandardScaler 还是 MinMaxScalersklearn 里最常用的缩放器有两个StandardScaler 和 MinMaxScaler。StandardScaler 把每个特征变成均值为 0、标准差为 1 的标准正态分布形态。公式是 z (x - mean) / std。它不把数据压到固定区间所以即使有离群点整体分布也能稳住。MinMaxScaler 把特征缩放到 [0, 1] 区间x_scaled (x - min) / (max - min)。如果特征本身是均匀分布又没有离群点这个缩放很直观。我的选择经验是KNN 默认优先用 StandardScaler。原因有两个。第一KNN 算的是距离标准化后的特征都围绕均值波动量级一致距离不会被单一特征绑架第二MinMaxScaler 对离群点极度敏感比如年龄特征里混入一个 300min 和 max 直接被拉偏大部分人被压缩到很窄的区间等于特征被压扁了。之前我做过一个信贷评分的小项目其中收入特征有几个异常值用 MinMaxScaler 之后 KNN 效果惨不忍睹换成 StandardScaler 立刻正常。如果你确认数据干净、区间天然有界比如像素 0-255、年龄 0-100用 MinMaxScaler 也完全可行。还要注意一个细节scaler 只能用训练集数据 fit然后用 fit 好的 scaler 去 transform 测试集。这是 sklearn 的规范要求但实操里经常有人图省事把全部数据一起 fit_transform再用 train_test_split 去切。这个习惯会带来数据泄露测试集的信息提前渗透进了训练流程评估结果会虚高。正确的写法是scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)不要对测试集单独 fit每一行都照这个结构写。2.3 类别型特征的处理如果数据里有性别、城市这类类别型特征KNN 不能直接吃原始字符串。常见的做法是 One-Hot 编码pd.get_dummies 或 sklearn 的 OneHotEncoder或者标签编码OrdinalEncoder。这里有个容易被忽略的坑无序类别特征不能直接用 OrdinalEncoder否则会强行制造大小关系。比如颜色特征有红黄蓝如果编码成 0、1、2KNN 算距离时就会认为黄(1) 比蓝(2) 更接近红(0)这完全是人为制造的序关系。无序类别应该用 One-Hot 编码每个类别变成一个二值维度距离才没有偏袒。但 One-Hot 也有代价特征维度膨胀。原本一个颜色特征编码后变成三列。对 KNN 来说维度增加意味着距离稀释加重所以类别特别多比如上千个类目时KNN 就比较吃力了这也是它的典型局限之一。还有一类介于连续和类别之间的数据比如评分1-5 星有人当连续特征直接缩放有人当类别做 One-Hot。我的意见是如果评分本身有明确排序和等距含义当连续特征处理更合理如果只是收藏/点赞/转发这种并列动作One-Hot 更安全。3. 最小可用流程Sklearn 跑通一次 KNN3.1 train_test_split 里容易忽略的分层参数建模第一步通常是划分训练集和测试集。大多数教程都会写这样一行from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3, random_state42)很多人不知道后面还能加一个 stratify 参数X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy )stratifyy 的含义是划分时保持 y 的类别比例在训练集和测试集中一致。如果一个二分类数据里正样本只占 10%不设置 stratify随机切分后有概率让测试集里正样本一个都没有模型评估直接失去参考意义。KNN 这种依赖投票的算法类别分布被切歪了结果会有明显偏差。我在自己项目里的默认习惯是只要做分类train_test_split 一定带 stratify数据量特别大、类别分布本身很均匀时不设影响不大但设了也无害。random_state 固定成某个具体值也是好习惯否则每次运行划分结果不同调参对比就会变成玄学比赛。3.2 fit、predict、score 三段式sklearn 的模型接口是高度统一的fit 拟合、predict 预测、score 评估。KNN 的 fit 在内部做了什么它并没有学习决策边界只是把训练数据和标签保存下来sklearn 的 KNeighborsClassifier 实际会用树结构组织数据但逻辑上仍然是把样本存下来。predict 才真正开始计算距离。这个特性带来一个很多人没意识到的好处你可以随时往里增加训练样本代价非常小。如果项目里样本是流式到达的KNN 天然适应增量更新Sklearn 的 KNeighborsClassifier 提供了 partial_fit 吗没有这是它一个限制。但通过手动把新样本 append 到训练集再重新 fit成本也比重新训练神经网络低太多。score 方法默认返回准确率accuracy即预测正确的比例。入门阶段用 score 够用但实际项目里只看准确率会漏掉很多信息这个留到第 5 章细说。3.3 最小可复现代码我用 sklearn 内置的鸢尾花数据集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 data load_iris() X, y data.data, data.target X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test) knn KNeighborsClassifier(n_neighbors5) knn.fit(X_train, y_train) print(训练集准确率:, knn.score(X_train, y_train)) print(测试集准确率:, knn.score(X_test, y_test))这一步跑起来输出大约在 0.95 以上。鸢尾花数据集很简单特征连续且基本线性可分KNN 在这里基本无敌手。但注意不要因为鸢尾花效果好就以为 KNN 天下无敌这只是数据集相对友好。真正检验模型的是更复杂的数据。理解这个流程之后下一步就是深入调参。4. 调参实战K 值、距离度量和权重4.1 K 值怎么选K 太小过拟合K 太大欠拟合K 是 KNN 里最重要的超参数它决定了每次投票的邻居圈子有多大。K 取得太小比如 K1模型只认最近的一个邻居。优点是能精确拟合训练数据里的任何细节缺点是极度容易过拟合——训练集里一个噪声点就会让决策边界跟着扭曲。K1 在训练集上准确率通常接近 100%测试集上往往惨不忍睹。K 取得太大比如 K 等于训练样本数的一半模型把大半个数据集都拉进来投票个别异常点的影响被稀释决策边界变得非常平滑但已经平滑到丢失局部信息欠拟合。极端情况下K 等于总样本数时模型永远预测全局多数类。实操里常用的几个参考方法使用训练样本总数的平方根作为起点再上下浮动试探。比如 150 个样本sqrt(150) 约等于 12可以在 5 到 15 之间搜索。K 选奇数两类问题下避免平票。多分类里可以看投票数是否并列并列时 sklearn 会按内部顺序取索引小的类不一定合理。用 GridSearchCV 对多个 K 值做网格搜索这是最稳的方式下一小节会说。我之前做过一个文本情感分类的小项目样本约 800 条特征维度 10。调出来的最优 K 是 7而默认 K5 的效果差了一个多点。别看一个点的提升不大在真实项目中一个超参数的合理选择往往就是 top 排行榜和及格线之间的差距。4.2 距离度量欧氏、曼哈顿与闵可夫斯基距离KNeighborsClassifier 里通过 p 参数控制距离度量方式。默认 p2对应欧氏距离p1对应曼哈顿距离p 取其他值就是闵可夫斯基距离Minkowski Distance是欧氏与曼哈顿的推广形式。欧氏距离是直线距离适合特征在每个维度上量级一致、相关性不强的连续数据。曼哈顿距离走的是网格路径在特征维度较高时往往比欧氏更稳因为高维下欧氏距离中平方项的差异容易被放大几个大差值的特征会压过其他特征曼哈顿距离用绝对值对这种干扰的抵抗力更强。文本数据、向量嵌入这类数据还可以直接用 metriccosine 做余弦相似度。尤其推荐试试。KNN 拿余弦距离做文本分类效果经常比欧氏距离好不少因为它不受向量长度影响只看方向一致性。另一个容易忽略的参数是 weights。默认是 uniform所有邻居的投票权重一样改成 distance 时越近的邻居权重越大。这个参数对 K 值大、局部密度不均的数据很有用。比如有些样本周围的邻居分布稀疏distance 权重能突出近邻的作用效果往往比 uniform 好但计算量会稍微增加。4.3 GridSearchCV 自动搜索最优参数手工试 K3、5、7、9 也能调但参数一多就乱。sklearn 的 GridSearchCV 做网格搜索直接帮你把参数字典的组合都跑一遍再用交叉验证评估每组参数的效果。from sklearn.model_selection import GridSearchCV from sklearn.neighbors import KNeighborsClassifier param_grid { n_neighbors: [3, 5, 7, 9, 11, 15], weights: [uniform, distance], p: [1, 2] } knn KNeighborsClassifier() grid GridSearchCV(knn, param_grid, cv5, scoringf1_macro, n_jobs-1) grid.fit(X_train_scaled, y_train) print(grid.best_params_) print(grid.best_score_) best_knn grid.best_estimator_ test_acc best_knn.score(X_test_scaled, y_test) print(测试集准确率:, test_acc)这里 cv5 表示五折交叉验证每个参数组合都会训练 5 次结果更稳。scoringf1_macro 在多分类里比准确率更能看出模型在各类别上的均衡表现。我遇到过一些人觉得 GridSearchCV 是过拟合测试集的操作其实不然。网格搜索在训练集内部做交叉验证测试集从头到尾没有参与参数选择只要你在调完参后再独立评估测试集就没有信息泄露。真正要小心的是不要看着测试集结果再去调参那样调出来的最优只是在记测试集答案。5. 模型评估不要只看 score5.1 混淆矩阵与分类报告怎么读准确率是最直观的指标但它会骗人。一个 99% 样本都是类别 A 的二分类数据集模型只要全部预测 A准确率就有 99%实际上对类别 B 一窍不通。KNN 在类别不平衡数据上的表现尤其需要结合混淆矩阵和分类报告来看。sklearn 里出混淆矩阵和分类报告很简单from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay y_pred best_knn.predict(X_test_scaled) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, target_namesdata.target_names))分类报告里每个类别有四列precision精确率预测为该类别的样本里有多少是真正该类别。可以理解为我没认错的能力。recall召回率真实属于该类别的样本里有多少被正确找回来了。可以理解为我别漏掉的能力。f1-scoreprecision 和 recall 的调和平均两个指标打架时综合看的指标。support该类别的真实样本数。多分类任务里报告还会给出 macro avg宏平均每个类先算指标再求平均不受类别数量影响和 weighted avg加权平均按各类样本占比加权。我一般优先看 macro avg因为 weighted avg 会被样本多的类别带着走类别不平衡时容易掩盖小类别的劣势。5.2 什么时候看准确率什么时候看 F1这个问题没有标准答案跟业务强相关。以风控场景为例真正坏账的客户可能只有 1%模型如果只追求准确率把所有人都判成正常就能拿到 99% 的准确率但业务上显然不可接受。此时应该紧盯坏客户的 recall——宁可多拦截一些误伤的客户也不能漏掉真坏账。反过来在商品推荐场景推荐错了用户只是刷掉一条信息流成本低更看重用户想看的商品是否被推荐上来precision 和 recall 要权衡取舍。KNN 的默认评估是准确率但如果你做的是一个类别不平衡的分类任务建议在 GridSearchCV 里直接换 scoring。比如二分类用 scoringroc_auc多分类用 scoringf1_macro。最好一开始就确定好业务关注的核心指标不要让模型毫无目标地追求准确率。我自己的习惯是先跑一遍默认模型拿到混淆矩阵确认错误集中在哪个类别再返回去看特征、看数据而不是立刻无脑堆调参。很多问题的根源根本不在 K 值而在特征质量或数据分布。KNN 这类简单模型的诊断路径通常很短这也是我喜欢拿它做 baseline 的原因——它反应快、可控一旦 baseline 就暴露出明显问题说明数据预处理环节大概率有大事要做。6. 红酒分类完整实战从零到调参的全流程复盘6.1 数据集概况为什么选红酒Wine 数据集是 sklearn 内置的经典多分类数据集共 178 个样本、3 个类别不同品种的葡萄酒、13 个连续型特征比如酒精含量、苹果酸、灰分、黄酮类化合物等化学检测数值。它很适合做 KNN 实战原因有三个样本量小特征全是连续值类别是天然多分类。这让我们能把注意力完全集中在特征缩放—建模—调参—评估这条主线上不用被数据清洗分散精力。按老规矩先划分数据并做标准化from sklearn.datasets import load_wine wine load_wine() X, y wine.data, wine.target print(X.shape, y.shape) print(wine.feature_names) print(wine.target_names)6.2 建模与调参过程直接跑一个不加缩放的 KNN 作对照你会看到准确率大概在 0.6 到 0.7 之间缩放之后再跑跳升到 0.9 以上。这个对照是理解特征缩放对 KNN 有多关键最有力的实验。我已经不只一次在分享里建议读者亲手跑这个对比比看任何理论都直观。完整流程如下from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.preprocessing import StandardScaler from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import classification_report, confusion_matrix X_train, X_test, y_train, y_test train_test_split( wine.data, wine.target, test_size0.3, random_state42, stratifywine.target ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) param_grid { n_neighbors: list(range(3, 20, 2)), weights: [uniform, distance], p: [1, 2] } grid GridSearchCV( KNeighborsClassifier(), param_grid, cv5, scoringf1_macro, n_jobs-1 ) grid.fit(X_train_scaled, y_train) print(最优参数:, grid.best_params_) print(交叉验证 F1:, grid.best_score_) best_model grid.best_estimator_ y_pred best_model.predict(X_test_scaled) print(测试集准确率:, accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred, target_nameswine.target_names, digits3)) print(confusion_matrix(y_test, y_pred))在我本机的跑分里最优参数通常落在 n_neighbors7、weightsdistance、p1 附近测试集准确率约 0.960.98。曼哈顿距离在红酒这种特征量级不完全一致的场景下超过了欧氏距离这并不意外因为特征之间存在尺度差异虽然标准化后量级统一了但绝对差值的累积方式不同曼哈顿距离对多维累加更鲁棒。6.3 结果分析关注被错分的样本看混淆矩阵时重点不是盯着对角线上的高数字高兴而是要问右下角那个类别 2 被错分类别 1 的样本到底长什么样把预测错误的样本单独挑出来看原始特征往往会发现它们处于两个类别的边界地带也就是特征数值介于两类典型值之间。KNN 对这类边界样本天然敏感因为少数几个邻居就可能左右投票结果。此时你可以做两件事一是检查这个样本周围的训练样本是否存在标注噪声二是尝试降低 K 值或改用 distance 权重给近邻更大话语权让决策边界更贴合局部密度。我做红酒案例复盘时发现一个小规律被错分的样本通常在脯氨酸这个特征上数值比较低。这个特征在类别 1 和类别 2 上有明显重叠区域单靠它区分不了必须结合黄酮类化合物等其他特征才能分隔。这说明什么问题如果你的模型在某个类别上持续犯错看看混淆矩阵里错误集中在哪几个类之间再回到特征分布图里找原因往往比盲目调参更高效。7. 把 KNN 用对地方和 KMeans 的关系以及性能优化7.1 KNN 和 KMeans名字像本质一点不像因为名字实在太像KNN 和 KMeans 的关系被问过无数次。简单说KNN 是监督学习算法用于分类/回归需要带标签的训练数据。它的 K 表示取多少个近邻来投票。KMeans 是无监督聚类算法不需要标签用来把数据划分成 K 个簇。它的 K 表示分成多少个簇。两者都依赖距离计算这是它们仅有的一点交集。KMeans 通过迭代更新簇中心来优化簇内距离KNN 则直接在样本间做距离度量。如果你把 KMeans 得到的簇标签当成类别标签喂给 KNN那只是赋予标签语义两者底层仍然是完全不同的东西。我想强调的是机器学习里很多看着像的概念用途天差地别。做分类任务时用 KNN做用户分群、数据探索时用 KMeans。把 KMeans 当分类器用结果一定是灾难把 KNN 拿去跑无监督聚类更是完全无从下手。7.2 样本量变大时 KNN 的性能优化思路KNN 真正的软肋是预测时的时间复杂度。训练阶段它几乎不耗时但每预测一个样本都要和训练集里所有样本算一次距离复杂度是 O(n)。当训练样本达到百万级一次预测要跑上百万次距离计算线上服务根本撑不住。sklearn 的 KNeighborsClassifier 也不是完全没优化。它在algorithm参数里有几个选择brute是暴力遍历所有样本算距离kd_tree和ball_tree是用树结构组织数据减少无效距离计算auto让 sklearn 根据数据自动选择。在低维数据上KD 树效果很好高维数据上球树Ball Tree更稳。knn KNeighborsClassifier( n_neighbors7, algorithmball_tree, leaf_size30 )leaf_size 控制树的叶节点大小影响构建和查询速度之间的平衡一般默认 30 即可不用过度调。如果树结构仍然撑不住现实项目里通常的解法是降维PCA 压缩特征到几十维、对训练样本做聚类后用簇中心代替原样本、或者换用能够显式学习决策边界的模型逻辑回归、SVM。KNN 适合作为快速 baseline 和小规模数据集的最终模型它在工业级推荐、超大规模检索里的位置已经被基于向量索引的近邻搜索方案所取代——但那是另一个话题了。从入门到能把这套流程用得熟练我当时花了大概两周。现在回头看KNN 真正教会我的不是调参技巧而是一个朴素但常被忽略的认识机器学习的上限很多时候从数据预处理那一刻就已经决定了。如果你也在用 Sklearn 跑分类任务不妨从 KNN 开始先把它彻底搞明白再去看那些复杂的模型。这条路的性价比比一上来就啃深度学习高得多。
返回列表