算法——k-近邻算法

简介:个人学习分享,如有错误,欢迎批评指正。

一、什么是k-近邻算法?

k-近邻算法(k-Nearest Neighbors, k-NN)是一种简单且直观的监督学习算法,常用于分类回归任务。它不需要显式的训练过程,只需在进行预测时通过比较已知样本的距离来决定结果。
k-NN算法的基本思想是:给定一个待分类样本,找出与其距离最近的k个训练样本(邻居),然后通过这k个邻居的类别来决定待分类样本的类别,即这K个样本的多数属于某个类,就把该输入样本分类到这个类中。(这就类似于现实生活中少数服从多数的思想)。在回归任务中,则通过这k个邻居的值来预测待测样本的值,即通过这k个邻居的目标变量值的平均值来预测待测样本的目标变量值。k-近邻算法三要素:距离度量k值选择分类决策规则

例:

在这里插入图片描述
如上图所示,有两类不同的样本数据,分别用蓝色的小正方形和红色的小三角形表示,而图正中间的那个绿色的圆所标示的数据则是待分类的数据。我们的目标是对于这个新的数据点,确定它的类别是什么?
k近邻的思想来给绿色圆点进行分类

  • 如果K=3,绿色圆点的最邻近的3个点是2个红色小三角形和1个蓝色小正方形,少数从属于多数,基于统计的方法,判定绿色的这个待分类点属于红色的三角形一类。
  • 如果K=5,绿色圆点的最邻近的5个邻居是2个红色三角形和3个蓝色的正方形,还是少数从属于多数,基于统计的方法,判定绿色的这个待分类点属于蓝色的正方形一类。

通过上面的例子,我们知道对于新数据点如何归类,即只要找到离它最近的k个实例,哪个类别最多就是新数据点类别。

二、K-近邻算法流程

  1. 选择参数k:选择一个正整数k,表示选择k个邻居。
  2. 计算距离:对待预测样本和训练样本中的每一个样本计算距离。常用的距离度量(个人总结版)方法有欧氏距离(Euclidean distance)、曼哈顿距离(Manhattan distance)等。
  3. 选择k个最近的邻居:从训练样本中选出与待预测样本距离最近的k个样本。
  4. 计算预测值:在分类任务中,通过投票法决定待分类样本的类别,即选择k个邻居中出现最多的类别作为结果。在回归任务中,通过取k个邻居的平均值作为预测结果。

算法1:k近邻法

输入:训练数据集

T = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , ⋯   , ( x N , y N ) } T = \{ (x_1, y_1), (x_2, y_2), \cdots, (x_N, y_N) \} T={(x1,y1),(x2,y2),,(xN,yN)}

其中, x i ∈ X ⊆ R n x_i \in \mathcal{X} \subseteq \mathbb{R}^n xiXRn 为实例的特征向量, y i ∈ Y = { c 1 , c 2 , ⋯   , c K } y_i \in \mathcal{Y} = \{ c_1, c_2, \cdots, c_K \} yiY={c1,c2,,cK}为实例的类别, i = 1 , 2 , ⋯   , N i = 1, 2, \cdots, N i=1,2,,N; 实例特征向量 x x x;

输出:实例 x x x所属的类 y y y

  1. 根据给定的距离度量,在训练集 T T T 中找出与 x x x 最邻近的 k k k个点,涵盖这 k k k 个点的 x x x的邻域记作 N k ( x ) N_k(x) Nk(x);

  2. N k ( x ) N_k(x) Nk(x) 中根据分类决策规则(如多数表决)决定 x x x 的类别 y y y:

y = arg ⁡ max ⁡ c j ∑ x i ∈ N k ( x ) I ( y i = c j ) , i = 1 , 2 , ⋯   , N ; j = 1 , 2 , ⋯   , K ( 1 ) y = \arg \max_{c_j} \sum_{x_i \in N_k(x)} I(y_i = c_j), \quad i = 1, 2, \cdots, N; j = 1, 2, \cdots, K \quad (1) y=argcjmaxxiNk(x)I(yi=cj),i=1,2,,N;j=1,2,,K(1)

式(1)中, I I I为指示函数,即当 y i = c j y_i = c_j yi=cj I I I 为 1,否则 I I I为 0。

k k k近邻法的特殊情况是 k = 1 k = 1 k=1的情形,称为最近邻算法。对于输入的实例点(特征向量) x x x,最近邻法将训练数据集中与 x x x最邻近点的类作为 x x x的类。

