手写KNN分类鸢尾花:从零掌握机器学习分类全流程

发布时间:2026/10/8 0:23:13
手写KNN分类鸢尾花:从零掌握机器学习分类全流程
手写KNN分类鸢尾花其实是在学一套通用的机器学习方法论。虽然网上教程铺天盖地但多数只丢给你几段能跑的代码至于为什么这么做、数据怎么处理、K值选多少、评估指标怎么看基本靠猜。这篇文章把我从零跑通鸢尾花KNN分类的完整过程记录下来从原理到底层逻辑从代码实现到调参避坑一次性讲透。无论你是刚接触机器学习的学生还是准备用分类模型做点实际东西的开发者这篇文章都能让你少走不少弯路。1. 项目概述1.1 核心需求解析鸢尾花数据集是机器学习领域最经典的入门数据集之一。它包含150条样本每条样本有4个特征——花萼长度、花萼宽度、花瓣长度、花瓣宽度标签是3种鸢尾花品种Setosa、Versicolor、Virginica。任务就是根据这4个特征构建一个分类模型让模型能够自动判断一朵未知的鸢尾花属于哪个品种。这个任务本身不复杂但它几乎是所有分类问题的“最小完备样例”有特征、有标签、有多类别、有特征尺度差异、有类别不平衡虽然鸢尾花数据集本身是均衡的这些元素在真实业务场景中都会遇到。跑通这个项目相当于把分类问题的完整流程走了一遍。很多人把“模型训练”简单理解为“调用fit()函数”其实远不止这些。完整的模型训练应该包含数据处理、特征理解、模型选择、超参数调优、评估验证、结果分析这样一个闭环。这篇博文会按照这个闭环来展开每一环节都会讲清楚“为什么这么做”。1.2 KNN算法原理速览KNNK-Nearest NeighborsK近邻算法的核心思想特别简单就一句话物以类聚人以群分。判断一个新样本属于哪一类就看它在特征空间中距离最近的K个样本都是什么类别然后投票决定。举个例子。假设你刚搬到一个新小区想判断这个小区是“程序员聚集地”还是“艺术家聚集地”方法就是看看周围邻居都是干什么的。如果离你家最近的5户里有4户是程序员那大概率你住进了程序员聚集区。KNN做的就是这件事只不过它把“家”换成了特征空间中的点把“距离”换成了数学上的距离度量。KNN有两个关键点决定了模型效果K值的选择K太小模型容易受噪声点影响过拟合K太大模型过于“随大流”欠拟合。距离度量方式最常用的是欧氏距离但曼哈顿距离、闵可夫斯基距离也各有适用场景。KNN是一个惰性学习Lazy Learning算法这意味着它没有显式的训练过程——训练阶段它只是把数据存起来真正的计算发生在预测阶段。所以KNN的“训练”和“预测”两个阶段在耗时上完全不对称这也是后面调参时需要理解的一个重要特性。2. 数据集准备与处理2.1 数据来源与加载方式鸢尾花数据集最方便的获取方式是通过scikit-learn内置接口直接加载。它内置在sklearn.datasets模块中无需单独下载非常适合入门学习。from sklearn.datasets import load_iris # 加载数据 iris load_iris() # 特征矩阵shape为(150, 4) X iris.data # 标签向量shape为(150,)取值为0、1、2分别对应三种鸢尾花 y iris.target # 查看特征名称 print(iris.feature_names) # [sepal length (cm), sepal width (cm), petal length (cm), petal width (cm)] # 查看类别名称 print(iris.target_names) # [setosa versicolor virginica]如果不想用接口也可以从UCI机器学习库下载iris.data文件然后通过pandas.read_csv()读取。需要注意Iris数据集原始文件的列是没有列名的读取时需要手动指定列名。import pandas as pd # 如果下载的是原始iris.data文件需要手动指定列名 columns [sepal_length, sepal_width, petal_length, petal_width, species] df pd.read_csv(iris.data, headerNone, namescolumns)顺便说一句很多初学者纠结要不要找xlsx格式的数据集下载。事实是CSV比xlsx通用得多而且这种经典数据集根本不需要手工下载sklearn、seaborn这些库都自带。少在数据获取上花时间把精力集中在模型训练本身。2.2 数据探索与可视化拿到数据之后先别急着训练。先看看数据长什么样类别分布是否均衡特征之间是否有明显的区分度这些信息直接影响后续的建模策略。import pandas as pd import seaborn as sns import matplotlib.pyplot as plt # 构造DataFrame方便查看 df pd.DataFrame(X, columnsiris.feature_names) df[species] y # 查看数据基本信息 print(df.info()) print(df.describe()) # 用散点图矩阵看特征分布 sns.pairplot(df, huespecies) plt.show()从散点图矩阵可以很直观地看到几个重要信息Setosa这个品种和其他两个品种在几乎所有特征维度上都能清晰分开线性可分。Versicolor和Virginica在花瓣长度和花瓣宽度上有一定重叠但整体趋势不同。花瓣长度和花瓣宽度的区分能力明显强于花萼长度和花萼宽度。这些观察告诉我们这个数据集不是“随便跑跑就能满分”的玩具它确实有分类难度尤其是在区分Versicolor和Virginica上存在天然的模糊边界。后面在做K值选择和距离度量时这些信息能帮我们解读模型表现。2.3 特征标准化不做必踩坑KNN是基于距离度量的算法特征尺度直接影响距离计算。如果某个特征的数值范围远大于其他特征它在距离计算中的权重就会被放大这显然不是我们想要的。看一下这组数据就能明白花萼长度的范围是4.3~7.9cm花萼宽度是2.0~4.4cm花瓣长度是1.0~6.9cm花瓣宽度是0.1~2.5cm。如果不对特征做标准化在计算欧氏距离时花萼长度因为数值大会无形中主导距离的计算结果而花瓣宽度因为数值小几乎被忽略。这就需要特征缩放来处理。最常用的方法有两种标准化Standardization$z \frac{x - \mu}{\sigma}$缩放后特征均值为0标准差为1。归一化Normalization$x \frac{x - x_{min}}{x_{max} - x_{min}}$缩放后特征范围在[0, 1]之间。对于KNN来说标准化更常用一些。原因是标准化对异常值的鲁棒性相对更好而且能保留特征的分布形状。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(X)注意fit_transform要在训练集上做测试集上只能用transform不能重新fit。这个细节很多初学者会忽略结果就是在测试集上数据泄露导致评估结果虚高。标准化之后每个特征都变成均值为0、方差为1的分布这样在计算距离时4个特征的地位是平等的。3. 模型训练核心实现3.1 数据集划分训练集和测试集的正确打开方式训练模型之前必须把数据分为训练集和测试集。训练集用来“教”模型测试集用来“考”模型。如果直接用全部数据训练再用全部数据评估模型相当于“开卷考试”准确率虚高完全不能反映泛化能力。这种做法叫数据泄露是机器学习里的低级错误。from sklearn.model_selection import train_test_split # 按7:3划分数据集random_state固定随机种子保证实验结果可复现 X_train, X_test, y_train, y_test train_test_split( X_scaled, y, test_size0.3, random_state42, stratifyy )两个关键参数test_size0.330%的数据作为测试集70%作为训练集。stratifyy分层采样保证训练集和测试集中三类鸢尾花的比例与原数据集一致各占1/3。分层采样非常重要。如果不设置stratify参数虽然这个数据集本身是均衡的但随机划分仍可能让训练集和测试集的类别比例失衡。在真实的业务数据中类别往往不均衡比如99%是负类1%是正类如果不做分层随机划分可能让测试集中连一个正类样本都没有评估结果毫无参考价值。划分后可以用如下方式验证比例是否合理print(训练集形状:, X_train.shape, 测试集形状:, X_test.shape) # 训练集形状: (105, 4) 测试集形状: (45, 4) print(训练集类别比例:, np.bincount(y_train)) print(测试集类别比例:, np.bincount(y_test))3.2 训练KNN模型与初始评估scikit-learn中的KNeighborsClassifier是KNN分类器的标准实现。先从最经典的配置开始K值为5欧氏距离。from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score, classification_report, confusion_matrix # 初始化KNN分类器K5使用欧氏距离 knn KNeighborsClassifier(n_neighbors5, metriceuclidean) # 训练模型 knn.fit(X_train, y_train) # 预测 y_pred knn.predict(X_test) # 评估准确率 accuracy accuracy_score(y_test, y_pred) print(f准确率: {accuracy:.4f})我跑的第一个版本未标准化K5准确率大约是0.8889标准化之后提升到了0.9556。别小看这两三个点的差距它说明特征缩放对KNN这类距离敏感型算法的影响非常大。只用准确率衡量模型好坏远远不够尤其是多分类问题需要看更细粒度的指标。这里用classification_report和confusion_matrix来深入分析。print(classification_report(y_test, y_pred, target_namesiris.target_names)) print(confusion_matrix(y_test, y_pred))输出格式大致如下precision recall f1-score support setosa 1.00 1.00 1.00 15 versicolor 0.93 0.93 0.93 15 virginica 0.93 0.93 0.93 15 accuracy 0.96 45 macro avg 0.96 0.96 0.96 45 weighted avg 0.96 0.96 0.96 45从混淆矩阵可以看到Setosa被完全正确分类而Versicolor和Virginica之间出现了各1个样本的互错。也就是说这2个错误样本恰好分布在最难区分的两个类别上。这和之前散点图矩阵观测到的特征重叠情况完全吻合说明模型的错误不是随机的而是反映了数据本身的内在结构。如果能在特征工程或者调参中让这两个类别的区分度更高模型表现会更好。3.3 手工实现KNN核心逻辑很多教程直接调库就完事了。但为了真正理解KNN在“训练”和“预测”时到底做了什么我建议至少手写一次KNN的核心逻辑。代码不长写一遍对距离计算、排序、投票这几个核心步骤会有非常直观的认识。import numpy as np from collections import Counter def knn_predict(X_train, y_train, x_test, k5): 针对单个测试样本的KNN预测 # 第1步计算测试样本与所有训练样本的欧氏距离 # 第2步按距离排序取前k个 k_nearest_indices np.argsort(distances)[:k] # 第3步取这k个邻居的标签 k_nearest_labels y_train[k_nearest_indices] # 第4步投票得票最多的类别作为预测结果 most_common Counter(k_nearest_labels).most_common(1) return most_common[0][0] # 用标准化后的训练数据手动预测测试集前5个样本 y_pred_manual [knn_predict(X_train, y_train, x, k5) for x in X_test[:5]] print(手动预测结果:, y_pred_manual) print(库函数预测结果:, knn.predict(X_test[:5]).tolist())这个手写版本虽然简单但它把KNN的本质展示得很清楚训练阶段没有任何“学习”动作只是存储数据预测阶段才是真正的计算密集区。这就是惰性学习算法的特征。理解了这一点你就能明白为什么KNN在训练集很大的时候预测速度会变慢以及为什么工业界在数据量巨大的场景下很少直接使用KNN——一次预测要遍历全部训练样本计算距离成本太高。4. 模型优化与调参实战4.1 K值选择策略与影响分析K值的选择是KNN调参中最核心的问题。K太小模型只参考极少数的“邻居”容易被噪声点干扰导致过拟合K太大模型会参考大量“邻居”把距离很远、实际上没什么关联的样本也计入投票导致欠拟合。为了直观感受K值的影响我写了一段循环代码分别在训练集和测试集上评估不同K值的准确率。import matplotlib.pyplot as plt k_values range(1, 31) train_scores [] test_scores [] for k in k_values: knn KNeighborsClassifier(n_neighborsk, metriceuclidean) knn.fit(X_train, y_train) train_scores.append(knn.score(X_train, y_train)) test_scores.append(knn.score(X_test, y_test)) plt.figure(figsize(10, 6)) plt.plot(k_values, train_scores, markero, label训练集准确率) plt.plot(k_values, test_scores, markers, label测试集准确率) plt.xlabel(K值) plt.ylabel(准确率) plt.legend() plt.grid(True) plt.show()从实验结果可以看到当K1时训练集准确率一定是100%因为最近邻就是样本自己但测试集准确率波动大、容易过拟合。随着K增大训练集准确率逐渐下降这是正常的——模型变得更“保守”。测试集准确率会先上升再下降存在一个最优区间。对于这个数据集K在7到15之间表现都比较稳定。这里有一个很多教程没提到的细节测试集上的准确率是一条波动曲线不是单调的。因为K值变化会改变决策边界某些K值下错误样本恰好占了多数准确率会突然下跌。不能只看单次划分下的最佳K值还需要通过交叉验证来更稳健地选择K。4.2 交叉验证与网格搜索用固定的训练集/测试集划分来选择K值结果会受划分方式影响。更可靠的做法是K折交叉验证把训练集均分为K份每次用K-1份训练、1份验证轮流做K次取平均准确率。from sklearn.model_selection import cross_val_score # 用5折交叉验证评估不同K值 for k in [3, 5, 7, 9, 11, 13, 15]: knn KNeighborsClassifier(n_neighborsk) scores cross_val_score(knn, X_train, y_train, cv5) print(fK{k}, 交叉验证平均准确率: {scores.mean():.4f} (/- {scores.std():.4f}))再用GridSearchCV自动化完成K值搜索from sklearn.model_selection import GridSearchCV # 定义参数搜索空间 param_grid { n_neighbors: [3, 5, 7, 9, 11, 13, 15], metric: [euclidean, manhattan, minkowski], weights: [uniform, distance] } grid_search GridSearchCV( KNeighborsClassifier(), param_grid, cv5, scoringaccuracy, verbose1 ) grid_search.fit(X_train, y_train) print(最优参数:, grid_search.best_params_) print(最优交叉验证准确率:, grid_search.best_score_) # 用最优参数在测试集上评估 best_knn grid_search.best_estimator_ test_accuracy best_knn.score(X_test, y_test) print(f测试集准确率: {test_accuracy:.4f})最终调参结果最优参数通常在n_neighbors9~11、metriceuclidean附近weightsdistance在这个数据集上效果与uniform差异不大。测试集准确率能达到0.9778左右。这说明模型已经从最初的0.8889提升到了接近98%的表现调参带来的效果是实打实的。需要强调的是网格搜索中的cv5只用了训练集数据做交叉验证测试集从头到尾没有参与调参过程。这是防止数据泄露的关键纪律测试集只能用来评估最终模型绝对不能用来选参数。否则你得到的“最优模型”可能是专门针对该测试集过拟合的结果换一个测试集表现就崩了。4.3 距离度量方式对比KNN除了欧氏距离还有曼哈顿距离、切比雪夫距离等度量方式。欧氏距离是直线距离曼哈顿距离是“沿坐标轴走”的距离。在二维平面上欧氏距离等于“两点间直线长度”曼哈顿距离等于“横坐标差绝对值纵坐标差绝对值”。对于鸢尾花数据集欧氏距离和曼哈顿距离的测试结果差异并不大因为特征的量纲经过标准化后已经统一了。但在真实场景中如果特征之间存在强烈的相关性马氏距离Mahalanobis Distance可能是更好的选择。鸢尾花数据集的4个特征之间存在一定的相关性比如花瓣长度和花瓣宽度高度相关但作为入门项目欧氏距离已经足够优秀。下表是我在测试集上对比的常见距离度量实验结果距离度量方式测试集准确率欧氏距离euclidean0.9778曼哈顿距离manhattan0.9556闵可夫斯基距离minkowski, p30.9556对于这个数据集欧氏距离表现最好这与特征空间的结构有关。花瓣特征经过标准化后欧氏距离能更合理地度量样本间的整体相似度而曼哈顿距离对特征间的协同变化不够敏感。5. 常见问题与排查技巧5.1 忘记特征标准化准确率虚低这是我见过新人犯得最多的一个错误。不标准化直接跑KNN鸢尾花的准确率大概在0.8889左右明显低于标准化后的0.9556以上。原因前面已经讲过——数值范围大的特征主导了距离计算。排查方法看模型在测试集上的准确率是否异常低再对比检查训练特征和测试特征的均值方差。如果特征值范围差异很大但模型表现不佳优先检查是否做了标准化。5.2 K值选择不当模型过拟合或欠拟合K1时模型极易过拟合。训练集准确率100%但测试集准确率波动很大因为决策边界完全由单个最近邻决定对噪声非常敏感。K值过大时模型欠拟合把所有样本都“平均”了丢失细节结构测试集准确率也会下降。经验法则K值不宜超过训练集样本数的平方根。对于105条训练样本大约K≤10。同时最好用交叉验证来选定K值而不是凭感觉拍脑袋。另一个实用技巧如果遇到投票平局的情况可以考虑增加weightsdistance参数让距离更近的样本拥有更大的话语权。5.3 数据泄露导致的评估虚高数据泄露是机器学习中一个隐蔽但危害极大的问题。在KNN项目中最常见的泄露方式就是树先用全部数据做标准化即用整个数据集的均值方差再划分训练集和测试集导致测试集的信息混入了训练阶段。正确做法先划分数据再做标准化。而且标准化器scaler只能在训练集上fit在测试集上只能transform。# 正确顺序先划分数据 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3, random_state42) # 后做标准化fit只在训练集上 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)5.4 类别不平衡问题鸢尾花数据集本身是均衡的每个类别50条样本不存在类别不平衡问题。但实际业务数据里正负样本比例失衡是常态。KNN对类别不平衡比较敏感因为多数类的样本在特征空间中密度更高投票时天然占优势。应对策略使用weightsdistance让少数类的近邻样本获得更高权重。使用过采样如SMOTE或欠采样技术平衡各类别样本数量。评估指标不看accuracy改用精确率、召回率、F1-score特别是宏平均macro avg和加权平均weighted avg的对比。5.5 预测阶段性能优化KNN训练快、预测慢在大数据量场景下预测阶段要遍历所有训练样本计算距离耗时随数据量线性增长。如果项目数据量变大有几个优化方向使用KD-Tree或Ball Treescikit-learn的KNeighborsClassifier有algorithm参数可选auto、kd_tree、ball_tree数据维度不高时树结构能加速近邻搜索。降维用PCA等降维方法减少特征数量降低距离计算成本。近似最近邻用NearestNeighbors配合n_neighbors参数在可接受的精度损失下大幅提升检索速度。6. 扩展方向与进阶建议6.1 从KNN到其他分类模型跑通鸢尾花KNN分类后你已经掌握了一套通用的分类建模流程。同一份数据完全可以换用其他模型来做对比。逻辑回归Logistic Regression、决策树Decision Tree、支持向量机SVM都是不错的进阶选择。每种模型对数据的假设不同决策边界不同对比它们的差异能加深对模型选择的理解。from sklearn.svm import SVC from sklearn.tree import DecisionTreeClassifier from sklearn.linear_model import LogisticRegression models { KNN: KNeighborsClassifier(n_neighbors10), SVM: SVC(kernelrbf), 决策树: DecisionTreeClassifier(max_depth4, random_state42), 逻辑回归: LogisticRegression(max_iter1000) } for name, model in models.items(): model.fit(X_train_scaled, y_train) score model.score(X_test_scaled, y_test) print(f{name} 测试集准确率: {score:.4f})6.2 从分类到回归KNN不仅能做分类还能做回归。KNeighborsRegressor的工作原理是取K个近邻样本的目标值做平均或者加权平均对于连续数值预测的场景同样适用。6.3 从单模型到集成学习进阶一步可以尝试随机森林、梯度提升树等集成学习模型。集成学习通过组合多个弱学习器来提升整体性能在真实业务中往往是比单模型更稳妥的选择。用鸢尾花数据集作为“试验田”跑一跑对比实验对理解Bagging和Boosting的思想非常有帮助。7. 写在最后再分享一个很多教程不会强调的点这个项目跑通之后重点不在于你记住了KNN的API调用而在于你理解了整个建模流程中每一步背后的“为什么”——为什么要标准化为什么要分层采样为什么要交叉验证为什么要看混淆矩阵而不是只看准确率。这些思维习惯是往后做任何机器学习项目都需要的基本素养。如果继续深入学习建议带着这些问题去实践把鸢尾花换成真实业务数据特征不只有4个而是40个类别分布不均衡你会怎么做从哪里开始着手哪些步骤需要调整那时候你才会真正体会到KNN作为入门项目背后承载的方法论价值。最后分享一个实用技巧实验中每一步都用random_state固定随机种子把数据集划分、特征标准化、模型训练封装成可复用的函数。你会发现当需要回溯实验、复现结果时这种“留痕”的习惯会替你省下大量时间。