ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

KNN鸢尾花分类实战:从数据预处理到模型调优的完整指南

KNN鸢尾花分类实战:从数据预处理到模型调优的完整指南 开篇先聊点实际的。前几天帮一个刚入门的朋友排查代码他照着网上的鸢尾花分类例子敲了一遍KNN 算法也能跑结果却一塌糊涂。我一看发现他把标准化放在了切分训练集之前整个测试集的信息等于提前“看”过了。这种问题在刚接触 scikit-learn 的人群里非常常见也恰恰说明鸢尾花分类这个看似经典的入门项目背后的细节远比想象得多。很多人把“鸢尾花分类”当成机器学习的第一堂必修课是因为它足够简单只有 150 个样本、4 个特征、3 个类别。但简单不代表没有讲究。KNN 算法作为非参数模型的代表不需要训练权重核心逻辑就是“找邻居、看投票”。真正要理解透的是距离怎么算、k 值怎么选、特征缩放为什么不可省以及评估方式怎样才公平。这篇文章就把整个流程拆开讲从环境准备到模型调优再到避坑经验全程用 scikit-learn 跑通给你一套可以直接复用的实操路线。1. 项目整体思路为什么拿鸢尾花练手1.1 鸢尾花数据集解决了什么问题鸢尾花数据集最早由统计学家 Fisher 整理包含三种鸢尾setosa、versicolor、virginica每种 50 个样本。每条样本记录花萼长度、花萼宽度、花瓣长度、花瓣宽度 4 个特征。这个数据集的经典之处在于特征之间区分度较高甚至只看花瓣长度和花瓣宽度就能把 setosa 和另外两类分开非常适合用来演示分类流程。很多在线实训平台——比如头歌或者其他学习社区里的“基于KNN算法模型的应用案例”第一关——都喜欢拿这个数据集做练习原因就是它既能体现完整的机器学习流程又不会让新手被数据清洗拖住脚步。用这个项目练手核心价值其实有两点。第一你能快速理解模型评估的基本流程训练集、测试集、准确率、混淆矩阵。第二你能直观感受到 KNN 这类基于实例的算法的特性——训练阶段几乎没有“学习”过程但预测阶段要做大量距离计算。这种特性在大数据集上的利弊可以在这个小数据集上先建立直觉。1.2 KNN在这个场景里的定位KNNK-Nearest Neighbors算法是所有分类算法里最容易解释的一个一个样本未知类别就去看它周围最近的 k 个样本k 个里面哪个类别占多数就把它归到那个类别。这种“少数服从多数”的策略在鸢尾花数据上表现相当稳定默认参数下准确率通常就能达到 95% 以上。它的另一个特点是几乎没有需要训练的参数——本质上只有 k 值和距离度量方式。这意味着你不用像训练神经网络那样考虑过拟合、梯度更新也不用像 SVM 那样理解核函数只需要把样本特征之间的距离算清楚。正因如此KNN 特别适合作为机器学习入门的第一个算法让人把注意力放在“数据和评估”上而不是陷入复杂的数学推导。但在实际项目中KNN 有两个明显短板一是当特征量纲不一致时欧氏距离会被量级大的特征主导二是当特征维度很高时距离区分度会急剧下降。这两个问题在鸢尾花数据集中都存在一定的表现却也有足够的空间去调整和验证。所以用鸢尾花来做 KNN 实战既能看到算法优雅的一面也能提前踩中后续工业场景中容易遇到的问题。2. 环境准备与数据初探2.1 安装依赖与初始导入动手之前建议先把环境准备好。我常用的组合是 Python 3.8 以上版本搭配 scikit-learn 1.0 以上版本、pandas、numpy、matplotlib 和 seaborn。如果你用的是 Anaconda这些库大多已经预装直接导入就行如果没有可以用 pip 装pip install scikit-learn pandas numpy matplotlib seaborn装完之后第一步是把常用库导入进来顺便把数据加载出来看看import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split, GridSearchCV, cross_val_score from sklearn.preprocessing import StandardScaler from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score, classification_report, confusion_matrix iris load_iris() print(iris.keys())运行之后你会看到dict_keys([data,target,frame,target_names,DESCR,feature_names])。data是特征矩阵target是标签target_names是类别名feature_names是特征名。这里不建议直接把它转成 DataFrame 后丢掉原始对象因为后续做预测和解释时target_names和feature_names会经常用到。2.2 用散点图快速认识数据结构只打印数据维度不够直观。数据量不大我建议直接画几张图把特征之间的关系看清楚。鸢尾花数据有 4 个特征两两组合就是 6 张散点图用 seaborn 一键就能完成df pd.DataFrame(iris.data, columnsiris.feature_names) df[species] pd.Categorical.from_codes(iris.target, iris.target_names) sns.pairplot(df, huespecies) plt.show()运行这幅图你会看到非常典型的规律setosa 这个类别在花瓣长和花瓣宽上明显分离出去而 versicolor 和 virginica 虽然有重叠但总体来说也呈现出一定的聚类趋势。这说明数据在二维投影下已经有了不错的可分性KNN 这种基于距离的方法会表现得很好。这种数据可视化看起来简单但价值很高。它可以帮助你预判模型的上限如果某些类别在特征空间里完全重叠再好的算法也很难分开反过来如果类别自然分离KNN 等相对朴素的算法也能取得不错的效果。所以我在做任何分类项目时都会先画一遍特征分布图再动手建模这个习惯可以帮我省掉很多无意义的调参时间。2.3 谨记特征尺度的隐藏问题看数据时还有一个容易被忽略的细节4 个特征的数值范围不同。花萼长度大约是 4.3 到 7.9花瓣宽度只有 0.1 到 2.5。如果你直接把这些原始数值丢给 KNN计算距离时花萼长度会天然占据主导地位但并不意味着它的判别力更强。KNN 的距离计算默认是欧氏距离所有特征被平等对待。如果特征的量纲不一致那就等于在模型里人为给某些特征加了更高的权重。好在鸢尾花数据集的特征范围相差并不夸张标准化前后的准确率差距有时只有几个百分点但在真实业务数据里量纲差异往往达到几千倍不处理的话模型基本会失效。所以在做更复杂的项目之前有一点必须先养成习惯特征数值型变量尤其是做距离类算法时先做标准化或归一化。不要等到模型结果异常了再回头排查。3. 拆开KNN原理、距离和k值3.1 “物以类聚”的三步决策KNN 的算法步骤用大白话讲就是三步选定邻居数量 k计算待预测样本与所有训练样本的距离找到最近的 k 个样本执行投票。投票方式在 scikit-learn 里由weights参数控制。默认值是uniform即每个邻居的投票权重相同另一个常用值是distance距离越近权重越大让离得更近的样本对结果有更大话语权。对于类别数量不均衡的数据distance通常比uniform更稳健。这里还要解释一个容易混淆的概念KNN 的训练与预测阶段不对称。训练阶段仅仅是把数据存下来没有任何“学习”动作预测阶段则需要把新样本和每一个已知样本做距离计算数据量越大预测越慢。这也是为什么 KNN 在中小型数据集上很好用但一旦到了百万级样本每一次预测都变成一次“全表扫描”性能就很难接受了。3.2 为什么必须先做标准化KNN 的决策完全建立在距离上所以距离的定义对这个算法至关重要。欧氏距离的公式是[ d \sqrt{\sum_{i1}^{n}(x_i - y_i)^2} ]如果其中一个特征的数值范围是 0 到 1000另一个是 0 到 1那么距离计算会被第一个特征完全控制。想要每个特征都平等地参与决策就需要把特征缩放到相近的范围。scikit-learn 里最常用的方法是StandardScaler它把每个特征变成均值为 0、标准差为 1 的分布。但使用StandardScaler有一个关键细节必须只对训练集计算均值和标准差然后用同样的均值和标准差去变换测试集。如果先对全量数据标准化再切分训练集和测试集测试集的信息就间接进入了训练过程这叫做数据泄漏。我在文章开头提到的那位朋友踩的正是这个坑。3.3 k值选择的经验法则k 值大小直接决定模型的偏差和方差。k 值太小比如 k1模型会被单个离群点带偏决策边界变得非常曲折k 值太大比如 k50会把离得很远的样本也拉进投票决策边界过于平滑导致分类精度下降。对鸢尾花数据集来说一个比较实用的区间是 3 到 15。你可以通过观察准确率随 k 值变化的曲线来确定合适的范围。如果画出的曲线呈锯齿状说明方差偏大可适当增大 k 值如果准确率在多个 k 值下都差不多就选择较小的 k 值因为计算量更小。这里有一个经验判断方法如果两类样本在边界处有重叠大的 k 值容易把边界附近的样本误分类如果数据本身比较干净大的 k 值反而能抵御噪声。所以不要死记某个 k 值要结合交叉验证结果来判断。4. 鸢尾花分类完整实现4.1 划分训练集与测试集进行模型评估时第一步是划分数据集。这里建议用train_test_split并且开启stratify参数实现分层抽样保证训练集和测试集中三类样本的比例和原始数据一致。不然切分过程中可能出现某类样本只出现在训练集里的情况准确率看起来高实际毫无泛化能力。X_train, X_test, y_train, y_test train_test_split( iris.data, iris.target, test_size0.3, random_state42, stratifyiris.target ) print(X_train.shape, X_test.shape) # 输出: (105, 4) (45, 4)这里选择random_state42是为了让结果可复现。随机种子不设固定的话每次运行切分结果不同后续对比调参效果也会收到不必要的干扰。划分完成之后下一步是标准化。记住我刚才提到的原则先切分再标准化scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)在这里我习惯把标准化放在切分后紧跟着做而不是等到建模前。这样可以避免将来代码越写越长时不小心回头对全量数据执行了fit_transform。4.2 训练与预测的完整代码数据准备好了模型构建就很直接。scikit-learn 里KNeighborsClassifier封装了全部逻辑某行代码就可以完成初始化、训练、预测knn KNeighborsClassifier(n_neighbors5) knn.fit(X_train_scaled, y_train) y_pred knn.predict(X_test_scaled)可能有人会疑惑KNN 的fit到底在做什么实际上它只是保存训练数据和标签并没有像线性回归那样计算权重。这也是模型存储体积比较大的原因因为你把全部训练样本都存了下来。预测之后先看前几个预测结果print(预测标签:, y_pred[:10]) print(真实标签:, y_test[:10])如果正确率不错你会看到两组数字基本一致。这套流程虽然简单却是所有分类任务的骨架切分、缩放、训练、预测、评估。4.3 用准确率、分类报告和混淆矩阵评估只看准确率是不完整的。对于类别均衡的数据准确率说明不了太多问题万一模型总是把 versicolor 错分到 virginica准确率可能依然很高。所以通常还要看分类报告和混淆矩阵print(准确率:, accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred, target_namesiris.target_names)) cm confusion_matrix(y_test, y_pred) print(混淆矩阵:\n, cm)分类报告会给出每个类别的精确率、召回率和 F1 值。混淆矩阵则能直观看到具体是哪个类别之间产生了混淆。比如 setosa 这类因为天然分离明显通常不会被分错但 versicolor 和 virginica 在花瓣特征上有重叠偶尔会出现互相误判。这里最常见的错误是直接看准确率 97% 就收工。实际上要继续看哪些样本被误判找到样本特征和预测结果才能真正理解模型在这个数据上的实际能力。5. 从“能跑通”到“跑得准”关键优化5.1 用GridSearchCV找最优k值手调 k 值能建立直觉但不同数据集的“最优 k”不一样。与其反复试不如用网格搜索加交叉验证自动寻找。param_grid { n_neighbors: [1, 3, 5, 7, 9, 11, 13, 15], weights: [uniform, distance] } grid GridSearchCV( KNeighborsClassifier(), param_grid, cv5, scoringaccuracy ) grid.fit(X_train_scaled, y_train) print(最优参数:, grid.best_params_) print(最优交叉验证分数:, grid.best_score_)交叉验证这里的作用是防止把模型“过拟合”到某一次固定的训练集划分上。5 折交叉验证意味着把训练集分成 5 份轮流拿出 1 份验证其余 4 份训练最终准确率取平均。这样找到的 k 值更稳定。需要注意网格搜索调参时标准化仍要小心处理。如果你在标准化前就把数据交给GridSearchCV它内部的切分和模型训练对全量数据做fit_transform一样会产生数据泄漏。推荐的做法是使用Pipeline组合标准化和分类器确保交叉验证的每一折都独立计算均值和标准差。from sklearn.pipeline import Pipeline pipeline Pipeline([ (scaler, StandardScaler()), (knn, KNeighborsClassifier()) ]) param_grid { knn__n_neighbors: [3, 5, 7, 9], knn__weights: [uniform, distance] } grid GridSearchCV(pipeline, param_grid, cv5) grid.fit(X_train, y_train)这样写标准化和分类器的参数调优被绑定成一个整体交叉验证每一折都不会看到测试集的信息。这是实际项目中更规范也更容易维护的写法。5.2 加权投票的实际效果在KNeighborsClassifier里把weights改成distance预测时距离更近的样本拥有更大的投票权重。这个优化的意义在于当某个样本周围恰巧有几类样本混在一起时距离近的样本通常更可靠权重自然应当更高。对鸢尾花数据集来说weightsdistance的准确率通常和weightsuniform差不多因为数据本身较干净。但在样本类别边界模糊的情况下距离权重常常能带来几个百分点的提升。我建议在网格搜索中同时考察这两种权重让数据告诉你答案。不过也要留意距离权重的副作用它更容易受噪声影响。如果一个离群样本距离待测样本非常近它会直接主导预测结果。所以在真实场景里权重模式的选择还是要结合噪声水平和数据量来权衡。5.3 不同距离度量的选择除了欧氏距离scikit-learn 还支持曼哈顿距离、切比雪夫距离、闵可夫斯基距离等通过metric参数设置。metricminkowski配合p2等价于欧氏距离p1等价于曼哈顿距离。曼哈顿距离对某些特征维度有效尤其是当数据中存在较多异常值时曼哈顿距离的鲁棒性会比欧氏距离好。但鸢尾花数据量少、特征维度低不同距离度量的差异不会特别大。真正需要谨慎对待的是距离度量一旦和数据分布不匹配模型的分类边界就会不合理。所以与其上来就用复杂度量不如先把欧氏距离配合标准化做好再尝试其他度量做对照实验。5.4 决策边界可视化调参时我经常把决策边界画出来用视觉确认模型行为。选取两个特征绘制网格对所有网格点做预测再用等高线填充不同类别区域from matplotlib.colors import ListedColormap def plot_decision_boundary(X, y, model, feature_idx(2, 3)): x_min, x_max X[:, feature_idx[0]].min() - 0.5, X[:, feature_idx[0]].max() 0.5 y_min, y_max X[:, feature_idx[1]].min() - 0.5, X[:, feature_idx[1]].max() 0.5 xx, yy np.meshgrid(np.linspace(x_min, x_max, 200), np.linspace(y_min, y_max, 200)) Z model.predict(np.c_[xx.ravel(), yy.ravel()]) Z Z.reshape(xx.shape) plt.contourf(xx, yy, Z, alpha0.3) scatter plt.scatter(X[:, feature_idx[0]], X[:, feature_idx[1]], cy, edgecolork) plt.xlabel(iris.feature_names[feature_idx[0]]) plt.ylabel(iris.feature_names[feature_idx[1]]) plt.colorbar(scatter)注意这里我只选了花瓣长度和花瓣宽度两个特征是为了能够可视化。完整模型还是应使用全部 4 个特征。观察不同 k 值的决策边界k 小时边界非常碎k 大时边界平滑这个直观感受比背书上的解释更有用。6. 常见问题与避坑指南6.1 StandardScaler必须放在切分之后这是我在所有 KNN 项目里遇到的最高频错误。很多人把数据集加载完直接一句scaler.fit_transform(iris.data)然后再切分训练测试集。从流程上看代码能跑但测试集的均值和标准差已经参与到了标准化过程中模型评估结果偏乐观。解决办法很简单先train_test_split再对训练集执行fit_transform对测试集只执行transform。如果使用Pipeline则交给框架自动处理。如果你发现自己在使用cross_val_score前先做了全量标准化准确率明显偏高这很可能就是数据泄漏造成的假象。6.2 特征符号和标签编码实操问题load_iris()返回的特征都是浮点数标签是整数 0、1、2正好符合KNeighborsClassifier的输入要求。但真实任务中如果特征是字符串类型或者带有缺失值就需要先做编码和填充。标签必须是非负整数。如果你的标签是字符串建议用sklearn.preprocessing.LabelEncoder或者直接用 pandas 的astype(category).cat.codes转换。特征则需要统一数值类型不能有object类型列混在中间否则fit时会直接报错。另外KNeighborsClassifier不支持缺失值存在NaN时距离计算会出错。建模之前最好用df.isnull().sum()检查一遍。6.3 高维场景下的适用边界鸢尾花只有 4 个特征KNN 表现良好原因之一是维度低。当特征维度增加到几十甚至上百时几何直觉会发生变化高维空间中样本之间的距离会趋向接近最近邻居和次近邻居之间的差异变得很小。这就是常说的“维度诅咒”。如果你遇到高维稀疏数据可以先用 PCA 降维或者在距离度量中选择更适合高维的内积相似度。但需要清醒认识到KNN 在处理高维数据时的优势会显著下降这也是它在图像、文本等高维场景中不如深度模型的原因之一。6.4 实战心得把鸢尾花代码改造成自己的项目我个人的建议是不要停留在照着教程敲一遍鸢尾花。拿下这个例子之后可以尝试做两件提升第一把数据集替换成 scikit-learn 自带的load_wine()或者从 UCI 下载一个真实表格数据体验特征类型、类别数量、数据规模变化后对调参的影响第二把代码封装成函数训练、评估、可视化独立模块化方便后续做更复杂的实验。踩过几次坑之后我最大的体会是KNN 本身并不难难的是把数据准备和评估方式做对。脚手架式的代码人人会写预处理步骤的顺序和评估指标的选择才是决定模型真实泛化能力的关键。只要在这两个环节养成习惯无论做什么分类项目都能稳定发挥。
返回列表