k k k近邻法没有显式的学习过程。

三、k值的选取

在k-近邻算法中,k值的选取对于模型的性能至关重要。如果选择较小的 k k k值,就相当于用较小的邻域中的训练实例进行预测,“学习”的近似误差(approximation error)会减小,只有与输入实例较近的(相似的)训练实例才会对预测结果起作用。但缺点是“学习”的估计误差(estimation error)会增大,预测结果会对近邻的实例点非常敏感。如果近邻的实例点恰巧是噪声,预测就会出错。换句话说, k k k值的减小就意味着整体模型变得复杂,容易发生过拟合。

如果选择较大的 k k k值,就相当于用较大邻域中的训练实例进行预测。优点是可以减少学习的估计误差,但缺点是学习的近似误差会增大。这时与输入实例较远的(不相似的)训练实例也会对预测起作用,使预测发生错误。 k k k值的增大就意味着整体的模型变得简单。

如果 k = N k = N k=N,那么无论输入实例是什么,都将简单地预测它属于在训练实例中最多的类。这时,模型过于简单,完全忽略训练实例中的大量有用信息,是不可取的。 选择合适的k值可以在避免过拟合和欠拟合之间找到平衡。k值的选择反映了对近似误差与估计误差之间的权衡,通常由交叉验证选择最优的k。常用的分类决策规则多数表决,对应于经验风险最小化。以下是一些关于如何选取k值的方法和建议:

1. 通过交叉验证选择k值

交叉验证是一种常用的方法,可以帮助我们选择最优的k值。通常使用k折交叉验证(k-fold cross-validation)来评估不同k值的表现,选择使模型性能最优的k值。

具体步骤

  1. 将训练数据集分成k个子集(通常选择k=5或k=10)。
  2. 针对每一个k值,进行k次训练和验证,每次用一个子集作为验证集,其他k-1个子集作为训练集。
  3. 计算每个k值的平均性能(如准确率、均方误差等)。
  4. 选择平均性能最优的k值。

2. 经验法则

虽然交叉验证是最科学的方法,但在某些情况下,可以依据经验法则来选择k值:

  1. 较小的k值:k值较小(如k=1, k=3)时,模型更为灵活,能够很好地拟合训练数据,但可能对噪声敏感,导致过拟合。
  2. 较大的k值:k值较大时,模型更为平滑,对噪声不敏感,但可能导致欠拟合。

3. 奇数选择

通常选择奇数作为k值,特别是在分类问题中,以避免出现平票的情况。

4. 使用肘部法则

肘部法则是一种图形化的方法,用于选择k值。在不同的k值下,计算模型的性能指标(如误差或准确率),并绘制成图。通常在曲线开始平缓处选择k值,这个点被称为“肘部”。

选择k值是k-近邻算法中一个重要的步骤。通过交叉验证可以有效地选择最优的k+值,确保模型在避免过拟合和欠拟合之间达到平衡。此外,经验法则、奇数选择以及肘部法则也可以作为辅助方法。在实际应用中,可以结合这些方法来选择最合适的k值,以提升模型的性能。

四、K-近邻法的实现:kd树

kd树是一种分区数据结构,将空间递归地划分为k维的超矩形。每个节点代表一个k维空间中的点,并将该空间划分成两个子空间,分别对应于其左子树和右子树。

构建kd树的过程如下:

  1. 选择分割维度:每次分割选择一个维度进行划分,通常按各维度循环选择。
  2. 选择分割点:在选择的维度上找到中位数点,作为当前节点的分割点。
  3. 递归构建:递归地对左子空间和右子空间构建子树。

算法2(构造平衡 kd 树)

输入:k 维空间数据集 T = { x 1 , x 2 , ⋯   , x N } T = \{x_1, x_2, \cdots, x_N\} T={x1,x2,,xN},其中 x i = ( x i ( 1 ) , x i ( 2 ) , ⋯   , x i ( k ) ) T x_i = (x_i^{(1)}, x_i^{(2)}, \cdots, x_i^{(k)})^T xi=(xi(1),xi(2),,xi(k))T i = 1 , 2 , ⋯   , N i = 1, 2, \cdots, N i=1,2,,N;

