knn中kd树与球树的实现原理

本文探讨了KNN算法在大规模数据集上的性能瓶颈,并介绍了KD树和球树两种优化方法,通过构建高效的搜索结构来加速最近邻查找,同时提出了半径最近邻和质心最近邻等KNN算法的扩展应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

knn算法的思考
我们在使用knn算法时是否会发觉,随着数据量的增加,预测过程会越来越缓慢。在K值的选择过程中是否会消耗过多性能,然而我们当前使用的就是蛮力模式。通过找到最近邻的k个点,通过距离公式来计算最近邻的k个点。我们是否可以解决这种预测时间过长的问题,那我们可以考虑使用sklearn中的两种内置方法 kd-树和球树去解决这个问题。
kd-树
kd-树方法需要两个重要步骤
1.构建kd-树
2.搜索树
假设我们一个数据集的维度为k,特征数为n,通过提取特征数据算出各特征的方差,选择出最大方差的ki维,在选取ki维特征nk求出中位数作为阈值,通过阈值进行分割,构建左子树和右子树并分别展开左右子树。
在这里插入图片描述
比如我们有二维样本6个,{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)},构建kd树的具体步骤为:

1)找到划分的特征。6个数据点在x,y维度上的数据方差分别为6.97,5.37,所以在x轴上方差更大,用第1维特征建树。

2)确定划分点(7,2)。根据x维上的值将数据排序,6个数据的中值(所谓中值,即中间大小的值)为7,所以划分点的数据是(7,2)。这样,该节点的分割超平面就是通过(7,2)并垂直于:划分点维度的直线x=7;

3)确定左子空间和右子空间。 分割超平面x=7将整个空间分为两部分:x<=7的部分为左子空间,包含3个节点={(2,3),(5,4),(4,7)};另一部分为右子空间,包含2个节点={(9,6),(8,1)}。

4)用同样的办法划分左子树的节点{(2,3),(5,4),(4,7)}和右子树的节点{(9,6),(8,1)}。最终得到KD树。
    在这里插入图片描述
    kd-树搜索最近邻
当我们生成KD树以后,就可以去预测测试集里面的样本目标点了。对于一个目标点,我们首先在KD树里面找到包含目标点的叶子节点。以目标点为圆心,以目标点到叶子节点样本实例的距离为半径,得到一个超球体,最近邻的点一定在这个超球体内部。然后返回叶子节点的父节点,检查另一个子节点包含的超矩形体是否和超球体相交,如果相交就到这个子节点寻找是否有更加近的近邻,有的话就更新最近邻。如果不相交那就简单了,我们直接返回父节点的父节点,在另一个子树继续搜索最近邻。当回溯到根节点时,算法结束,此时保存的最近邻节点就是最终的最近邻。
kd树预测
有了KD树搜索最近邻的办法,KD树的预测就很简单了,在KD树搜索最近邻的基础上,我们选择到了第一个最近邻样本,就把它置为已选。在第二轮中,我们忽略置为已选的样本,重新选择最近邻,这样跑k次,就得到了目标的K个最近邻,然后根据多数表决法,如果是KNN分类,预测为K个最近邻里面有最多类别数的类别。如果是KNN回归,用K个最近邻样本输出的平均值作为回归预测值。

从上面的描述可以看出,KD树划分后可以大大减少无效的最近邻搜索,很多样本点由于所在的超矩形体和超球体不相交,根本不需要计算距离。大大节省了计算时间。

球树
但是我们会发现,因为使用kd树最近邻预测时,矩形与超球面易于相交,时常会因为菱角相交导致一些,无关多余的搜索,球树就是在kd树这个缺点上进行改进而生,通过将特征点转化为球状分割,从而减少无效相交。

  1. 先构建一个超球体,这个超球体是可以包含所有样本的最小球体。

  2. 从球中选择一个离球的中心最远的点,然后选择第二个点离第一个点最远,将球中所有的点分配到离这两个聚类中心最近的一个上,然后计算每个聚类的中心,以及聚类能够包含它所有数据点所需的最小半径。这样我们得到了两个子超球体,和KD树里面的左右子树对应。

