尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

KNN算法实战:鸢尾花分类项目从原理到调参的完整指南

KNN算法实战:鸢尾花分类项目从原理到调参的完整指南 简介机器学习入门常从分类任务开始而K近邻KNN算法因其直观的“物以类聚”思想成为理解监督学习的最佳起点之一。该算法无需显式训练通过计算样本间距离并多数表决完成预测在数据标准化、邻居数K选取等环节中蕴含着工程实践的关键细节。Python生态中的scikit-learn、pandas与matplotlib为快速实现与可视化提供了强大支持从加载数据到模型评估的标准流程更是后续深度学习与复杂项目的基础模板。本文以经典鸢尾花数据集为载体演示基于Python的完整分类流程涵盖数据探索、特征缩放、手写KNN与调包实现、以及准确率与混淆矩阵的评估方法帮助读者避开数据泄露与过拟合陷阱在实战中建立扎实的机器学习工程思维。 很多刚接触机器学习的朋友第一次上手做的项目大概率就是鸢尾花分类。这个项目在圈子里几乎等同于Hello World的存在——它数据量小、维度低、结果直观又恰好覆盖了从数据加载到模型评估的完整流程非常适合用来打通Python 机器学习的任督二脉。如果你正在学Python、刚看完几集机器学习入门视频、或者准备交一份期末作业那么基于KNN算法的鸢尾花分类项目是一个绝不会出错的选择。这篇文章我会把整个项目从数据到手写算法、再到调参避坑的完整过程拆开讲清楚让你不仅能跑通代码还能真正理解它在干什么。1. 项目背景与核心价值1.1 鸢尾花数据集为什么是入门标配鸢尾花Iris数据集是机器学习领域最经典的数据集之一它由统计学家Ronald Fisher在1936年首次用于判别分析研究。数据集里记录了三种鸢尾花——山鸢尾Setosa、变色鸢尾Versicolor、维吉尼亚鸢尾Virginica各50条样本每条样本包含四个特征花萼长度、花萼宽度、花瓣长度、花瓣宽度单位都是厘米。也就是说我们手里有150条数据、4个输入特征、1个三分类的目标标签。这个数据集的厉害之处在于它的恰到好处。一方面样本量只有150条运算量极小几毫秒就能出结果非常适合用来验证算法逻辑另一方面它的特征和标签之间存在明显的相关性——尤其是花瓣长度和花瓣宽度在不同种类之间有较好的区分度但又不像那些人造玩具数据一样区分得太明显仍然需要算法去学习其中的边界。这种有一定规律但又不完全线性可分的特性恰好能体现出机器学习算法的价值而不是靠肉眼或者简单规则就能糊弄过去。很多人会问现在有那么多复杂的数据集为什么还要做这个老古董我的看法是入门项目的意义不在于解决问题本身而在于让你熟悉一整套工作流——加载数据、查看数据、数据预处理、划分训练测试集、训练模型、评估结果。这套流程不管以后你做图像识别、自然语言处理还是推荐系统都是完全一致的。鸢尾花数据集的低门槛正好能让你把注意力全部放在理解流程和算法原理上而不是被数据处理本身劝退。1.2 KNN算法原理多数表决与物以类聚KNNK-Nearest NeighborsK近邻算法是机器学习里最直观、最容易理解的分类算法之一它不需要训练过程本质上是懒惰学习——把所有训练数据存下来等新样本来了再现场找它最近的K个邻居让这些邻居投票决定新样本的类别。理解KNN只需要记住一句话物以类聚人以群分。如果一颗水果无论颜色、形状都和苹果特别像那它大概率就是苹果。KNN做的正是这件事对于一个待分类的样本计算它与所有已知样本之间的距离挑出距离最近的K个样本然后看这K个样本中哪个类别的数量最多就把这个新样本归为该类。举个例子K3的时候如果我们找到了离新样本最近的3个已知样本其中2个是山鸢尾、1个是变色鸢尾那么新样本就会被判定为山鸢尾。这里有两个关键的决策因素一是怎么定义距离二是K值取多少。距离度量方式有很多种最常用的是欧氏距离也就是我们在中学几何里学的两点间直线距离而K值的选取则直接决定了模型的平滑程度和泛化能力。K取得太小模型容易受单个噪声点影响K取得太大会把远距离的样本也拉进投票可能导致分类错误。关于K值怎么调我后面会详细展开。1.3 项目目标拆解从读数据到出结果的全流程体验这个项目表面上是对鸢尾花进行分类但实际目标远不止于此。我认为它的价值在于让你完整体验一次机器学习项目的标准工作流整个过程可以分为六个环节第一环环境准备和依赖安装——创建Python环境安装必要的第三方库让程序能跑起来。第二环数据探索与可视化——把数据加载进来后用pandas查看数据形状、数据类型、统计描述用matplotlib绘制散点图观察特征分布和类别可分性。第三环数据预处理——包括特征标准化或者叫归一化、将标签编码为数值等。这一步在KNN算法里尤其重要因为KNN基于距离计算如果特征量纲不一致数值大的特征会主导距离计算。第四环划分数据集——把150条样本按比例划分为训练集和测试集一般用70%训练、30%测试或者80%训练、20%测试。第五环模型训练与预测——用训练集喂给KNN模型再对测试集做预测。第六环模型评估——用准确率、混淆矩阵等指标评估模型表现并尝试调参优化。这六个环节就是你以后做任何机器学习项目的骨干。基于KNN的鸢尾花分类项目最大的好处是即使你在第三步预处理或者第五步模型调参上做得不够精细结果也不会太差因为数据集本身就比较好分这就给了新手很大的容错空间不至于一上来就被玄学调参劝退。我在带新人入门的时候通常建议用这个项目作为第一个动手实践比单纯看网课有效得多。2. 环境准备与数据探索2.1 Python环境与依赖库安装在做任何机器学习项目之前第一步一定是把环境准备好。这个项目需要Python 3.7以上版本以及以下四个库numpy科学计算基础库用来处理数组和矩阵运算pandas数据分析工具库用来读取和处理结构化数据matplotlib数据可视化库用来绘制图表scikit-learn机器学习库里面集成了KNN算法、数据集、数据切分工具和评估指标安装方式很简单如果你用的是pip依次执行pip install numpy pandas matplotlib scikit-learn如果你用的是Anaconda发行版那么numpy、pandas、matplotlib这些基础库基本已经预装了大概率只需要补装scikit-learnconda install scikit-learn我在实际教学中遇到过很多人卡在环境配置上这里分享一个自己的经验建议给每个项目建立独立的虚拟环境不要一股脑把所有包都装到全局环境里。因为不同项目依赖的库版本可能冲突比如有的项目要求pandas1.x有的要求2.x混装在一起很容易出问题。可以用conda create -n iris python3.9创建一个独立环境然后在这个环境里安装依赖项目完事了也不会污染全局。注意如果你在import matplotlib或者import sklearn时报错大概率是当前终端环境和你安装包的环境不是同一个。建议在代码开头打印一下import sys; print(sys.executable)确认自己正在用的是哪个Python解释器。2.2 加载鸢尾花数据集并观察数据形态环境准备好了以后第一步是把数据加载进来。scikit-learn里内置了这个数据集用一行代码就能加载from sklearn.datasets import load_iris # 加载鸢尾花数据集 iris load_iris() # 特征矩阵 X iris.data # 目标标签0、1、2分别对应三种鸢尾花 y iris.target就这么简单数据已经拿到了。这时候很多人会直接开始建模但我建议你多花几分钟观察一下数据长什么样这是培养数据敏感度的好机会。把数据转换成pandas的DataFrame格式看起来更直观import pandas as pd # 转换为DataFrame顺便把特征名加上 df pd.DataFrame(X, columnsiris.feature_names) # 把目标标签加进来 df[target] y # 加一列品类名方便阅读理解 df[species] df[target].map({0: setosa, 1: versicolor, 2: virginica}) # 查看前五行 print(df.head()) # 查看整体统计信息 print(df.describe()) # 查看类别分布 print(df.groupby(species).size())运行结果会让你对数据有一个全局认知。你会发现每种类别都是50条样本没有类别不平衡问题省去了很多处理麻烦。describe()输出中可以看到四个特征的均值和标准差比如花萼长度的均值大约是5.84厘米花瓣宽度的均值大约是1.20厘米。值得注意的是花瓣宽度的标准差明显小于花萼长度的标准差这说明花瓣宽度在样本间的波动更小。这些信息在KNN的标准化环节会用到。2.3 数据标准化为什么是KNN的关键一步这一步我在标题里就用了关键两个字因为太多新手在KNN上栽跟头都是栽在没做标准化上。我们来看一下为什么。KNN算法的核心是计算样本之间的距离最常用的是欧氏距离。假设我们有一个新样本它的花萼长度是5.0厘米花瓣宽度是2.0厘米而训练集样本的花萼长度是5.2厘米、花瓣宽度是0.3厘米。那么计算距离时花萼长度维度上的差值只有0.2而花瓣宽度维度上的差值有1.7。由于花瓣宽度的数值范围比花萼长度小很多最大值才2.5而花萼长度最大7.9这个1.7的差值在距离计算中占据了绝对主导地位。换句话说如果某些特征的单位或者量纲不一样那么在距离计算中数值范围更大的特征会被天然地赋予更高的权重这显然不是我们想要的。更极端的例子是如果一个特征是身高的厘米数值170左右另一个特征是体重的吨数值0.07左右那么距离计算基本完全由身高决定体重特征等于没起作用。解决这个问题的标准做法是标准化Standardization或者归一化Normalization让每个特征都分布在一个可比的范围内。常用的标准化方法是Z-score标准化公式是z (x - μ) / σ其中μ是特征均值σ是特征标准差。标准化之后每个特征的均值变为0标准差变为1。在scikit-learn中这只需要一行调用from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(X)这一步做完后你会发现KNN模型的准确率通常会有所提升尤其是在特征量纲差异大的数据集上。对于鸢尾花数据集虽然四个特征的量纲都是厘米、数值范围差异不算悬殊标准化之后依然能带来微小的准确率提升。我的建议是只要用KNN这类基于距离的算法一律先做标准化这是铁律不需要犹豫。3. 手写KNN核心实现3.1 距离计算从欧氏距离到代码很多人用scikit-learn调包调得很溜但问到底层怎么算的就说不清楚了。我强烈建议你做这个项目时亲手写一遍KNN的实现哪怕只用几十行代码这个过程对理解算法的帮助远超调包。我们自己动手的话第一步就是写距离计算。欧氏距离的公式在二维平面上就是勾股定理两个点(x1, y1)和(x2, y2)之间的距离等于√((x1-x2)²(y1-y2)²)。推广到四维也就是我们鸢尾花的四个特征就是√((a1-a2)²(b1-b2)²(c1-c2)²(d1-d2)²)。在代码里可以用numpy的向量化运算优雅地实现import numpy as np def euclidean_distance(x1, x2): 计算两个样本之间的欧氏距离 x1, x2: 一维numpy数组 return np.sqrt(np.sum((x1 - x2) ** 2))这段代码的逻辑非常清晰先算出每个维度上的差值然后求平方再求和最后开根号。numpy的广播机制会自动逐元素操作不需要我们手写循环。如果你不理解广播这个概念你可以简单理解为x1 - x2会把两个数组按对应位置相减得到一个新的数组。3.2 KNN分类器的完整实现有了距离函数我们就可以写出完整的KNN分类器了。这里是整个项目最核心的代码部分我建议一行一行看明白再动手敲class KNN: def __init__(self, k3): k: 邻居数量 self.k k def fit(self, X, y): 训练方法KNN的训练就是记住所有数据 X: 训练集特征, shape (n_samples, n_features) y: 训练集标签, shape (n_samples,) self.X_train X self.y_train y def predict(self, X): 对新样本进行预测 X: 需要预测的样本, shape (n_samples, n_features) predicted_labels [self._predict_one(x) for x in X] return np.array(predicted_labels) def _predict_one(self, x): 对单个样本进行预测 # 计算x与所有训练样本的距离 distances [euclidean_distance(x, x_train) for x_train in self.X_train] # 按照距离升序排序取前k个的索引 k_indices np.argsort(distances)[:self.k] # 取出这k个邻居的标签 k_nearest_labels [self.y_train[i] for i in k_indices] # 多数表决返回出现次数最多的标签 most_common np.bincount(k_nearest_labels).argmax() return most_common注意这里的fit方法体里什么也没干只是把数据存下来了。这就是我前面提到的懒惰学习——KNN没有显式的训练过程它把所有计算都推迟到了预测阶段。这在样本量小的时候完全可行但随着样本量增大预测速度会越来越慢。因为每个新样本都要和所有训练样本计算一次距离时间复杂度是O(n)n是训练样本数。多数表决的部分用的是np.bincount和argmax的组合。一句话解释bincount会统计数组里每个非负整数出现的次数然后argmax取出出现次数最多的那个索引。比如标签数组是[0, 2, 0]bincount的结果就是[2, 0, 1]argmax取到0正好是出现次数最多的类别。3.3 从手写版看K值选择对结果的影响手写版实现好以后我们可以试着用不同的K值来测试一下准确率这比直接调包更能直观感受K值对模型的影响。先加载数据、切分训练测试集、标准化然后循环测试K1到K15的准确率from sklearn.model_selection import train_test_split from sklearn.datasets import load_iris from sklearn.preprocessing import StandardScaler iris load_iris() X, y iris.data, iris.target # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) # 标准化 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 测试不同K值 for k in range(1, 16): knn KNN(kk) knn.fit(X_train_scaled, y_train) y_pred knn.predict(X_test_scaled) accuracy np.mean(y_pred y_test) print(fK{k:2d}, 准确率{accuracy:.4f})在我本机运行的结果中K1时准确率大约在93%左右在45个测试样本中错分3个K从3到10之间准确率稳定在95%甚至更高K超过12之后准确率开始下降。这个现象背后的逻辑是K1时模型过于敏感任何一个训练样本的极端位置都会直接影响预测结果K太大时又会让投票结果被远处的大多数淹没丢失了局部信息。对于这个数据集K取值在3到10之间通常是最稳的区间。我发现很多人在这一步会得到一个K1准确率也还不错的结论然后误以为模型已经足够好了。实际上K1在一个只有150条样本的简单数据集上表现好是正常的但在真实项目中K1往往意味着严重的过拟合——模型记住的是训练数据里的个体而不是规律。这也是为什么我们要手动测试多个K值而不是凭感觉取一个。4. scikit-learn快速实现与模型评估4.1 用train_test_split合理划分数据集手写版搞清楚原理之后我们就可以用scikit-learn快速实现同样的功能了。现实的机器学习项目中没有人会真的手写KNN因为官方实现已经高度优化过而且提供了各种便捷的API。但手写版的经验能帮我们更好地理解官方API背后的逻辑少踩很多坑。数据划分这一步就很有讲究。train_test_split是数据集划分的标准工具它默认会随机打乱数据后切分。这里有一个重要的细节如果你不设置random_state每次运行程序都会得到不同的随机划分导致结果不稳定、无法复现。解决方法是设置一个固定的随机种子比如random_state42这样每次运行都得到相同的划分结果。另一个细节是stratify参数——按类别比例分层抽样。鸢尾花数据集中每个类别恰好50条如果不设置stratify随机划分可能导致训练集里某个类别的样本数偏少或者偏多影响模型的训练质量。设置stratifyy后代码会确保训练集和测试集里三个类别的比例与原数据集保持一致这在小数据集上尤其重要。from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X_scaled, y, test_size0.3, random_state42, stratifyy )test_size0.3表示30%的数据用于测试、70%用于训练。对于150条样本的小数据集这个比例比较合理——测试集有45条样本足够看出模型好坏训练集有105条样本也能为KNN提供足够的邻居参考。如果你数据量很大比如几万条测试集比例设置在20%-30%之间都没问题但小数据集上我建议不要低于20%否则测试结果太依赖运气波动会很大。4.2 KNeighborsClassifier核心参数详解scikit-learn的KNN分类器是KNeighborsClassifier我们来看一下它的核心参数按重要性排序参数默认值作用该项目的建议n_neighbors5邻居数量K从5开始用交叉验证调整weightsuniform是否按距离加权投票distance可以让近邻居权重更大metricminkowski距离度量方式minkowski配合p2即欧氏距离p2闵可夫斯基距离的指数p1是曼哈顿距离p2是欧氏距离algorithmauto搜索算法数据量小保持auto即可这里我想重点说下weights参数的直觉含义。默认的uniform模式下K个邻居投出的票等权但如果某个邻居离新样本特别近另外几个邻居离得稍远直觉告诉我们近朱者赤离得越近的样本应该更有发言权。把weights设置为distance后投票权重大小与距离成反比——距离越近的邻居权重越大。在鸢尾花这个数据集上distance和uniform的差异不会特别明显但对于特征分布不均匀的真实数据集distance往往能带来更稳定、更自然的效果。metric和p这两个参数建议大家了解一下含义但不急着调。默认的minkowski是一个泛化距离公式p2就是欧氏距离p1就是曼哈顿距离各维度差值的绝对值之和。对于连续特征欧氏距离是最常用的选择如果有离散或高维稀疏特征曼哈顿距离有时更合适。鸢尾花数据集全部是连续数值特征直接用欧氏距离就好。4.3 训练、预测与评估下面我们用scikit-learn跑通完整流程代码非常简洁from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score, classification_report, confusion_matrix # 创建KNN分类器 knn KNeighborsClassifier(n_neighbors5) # 训练 knn.fit(X_train, y_train) # 预测 y_pred knn.predict(X_test) # 评估 accuracy accuracy_score(y_test, y_pred) print(f模型准确率: {accuracy:.4f}) # 分类报告 print(classification_report(y_test, y_pred, target_namesiris.target_names)) # 混淆矩阵 cm confusion_matrix(y_test, y_pred) print(混淆矩阵:) print(cm)accuracy_score就是预测正确的样本数除以总样本数直观明了。分类报告则提供了每个类别的精确率Precision、召回率Recall和F1值。这三个指标的区别很重要精确率预测为正例的样本中有多少是真的正例召回率真实正例的样本中有多少被正确找出来了F1值精确率和召回率的调和平均值综合评价对于鸢尾花分类我们碰到的绝大多数情况里Setosa类都能100%正确分类因为它的花瓣特征和其他两类差异巨大几乎是肉眼可辨的程度。真正的难点在于Versicolor和Virginica的区分这两类在特征空间中有部分重叠分类器很容易在这两种之间产生混淆。如果你运行完代码发现混淆矩阵中Versicolor被误判为Virginica这是完全正常的现象不代表代码有问题这正是数据集本身特征分布的自然反映。5. 可视化让分类结果看得见5.1 特征分布散点图直观感受数据可分性代码跑通了结果也出来了但如果只停留在打印一串数字的层面这个项目就白做了一半。可视化是机器学习里非常重要的分析手段它能帮助我们从直觉层面理解数据。首先来看特征分布散点图。我们选两个最具区分度的特征——花瓣长度和花瓣宽度绘制散点图用不同颜色标记不同类别import matplotlib.pyplot as plt plt.figure(figsize(8, 6)) scatter plt.scatter( X[:, 2], X[:, 3], # 第2列是花瓣长度第3列是花瓣宽度 cy, cmapviridis, edgecolork, s100 ) plt.xlabel(Petal length (cm)) plt.ylabel(Petal width (cm)) plt.colorbar(scatter, ticks[0, 1, 2], labelSpecies) plt.title(Iris dataset: Petal length vs Petal width) plt.show()运行之后你会看到一副非常有信息量的图Setosa标签0的点密密麻麻聚集在左下角和另外两类完全分开Versicolor标签1和Virginica标签2则分布在右上方两者之间有边界但存在少量重叠。这就解释了为什么Setosa的分类准确率永远是100%——它的特征分布和其他两类几乎没有交集。这也是为什么很多模型在鸢尾花数据集上能轻松达到95%以上的准确率——靠近边界的重叠区域才是模型真正犯难的地方。5.2 决策边界可视化模型到底学了什么散点图看的是数据本身的分布而决策边界图看的是模型认为的边界在哪里。绘制决策边界需要把二维特征空间划分成网格然后让模型对网格上的每个点进行预测再用等高线或颜色填充把不同预测区域涂上颜色import numpy as np import matplotlib.pyplot as plt from matplotlib.colors import ListedColormap def plot_decision_boundary(X_data, y_data, model, ax): # 设定网格范围留一些边距 x_min, x_max X_data[:, 0].min() - 0.5, X_data[:, 0].max() 0.5 y_min, y_max X_data[:, 1].min() - 0.5, X_data[:, 1].max() 0.5 # 生成网格点 xx, yy np.meshgrid( np.arange(x_min, x_max, 0.02), np.arange(y_min, y_max, 0.02) ) # 对网格点做预测 Z model.predict(np.c_[xx.ravel(), yy.ravel()]) Z Z.reshape(xx.shape) # 绘制等高线填充 ax.contourf(xx, yy, Z, alpha0.6, cmapListedColormap([#FFAAAA, #AAFFAA, #AAAAFF])) # 绘制原始数据点 scatter ax.scatter(X_data[:, 0], X_data[:, 1], cy_data, cmapListedColormap([#FF0000, #00AA00, #0000FF]), edgecolork, s50) ax.set_xlabel(Petal length (cm)) ax.set_ylabel(Petal width (cm)) # 用标准化后的花瓣长度和花瓣宽度做示例 X_2d X_train_scaled[:, 2:4] # 只用花瓣长度和花瓣宽度 # 重新训练模型用二维特征 knn_2d KNeighborsClassifier(n_neighbors5) knn_2d.fit(X_2d, y_train) fig, ax plt.subplots(figsize(9, 6)) plot_decision_boundary(X_2d, y_train, knn_2d, ax) plt.title(KNN Decision Boundary (K5, using petal features)) plt.show()运行结果里你会发现分类边界不是平滑的曲线而是一条条类似晶格的折线——这是KNN算法的典型特征。因为KNN的决策边界是由局部训练样本决定的不同区域的最近邻居集合不同边界就会跟着样本分布产生不规则起伏。K值越小边界越复杂、越精细K值越大边界越平滑、越简化。这也是前面说K值控制模型复杂度的直观体现。5.3 混淆矩阵避开准确率陷阱准确率是最常用的评估指标但它有一个陷阱在类别分布不均衡的时候准确率会掩盖模型的真实问题。假设一个数据集90%是A类、10%是B类那么一个无脑全部预测为A的模型准确率也有90%看起来很高实际完全没学到B类的规律。鸢尾花数据集三个类别分布均衡准确率的参考价值相对可靠但我们依然要养成看混淆矩阵的习惯。混淆矩阵是一个n×n的表格行表示真实类别列表示预测类别对角线上的数字表示预测正确的样本数非对角线上的数字表示误判的样本数和具体误判方向。import seaborn as sns plt.figure(figsize(7, 5)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsiris.target_names, yticklabelsiris.target_names) plt.xlabel(Predicted) plt.ylabel(Actual) plt.title(Confusion Matrix) plt.show()通过混淆矩阵你能清楚地看到如果模型把2个Versicolor误判成了Virginica你就能快速定位到问题出在这两个难分的类别上而不是只是笼统地知道准确率95.56%。更进一步你可以对比不同K值下的混淆矩阵看哪些类别的误判在增加、哪些在减少这比单纯盯着准确率数字来得有用得多。6. 常见问题、踩坑记录与调参技巧6.1 标准化顺序错在哪数据泄露的风险我看到过不少人的代码是这么写的先切分训练集和测试集然后对X_train和X_test分别调一次fit_transform。这个写法错就错在分别fit上。正确做法是只在训练集上fit_transform先计算均值和标准差再应用变换然后在测试集上只做transform直接用训练集学到的均值和标准差进行变换。原因是测试集的角色是模拟未来的新数据我们不能用新数据的信息来教模型做任何处理否则就会造成信息泄露——模型在训练阶段就已经偷看了测试集的分布信息评估结果会偏乐观。具体的操作是scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) # 测试集只用transform不要用fit_transform X_test_scaled scaler.transform(X_test)这个坑非常隐蔽因为就算你写错了两行fit_transform也不会报错模型准确率照样能算出来看起来一切正常。但实际上你已经把一个技术性的细节做错了。这个习惯在真实项目中会带来严重的问题——你的模型上线后面对真实的新数据时表现会明显比测试时差因为你训练时作弊了。我建议所有人在做数据预处理时都养成这个肌肉记忆任何从数据中计算得到的统计量均值、标准差、最大值、最小值等都只能在训练集上计算。6.2 K值、距离度量和权重一个经典的调参练习KNN模型的参数虽然不多但每一个都值得仔细调。我们以K值选择为例讲讲标准做法。刚才我们在测试集上直接试了不同K值的准确率这种做法能大致感知K值的影响但它有一个隐患如果你反复用同一个测试集来试参数选择准确率最高的那个K那么测试集的信息实际上已经被泄露到参数选择中了最终评估结果会偏乐观。更规范的做法是交叉验证Cross-validation。简单来说交叉验证把训练集再分成多个小份轮流拿其中一份做验证、其余做训练最终综合多次验证结果来评估参数的稳定性。在scikit-learn中可以用GridSearchCV来自动搜索from sklearn.model_selection import GridSearchCV # 定义参数搜索范围 param_grid { n_neighbors: range(1, 21), weights: [uniform, distance], p: [1, 2] } # 网格搜索 5折交叉验证 grid_search GridSearchCV( KNeighborsClassifier(), param_grid, cv5, scoringaccuracy ) grid_search.fit(X_train_scaled, y_train) print(f最佳参数: {grid_search.best_params_}) print(f最佳交叉验证准确率: {grid_search.best_score_:.4f})运行结果通常会在n_neighbors为5到8之间、weightsdistance、p2的组合附近得到一个较优的结果。通过这个流程你体会到的不仅是如何调K值更是为什么不能在测试集上调参这一思维习惯。6.3 训练集与测试集比例怎么定很多初学者会问test_size设30%还是20%好其实这个没有绝对标准取决于你的数据量。关键原则是测试集要大到能稳定评估模型同时训练集要大到能让模型学到足够信息。对于150条样本的数据集我建议用25%-30%作为测试集你的数据量如果有几千条20%足够有几万条10%-20%都可以。在鸢尾花项目里我曾经试过把测试集比例降到5%准确率波动就会非常大——有时候100%、有时候90%完全看运气。这说明测试集太小会导致评估结果不可靠你根本分不清模型好坏是因为算法问题还是因为运气问题。6.4 为什么K1时模型看起来更好如果你运行了前面的K值循环测试你会发现K1时准确率也能到93%-95%左右甚至有时候比K5还高。这是不是说明K1更好完全不是。K1意味着模型新样本只依据最近的一个邻居分类这个邻居如果恰好是一个离群点预测就会受到极大干扰。在鸢尾花数据集上由于类别间重叠区域有限K1的运气成分表现得不算太明显但在真实数据集上K1几乎必然导致过拟合——模型的决策边界完全跟着训练样本的个体抖动训练集准确率接近100%但测试集表现却不稳定。一个简单的验证方法是把训练集和测试集多切分几次用不同的random_state观察K1和K7的准确率波动范围。你会发现K1的结果在小范围内剧烈跳动而K7的结果相对稳定。模型的稳定性跟准确率一样重要——一个在某些划分下能到98%、在某些划分下掉到88%的模型在实际部署中是不可信的。6.5 距离标准化是唯一的预处理吗对于KNN算法来说标准化是最常见的预处理方式但它不是唯一的。有时候我们还需要处理离群点。因为KNN基于距离一个极端离群点在距离计算中可能会吸引很多本不属于它的邻居进而影响一片区域的分类结果。鸢尾花数据集没有明显的离群点但如果你后续换到其他真实数据集比如带噪声的传感器数据建议先做异常值检测比如用箱线图或者IQR法则把明显的异常点处理掉再做标准化和建模。这一步优先级不高但知道有这件事的存在很重要。我遇到很多新手的想法是预处理就是标准化——其实不然优秀的数据分析师会根据数据特点选择不同的处理流程。不过在鸢尾花这个项目里标准化已经足够了不需要额外加戏。7. 从这20%到真实项目的100%你能做哪些扩展鸢尾花分类项目做完之后你的机器学习之旅其实才刚刚开始。这个项目是一个特别好的起点它教你走完了标准的流程但真实世界的项目远比这个复杂。如果你还想继续深入这里有几个特别推荐的扩展方向换不同的分类算法横向对比同样的数据改用逻辑回归、决策树、支持向量机SVM对比它们的准确率和决策边界差异。你会发现不同算法对相同数据的理解完全不同这是理解没有免费的午餐定理的最好方式。把KNN用在更多数据集上scikit-learn里还有乳腺癌数据集二分类、手写数字数据集10分类等把KNN跑上去你会感受到高维特征、样本量增大对KNN计算速度的影响。尝试特征选择现在四个特征一起用你可以试试只用花瓣长度和花瓣宽度两个特征或者只用花萼的两个特征看看准确率变化多大。这个实验能帮你建立特征质量比数量更关键的直觉。自己造一个难以分类的数据集比如用make_moons或make_circles生成非线性的模拟数据观察KNN在非线性边界上的表现顺便理解为什么有些问题线性模型搞不定而KNN可以。这些扩展方向做下来你对机器学习基础的理解会远超周围其他还停留在跑通教程阶段的人。我个人最推荐的方法是每学一个新算法就在鸢尾花数据集上跑一遍横向对比多个算法的表现——这种算法对比实验比你盲目刷课程有效得多。8. 写在最后一点过来人的经验这个项目我自己带过很多次也看过很多初学者卡在不同的地方。最想对你说的一点是不要为了跑通代码而去抄代码。如果你只是复制粘贴然后看到准确率95.56%就收工了这个项目等于白做。真正有价值的是过程——是你亲手在纸上画过KNN的投票流程是你因为忘了标准化而发现准确率忽高忽低时的顿悟是你看到混淆矩阵里Versicolor和Virginica纠缠不清时的困惑。把这些困惑记录下来去查资料、去实验、去验证这个困惑—求解的过程才是机器学习能力进步的核心路径。另外还有一个非常实用的小建议做项目时养成每次运行都记录结果的习惯。可以是简单的Excel表格也可以直接在Jupyter Notebook里留备注——比如K5uniform标准化后准确率0.9556。不要小看这个习惯当你做第二个、第三个项目时回头翻看这些记录会让你对参数敏感性有远超常人的直觉。这种记录-对比-复盘的工作方式也是把一个入门小项目变成简历上真正实践经验的秘密武器。我希望这篇文章不仅帮你跑通了这个经典项目更帮你理解了它背后的逻辑和训练过程中那些容易被忽略的细节。接下来把代码自己敲一遍改一改K值画一画图你会收获远比这篇文章更多的内容。本文还有配套的精品资源点击获取
返回列表