news 2026/9/13 17:04:00

手工实现KNN与朴素贝叶斯:鸢尾花分类算法全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手工实现KNN与朴素贝叶斯:鸢尾花分类算法全解析

简介:这一项目以鸢尾花数据集为对象,手工实现KNN与朴素贝叶斯两种经典分类算法,适合机器学习初学者对照理论动手实践。压缩包内共5个文件,包含两个Python代码文件、鸢尾花数据csv、结果txt以及README说明,整个资源仅4KB,结构精简便于快速上手。已有935人学习使用,代码可直接运行,读者不仅能直接观察分类结果,还可通过修改K值、启停拉普拉斯平滑等方式,直观对比两种算法在准确率和运行时间上的差异。在实现层面,KNN部分基于欧氏距离完成近邻投票,朴素贝叶斯部分则依照贝叶斯定理计算先验与条件概率;随附csv数据覆盖三类鸢尾花各50个样本,有助于理解特征条件独立假设的实际影响。这份资料对课程设计、算法入门或期末复习均有帮助,可作为对照理论公式动手编码的参考。

1. 鸢尾花分类为什么值得手工写一遍KNN和朴素贝叶斯

鸢尾花数据集几乎是每个机器学习入门者都会撞上的第一道坎。用sklearn三行代码就能跑完分类,准确率还轻松超过95%,于是多数人调完包就过去了,对算法内部到底怎么决策一无所知。我见过不少简历写着熟悉KNN和朴素贝叶斯的候选人,被问一句"K值怎么调、先验概率怎么算"就卡壳。这次分享的手工实现项目,把knn.py、nbayes.py两份代码和iris.csv数据放在一起,不依赖任何机器学习框架,纯Python从头写距离计算、概率估计和类别判定,跑一遍就能把这两个算法的全部细节看得清清楚楚。KNN作为惰性学习的代表,朴素贝叶斯作为生成式模型的典型,两者在鸢尾花这个150样本、4特征、3类别的小规模数据集上,正好能暴露各自的特性和局限。

2. KNN手工实现:从距离度量到投票决策的完整链路

2.1 KNN的惰性学习本质与鸢尾花场景的适配性

KNN的思路非常直接:新样本的类别由它最近的K个已知样本投票决定。这个"最近"在数学上落到具体计算就是距离度量,最常见的欧氏距离公式是d(x, y) = √(Σ(x_i - y_i)²)。手工实现时,这个公式要自己动手写成循环,而不是调用现成的pairwise_distances。理解这一层很关键,因为距离度量的选择直接影响分类边界。

Iris数据集的特征是花萼长度、花萼宽度、花瓣长度、花瓣宽度,单位都是厘米,4个特征的数值范围差别不大,所以用欧氏距离不会出现某个特征主导整个距离计算的问题。如果换到量纲差异悬殊的数据集,比如年龄和工资放在一起,就必须先做标准化,否则距离计算会完全被大数值特征绑架。

KNN在这个场景下的优势是决策边界可以非常灵活,能适应鸢尾花三个类别中Setosa线性可分、另外两个类别有重叠这种混合结构。代价是预测时需要遍历全部训练样本计算距离,150个样本跑起来不觉得慢,但样本量到万级时,每次预测的耗时就会线性恶化,这是"懒惰学习"最直接的代价。

2.2 手工实现代码与运行逻辑

import pandas as pd import numpy as np from collections import Counter def load_data(path): # 读取鸢尾花数据集,最后一列是标签 data = pd.read_csv(path) X = data.iloc[:, :-1].values y = data.iloc[:, -1].values return X, y def euclidean_distance(a, b): # 手工计算欧氏距离,不使用任何现成距离函数 return np.sqrt(np.sum((a - b) ** 2)) def knn_predict(X_train, y_train, x_test, k): # 保存所有训练样本与新样本的距离 distances = [] for i in range(len(X_train)): dist = euclidean_distance(x_test, X_train[i]) distances.append((dist, y_train[i])) # 按距离升序排序,取前K个 distances.sort(key=lambda item: item[0]) neighbors = distances[:k] # 统计K个邻居中出现最多的类别 votes = Counter([label for _, label in neighbors]) return votes.most_common(1)[0][0]

这段代码把KNN预测的全过程展开成了三个动作。euclidean_distance函数用numpy的数组逐元素相减、平方、求和、开方,逻辑上等价于手写for循环,但执行效率更高。knn_predict函数里用distances列表保存了每个样本的距离和标签,排序后切片取前K个,最后用Counter统计票数。整个流程没有任何一步是黑盒操作。

