news 2026/9/26 11:28:01

KNN红酒分类实战:从课程作业到可复现调参流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
KNN红酒分类实战:从课程作业到可复现调参流程

简介:这份资源是面向计算机相关专业在校学生与初学者的机器学习课程作业包,围绕KNN算法完成红酒分类实验,适合作为课程设计、大作业或入门练手项目。压缩包共3个文件,包含1个py源码、1个data数据集和1个txt说明文件,整体约6KB,体积轻量便于快速下载与本地运行。源码配有详细注释,覆盖数据读取、特征处理、KNN建模与分类评估等关键环节,读者可据此理解K近邻算法的完整实现流程,并在此基础上调整参数或替换数据集,拓展其他分类任务。目前已有474人学习下载,说明该实验方案具备一定的参考与借鉴价值,适合需要完成机器学习课程作业或希望动手实践KNN算法的同学下载使用。

1. 从一份课程作业说起:KNN 做红酒分类到底能跑出什么结果

很多人第一次接触 KNN 算法,都是在一份「课程作业-基于KNN算法实现红酒分类实验源码+详细注释+数据集.zip」这样的压缩包里。解压之后通常是一个 csv 数据集、一个 py 脚本、几行注释,跑一下python main.py,控制台打印出准确率 0.94 左右,然后就没有然后了。这份作业真正值钱的地方不在那个数字,而在于它是一条完整的、可复现的监督学习最小闭环:读数据、切分训练测试集、标准化、调 K、评估、看混淆矩阵。红酒分类这个场景选得也巧,它用的是经典的 Wine 数据集,13 个特征全是连续数值,没有缺失值,样本量 178 条,三个类别,天然适合拿来把 KNN 的每一个参数都摸一遍。

如果你正在做课程设计、想找一个能写进简历的小项目,或者单纯想搞明白「KNN 到底怎么调、为什么我的准确率上不去」,这份作业的骨架足够你拆开重装。下面我按自己复现这类作业的习惯,把数据、源码、参数、坑点一层层讲清楚,你照着敲一遍,比直接抄那份 zip 里的代码收获大得多。

2. 红酒数据集长什么样:13 个特征、3 个类别和它的分布陷阱

2.1 Wine 数据集的字段含义与加载方式

Wine 数据集来自 UCI 机器学习库,178 个样本,每个样本 13 个化学特征,标签是三个品种。常见的 csv 版本第一列是类别标签(1/2/3),后面 13 列是特征。字段含义大致是酒精含量、苹果酸、灰分、灰分碱度、镁、总酚、黄酮类、非黄酮类酚、原花青素、颜色强度、色调、稀释酒 OD280/OD315、脯氨酸。这些名字看着唬人,但对 KNN 来说你不需要理解化学意义,只需要知道它们是数值、量纲差异极大。

加载方式我一般用 pandas,比手写 csv 解析稳:

import pandas as pd import numpy as np # 假设 csv 无表头,第一列是类别 df = pd.read_csv("wine.csv", header=None) X = df.iloc[:, 1:].values.astype(float) # 13 个特征 y = df.iloc[:, 0].values.astype(int) # 类别标签 1/2/3 print("样本数:", X.shape[0], "特征数:", X.shape[1]) print("类别分布:", np.bincount(y))

这段代码的关键点是header=None,因为很多课程作业附带的 csv 没有表头,如果你按默认header=0读,第一行数据会被当成列名吃掉,样本数直接少一条,后面所有指标都对不上。iloc[:, 1:]是切掉标签列,astype(float)是防止某些列被 pandas 推断成 object。跑完你应该看到样本数 178、特征数 13、类别分布大致是 59/71/48。

2.2 为什么必须先做标准化:量纲差异是 KNN 的第一杀手

KNN 的核心是距离。欧氏距离把每个维度的差值平方后相加,这意味着量纲大的特征会主导距离计算。Wine 数据集里脯氨酸的数值范围能到 1000 以上,而色调只有 0.x 级别,如果不做标准化,距离几乎完全由脯氨酸决定,其他 12 个特征等于白给。这就是很多人「代码没报错但准确率死活上不去」的头号原因。

标准做法是 z-score 标准化,让每个特征均值 0、方差 1:

from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split # 先切分再标准化,避免测试集信息泄漏 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) scaler = StandardScaler() X_train = scaler.fit_transform(X_train) # 只在训练集上 fit X_test = scaler.transform(X_test) # 测试集只 transform

这里有两个参数值得说。stratify=y保证切分后训练集和测试集的类别比例一致,Wine 三个类别样本不均衡,不加这个参数可能出现某个类别在测试集里只剩两三条。random_state=42是为了结果可复现,你换个数准确率会小幅波动,这是正常的,不是代码有 bug。标准化必须「先切分再 fit」,如果对全量数据 fit 再切分,测试集的均值和方差信息就泄漏进了训练过程,评估结果会虚高,这是课程作业里最常见的隐性错误。

3. 把 KNN 源码拆开:从手写距离到 sklearn 的 KNeighborsClassifier

3.1 手写一版 KNN:理解 predict 里到底发生了什么