输出:kd 树。

  1. 开始:构造根结点,根结点对应于包含 T T T 的 k 维空间的超矩形区域。选择 x ( 1 ) x^{(1)} x(1) 为坐标轴,以 T T T 中所有实例的 x ( 1 ) x^{(1)} x(1) 坐标的中位数为切分点,将根结点对应的超矩形区域划分为两个子区域。切分由通过切分点并与坐标轴 x ( 1 ) x^{(1)} x(1)垂直的超平面实现。

    由根结点生成深度为 1 的左、右子结点:左子结点对应坐标 x ( 1 ) x^{(1)} x(1) 小于切分点的子区域,右子结点对应于坐标 x ( 1 ) x^{(1)} x(1)大于切分点的子区域。将落在切分超平面上的实例点保存在根结点。

  2. 重复:对深度为 j j j 的结点,选择 x ( l ) x^{(l)} x(l) 为切分的坐标轴, l = j ( m o d    k ) + 1 l = j (\mod k) + 1 l=j(modk)+1,以该结点的区域中所有实例的 x ( l ) x^{(l)} x(l) 坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 x ( l ) x^{(l)} x(l) 垂直的超平面实现。
    由该结点生成深度为 j + 1 j + 1 j+1 的左、右子结点:左子结点对应坐标 x ( l ) x^{(l)} x(l) 小于切分点的子区域,右子结点对应坐标 x ( l ) x^{(l)} x(l) 大于切分点的子区域。将落在切分超平面上的实例点保存在该结点。

  3. 直到两个子区域没有实例存在时停止。从而形成 kd 树的区域划分。


给定一个二维空间的数据集:

T = { ( 2 , 3 ) T , ( 5 , 4 ) T , ( 9 , 6 ) T , ( 4 , 7 ) T , ( 8 , 1 ) T , ( 7 , 2 ) T } T = \{ (2,3)^T, (5,4)^T, (9,6)^T, (4,7)^T, (8,1)^T, (7,2)^T \} T={(2,3)T,(5,4)T,(9,6)T,(4,7)T,(8,1)T,(7,2)T}

构造一个平衡 kd 树。

根据结点对应包含数据集 T T T 的矩形,选择 x ( 1 ) x^{(1)} x(1) 轴,6 个数据点的 x ( 1 ) x^{(1)} x(1) 坐标的中位数是 7,以平面 x ( 1 ) = 7 x^{(1)} = 7 x(1)=7 将空间分为左右两个子矩形(子结点);接着,左矩形以 x ( 2 ) = 4 x^{(2)} = 4 x(2)=4 分为两个子矩形,右矩形以 x ( 2 ) = 6 x^{(2)} = 6 x(2)=6 分为两个子矩形,如此递归,最后得到如图 3.3 所示的特征空间划分和如图 3.4 所示的 kd 树。

在这里插入图片描述

在这里插入图片描述

算法 3(用 kd 树的最近邻搜索)

输入:已构造的 kd 树,目标点 x;
输出:x 的最近邻。

  1. 在 kd 树中找到包含目标点 x 的叶结点:从根结点出发,递归地向下访问 kd 树。若目标点 x 当前维的坐标小于切分点的坐标,则移动到左子结点,否则移动到右子结点。直到子结点为叶结点为止。

  2. 以此叶结点为“当前最近点”。

  3. 递归地向上回退,在每个结点进行以下操作:
    a. 如果该结点保存的实例点比当前最近距离目标点更近,则以该实例点为“当前最近点”。
    b. 当前最近点一定存在于该结点一个子结点对应的区域。检查该子结点的父结点的另一个子结点对应的区域是否有更近的点。具体地,检查另一个子结点对应的区域是否与以目标点为球心、以目标点与“当前最近点”间的距离为半径的超球体相交。
    如果相交,可能在另一个子结点对应的区域内存在距目标点更近的点,移动到另一个子结点。接着,递归地进行最近邻搜索;
    如果不相交,向上回退。

  4. 当回退到根结点时,搜索结束。最后的“当前最近点”即为 x 的最近邻点。

如果实例点是随机分布的,kd 树搜索的平均计算复杂度是 O ( l o g N ) O(log N) O(logN),这里 N N N 是训练实例数。kd 树更适用于训练实例数远大于空间维数时的 k 近邻搜索。当空间维数接近训练实例数时,它的效率会迅速下降,几乎接近线性扫描。

给定一个如图 3.5 所示的 kd 树,根结点为 A,其子结点为 B、C 等。树上共存储 7 个实例点;另有一个输入目标实例点 S,求 S 的最近邻。