3)对于这两个子超球体,递归执行步骤最终得到了一个球树

knn中的扩展
半径最近邻,通过找到指定半径内最近的k个值,这种算法思想找到指定范围内的最近邻的k个值,来进行预测
质心最近邻,通过在特征值中的平均值来获得质心点,最终该类别所有维度的n个平均值形成所谓的质心点。对于样本中的所有出现的类别,每个类别会最终得到一个质心点。当我们做预测时,仅仅需要比较预测样本和这些质心的距离,最小的距离对于的质心类别即为预测的类别。这个算法通常用在文本分类处理上。

KNN的主要优点有:

1) 理论成熟,思想简单,既可以用来做分类也可以用来做回归

2) 可用于非线性分类

3) 训练时间复杂度比支持向量机之类的算法低,仅为O(n)

4) 和朴素贝叶斯之类的算法比,对数据没有假设,准确度高,对异常点不敏感

5) 由于KNN方法主要靠周围有限的邻近的样本,而不是靠判别类域的方法来确定所属类别的,因此对于类域的交叉或重叠较多的待分样本集来说,KNN方法较其他方法更为适合
    6)该算法比较适用于样本容量比较大的类域的自动分类,而那些样本容量较小的类域采用这种算法比较容易产生误分

KNN的主要缺点有:

1)计算量大,尤其是特征数非常多的时候

2)样本不平衡的时候,对稀有类别的预测准确率低

3)KD树,球树之类的模型建立需要大量的内存

4)使用懒散学习方法,基本上不学习,导致预测时速度比起逻辑回归之类的算法慢

5)相比决策树模型,KNN模型可解释性不强

参考 https://www.cnblogs.com/pinard/p/6061661.html