想真正搞懂 KNN,我建议先手写一遍最朴素的版本,哪怕只有二十行。它的逻辑就三步:算测试样本到所有训练样本的距离、取最近的 K 个、投票决定类别。

def knn_predict(X_train, y_train, x_test, k=5): # 1. 计算欧氏距离 diff = X_train - x_test # 广播,形状 (n_train, n_features) dist = np.sqrt(np.sum(diff ** 2, axis=1)) # 2. 取距离最小的 k 个索引 idx = np.argsort(dist)[:k] # 3. 多数投票 labels = y_train[idx] return np.bincount(labels).argmax() # 单条预测验证 pred = knn_predict(X_train, y_train, X_test[0], k=5) print("预测:", pred, "真实:", y_test[0])

np.argsort(dist)[:k]是取最近邻的标准写法,np.bincount(labels).argmax()做多数投票。注意argmax返回的是出现次数最多的标签值,前提是标签从 0 开始连续,Wine 标签是 1/2/3,bincount会返回长度 4 的数组,索引 0 计数为 0,不影响结果但要知道这个细节。手写版跑单条没问题,跑全量测试集会慢得让你怀疑人生,因为它没有任何向量化优化,这也解释了为什么生产环境都用 sklearn。

3.2 sklearn 版本:三个必调参数和它们的默认值

实际作业里用KNeighborsClassifier就够了,但默认参数不一定适合你的数据。下面是我一般会显式写出来的配置:

