资讯详情

KNN实战教程:葡萄酒数据三分类与超参数调优

📅 2026/10/20 4:06:38 | 华诺云谱 👁 阅读
KNN实战教程:葡萄酒数据三分类与超参数调优
K最近邻算法KNN是很多初学者接触的第一个机器学习模型不是因为它的数学推导最复杂而是因为它的原理足够直观物以类聚人以群分。这个项目用葡萄酒数据做三分类正好能把KNN的核心机制讲透同时又不至于让代码量淹没学习重点。我最早做这个项目的时候走了不少弯路比如没做特征缩放导致准确率惨不忍睹、选了不合适的K值让模型过拟合这些坑我会在文章里全部摊开说清楚。这篇实战笔记适合刚学完Python基础、想快速上手第一个完整机器学习项目的同学也适合想复习KNN细节的开发者。1. 项目定位这个分类任务到底在解决什么问题1.1 酒分类的本质是一个模式识别问题拿到一个酒的数据集里面有多个品种的酒每个样本包含若干项化学成分指标比如酒精度、苹果酸含量、灰分含量、镁含量等等。目标是根据这些数值特征判断某瓶酒属于哪个品种。这就是一个典型的多分类监督学习任务类别标签是已知的特征矩阵是数值型的样本量不算特别大。为什么不直接查产地或者看标签现实场景中酒的品种鉴别往往需要专业品酒师通过感官评估成本高且主观性强。如果能用化学成分数据训练一个模型输入检测指标输出品种判断整个流程就标准化了也可以扩展到防伪鉴别、质量筛查等场景。从教学角度说这个数据集的特征维度适中类别平衡样本没有大量缺失值是练手KNN的绝佳素材。1.2 为什么选KNN而不是其他模型面对这个任务可选的模型有很多逻辑回归、决策树、支持向量机、朴素贝叶斯。但KNN有两个不可替代的定位第一KNN是惰性学习的代表训练阶段几乎不做什么只是把样本存下来预测时才真正开始计算距离。这跟其他急切学习的模型完全不同理解KNN有助于建立对模型本质差异的认知。第二KNN的决策边界是非线性的它不对数据分布做任何假设。酒类数据中不同品种的特征分布可能有重叠也可能存在非线性的分界KNN能用最朴素的方式逼近这种边界。当然KNN也有明显的短板特征维度高了之后距离度量会失真样本量大了之后预测速度会变慢这就是后文要做特征标准化和交叉验证的原因所在。2. 核心机制拆解KNN的四个关键决策点2.1 距离度量用欧氏距离还是别的KNN的预测逻辑本质上是在特征空间中找最近的K个已知样本然后投票决定新样本的标签。那么这个“最近”怎么定义就直接决定了模型行为。最常用的是欧氏距离也就是L2范数。假设特征向量是二维的两个点之间的距离就是坐标差的平方和再开方。推广到高维公式就变成d(x, y) sqrt(Σ(xi - yi)^2)这个公式看起来简单但隐含了一个大问题量纲敏感。酒的各项化学成分度量单位不同数值范围差异很大比如镁含量可能是几十到上百而某个占比类指标可能只有零点几。如果不做标准化欧氏距离会被数值范围大的维度主导KNN就退化成了单特征分类器。除了欧氏距离曼哈顿距离L1范数也偶尔会用到它计算的是坐标差绝对值之和。高维场景下曼哈顿距离在某些情况下比欧氏距离更稳定但对这个项目而言标准化之后的欧氏距离已经足够好。2.2 K值的直觉理解与实验验证K值太小比如K1模型会非常敏感训练集里的噪声点会直接扭曲预测结果这是过拟合的极端形态。K值太大比如K总样本数那模型对所有新样本的输出都是同一个类别完全无视个体差异这是欠拟合。实际项目中K值一般取奇数目的是在二分类投票时避免出现平票的尴尬。对于这个三分类任务奇数不一定能完全避免平票但习惯上还是先试奇数。更科学的做法是画一条K值与交叉验证准确率的曲线找到拐点附近的K值。我在这个项目里跑了K从1到30的交叉验证分数结果是K在15到20之间稳定在一个高位平台超过25之后开始缓慢下滑。最终选了K15不是因为准确率最高而是因为在这个点模型在验证集和测试集上的表现最一致。2.3 投票权重等权还是距离加权基本KNN是等权投票K个邻居每人一票少数服从多数。这个策略简单但如果K个邻居里有三个属于A类、两个属于B类而这三个A类其实距离新样本很远那两个B类比它们近得多等权投票就会得出不太合理的结论。改进方案是距离加权投票距离越近的样本话语权越大。常见做法是把权重设为1/d或者1/(d^2)距离为0时直接取该样本的标签。基于这个项目的数据距离加权在K值较大时能明显提升准确率因为它缓解了K值过大把远距离噪声样本拉进投票池的问题。所以我最终用的权重模式是distance而不是uniform。2.4 时间复杂度与项目规模匹配KNN的训练时间复杂度是O(1)因为惰性学习不建模型。但预测时间复杂度是O(n)n是训练样本数每次预测都要扫描所有样本算距离。这个项目的样本量只有一百多个特征只有十几个预测一次也就几毫秒完全够用。如果以后换到几十万样本的高维数据集KNN就会非常吃力。届时的优化思路包括用KD树或者球树做空间索引把最近邻搜索复杂度降低到O(log n)或者用近似最近邻库用精度换速度。但这些在这个项目里都没必要先用朴素实现跑通流程理解原理才是重点。3. 数据准备拿到数据之后的第一件事不是训练3.1 检查数据形态与缺失值拿到一份酒类化学成分数据集先把数据结构看清楚。通常它是一个二维表每一行是一个样本每一列是一个特征最后一列或者单独一个文件里存放的是品种标签。先打印前五行和shape确认行列数是否符合预期再检查有没有缺失值。缺失值处理要根据情况来如果缺失比例很小可以用该列的中位数填充如果缺失比例很高这列特征可能直接删掉更干净。这个项目的数据质量和完整性都比较好基本不需要数据清洗但检查这一步一定要做我见过太多人跳过这一步后边模型报错半天找不到原因。3.2 特征标准化不是可选项是必选项前面说过KNN严重依赖距离度量而距离对特征量纲极其敏感。因此标准化处理在这个项目中是不可跳过的环节。常用的方法有两种Z-score标准化用特征均值减去原值再除以标准差让每个特征变成均值0、方差1的分布适合特征近似正态分布的情况。MinMax缩放把特征缩放到0到1区间保留原始分布形状适合分布未知或非正态的情况。这个项目里不同特征的分布差别明显Z-score标准化实测效果要比MinMax缩放稳定一些。需要注意的关键点是标准化要用训练集拟合出来的参数不要用整个数据集去拟合。如果你拿全量数据的均值和标准差去做标准化测试集的信息会泄漏到训练过程中导致验证指标虚高这在真实项目中是非常容易犯的错误。正确的操作是先拆分训练集和测试集在训练集上fit标准化器然后用同一个标准化器transform训练集和测试集。代码层面用StandardScaler可以实现得很优雅。3.3 训练集与测试集划分策略划分比例我一般用七比三也就是70%训练、30%测试。这个比例兼顾了训练需要的数据量和验证需要的样本量。如果数据集特别小还可以考虑留一法交叉验证样本量只有一百多的话留一法的成本也在可接受范围内。划分时必须设置随机种子否则每次运行的训练集不同模型结果无法复现。我在代码里固定random_state42这是个人习惯谁用谁知道调参对拍的时候有固定种子能省很多事。还需要考虑一点如果直接用原始分类标签做划分可能出现某一类在训练集中偏少的情况。对于类别分布本来就很均衡的数据通常随机划分已经足够不要过度设计。4. 核心代码实现从零手写KNN到调库封装4.1 手写KNN的核心逻辑先自己实现一个朴素的KNN用这个代码理解算法的每一步这段代码也方便之后调试和理解框架版KNN的行为。import numpy as np from collections import Counter def knn_predict(X_train, y_train, x_test, k5): distances [] for i in range(len(X_train)): diff X_train[i] - x_test dist np.sqrt(np.sum(diff**2)) distances.append((dist, y_train[i])) distances.sort(keylambda x: x[0]) top_k [label for _, label in distances[:k]] counter Counter(top_k) return counter.most_common(1)[0][0]这段逻辑简单到有点感人但它把KNN的完整流程都走了一遍计算距离、找出最近的K个、投票决出标签。实际项目不推荐用这种循环写法数据量一大就卡死但用来调试和理解原理极好。我在调试的时候会把k值设成1手动检查某几个样本的最近邻和手算结果是否一致验证基础实现是否正确。4.2 用sklearn实现完整流程框架实现的重点不在于模型本身因为KNeighborsClassifier的API太简单了难点在于把整个流程组织好。from sklearn.datasets import load_wine from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV from sklearn.preprocessing import StandardScaler from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import classification_report, confusion_matrix # 1. 加载数据 data load_wine() X, y data.data, data.target # 2. 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) # 3. 标准化训练集并转换测试集 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 4. 创建KNN模型并用交叉验证调参 knn KNeighborsClassifier( n_neighbors15, weightsdistance, metriceuclidean ) # 5. 用训练集做5折交叉验证 cv_scores cross_val_score(knn, X_train_scaled, y_train, cv5) print(fCross-validation accuracy: {cv_scores.mean():.4f} (/- {cv_scores.std():.4f})) # 6. 在全部训练数据上重新训练并评估测试集 knn.fit(X_train_scaled, y_train) y_pred knn.predict(X_test_scaled) print(classification_report(y_test, y_pred)) print(confusion_matrix(y_test, y_pred))代码里的stratifyy是一个非常关键的细节它能让划分后的训练集和测试集保持和原始数据相似的类别比例。如果三类的样本比例原本是均衡的那么划分后也应该保持均衡避免某一类在测试集里缺失或者极端稀疏。cross_val_score的用法也值得注意它会在训练集内部再做K折切分每一折轮流当验证集最终返回K个分数。交叉验证的意义在于单次划分验证指标波动太大K折交叉验证可以更稳定地估计模型在新数据上的表现。4.3 用网格搜索系统化调参手动试K值太费劲而且没有系统验证多个参数组合的效果。网格搜索是最直观的调参工具它穷举参数组合对每种组合做交叉验证最后返回最优参数。param_grid { n_neighbors: [5, 10, 15, 20, 25, 30], weights: [uniform, distance], metric: [euclidean, manhattan] } grid_search GridSearchCV( KNeighborsClassifier(), param_grid, cv5, scoringaccuracy, n_jobs-1 ) grid_search.fit(X_train_scaled, y_train) print(grid_search.best_params_) print(grid_search.best_score_)我实际跑出来的最优组合是n_neighbors15, weightsdistance, metriceuclidean这个结果和之前手动调参的结论基本吻合。n_jobs-1表示用所有CPU核心并行计算能显著缩短搜索时间。注意网格搜索所用到的交叉验证已经是在训练集上进行的测试集在这个过程中完全不参与属于最后的模型评估环节。5. 超参数深挖K值和权重对模型行为的真实影响5.1 不同K值下的准确率变化曲线把K值从1到30遍历一遍记录每组参数在交叉验证和测试集上的准确率画一条曲线能直观看到模型从过拟合到平稳再到欠拟合的变化过程。我在这条曲线上的发现是这样K1到5交叉验证准确率波动很大围绕0.93上下震荡这是典型的高方差表现。K10到20准确率稳定在0.97左右方差减小泛化能力好。K超过25准确率开始回落说明邻居数量太多把远距离的异类样本也拉进了投票池。这条曲线的形状是这个数据集的特征决定的换一个数据集最优K值区间会不同。但曲线的整体趋势是有普适规律的高K值欠拟合、低K值过拟合这个准则在所有KNN项目中都成立。5.2 距离加权和等权投票的对比实验我在相同参数条件下做了权重对比结果很有意思K值uniform准确率distance准确率10.9630.96350.9630.963100.9440.963150.9440.981200.9440.944K1时两种权重策略没有区别因为只有一个邻居投票权重本来就无所谓。K10和15时距离加权显著优于等权这说明当K值变大之后近邻样本和远邻样本的分类信息量差距很大等权投票会让相对较远的样本和最近邻享有同样的分量稀释了预测依据的有效性。项目里最终选择distance理由不仅是准确率高一点而是这个选择让模型对K值不那么敏感即使K值稍微调偏一点模型表现也不会剧烈滑坡。这在实际工程中是极有价值的性质。5.3 特征相关性分析能告诉我们什么对特征做相关性热图分析会发现某些特征之间存在较强的正相关。比如两个和有机酸相关的成分指标相关系数接近0.7。这个现象给KNN带来的影响是相关特征相当于在计算距离时被重复计算了权重某个隐含的物理因素被赋予了过高的影响力。处理方式有两种思路。一种是用主成分分析降维把相关特征压缩成互相独立的几个主成分再用KNN训练降维后的数据在距离计算上更合理。另一种是直接用原始特征因为KNN对特征冗余有一定容忍度而且这个项目样本量不大降维收益不明显。实测下来在这个数据上降维对准确率的影响微乎其微所以我保留了原始特征主要精力还是放在调K值上。6. 结果评估准确率不是唯一的指标6.1 混淆矩阵看分类细节单纯看准确率是不够的不同品种之间的混淆情况才是模型质量的关键。打印混淆矩阵之后我注意到一个现象某个品种和另一个品种偶尔会互相误判但第三个品种被误判的次数非常少。这个信息暗示了数据的结构特点。某些酒在成分指标上本身就有相似性边界区域存在重叠所以即使整体准确率很高模型对特定类别的区分能力还是有差别的。如果这是一个更重要的应用场景就需要针对混淆严重的类别单独分析比如查看那些被误判的样本到底长什么样也许能发现新的特征筛选方向。6.2 精确率、召回率与F1值分类报告里的precision和recall能提供比准确率更细粒度的信息。从项目输出结果看每个类别的precision和recall都比较均衡没有出现偏高或偏低的情况。这说明类别样本量均衡、特征可分性好。精确率和召回率的权衡关系是这个项目中没有显现出来的问题因为类别不平衡不明显。但如果换成一个样本量悬殊的数据集F1值就要成为主要关注指标准确率反而可能产生误导。6.3 和几个基线模型的横向对比为了确认KNN在这个任务上的表现我用同一个训练集和测试集跑了几个对照模型模型测试集准确率KNN (K15, distance)0.981决策树 (深度限制)0.944逻辑回归 (C1.0)0.963支持向量机 (RBF核)0.981KNN和支持向量机并列第一逻辑回归略低决策树相对最弱后来才想到决策树没有标准化也能用但这里统一数据后也行转一下。这个对比结论并不意外KNN在这个中小规模数值型数据集上天然有优势因为它的归纳偏置正好匹配了数据的分布形态。需要注意SVM在RBF核下同样需要标准化但它的决策机制和KNN完全不同。这两个模型的相似表现说明一个问题数据的内在结构比较容易划分多种模型都能逼近较高的上限。此时选择模型就取决于业务约束了KNN的优势是可解释性强每个预测结果都能追溯到具体的训练样本。7. 实操中的常见问题与排查记录7.1 数据泄漏最隐蔽的坑我最初写代码的时候犯过一个典型错误先对整个数据集做了标准化然后才划分训练集和测试集。结果交叉验证得分非常高测试集得分也很高看起来一切完美。后来把划分顺序调整过来在训练集上fit、在测试集上transform测试集得分就明显掉了一截这才意识到之前的流程存在数据泄漏。数据泄漏的本质是测试集的信息通过均值、标准差过多年的统计量传递到了训练过程里。由于这些统计量是全局统计量相当于测试集已经被模型偷瞄了一遍评估结果自然乐观。这个坑非常隐蔽因为最终模型的输入数据看起来都一样代码也不会报错只有对比正确流程才能发现问题。7.2 标准化器重复fit导致数据分布漂移第二个常见问题是清洗数据时把标准化器放在了交叉验证循环内部。如果用cross_val_score传入未经标准化的数据并把标准化步骤写在预处理的管线里配合Pipeline是最可靠的解决方案。手动做很容易在交叉验证时用全量数据的统计量或者漏掉某个fold的标准化处理。正确的做法是使用Pipeline把标准化和KNN封装成一个整体或者老老实实先fit训练集transform训练和测试集再进入交叉验证环节。Pipeline本质上是把标准化器的fit限制在每一折的训练数据内部彻底避免泄漏。from sklearn.pipeline import Pipeline pipeline Pipeline([ (scaler, StandardScaler()), (knn, KNeighborsClassifier(n_neighbors15, weightsdistance)) ]) scores cross_val_score(pipeline, X_train, y_train, cv5)这段代码的优雅之处在于整个过程不需要担心标准化器是在哪一部分数据上fit的Pipeline会在每一折的拆分逻辑里自动管理参数学习边界。7.3 平票问题怎么处理即使K值取奇数三分类任务中也可能出现两个类别同时得票最高、第三类较少的情况。比如K5时A类两票、B类两票、C类一票平票就产生了。sklearn默认采用随机打破平票的策略也不会报错但结果不可控。想避免这个问题调整方案有很多。最简单的就是把K值再调大或者调小让投票组合发生变化。更稳妥的是使用距离加权平票情况下距离近的一侧权重大自然破局。这也是我最终选distance权重的一个重要原因。7.4 特征值范围差异导致的隐形偏差有些初学者会忽略特征可视化这一步直接开始建模。但做KNN之前至少应该画一次特征分布直方图或者箱线图。我第一次做的时候发现某个特征多数值集中在很小范围内但有零星数据点而没有标准化时这个小范围特征几乎不起作用。标准化解决了这个问题但前提是你知道问题存在。查看特征分布还能帮助判断合适的数据变换策略。如果某个特征的分布是严重长尾的先做对数变换再标准化效果可能比直接标准化更好。这个项目的数据相对规整不需要这一步但了解这个思路对后续处理更复杂的数据集有帮助。8. 项目扩展思路从一个入门项目走向更多可能性8.1 参数搜索的进阶玩法网格搜索虽然好用但组合一多、参数空间一大计算成本就非常可观。这个项目参数组合只有几十种几秒钟就跑完了。但如果换一个参数较多的模型网格搜索的耗时就会飙升。更聪明的做法是随机搜索它随机抽样参数组合在更短的耗时内覆盖更大的搜索空间。还有一种更高效的方式是HalvingGridSearchCV它先用部分数据快速淘汰明显不佳的参数组合再逐步加大数据量精挑细选效率比全网格搜索高很多。8.2 尝试其他距离度量除了欧氏距离和曼哈顿距离还有更复杂的距离度量可以实验比如切比雪夫距离、闵可夫斯基距离的p值调参。用GridSearchCV把metric设为minkowski把p作为一个超参数加入搜索网格观察不同p值对结果的影响这个过程本身比最终调优结果更有教学价值。某些场景下特征本身就是非数值型的或者混合类型的就需要换用汉明距离或者自定义距离函数。虽然这个项目用不上但知道KNN可以通过自定义metric适配特殊数据场景能够帮助你看到KNN的更多潜力。8.3 从分类到回归KNN不仅是分类器也可以做回归任务。把KNeighborsClassifier换成KNeighborsRegressor投票模式变成取K个邻居标签的平均值就能预测连续数值。比如用酒的成分预测某个品质评分KNN回归就是一个不错的起点数据规模不大、变量关系不明朗时这种简单模型往往非常稳定。这就把同一个算法思想迁移到了不同的任务类型学到的是方法骨架而不是某个特定API的机械记忆。动手迁移一次理解就会更进一步。8.4 部署到真实场景的注意事项如果这个模型要部署到实际环境比如填充到一个小型Web服务里用来给用户上传的成分数据做品种预测有几个关键点需要注意。模型文件要序列化保存推荐用joblib.dump把训练好的KNN和标准化器一起打包部署加载时先反序列化然后在请求处理函数中依次transform和predict。KNN是惰性模型序列化出来的是整个训练样本集这个项目的训练样本只有一百多条文件很小加载很快。如果数据量增长到百万级别就需要考虑用向量检索工具或者分布式预测方案KNN朴素实现对大规模在线预测是不太友好的。把这些约束想清楚项目的完整度就不一样了这套思路可以迁移到任何机器学习实际项目里。9. 实操总结与经验心得走完这个KNN实战项目有几点体会是这个项目本身最有价值的部分。KNN的代码实现非常简洁调库版本不到十行核心代码但它牵涉到的关键决定却不少。数据标准化、训练测试划分、K值选择、权重策略、交叉验证方式每个环节都对最终效果有实质性影响。入门者往往觉得模型调用就是核心但项目做完整了才会发现数据准备和评估流程才是工程上最耗时也最容易出错的部分。关于调参这件事网格搜索和交叉验证的结果只能说明模型在给定数据上的表现不能脱离业务背景空洞地追求准确率。这个项目中K15配合距离加权在准确率和稳定性上都是最优解但如果样本量变化、特征增减这个结论就需要重新验证模型选择永远是具体问题具体分析。最后说一个操作习惯上的建议。做类似的小型项目时可以把随机种子固定下来在代码开头写一个配置区域把所有可变参数集中管理。这个习惯的收益在刚起步时看不出来但当你需要重新运行项目、修改特征组合或者对比多个方案的时候能节省大量的返工时间。机器学习项目的可复现性是专业开发者的基本功从第一个项目开始养成这个习惯比之后纠正容易得多。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