<think>我们讨论的是KDKNN算法中的应用,特别是在scikit-learn中的实现详细推导过程。下面我将分步骤进行讲解,包括KD的构建、搜索过程、在scikit-learn中的使用方式以及背后的数学原理。###1.KD的基本概念KD(k-dimensionaltree)是一种用于组织k维空间中点的数据结构,用于范围搜索最近邻搜索。它是一种二叉搜索,每个节点代表一个超矩形区域,通过交替选择不同维度进行划分。**关键特性**:-每个节点表示一个数据点一个划分的维度-左子包含在当前划分维度上小于该点的点-右子包含在当前划分维度上大于或等于该点的点###2.KD的构建算法给定一个k维数据集$X$,包含$n$个点,每个点是一个k维向量:$x_i=(x_i^{(0)},x_i^{(1)},\dots,x_i^{(k-1)})^T$####构建步骤(递归过程):1.**选择划分维度**:通常有两种策略:-轮流选择:第j层选择维度$l=j\modk$(最常用)-最大方差维度:选择方差最大的维度进行划分(计算更复杂,但可能产生更平衡的)2.**选择切分点**:在选定维度上,选择中位数作为切分点(此时构建的是平衡的,高$O(\logn)$)。也可以选择其他点,但中位数可以保证的平衡。3.**递归构建左右子**:将数据划分为两个子集,分别构建左子右子。####数学描述对于节点$N$,其对应数据集$S$,当前划分维度$d$:-切分点$p$:$p=\text{median}\{x^{(d)}|x\inS\}$-左子:$S_{\text{left}}=\{x\inS|x^{(d)}< p^{(d)}\}$-右子:$S_{\text{right}}=\{x\inS|x^{(d)}\geqp^{(d)}\}$####构建的终止条件当节点包含的数据点个数不超过预设的`leaf_size`参数时,该节点成为叶子节点。**时间复杂度分析**:-找中位数:平均$O(n)$,使用快速选择算法-总时间复杂度:$O(n\logn)$###3.KD的最近邻搜索(KNNKD的最近邻搜索是一个递归过程,使用了剪枝策略(pruning)来减少搜索的节点数。####基本步骤(以单个查询点$q$为例):1.**向下递归**:从根节点开始,根据当前节点的划分维度比较,递归进入左子或右子(类似二叉搜索查找),直到叶子节点。记录当前最近邻点(candidate)最近距离(min_dist)。2.**回溯路径**:递归向上回溯,检查每个节点:-a.计算查询点$q$当前节点代表的点的距离$d$,更新最近邻-b.**剪枝条件**:检查当前节点的另一个子是否可能存在更近的点。判断方法是:以$q$为中心,当前最小距离$r$为半径的超面是否当前节点的分割超平面相交。-若相交(即$|q^{(d)}-N_{\text{split}}|< r$),则需要搜索另一个子-否则,跳过该子####剪枝的几何意义对于节点$N$的划分维度$d$,分割点为$s$。则分割超平面为$x^{(d)}=s$。在回溯过程中,若当前最近邻距离为$r$,只有当$q^{(d)}$到分割超平面的距离小于$r$时,超面才可能另一个子区域相交,因此需要搜索另一个子。否则,另一个子不可能有更近的点(因为整个子都在超面外)。####K近邻搜索(K>1)最近邻类似,但是需要维护一个优先队列(按距离从大到小排序)来保存当前找到的K个最近邻点。剪枝条件改为:当前第K远的点的距离$r_k$进行比较。###4.KD在scikit-learn中的实现在scikit-learn中,KDTree实现在`sklearn.neighbors.KDTree`类中。其核心方法包括:-`__init__`:构建KD-`query`:查询K近邻####关键参数-`leaf_size`:控制叶节点包含的最大样本数(影响的深度搜索效率)-`metric`:距离度量(默认欧氏距离$L_2$)####构建的代码框架以下是简化版的构建流程(展示关键逻辑):```pythonclassKDTree:def__init__(self,X,leaf_size=40,metric='euclidean'):self.leaf_size=leaf_sizeself.metric=metricself.root=self._build_tree(X,depth=0)def_build_tree(self,X,depth):n_samples,n_features=X.shapeifn_samples<=self.leaf_size:returnNone#标记为叶子节点,具体存储数据#选择划分维度:按层轮流选择axis=depth%n_features#找到该维度的中位数median=np.median(X[:,axis])median_idx=np.where(X[:,axis]==median)[0][0]#实际实现通过快速选择避免全排序#划分数据left_mask=X[:,axis]< medianright_mask=~left_maskleft_data=X[left_mask]right_data=X[right_mask]#构建节点node={'axis':axis,'position':X[median_idx],'left':self._build_tree(left_data,depth+1),'right':self._build_tree(right_data,depth+1)}returnnode```####查询过程(回溯+剪枝)查询伪代码:```pythondefquery(self,q,k=1):#初始化优先级队列(大顶堆,存距离的负值)neighbors=[]self._query_recursive(self.root,q,k,neighbors,depth=0)#返回最近的k个邻居returnsorted(neighbors,key=lambdax:x[0])[:k]def_query_recursive(self,node,q,k,neighbors,depth):ifnodeisNone:#叶子节点,处理其存储的数据点#这里遍历该叶子节点中的所有点,更新最近邻#(代码略)returnaxis=node['axis']#根据划分维度进入左子或右子ifq[axis]< node['position'][axis]:go_first=node['left']go_second=node['right']else:go_first=node['right']go_second=node['left']#递归进入第一个子self._query_recursive(go_first,q,k,neighbors,depth+1)#检查当前节点dist=self.distance(q,node['position'])#更新邻居列表(维护大小为k的堆)#(具体更新代码略)#计算查询点到当前分割平面的距离plane_dist=np.abs(q[axis]-node['position'][axis])#如果当前第k近的距离(或者当前最远的距离)大于分割面距离,则另一个子可能存在更近的点ifkisNoneorlen(neighbors)< kor(neighborsanddist_to_plane< neighbors[-1][0]):self._query_recursive(go_second,q,k,neighbors,depth+1)```###5.KD搜索的时间复杂度-**构建**:$O(n\logn)$-**查询**:-最坏情况$O(n)$(当数据分布使得剪枝失效时)-平均情况$O(\logn)$(适用于低维空间)-高维情况下效率可能退化到接近$O(n)$(维度灾难)###6.在KNN分类中的应用在scikit-learn的`KNeighborsClassifier`中,当设置`algorithm='kd_tree'`时,使用KDTree来加速最近邻搜索。训练时构建KDTree索引,预测时对每个测试样本使用该索引快速找到最近邻。####示例代码```pythonfromsklearn.neighborsimportKNeighborsClassifierfromsklearn.datasetsimportload_irisfromsklearn.model_selectionimporttrain_test_splitdata=load_iris()X,y=data.data,data.targetX_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.3)#使用KDTreeKNN分类器knn=KNeighborsClassifier(n_neighbors=5,algorithm='kd_tree',leaf_size=30)knn.fit(X_train,y_train)score=knn.score(X_test,y_test)print(f"Accuracy:{score:.4f}")```###7.数学推导:KD剪枝的正确性为什么剪枝条件成立?设当前最近邻距离为$r$,查询点为$q$,待检查节点$N$的分割维度$d$,分割点$s$。我们需要证明:如果$|q^{(d)}-s|\geqr$,那么另一侧的子中不可能存在比当前最近邻更近的点。**证明**:设分割点$s$将空间分为两个半空间:$x^{(d)}< s$$x^{(d)}\geqs$。假设$q$在左半空间(即$q^{(d)}<s$),且$|q^{(d)}-s|\geqr$。那么,对于右子中的任意一点$p$,都有$p^{(d)}\geqs$。因此,$|p^{(d)}-q^{(d)}|\geqs-q^{(d)}\geqr$。由勾股定理,$q$$p$的欧氏距离:$$d(q,p)=\sqrt{\sum_{i=0}^{k-1}(q^{(i)}-p^{(i)})^2}\geq|p^{(d)}-q^{(d)}|\geqr$$所以,$p$不可能比当前最近邻更近。因此,剪枝操作是安全的。###8.总结注意事项-**适用维度**:KD在处理低维数据(例如维度<20)时效率很高,但高维时退化为线性扫描(称为“维度灾难”)。此时可考虑替代方法如BallTree或LSH(局部敏感哈希)。-**数据量影响**:对于样本量$n$,当$n$较大(如$n>1000$)时,KDTree比暴力搜索快很多。-**参数`leaf_size`**:需要权衡。增大`leaf_size`会减少的深度,减少递归开销,但可能增加回溯时的距离计算次数。§§相关问题§§1.为什么在高维空间中KD搜索效率会退化?如何改进高维数据的最近邻搜索?2.比较KDBall在结构、构建查询上的区别,以及在什么情况下选择哪种?3.如何通过选择不同的分割策略来优化KD的平衡性?4.KD在范围搜索(半径搜索)中的应用实现?5.KD在非欧几里得距离度量(如曼哈顿距离)下的适用性?</think> ### KDKNN算法中的详细推导scikit-learn实现 #### 一、KD的数学原理构建过程 KD是一种二叉搜索,通过递归分割k维数据空间实现高效检索。给定n个k维数据点 $\{\mathbf{x}_i\}_{i=1}^n$,其中 $\mathbf{x}_i = (x_{i1}, x_{i2}, \dots, x_{ik})$: 1. **划分维度选择**: 在深度为$d$的节点上,选择分割维度 $l = d \mod k$(维度轮换策略)。实践中可选方差最大的维度: $$l = \arg\max_{j} \text{Var}(X^{(j)})$$ $X^{(j)}$ 表示第$j$维特征向量。 2. **切分点选择**: 在维度$l$上取中位数 $\text{median}(X^{(l)})$,满足: $$\left|\{ \mathbf{x}_i | x_{il} < \text{median} \}\right| \approx \left|\{ \mathbf{x}_i | x_{il} \geq \text{median} \}\right|$$ 3. **递归构建**: 分割平面为 $x_l = \text{median}$,左右子分别处理对应子集: $$\begin{cases} \text{左子:} \{\mathbf{x}_i | x_{il} < \text{median}\} \\ \text{右子:} \{\mathbf{x}_i | x_{il} \geq \text{median}\} \end{cases}$$ 4. **终止条件**: 当节点样本数 $\leq$ `leaf_size` 时停止递归(控制精度平衡)。 --- #### 二、最近邻搜索的剪枝机制 给定查询点 $\mathbf{q}$,搜索最邻近点 $\mathbf{x}^*$ 的核心思想是**空间距离剪枝**: 1. **向下递归**: 从根节点开始,按当前节点分割维度$l$比较 $q_l$ 节点值,选择分支路径,直到叶节点。初始暂存候选点 $\mathbf{x}_c$ 距离 $d_c = \|\mathbf{q} - \mathbf{x}_c\|_2$ 2. **回溯剪枝**: 计算查询点到分割超平面的距离: $$\delta = |q_l - \text{node.split\_value}|$$ - 若 $\delta \geq d_c$ → 另一侧子无更优点(超分割面不相交) - 若 $\delta < d_c$ → 需搜索另一子 几何解释: <div align=center> <img src="https://ai-studio-static-online.cdn.bcebos.com/5d8f07c01f4a476e8a4f6dc1ebf3cdd15f7b5f48bd1c4a50a5ffd4a3f4c8bf11" width="50%"> </div> --- #### 三、KD在scikit-learn中的实现 在scikit-learn中,KDTree核心实现位于 `sklearn/neighbors/_kd_tree.pyx`,关键流程: 1. **数据结构**: ```python cdef class KDTree: cdef: np.ndarray data # 数据矩阵 (n_samples, n_features) int leaf_size # 叶节点最大样本数 NodeType root # 根节点 cdef class NodeType: int idx_start, idx_end # 节点数据在数组中的起止索引 int split_dim # 分割维度 float split_threshold # 分割阈值 NodeType left, right # 左右子 ``` 2. **构建过程简化代码**: ```python def _build_tree(int node_id, int depth, indices): if len(indices) <= leaf_size: # 叶节点终止 return LeafNode(indices) dim = depth % k # 维度轮换 median = _fast_median(data[indices, dim]) left_idx = indices[data[indices, dim] < median] right_idx = indices[data[indices, dim] >= median] node = InternalNode( split_dim=dim, split_threshold=median, left=_build_tree(2*node_id+1, depth+1, left_idx), right=_build_tree(2*node_id+2, depth+1, right_idx) ) return node ``` 3. **搜索过程优化**: - **优先级队列**:维护候选邻居的堆结构 - **双遍历**:当 `dualtree=True` 时同时遍历查询目标 - **距离计算优化**:使用距离下界进行剪枝 --- #### 四、数学推导:剪枝策略的正确性 **定理**:若 $\delta \geq d_c$,则分割平面另一侧不存在更近邻。 **证明**: 设查询点 $\mathbf{q}$,候选点 $\mathbf{x}_c$,分割平面 $H: x_l = s$,右侧任一点 $\mathbf{x}_r$。 由分割平面性质 $x_{rl} \geq s$,且由 $\delta = |q_l - s| \geq d_c$: $$\begin{align*} \|\mathbf{q} - \mathbf{x}_r\|_2^2 &\geq |q_l - x_{rl}|^2 & \text{(距离分量关系)} \\ &\geq (x_{rl} - q_l)^2 & \text{(右侧点特性)} \\ &\geq (s - q_l)^2 & \text{(因 $x_{rl} \geq s$)} \\ &= \delta^2 \geq d_c^2 \end{align*}$$ 故 $\|\mathbf{q} - \mathbf{x}_r\|_2 \geq d_c$,证毕。 --- #### 五、scikit-learn中的调用实例 ```python from sklearn.neighbors import KDTree import numpy as np # 生成样本数据(500个3维点) X = np.random.rand(500, 3) # 初始化KDTree(叶节点大小=20) kdt = KDTree(X, leaf_size=20, metric='euclidean') # 查询点K近邻搜索 query_point = np.array([0.5, 0.5, 0.5]) distances, indices = kdt.query(query_point.reshape(1, -1), k=5) print(f"最近邻索引:{indices[0]}") print(f"对应距离:{distances[0]}") ``` **关键参数说明**: - `leaf_size=20`:平衡距离计算次数 - `metric='minkowski'`:支持欧氏/曼哈顿距离等 - `k=5`:返回5个最近邻 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值