from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score, classification_report clf = KNeighborsClassifier( n_neighbors=5, # K 值,最核心的参数 weights="uniform", # 投票权重,uniform 或 distance metric="minkowski", # 距离度量 p=2 # p=2 是欧氏距离,p=1 是曼哈顿距离 ) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print("准确率:", accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred))

n_neighbors是 K 值,直接决定决策边界的平滑程度。weights设成distance时,距离越近的邻居投票权重越大,样本不均衡或边界模糊时通常比uniform好一点。p=2是欧氏距离,如果你怀疑数据有离群点,可以试p=1曼哈顿距离,它对异常值更鲁棒。classification_report会给出每个类别的 precision、recall、f1,比只看一个总准确率有用得多,因为总准确率高不代表每个类别都分对了。

3.3 用交叉验证选 K:别拿测试集当调参集

新手最容易犯的错是:写个 for 循环把 K 从 1 试到 20,看哪个在测试集上准确率最高就选哪个。这等于用测试集调参,最后报出来的准确率是乐观偏差的。正确做法是用交叉验证在训练集内部选 K:

from sklearn.model_selection import cross_val_score k_scores = [] for k in range(1, 21): clf = KNeighborsClassifier(n_neighbors=k) # 5 折交叉验证,只在训练集上做 scores = cross_val_score(clf, X_train, y_train, cv=5, scoring="accuracy") k_scores.append(scores.mean()) best_k = np.argmax(k_scores) + 1 print("最佳 K:", best_k, "交叉验证准确率:", max(k_scores))

cv=5是 5 折交叉验证,把训练集分成 5 份,轮流用 4 份训练 1 份验证,最后取平均。这样选出来的 K 更可信。Wine 数据集上最佳 K 通常在 3 到 8 之间,K 太小容易过拟合(对噪声敏感),K 太大容易欠拟合(决策边界过于平滑)。你可以把k_scores画成折线图,会看到一条先升后降的曲线,那个拐点就是你要的 K。

4. 避坑与排查:红酒分类实验里最容易翻车的 5 个地方

4.1 准确率 100% 或低得离谱

现象:跑出来准确率要么接近 1.0,要么只有 0.3 左右。原因:前者通常是数据泄漏,比如先对全量数据做了标准化再切分,或者把标签列也当成特征喂进去了;后者多半是没做标准化,量纲大的特征主导了距离。解决:检查特征矩阵是否误含标签列,确认fit_transform只作用在训练集上,测试集只transform。标准化这一步用Pipeline包起来最保险。

4.2 换了 random_state 结果就变

现象:每次运行准确率都不一样,差个两三个百分点。原因:train_test_split的随机切分导致训练集和测试集构成不同,小数据集上这个波动很正常。解决:固定random_state用于复现,但报告结果时应该用交叉验证的均值而不是单次切分的结果。如果单次结果波动超过 5 个百分点,说明数据量太小,考虑用分层抽样stratify=y稳定类别比例。

4.3 类别预测全挤到一个类

现象:混淆矩阵显示某个类别 recall 接近 0,所有样本都被预测成样本量最大的那一类。原因:类别不均衡加上 K 值偏大,多数投票被大类别垄断。解决:把weights改成distance,让近邻说话更有分量;或者减小 K 值;再不行就上class_weight或对少数类做重采样。Wine 数据集不均衡程度不严重,但这个方法在真实不均衡数据上是通用的。

4.4 手写 KNN 预测结果和 sklearn 对不上

现象:自己写的knn_predict和KNeighborsClassifier对同一个样本给出不同标签。原因:多半是距离度量不一致,sklearn 默认minkowski且p=2,等价于欧氏距离,但如果你手写时忘了开方,或者标准化只做了一半,结果就会偏。解决:先确认两边用的是同一份标准化后的数据,再逐条打印距离排序,对比前 K 个邻居的索引是否一致。平票时 sklearn 的打破规则和argmax也不同,K 取奇数能规避大部分平票。

4.5 数据集读取后样本数不对

现象:X.shape[0]不是 178,或者类别数不是 3。原因:csv 有表头但用了header=None,或者分隔符不是逗号(有些版本是分号或制表符)。解决:先用pd.read_csv(path, nrows=3)看一眼原始结构,确认header和sep参数。如果第一列是字符串标签,记得做标签编码或者直接astype(int)。这一步花两分钟,能省掉后面半小时的排查。

5. 把实验做扎实:从单次评估到可复现的调参流程

5.1 用 Pipeline 把标准化和 KNN 绑在一起

前面反复强调「先切分再标准化」,但手动管理容易出错。sklearn 的Pipeline能把这两步串起来,交叉验证时自动对每个折内部做标准化,彻底杜绝泄漏:

from sklearn.pipeline import Pipeline from sklearn.model_selection import GridSearchCV pipe = Pipeline([ ("scaler", StandardScaler()), ("knn", KNeighborsClassifier()) ]) param_grid = { "knn__n_neighbors": range(1, 21), "knn__weights": ["uniform", "distance"], "knn__p": [1, 2] } grid = GridSearchCV(pipe, param_grid, cv=5, scoring="accuracy", n_jobs=-1) grid.fit(X_train, y_train) print("最佳参数:", grid.best_params_) print("最佳交叉验证分数:", grid.best_score_) print("测试集分数:", grid.score(X_test, y_test))

Pipeline里的步骤名scaler、knn是自定义的,param_grid里用双下划线knn__n_neighbors指定具体步骤的参数。GridSearchCV会穷举所有组合,n_jobs=-1用满所有 CPU 核心。注意grid.score(X_test, y_test)才是最终在测试集上的表现,best_score_是交叉验证分数,两者通常接近但不相等,差太多说明参数过拟合了验证集。

5.2 混淆矩阵和特征量纲的复查清单

调完参别急着交作业,做两件事。第一,打印混淆矩阵,看错分集中在哪两个类别之间:

from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt cm = confusion_matrix(y_test, grid.predict(X_test)) sns.heatmap(cm, annot=True, fmt="d", cmap="Blues") plt.xlabel("预测") plt.ylabel("真实") plt.show()

第二,复查标准化后的特征统计量,确认每个特征均值接近 0、标准差接近 1。如果某个特征标准差还是几十,说明标准化没生效,多半是fit_transform用错了对象。这两步做完,你对这份作业的掌握程度就超过 90% 只跑了一遍main.py的人了。

5.3 一个我踩过的坑:别在循环里反复 fit scaler

最后说个血泪经验。我早期写调参脚本时,图省事在 K 值循环外面 fit 了一次 scaler,然后循环里反复用同一个 scaler 去 transform 不同的训练折。结果交叉验证分数虚高,因为每一折的验证集都用了全局的均值方差。正确做法是把 scaler 放进 Pipeline,让GridSearchCV在每一折内部重新 fit。这个坑不报错、不警告,只会让你的分数好看得可疑。后来我养成了一个习惯:只要看到标准化和交叉验证同时出现,第一反应就是检查有没有用 Pipeline。希望这个习惯也能帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 11:27:01

我花了3周把数据库备份从手动改成自动化的真实记录

我花了3周把数据库备份从手动改成自动化的真实记录上个月接了个制造业客户的运维改造活儿,他们核心业务库是MySQL 8.0,跑在阿里云ECS上,数据量大概2.5TB。最让我头疼的是,之前全靠DBA每天早上手动执行mysqldump,不仅容…

作者头像 李华
网站建设 2026/9/26 11:25:37

ASP+ACCESS网上服装销售系统毕设:环境配置、源码改造与答辩要点

简介:一套基于ASP与ACCESS的网上服装销售系统毕业设计资料包,面向计算机专业毕业生和网页开发初学者,解决从需求分析、数据库设计、编码实现到论文撰写与答辩全过程缺少完整参照的问题,适用于课程设计、毕业设计或个人自学。压缩包…

作者头像 李华
网站建设 2026/9/26 11:25:34

VS2015下FFmpeg静态库编译:x86/x64双架构完整指南

简介:ffmpeg n4.4.1 对应的 vs2015 静态库编译包,提供 x86/x64 双平台 lib 文件,专门面向需要在 Windows 下生成独立可执行程序、又不希望逐台部署 DLL 的音视频开发者。压缩包共 140 个文件,包含 125 个 C/C 头文件、14 个静态库…

作者头像 李华
网站建设 2026/9/26 11:23:52

二手车价格预测竞赛:数据清洗、特征工程与模型融合实战

简介:这份资料包是阿里天池与Datawhale联合举办的二手车交易价格预测竞赛的优胜奖方案,面向计算机、应用数学、电子信息工程等专业的学生与研究人员,可作为课程设计、毕业设计或学术竞赛的参考素材,帮助读者理解特征工程、模型构建…

作者头像 李华