2.3 K值选择、平票处理与性能边界

K值是KNN里唯一需要人工确定的超参数,也是第一个会踩的坑。K设成1的时候,决策边界极度曲折,训练集准确率接近100%,但泛化能力差,一个噪声点就能改变一片区域的分类结果,这是过拟合的典型表现。K设得过大,比如在150个样本里取K=40以上,少数类别的样本会被多数类别淹没,决策边界被严重平滑,欠拟合随之而来。

平票问题是第二个坑。K=4时,四邻居可能出现2比2,这时候代码里的most_common(1)只能返回先遇到的类别,带有隐性的顺序偏差。普遍的处理策略是优先选择距离更近的邻居类别,或者直接跳过偶数K值,这也是"K通常取奇数"这句话的根本来源——奇数天然避免大部分平票情况。

KNN在这里的性能边界也很明确:预测时的时间复杂度是O(n),n是训练样本数。iris数据集只有150个样本,单次预测在毫秒级,完全无感。但这个复杂度意味着,如果训练集变成10万条记录,每次预测都要算十万次距离,在高QPS的在线预测场景下基本不可用。所以KNN的实际应用场景集中在样本量可控的小规模分类、推荐系统里的相似项查找,以及作为后续更复杂算法的baseline。

3. 朴素贝叶斯手工实现:先验概率、条件概率与拉普拉斯平滑

3.1 从贝叶斯定理到条件独立假设的概率链

朴素贝叶斯走的是完全不同的路线——它不存样本,而是从训练数据里学习一组概率参数。核心是贝叶斯定理:P(类别|特征) = P(类别) × P(特征|类别) / P(特征)。分母P(特征)对所有类别是一样的,所以在做类别比较时可以直接省略,只需比较分子的大小。

"朴素"二字落在条件独立假设上:假设四个特征在给定类别时彼此独立,这样联合概率P(特征|类别)可以拆成P(花萼长度|类别) × P(花萼宽度|类别) × P(花瓣长度|类别) × P(花瓣宽度|类别)的连乘形式。真实数据里花瓣长度和花瓣宽度强相关,这个假设并不成立,但拆开后参数估计变得异常简单——只需要为每个特征在每个类别下分别建模,然后相乘。

3.2 连续特征的概率估计方法对比

鸢尾花的四个特征全是连续值,处理连续特征有三种做法。

第一种是把特征离散化,分箱后统计频率,缺点是分箱粒度难以把握。第二种是假设特征服从高斯分布,在训练阶段计算每个类别下每个特征的均值μ和标准差σ,预测时直接代入高斯分布公式算概率密度。第三种是用核密度估计,更灵活但计算开销大。

我在手工实现里选择了第二种,高斯朴素贝叶斯,因为鸢尾花的每个特征在各类别下的分布大致呈钟形,高斯假设与数据基本吻合,代码量也最小。有一个容易被忽略的操作:预测时用的是概率密度值,不是概率,密度值可以大于1,直接连乘会导致浮点下溢。标准做法是对连乘取对数,把乘法变成加法,数值稳定性会好很多。

3.3 核心代码与拉普拉斯平滑的落地

import numpy as np class GaussianNaiveBayes: def __init__(self, smoothing=1e-9): self.smoothing = smoothing # 防止方差为0导致除零错误 self.classes = None self.mean = {} # 每个类别下每个特征的均值 self.var = {} # 每个类别下每个特征的方差 self.priors = {} # 每个类别的先验概率 def fit(self, X, y): self.classes = np.unique(y) for c in self.classes: X_c = X[y == c] self.mean[c] = X_c.mean(axis=0) # 加上平滑项,避免特征完全相同时方差为0 self.var[c] = X_c.var(axis=0) + self.smoothing self.priors[c] = len(X_c) / len(X) return self def _gaussian_pdf(self, x, mean, var): """手工实现高斯概率密度函数""" exponent = -((x - mean) ** 2) / (2 * var) coefficient = 1.0 / np.sqrt(2 * np.pi * var) return coefficient * np.exp(exponent) def predict(self, X): predictions = [] for x in X: # 存储每个类别的对数后验得分 scores = {} for c in self.classes: # 对数先验概率作为初始值 scores[c] = np.log(self.priors[c] + self.smoothing) # Verify和每个特征的条件分布 for i in range(len(x)): pdf = self._gaussian_pdf(x[i], self.mean[c][i], self.var[c][i]) scores[c] += np.log(pdf + self.smoothing) predictions.append(max(scores, key=scores.get)) return np.array(predictions)

