2. K近邻
K近邻算法(KNN)的基本思想是通过计算待分类样本与训练集中所有样本之间的距离,选取距离最近的 K 个样本,根据这些样本的标签进行分类或回归。KNN 属于非参数学习算法,因为它不假设数据的分布形式,主要依赖距离度量来进行决策。
优点
- 简单易懂:KNN算法非常直观,容易理解和实现。
- 无假设:KNN算法对数据没有假设,适用于复杂分布的数据集。
- 适用于多类分类问题:KNN能够处理多类分类问题,只需在投票过程中考虑不同的类别即可。
缺点
- 计算复杂度高:对于大规模数据集,KNN算法的计算复杂度较高,需要计算每个测试样本与所有训练样本之间的距离,耗时较长。
- 对数据的尺度和噪声敏感:KNN对数据的尺度敏感,通常需要对数据进行标准化或归一化处理。此外,KNN对噪声敏感,可能会受到
异常值的影响。
- 需要大存储空间:由于需要存储整个训练集,KNN算法占用较大的内存空间。
2.1 基本模型
对于分类问题,KNN 模型可以表示为:
f ( x ) = argmax c ∑ i = 1 K 1 ( y i = c ) f(\mathbf{x}) = \underset{c}{\text{argmax}} \sum_{i=1}^K \mathbf{1}(y_i = c) f(x)=cargmaxi=1∑K1(yi=c)
其中:
- x \mathbf{x} x 是待分类的输入样本。
- y i y_i yi 是 K 个邻近样本的标签。
- 1 ( ⋅ ) \mathbf{1}(\cdot) 1(⋅) 是指示函数,当条件为真时取值为 1,否则为 0。
- c c c 是所有可能的类别。
对于回归问题,KNN 模型的预测可以通过 K 个邻近样本的标签值的平均来实现:
y ^ = 1 K ∑ i = 1 K y i \hat{y} = \frac{1}{K} \sum_{i=1}^K y_i y^=K1i=1∑Kyi
或使用加权平均:
y ^ = ∑ i = 1 K w i ⋅ y i ∑ i = 1 K w i \hat{y} = \frac{\sum_{i=1}^K w_i \cdot y_i}{\sum_{i=1}^K w_i} y^=∑i=1Kwi∑i=1Kwi⋅yi
其中 w i w_i wi 是样本 y i y_i yi 的权重,通常可以根据距离的倒数或其他方式定义。
2.2 距离度量
KNN 算法中,距离度量是核心部分,决定了如何选择邻近样本。常用的距离度量方法包括:
欧氏距离(Euclidean Distance):
欧氏距离是最常见的距离度量方法,适用于特征尺度相同的场景。对于两个样本 x i \mathbf{x}_i xi 和 x j \mathbf{x}_j xj,欧氏距离定义为:
d ( x i , x j ) = ∑ k = 1 n ( x i k − x j k ) 2 d(\mathbf{x}_i, \mathbf{x}_j) = \sqrt{\sum_{k=1}^n (x_{ik} - x_{jk})^2} d(xi,xj)=k=1∑n(xik−xjk)2
其中:
- n n n是特征的维度数。
- x i k x_{ik} xik 和 x j k x_{jk} xjk分别是样本 x i \mathbf{x}_i xi 和 x j \mathbf{x}_j xj 在第 k 个维度上的特征值。
曼哈顿距离(Manhattan Distance):
曼哈顿距离适用于高维空间且特征不相关的情况。曼哈顿距离定义为:
d ( x i , x j ) = ∑ k = 1 n ∣ x i k − x j k ∣ d(\mathbf{x}_i, \mathbf{x}_j) = \sum_{k=1}^n |x_{ik} - x_{jk}| d(xi,xj)=k=1∑n∣xik−xjk∣
曼哈顿距离计算的是两个样本在各个维度上的绝对差值之和。
闵可夫斯基距离(Minkowski Distance):
闵可夫斯基距离是欧氏距离和曼哈顿距离的泛化形式,定义为:
d ( x i , x j ) = ( ∑ k = 1 n ∣ x i k − x j k ∣ p ) 1 / p d(\mathbf{x}_i, \mathbf{x}_j) = \left(\sum_{k=1}^n |x_{ik} - x_{jk}|^p\right)^{1/p} d(xi,xj)=(k=1∑n∣xik−xjk∣p)1/p
其中:
- p p p 是参数,决定距离度量的形式。
- 当 p = 2 p=2 p=2 时,闵可夫斯基距离即为欧氏距离;当 p = 1 p=1 p=1 时,即为曼哈顿距离。
2.3 决策规则
KNN 的决策规则基于多数投票或加权投票的原则:
分类问题:
- 对于分类任务,KNN 会根据 K 个邻近样本中出现频率最高的类别来决定待分类样本的类别。若出现频率相同,则可以随机选择,或考虑引入距离的权重来打破平局。
回归问题:
- 对于回归任务,KNN 会取 K 个邻近样本标签的平均值或加权平均值作为预测结果。加权时,权重 w i w_i wi 通常与样本 x i \mathbf{x}_i xi 和待分类样本 x \mathbf{x} x 之间的距离的倒数成正比:
w i = 1 d ( x i , x ) w_i = \frac{1}{d(\mathbf{x}_i, \mathbf{x})} wi=d(xi,x)1
2.4 K值选择
K 值是 KNN 算法中的重要参数,决定了选择多少个邻近样本参与投票。K 值的选择通常通过交叉验证来确定。一般来说:
- 较小的 K 值:模型对训练数据的拟合度较高,但可能会受到噪声影响,导致过拟合。
- 较大的 K 值:模型变得平滑,减少了过拟合的风险,但也可能导致欠拟合。
2.5 KD树(K-Dimensional Tree)
为了加速 KNN 的搜索过程,可以使用 KD 树进行优化。KD 树是一种对 k 维空间数据进行分割的树形数据结构,通过这种数据结构可以有效地减少搜索空间,从而提高查找效率。
KD树的构建过程:
- 选择分割维度:选择一个维度将数据按照该维度的中位数分割成两部分。选择的维度通常是通过计算当前节点数据在各维度的方差,选择方差最大的维度进行划分。
- 分割数据:在选定的分割维度上,以中位数为分割点,将数据集划分为两部分,分别作为左右子节点的数据。
- 递归构建:对每个子节点递归进行上述步骤,直到每个子节点的样本数达到预定阈值或所有样本在某一维度上的值都相同。
- 节点表示:每个节点存储一个样本点及其对应的分割维度。
KD树的搜索过程:
- 递归搜索叶节点:从根节点开始,比较目标点与当前节点在分割维度上的值,决定进入左子树还是右子树,直到到达叶节点。
- 回溯搜索:到达叶节点后,计算当前叶节点样本与目标点之间的距离,并更新当前最近邻居。如果当前节点的父节点到目标点的距离小于当前最近邻居的距离,则有可能在兄弟子树中找到更近的邻居,需要回溯检查。
- 剪枝操作:如果兄弟子树的区域与目标点之间的最小距离大于当前已知最近邻居的距离,则可以剪枝,即不需要搜索该子树。
- 返回最近邻居:最终返回 K 个最近邻居。
优缺点:
- 优点:KD 树在低维空间中能显著减少 KNN 搜索的计算量,提高查询效率。对于静态数据集,KD 树是一种非常有效的加速工具。
- 缺点:在高维空间中,KD 树的性能会下降,尤其是当维度数接近样本数时,KD 树的效率可能退化为线性搜索。此外,KD 树不适用于动态数据集,因为在插入或删除样本后需要重建树结构。
KNN 的时间复杂度较高,特别是在大规模数据集上,因为需要计算待分类样本与训练集中所有样本之间的距离。对于 N 个训练样本,每次分类的时间复杂度为 O ( N × D ) O(N \times D) O(N×D),其中 D 为特征维度。这使得 KNN 在大规模数据集或高维空间中不太适用。
2.6 代码实现
以下是KNN算法的简单实现:
import numpy as np
from collections import Counterdef knn_predict(X_train, y_train, X_test, k=3):predictions = []for x_test in X_test:# 计算测试样本与所有训练样本之间的距离distances = np.linalg.norm(X_train - x_test, axis=1)# 获取距离最近的 k 个样本的索引k_indices = np.argsort(distances)[:k]# 获取这些样本的标签k_nearest_labels = y_train[k_indices]# 投票选择出现频率最高的标签most_common = Counter(k_nearest_labels).most_common(1)predictions.append(most_common[0][0])return predictions
如果需要在大规模数据集上提高效率,可以使用 KD树 优化的版本。以下是构建 KD 树的伪代码:
class KDNode:def __init__(self, point, left=None, right=None):self.point = pointself.left = leftself.right = rightdef build_kdtree(points, depth=0):if not points:return Nonek = len(points[0]) # 维度axis = depth % k # 选择分割维度points.sort(key=lambda x: x[axis])median = len(points) // 2 # 选择中位数# 递归构建 KD 树return KDNode(point=points[median],left=build_kdtree(points[:median], depth + 1),right=build_kdtree(points[median + 1:], depth + 1))
KNN算法是一种简单直观且有效的分类和回归方法,但其计算复杂度较高,特别是在高维空间和大规模数据集上。通过引入 KD树,可以在低维空间中显著提高KNN的效率。然而,在高维空间中,KD树的效果会减弱。因此,在实际应用中,需要根据数据集的具体特征选择合适的优化技术。