K近邻与朴素贝叶斯
机器学习专题 · 掌握KNN和朴素贝叶斯分类算法
专题:Python机器学习系统学习
关键词:Python, 机器学习, KNN, K近邻, 朴素贝叶斯, 贝叶斯定理, 高斯NB, 文本分类, scikit-learn
一、K近邻算法(KNN)
K近邻算法(K-Nearest Neighbors,简称KNN)是机器学习中最简单直观的算法之一,由Cover和Hart于1967年提出。它属于基于实例的学习(Instance-based Learning),也称为懒惰学习(Lazy Learning),因为它在训练阶段并不显式地构建模型,而是将所有训练样本直接存储下来,等到预测时再进行计算。
核心思想
KNN的核心思想用一句中国古语概括就是"近朱者赤"——如果一个样本在特征空间中与某类的大多数样本最相似(距离最近),那么它就属于这个类别。换句话说,事物的性质由其邻居决定。这种思想非常朴素且符合人类的直觉认知:我们通常会根据一个人周围的朋友来判断他的倾向。
算法步骤
KNN算法的执行流程可以分为以下四个步骤:
- 计算距离:对于待分类的测试样本,计算它与训练集中每一个样本之间的距离。
- 选择K个邻居:根据计算出的距离,找出距离最近的K个训练样本。
- 投票决策:对于分类任务,统计K个邻居中各个类别的出现频率,将出现频率最高的类别作为预测结果。
- 输出结果:返回预测的类别标签(或回归预测值)。
K值的选择
K值是KNN算法最重要的超参数,它决定了需要参考多少个邻居来进行决策。K值的选择对模型性能有显著影响:
- K值过小(如K=1):模型过于复杂,容易受到噪声点的影响,导致过拟合。当K=1时称为最近邻算法,决策边界非常曲折。
- K值过大:模型过于简单,决策边界趋于平滑,可能忽略数据中的真实模式,导致欠拟合。当K等于训练集大小时,所有测试样本都会被预测为训练集中样本数最多的类别。
- K值选择方法:通常使用交叉验证(Cross-Validation)来选择最佳的K值。实践中常用K=3、5、7等较小的奇数,避免出现平局情况。
距离度量
距离度量是KNN算法中另一个关键因素,不同的距离度量方式会得到不同的邻居集合。常用的距离度量包括:
- 欧氏距离(Euclidean Distance):最常用的距离度量,计算两点之间的直线距离。公式为 d(x,y) = sqrt(sum((xi - yi)^2))。适用于连续数值型特征。
- 曼哈顿距离(Manhattan Distance):计算两点在各坐标轴上的绝对距离之和,也称为城市街区距离。公式为 d(x,y) = sum(|xi - yi|)。在高维空间中,曼哈顿距离比欧氏距离更稳定。
- 闵可夫斯基距离(Minkowski Distance):欧氏距离和曼哈顿距离的推广形式。当p=1时为曼哈顿距离,p=2时为欧氏距离。公式为 d(x,y) = (sum(|xi - yi|^p))^(1/p)。
- 余弦相似度(Cosine Similarity):衡量两个向量之间的夹角余弦值,关注方向而非距离。常用于文本分类等高维稀疏数据场景。
权重策略
在基本的KNN中,所有K个邻居对最终决策的贡献是相同的(uniform权重)。但有时我们希望距离更近的邻居拥有更大的话语权:
- uniform(等权):所有K个邻居投票权重相同,每个邻居一票。
- distance(距离加权):邻居的投票权重与距离成反比,距离越近权重越大。通常使用权值 w = 1/d 或 w = 1/(d^2)。这种方式可以有效减少噪声点的影响。
scikit-learn实现
在scikit-learn中,KNN分类器通过KNeighborsClassifier类实现,使用非常方便:
from sklearn.neighbors import KNeighborsClassifier
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
# 数据标准化(重要!)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X_scaled, y, test_size=0.3, random_state=42
)
# 创建KNN分类器(K=5,距离加权)
knn = KNeighborsClassifier(n_neighbors=5, weights='distance')
# 训练(实际上只是存储数据)
knn.fit(X_train, y_train)
# 预测
y_pred = knn.predict(X_test)
# 评估
accuracy = knn.score(X_test, y_test)
print(f'准确率: {accuracy:.4f}')
二、KNN的优缺点
优点
- 简单直观:KNN的算法思想非常容易理解,不需要复杂的数学推导。对于机器学习初学者来说,KNN往往是接触的第一个算法,因为它几乎不需要任何假设。
- 无需训练过程:KNN是懒惰学习算法,训练阶段仅需存储数据,不需要训练时间。这使得它非常适合需要频繁更新数据的场景。
- 适用于多分类问题:KNN天然支持多分类任务,不需要像SVM或逻辑回归那样进行一对多或一对一的扩展。
- 适用于非线性决策边界:KNN是非参数模型,可以对任意复杂的决策边界进行拟合。
缺点
- 预测速度慢:KNN在预测时需要计算测试样本与所有训练样本之间的距离。当训练集很大时,预测速度会非常慢,难以应用于实时场景。
- 对特征缩放敏感:KNN依赖于距离计算,因此不同特征的量纲差异会严重影响距离计算结果。例如,身高(厘米)和体重(千克)如果数值范围不同,数值较大的特征会主导距离计算。必须进行标准化或归一化处理。
- 维度灾难(Curse of Dimensionality):随着特征维度的增加,空间中数据点的分布变得稀疏,距离计算的区分度急剧下降。在高维空间中,所有点之间的距离几乎相等,导致KNN失效。这被称为维度灾难。
- 存储开销大:KNN需要存储整个训练集,内存占用较高。
优化方法
为了克服KNN预测速度慢的问题,研究者提出了多种优化数据结构:
- KD树(KD-Tree):KD树是一种对K维空间进行分割的二叉树结构。它通过递归地按坐标轴将空间划分为多个区域,在查找最近邻时,只需搜索树的部分分支,从而将查找时间复杂度从O(n)降低到O(log n)(在低维空间中有效)。但在高维空间中,KD树的性能会退化。
- Ball树(Ball Tree):Ball树通过一系列嵌套的超球体来划分数据空间,比KD树在高维空间中表现更好。它将数据点组织成树状结构,每个节点对应一个超球体,搜索时通过球体间的距离来剪枝,效率更高。
三、KNN回归
KNN不仅可以用于分类任务,也可以用于回归任务。KNN回归的核心思想与分类完全一致:对于一个待预测的测试样本,找到特征空间中离它最近的K个训练样本,然后将这K个样本的目标值进行平均(或加权平均)作为预测结果。
在scikit-learn中,KNN回归器通过KNeighborsRegressor类实现:
from sklearn.neighbors import KNeighborsRegressor
import numpy as np
# 生成示例数据
X = np.array([[1], [2], [3], [4], [5], [6]])
y = np.array([1.5, 2.0, 2.8, 4.1, 5.2, 5.9])
# 创建KNN回归器
knn_r = KNeighborsRegressor(n_neighbors=3, weights='distance')
# 训练
knn_r.fit(X, y)
# 预测新样本
X_new = np.array([[3.5]])
y_pred = knn_r.predict(X_new)
print(f'预测值: {y_pred[0]:.2f}')
# 输出:预测值: 3.45(取决于距离加权结果)
KNN回归与分类的区别:分类任务是对K个邻居的类别标签进行投票(取众数),而回归任务是对K个邻居的数值标签取平均(或加权平均)。两者的算法框架完全一致,只是最终的聚合策略不同。KNN回归适用于目标值为连续变量的场景,如房价预测、温度预测等。
四、朴素贝叶斯概述
朴素贝叶斯(Naive Bayes)是一种基于贝叶斯定理的分类算法,它以概率论为基础,通过计算样本属于各个类别的概率来进行分类决策。朴素贝叶斯因其简单、高效的特点,在文本分类、垃圾邮件过滤等场景中得到了广泛应用。
朴素假设
朴素贝叶斯的核心假设是:特征之间在给定类别条件下相互独立。也就是说,每个特征对分类结果的影响是独立的,特征之间没有关联和依赖关系。例如,在判断一封邮件是否为垃圾邮件时,"包含'免费'一词"和"包含'点击'一词"这两个特征被认为是相互独立的。
为什么称为"朴素"
之所以称为"朴素"(Naive),正是因为"特征之间条件独立"这个假设在现实世界中几乎不可能成立。在实际数据中,特征之间往往存在一定程度的相关性。例如,在文本分类中,"买"和"便宜"这两个词经常同时出现,它们并非完全独立。然而,令人惊讶的是,即使这个假设非常强烈且通常不成立,朴素贝叶斯在实际应用中仍然表现出色,尤其是在文本分类领域。
"朴素贝叶斯之所以有效,是因为分类决策通常只需要正确的相对概率,而不是精确的概率值。即使概率估计有偏差,只要各类别概率的相对大小保持正确,分类结果就不会受影响。"
五、贝叶斯定理
贝叶斯定理是朴素贝叶斯算法的数学基础,它描述了在已知某些条件的情况下,如何更新对事件的概率估计。贝叶斯定理的公式如下:
P(y|x) = P(x|y) * P(y) / P(x)
其中:
P(y|x) —— 后验概率(Posterior):给定特征x的情况下,样本属于类别y的概率
P(y) —— 先验概率(Prior):在观察到特征之前,样本属于类别y的概率
P(x|y) —— 似然(Likelihood):在类别y的条件下,观察到特征x的概率
P(x) —— 证据(Evidence):观察到特征x的概率,用于归一化
各分量详解
- 先验概率 P(y):反映了我们在观察任何特征之前对类别的信念。例如,在所有邮件中垃圾邮件的比例大约是20%,那么P(垃圾邮件)=0.2。先验概率通常从训练数据中估计。
- 似然 P(x|y):给定某个类别,观察到特定特征组合的概率。例如,在垃圾邮件中看到"免费"这个词的概率。由于朴素贝叶斯假设特征独立,P(x|y)可以分解为各特征条件概率的乘积:P(x1|y) * P(x2|y) * ... * P(xn|y)。
- 证据 P(x):用于归一化的因子,确保后验概率之和为1。在实际分类中,由于我们只关心哪个类别的后验概率最大,证据项对所有类别都是一样的,因此可以省略。
- 后验概率 P(y|x):这是我们最终需要的结果,表示在观察到特征后,样本属于某个类别的概率。
最大化后验概率(MAP)
在朴素贝叶斯分类中,我们选择使后验概率最大的类别作为预测结果,这被称为最大后验概率估计(Maximum A Posteriori,简称MAP)。由于P(x)对所有类别都是常数,MAP决策规则简化为:
y_pred = argmax P(y) * P(x1|y) * P(x2|y) * ... * P(xn|y)
即:选择先验概率与条件概率乘积最大的那个类别。在实际计算中,由于概率值可能非常小(大量小数相乘),通常会取对数将乘法转换为加法,以防止数值下溢:y_pred = argmax [log P(y) + sum(log P(xi|y))]。
六、朴素贝叶斯的三种变体
根据特征的不同分布假设,朴素贝叶斯算法发展出了三种主要的变体。选择合适的变体取决于特征的数据类型:
高斯朴素贝叶斯(GaussianNB)
高斯朴素贝叶斯假设连续特征服从正态分布(高斯分布)。对于每个类别下的每个特征,它都会估计该特征在该类别下的均值和方差,然后根据高斯分布的概率密度函数来计算条件概率。GaussianNB适用于连续数值型特征,如身高、体重、温度、房价等。在scikit-learn中使用GaussianNB类实现。
多项式朴素贝叶斯(MultinomialNB)
多项式朴素贝叶斯假设特征服从多项分布,适用于离散计数特征。它假设每个特征代表某个事件发生的次数,如文本中某个词出现的频次。MultinomialNB是文本分类中最常用的变体,广泛应用于垃圾邮件过滤、新闻分类、情感分析等任务。在scikit-learn中使用MultinomialNB类实现,常见的参数包括alpha(拉普拉斯平滑系数,防止零概率问题)。
伯努利朴素贝叶斯(BernoulliNB)
伯努利朴素贝叶斯假设特征服从伯努利分布,适用于二值特征(0或1)。它关注的是特征是否出现,而不是出现的频次。例如,在文本分类中,BernoulliNB只关心某个词是否出现在文档中(出现为1,不出现为0),而不关心它出现了多少次。BernoulliNB在某些短文本分类任务中可能比MultinomialNB表现更好。在scikit-learn中使用BernoulliNB类实现。
三种变体的对比
| 变体 |
特征类型 |
分布假设 |
典型应用 |
| GaussianNB |
连续数值 |
正态分布 |
医疗诊断、鸢尾花分类 |
| MultinomialNB |
离散计数 |
多项分布 |
文本分类、垃圾邮件过滤 |
| BernoulliNB |
二值(0/1) |
伯努利分布 |
短文本分类、特征出现与否 |
选择建议:如果特征是连续数值型(如身高、温度),使用GaussianNB;如果特征是离散计数(如词频),使用MultinomialNB;如果特征是二值型(如是否出现),使用BernoulliNB。对于文本分类,MultinomialNB通常是首选,因为它利用了词频信息。
七、朴素贝叶斯的应用
文本分类
朴素贝叶斯在文本分类领域有着非常广泛的应用,是其最主要的应用场景之一。常见的文本分类任务包括:
- 垃圾邮件过滤:判断一封电子邮件是正常邮件还是垃圾邮件。这是朴素贝叶斯最经典的应用,早期邮件系统广泛使用。
- 情感分析:判断一段文本的情感倾向是正面、负面还是中性。例如,评论"这个产品非常好用"被分类为正面情感。
- 新闻分类:将新闻文章自动分类到预定义的类别中,如体育、财经、科技、娱乐等。
文档分类
将文档按主题或内容自动归类,是朴素贝叶斯的另一个重要应用。与文本分类类似,文档分类通过分析文档中词汇的出现概率来判断文档所属的类别。
实时预测
朴素贝叶斯的一个突出优势是训练和预测速度都非常快。训练阶段仅需计算类别的先验概率和各特征的条件概率(一次扫描即可完成),预测阶段也只需要进行简单的乘法或加法运算。因此,朴素贝叶斯非常适合需要实时预测的场景。
scikit-learn实现示例
from sklearn.naive_bayes import GaussianNB, MultinomialNB, BernoulliNB
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
# 示例数据:简单文本分类
texts = [
'便宜好用的产品推荐',
'免费领取大奖',
'质量很好非常满意',
'点击链接领取红包',
'物流很快包装精美',
'恭喜中奖免费领取'
]
labels = [0, 1, 0, 1, 0, 1] # 0:正常 1:垃圾
# 文本向量化(词频)
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(texts)
# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(
X, labels, test_size=0.3, random_state=42
)
# 使用多项式朴素贝叶斯
mnb = MultinomialNB(alpha=1.0)
mnb.fit(X_train, y_train)
# 预测
y_pred = mnb.predict(X_test)
print(classification_report(y_test, y_pred))
八、算法对比
KNN vs 朴素贝叶斯
| 对比维度 |
K近邻(KNN) |
朴素贝叶斯 |
| 模型类型 |
基于实例(懒惰学习) |
基于概率(生成式模型) |
| 训练速度 |
极快(仅存储数据) |
快(计算先验和条件概率) |
| 预测速度 |
慢(需计算所有距离) |
极快(简单概率计算) |
| 对特征缩放 |
敏感(必须标准化) |
不敏感(基于概率) |
| 高维数据处理 |
差(维度灾难) |
较好(特征独立假设简化计算) |
| 可解释性 |
好(可查看邻居样本) |
较好(概率可解释) |
| 对小样本 |
较好 |
较差(概率估计不准确) |
| 缺失值处理 |
不支持 |
可忽略缺失特征 |
适用场景建议
选择KNN的场景:数据量适中(几万条以内)、特征维度较低(几十维以内)、数据需要频繁更新、需要查看相似样本的可解释性。KNN非常适合于推荐系统(如找相似用户)、图像识别中的相似度比较等。
选择朴素贝叶斯的场景:文本分类(垃圾邮件、新闻分类)、特征维度很高(如上万维的词袋特征)、需要快速实时预测、数据量较大(百万级以上)。朴素贝叶斯在NLP领域的文本分类任务中几乎是基线标配。
小结:KNN和朴素贝叶斯是两种风格迥异但同样重要的分类算法。KNN依靠样本之间的相似性进行分类,直观易懂但预测速度慢;朴素贝叶斯依靠概率模型进行分类,计算高效但对特征独立性假设要求严格。在实际项目中,建议根据数据特性和业务需求选择合适的算法,也可以同时尝试两种算法,通过交叉验证选择表现更好的一方。