fit方法是学习阶段的核心:对每个类别,分别计算该类别下所有样本的特征均值和方差,再统计该类别占比作为先验概率。predict方法里关键在对数空间操作:先取先验概率的对数,再累加每个特征的条件概率密度对数值。self.smoothing这个参数需要特别说明,它同时承担两个职责——对方差加一个极小值防止除零,对概率加一个极小值防止log(0)报错。

拉普拉斯平滑更标准的做法是分子加1、分母加类别数,适用于离散特征计数。这里的高斯版本把拉普拉斯的思想迁移到连续场景,用epsilon加性平滑替代了原始的1-加性平滑,因为概率密度理论上可以是任意正数,不存在"未观测到的类别"这个语义。跑代码时可以在iris上试着调大self.smoothing到1.0,然后观察准确率的变化,会发现它对最终结果的影响远小于KNN里K值的影响,这说明朴素贝叶斯对概率参数的细微扰动不敏感,这是它在小样本场景下稳定性的来源。

3.4 边界场景与失效模式分析

高斯朴素贝叶斯的场景失效模式是一个值得注意的点。如果数据特征存在极端偏差分布,高斯假设就不成立。想象一个特征在某个类别下的分布是双峰或长尾的,用均值±2σ范围内覆盖95%以上样本的假设去拟合,中间密度最高的区域反而会欠拟合。另一个失效场景是特征数量远大于样本数量,比如文本分类里词表几万维、训练文档只有几百篇,高斯方差估计会严重失真,这时候更适合用多项式朴素贝叶斯结合词频直方图。

先验概率的影响容易被低估。在iris数据集上,三个类别各占50条,先验概率均为1/3,均匀分布下先验对结果几乎没有干扰。但现实中如果训练集的类别比例和真实场景不一致,比如训练数据里Setosa占80%、现场实际只有10%,那么预测结果会偏向先验概率高的类别。手工实现里可以单独打印self.priors看看值,然后手动改一份不均衡的样本模拟一下,切身体会比理论说明更有说服力。

4. 两算法对比评估:交叉验证与边界样本分析

4.1 评估脚本与准确率基准

import numpy as np from knn import knn_predict from nbayes import GaussianNaiveBayes def cross_validate(X, y, k_folds=5): indices = np.arange(len(X)) np.random.shuffle(indices) fold_size = len(X) // k_folds knn_scores = [] nb_scores = [] for fold in range(k_folds): val_idx = indices[fold * fold_size:(fold + 1) * fold_size] train_idx = np.setdiff1d(indices, val_idx) X_train, y_train = X[train_idx], y[train_idx] X_val, y_val = X[val_idx], y[val_idx] # KNN评估,K=5 knn_correct = 0 for x, true_label in zip(X_val, y_val): pred = knn_predict(X_train, y_train, x, k=5) if pred == true_label: knn_correct += 1 knn_scores.append(knn_correct / len(val_idx)) # 朴素贝叶斯评估 nb = GaussianNaiveBayes(smoothing=1e-9) nb.fit(X_train, y_train) nb_pred = nb.predict(X_val) nb_scores.append(np.mean(nb_pred == y_val)) return np.mean(knn_scores), np.mean(nb_scores), knn_scores, nb_scores

这脚本把5折交叉验证做成了手工循环,目的是同时考核两种算法在相同数据划分下的表现。fold_size是每一折的验证集大小,np.setdiff1d用来从全集中剔除验证集获得训练索引,保证训练集和验证集不重叠。运行后你会看到,KNN和朴素贝叶斯在鸢尾花上的准确率都稳定在90%到96%区间,具体数值取决于随机划分的方式。

4.2 耗时对比和计算复杂度分析

从运行时间看,两算法呈现明显分野。KNN的训练阶段零耗时,时间全部花在预测,150个样本的交叉验证还可以接受,但如果你把样本复制20倍,单次预测就要多算3000个距离。朴素贝叶斯的fit阶段需要对每个特征遍历一次计算均值方差,复杂度是O(n×d),预测阶段只做K次乘法累加,与训练样本量无关。

两算法在准确率上的差异可以用一个具体场景说明。取第51号样本,它是Versicolour里最接近Virginica区域的一个数据点,属于特征空间上的模糊地带。KNN只看它最近的5个邻居是怎么投的票,只要邻居里多数标签指向Versicolour就判Versicolour。朴素贝叶斯计算的是该样本四个特征在Versicolour和Virginica两类下谁的概率乘积更大,不关心邻域的局部结构。这两种决策逻辑在面对同一个模糊样本时,结果可能是相反的,代码跑出来的每一个预测值背后都有明确的概率或距离依据。