首先在 kd 树中找到包含点 S 的叶结点 D(图中的右下区域),以点 D 作为近似最近邻。真正最近邻一定在以点 S 为中心通过点 D 的圆的内部。然后返回结点 D 的父结点 B,在结点 B 的另一子结点 F 的区域内搜索最近邻。结点 F 的区域与圆不相交,不可能有最近邻点。继续返回上一级父结点 A,在结点 A 的另一子结点 C 的区域内搜索最近邻。结点 C 的区域与圆相交,该区域在圆内的实例点有点 E,点 E 比点 D 更近,成为新的最近邻近似。最后得到点 E 是点 S 的最近邻。

在这里插入图片描述

五、代码示例

调包:

import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score

# 示例数据
X = np.array([[2, 3], [5, 4], [9, 6], [4, 7], [8, 1], [7, 2]])
y = np.array([0, 1, 0, 1, 0, 1])  # 标签

# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)

# 初始化和训练模型
k = 3
knn = KNeighborsClassifier(n_neighbors=k)
knn.fit(X_train, y_train)

# 预测
y_pred = knn.predict(X_test)

# 计算准确率
accuracy = accuracy_score(y_test, y_pred)
print(f'Accuracy: {accuracy:.2f}')

不调包:

import numpy as np
from collections import Counter

# 自定义k-NN类
class KNN:
    def __init__(self, k=3):
        self.k = k

    def fit(self, X, y):
        self.X_train = X
        self.y_train = y

    def predict(self, X):
        y_pred = [self._predict(x) for x in X]
        return np.array(y_pred)

    def _predict(self, x):
        # 计算距离
        distances = [np.linalg.norm(x - x_train) for x_train in self.X_train]
        # 找到k个最近的点
        k_indices = np.argsort(distances)[:self.k]
        k_nearest_labels = [self.y_train[i] for i in k_indices]
        # 投票
        most_common = Counter(k_nearest_labels).most_common(1)
        return most_common[0][0]

# 示例数据
X = np.array([[2, 3], [5, 4], [9, 6], [4, 7], [8, 1], [7, 2]])
y = np.array([0, 1, 0, 1, 0, 1])  # 标签

# 划分数据集
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)

# 初始化和训练模型
k = 3
knn = KNN(k=k)
knn.fit(X_train, y_train)

# 预测
y_pred = knn.predict(X_test)

# 计算准确率
accuracy = np.mean(y_pred == y_test)
print(f'Accuracy: {accuracy:.2f}')

六、总结

优点

  1. 简单易理解:k-NN算法直观且简单,无需复杂的模型训练过程,只需计算距离和投票即可进行分类或回归。
  2. 无假设:k-NN算法对数据分布没有任何假设,因此适用于多种类型的数据集,包括非线性数据。
  3. 适合小数据集:在小数据集上,k-NN算法能够快速且高效地进行分类和回归。
  4. 灵活性:k-NN可以用于分类和回归任务,适用范围广泛。
  5. 增量学习:k-NN可以处理新增的数据点而无需重新训练模型,只需将新数据点添加到样本集中即可。

缺点

  1. 计算成本高:对于每一个待分类或回归的样本,k-NN算法需要计算与所有训练样本的距离,导致计算开销较大,尤其是数据量大时。
  2. 空间复杂度高:k-NN算法需要存储所有训练数据,因此在大规模数据集上,内存消耗较高。
  3. 对维度敏感:在高维数据中,所有点之间的距离变得相似(维度灾难),这会降低k-NN算法的效果。
  4. 对噪声敏感:较小的k值会导致模型对噪声敏感,较大的k值会导致模型过于平滑,无法捕捉数据的细节特征。
  5. 缺乏模型解释性:k-NN算法无法提供模型的解释性,无法理解特征与预测结果之间的关系。

k-近邻算法因其简单、直观和灵活性而在许多应用中得到广泛使用,特别是在小规模数据集和多样性数据上表现良好。然而,由于计算成本高、对维度敏感和对噪声敏感等问题,其在大规模和高维数据集上的应用受到了限制。选择合适的k值和距离度量方法,可以在一定程度上改善算法的性能。通过结合其他技术,如降维技术和索引结构(如kd树),可以提升k-NN算法在大规模数据集上的效率。

参考文献:
统计学习方法(李航)
一文搞懂k近邻(k-NN)算法(一)

结~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值