4.3 混淆矩阵与类别偏置解读

只盯准确率会掩盖算法的偏好,这一点在手工实现项目里很容易验证。手动写一个混淆矩阵的统计代码,记录每个真实类别被预测成了哪些类别:

def confusion_matrix(y_true, y_pred, classes): # 初始化全零矩阵 matrix = np.zeros((len(classes), len(classes)), dtype=int) for true, pred in zip(y_true, y_pred): i = list(classes).index(true) j = list(classes).index(pred) matrix[i][j] += 1 return matrix

观察重点在Versicolour和Virginica这两行,它们是混淆矩阵里非对角线区域最活跃的位置,因为这两类样本在花瓣长度和花瓣宽度上的分布高度重叠。Setosa几乎永远不会被分错,它的四个特征和其他两类完全分离,线性分类器都能达到100%分离率。KNN在重叠区域的边界更依赖邻域密度,朴素贝叶斯则用概率密度覆盖范围来判断,清晰的还是模糊的,采集到的样本多,边界偏向就是两方对称的,采集到的样本在某个区域缺了一块,两个算法的表现差异立刻就会放大。

5. 参数调优实战:K值搜索与高斯平滑项的实验技巧

KNN里K值的选取有规律可循。一个通用的实验模板是,按奇数从1到21逐一遍历,交叉验证后记录每个K值的平均准确率,然后把曲线拉平出来看趋势,精调K值就是把策略定在曲线平台期的最左侧,也就是准确率表现稳定时的最小K值。

k_candidates = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21] for k in k_candidates: acc = evaluate_knn_with_k(X_train, y_train, X_val, y_val, k) print(f"K={k:2d} accuracy={acc:.4f}")

对朴素贝叶斯,尝试把smoothing设成1e-30.010.11.0这几个量级,观察准确率的变化幅度。在训练样本足够覆盖特征空间时,它几乎不变。把训练集缩减到每类只留15条样本,smoothing的影响就会被放大,加性平滑的护城河作用就显现出来了,此时记录下不加平滑时对应特征的方差值,能看到部分特征方差已经接近零,这是概率估计的数值危机信号。

数据剂量也是一个被忽视的调参维度。分别用每类10条、25条、50条样本训练同一个朴素贝叶斯模型,记录准确率随训练样本量的增长曲线,能直接看到朴素贝叶斯的参数估计收敛速度比KNN快——KNN要达到同等准确率往往需要更多的样本,因为它依赖样本密度覆盖特征空间,而朴素贝叶斯是把数据压缩成几个统计量,具备一定的数据效率优势。

最后建议把两个模型的预测结果逐条对齐,找出哪几个样本被KNN判对而朴素贝叶斯判错,反向去找这些样本的特征值,看它们到底处于类别边界的哪个位置,对比两个算法的分歧样本,对算法理解更有价值。

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

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

Python人工智能案例包:环境配置与代码实战全流程指南

简介:一套聚焦Python人工智能入门与进阶的经典案例合集,面向希望结合真实数据动手实践的学习者,覆盖数据加载、特征分析、模型训练与结果可视化等典型环节。压缩包内共104个文件,主要包含28个csv数据集、24个py案例脚本与10张jpg效…

作者头像 李华
网站建设 2026/9/13 17:03:40

MATLAB BP神经网络分类实战:从蠓虫到鸢尾花全流程

简介:这是一份面向MATLAB初学者的ANN神经网络入门资料,聚焦BP神经网络在分类问题中的完整实现,适合刚接触神经网络或希望在MATLAB中动手实践分类任务的读者。压缩包内共7个文件,含5个txt数据文件与2个m源程序,结构清晰…

作者头像 李华
网站建设 2026/9/13 17:02:51

LabVIEW UDS上位机从TOOMOSS到ZLG的CAN硬件移植指南

1. 项目概述:为什么一个CAN UDS上位机的移植值得专门写十三篇?“基于周立功的CAN UDS升级上位机-LabVIEW版本(十三):从图莫斯到ZLG的移植指南”——这个标题里藏着三个关键信号:CAN总线、UDS诊断协议、LabV…

作者头像 李华
网站建设 2026/9/13 16:58:34

PID目标跟踪从误差建模到参数整定的完整实践指南

简介:面向目标跟踪与路径规划方向的Matlab开发者,这份压缩包提供了基于PID的目标跟踪规划完整实现代码。资源以PID控制为核心,结合纯追踪、运动学模型、模型预测控制等模块,帮助理解目标跟踪中的控制策略与轨迹生成方法。包内包含…

作者头像 李华