02-17 kd树

目录

  • kd树
  • 一、kd树学习目标
  • 二、kd树引入
  • 三、kd树详解
    • 3.1 构造kd树

      • 3.1.1 示例
    • 3.2 kd树搜索
      • 3.2.1 示例
  • 四、kd树流程
    • 4.1 输入
    • 4.2 输出
    • 4.3 流程
  • 五、kd树优缺点
    • 5.1 优点
    • 5.2 缺点
  • 六、小结

更新、更全的《机器学习》的更新网站,更有python、go、数据结构与算法、爬虫、人工智能教学等着你:https://www.cnblogs.com/nickchen121/

kd树

k近邻算法中讲到它有一个较为致命的缺点就是每个实例到未来新数据之间都需要计算一次距离,如果实例数趋于无穷,那么计算量是很庞大的。但是我们要知道的是计算距离是为了找到距离目标点最近的\(k\)个实例,那么是不是有另外一种更好的方法,能够更快速找到这\(k\)个最近的实例呢?由此kd树被发明了出来。

kd树(k-dimensional tree)简单而言就是\(k\)个特征维度的二叉树,要注意这里的\(k\)值和k近邻算法中的\(k\)值不同。

一、kd树学习目标

  1. kd树的构造与搜索
  2. kd树流程
  3. kd树优缺点

二、kd树引入

# kd树引入图例
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
%matplotlib inline
font = FontProperties(fname='/Library/Fonts/Heiti.ttc')

# 测试点
plt.scatter(5, 6, marker='s', c='k', s=50)
plt.text(5, 5, s='《未知类型的电影》(5,6)', fontproperties=font, ha='center')

# 生成动作片
np.random.seed(1)
action_x1 = np.random.randint(1, 7, 15)
action_x2 = np.random.randint(8, 17, 15)
plt.scatter(action_x1, action_x2, marker='o', s=30, c='r', label='动作片')

# 生成爱情片
romance_x1 = np.random.randint(8, 17, 15)
romance_x2 = np.random.randint(1, 7, 15)
plt.scatter(romance_x1, romance_x2, marker='x', s=30, c='g', label='爱情片')

# 测试文本标记
plt.text(4, 11, s='A', ha='center')
plt.text(5, 7, s='B', ha='center')
plt.text(1, 14, s='C', ha='center')

plt.xlim(0, 18)
plt.ylim(0, 18)
plt.xlabel('接吻镜头(次)', fontproperties=font)
plt.ylabel('打斗场景(次)', fontproperties=font)
plt.title('kd树引入图例', fontproperties=font, fontsize=20)
plt.legend(prop=font)
plt.show()

这里将继续沿用k近邻算法的例子。

如上图所示,假设现有一部《未知类型的电影》,k近邻的思想是计算出数据集中每一部电影到《未知类型的电影》的距离,之后找到\(k\)个近邻对《未知类型的电影》分类。但是可以发现如电影C这样偏僻位置的电影明显是可以不用计算它们与《未知类型的电影》的距离的。

那现在假设有没有这么一种可能?我们不再计算每部电影到《未知类型的电影》的距离,而是直接得到《未知类型的电影》最近的\(k\)部电影?首先这种假设应该不可能,其次真的有,那还要其他机器学习算法作甚?那么再想想能不能通过某种方法找到离《未知类型的电影》较近的电影呢?我们假设有这种方法,并且这种方法被称作kd树,我们看看它是如何实现的。

  1. 比如kd树找到电影A即离《未知类型的电影》较近的某部电影,然后把距离大于电影A到《未知类型的电影》之间的电影排除掉,或者是继续找找看有没有哪部电影到《未知类型的电影》的距离更近?
  2. 如果可以找到电影A,则也可以kd树找到电影B
  3. 递归使用kd树搜索直到找到最近的某几部电影,停止搜索

上述整个过程其实就是kd树实现的一个过程,接下来将从理论层面抽象的讲解kd树。

三、kd树详解

3.1 构造kd树

kd树主要是不断地划分\(k\)维空间。

在一个二维空间平面中,kd树则是不断地使用垂直于\(x\)轴或\(y\)轴的直线把平面划分成一系列较小的矩形。在\(k\)维空间中,kd树则是不断地用垂直于坐标轴的超平面将\(k\)维空间切分成一系列的\(k\)维超矩形区域。

假设有\(m\)个实例\(n\)维特征的数据集
\[
T=\{(x_1,y_1),(x_2,y_2),\cdots,(x_m,y_m)\}
\]
其中\(x_i\)是实例的特征向量即\(({x_i}^{(1)},{x_i}^{(2)},\cdots,{x_i}^{(n)})\),\(y_i\)是实例的类别,数据集有\(\{c_1,c_2,\cdots,c_j\}\)共\(j\)个类别。

  1. 构造k维空间的超矩形区域。如果是有两个特征\(x_1\)和\(x_2\)的二维空间,则是由\((x1_{min},x1_{max},x2_{min},x2_{max})\)构成的矩形区域
  2. 选择特征\(x^{(1)}\)为坐标轴,以\(T\)中所有实例的\(x^{(1)}\)坐标的中位数作为切分点,将根结点对应的超矩形区域切分成左右两个子区域。将\(x^{(1)}\)对应值小于切分点的实例划入左子区域;将\(x^{(1)}\)对应值大于切分点的实例划入右子区域;将切分点保留在根结点
  3. 重复2步骤,即选择\(x^{(l)}, \quad (l=1,2,\cdots,n)\)为切分的坐标轴,以该结点中还剩下的\(k\)个实例的\(x^{(l)}\)坐标的中位数作为切分点,将该结点对应的超矩形区域切分成左右两个子区域。将\(x^{(l)}\)对应值小于切分点的实例划入左子区域;将\(x^{(l)}\)对应值大于切分点的实例划入右子区域;将切分点保留在该结点
  4. 切割后的子区域没有实例时,即无法划分时结束

构造kd树时需要注意左子结点对应坐标\(x^{(l)}\)小与切分点的\(x^{(l)}\)坐标,右子结点对应坐标\(x^{(l)}\)大于切分点的\(x^{(l)}\)坐标。

3.1.1 示例

假设有一个二维空间\((x_1,x_2)\)的数据集
\[
T = \{(2,3)^T,(5,4)^T,(9,6)^T,(4,7)^T,(8,1)^T,(7,2)^T\}
\]
首先\(x\)轴上的数据有\((2、4、5、7、8、9)\),中位数为\(7\)(注:如果以\(5\)划分也行,但是本文都是以较大的作为切分点,但是不能以\({\frac{5+7}{2}}\)划分,总之划分点符合kd树的构造要求即可),则以\(x=7\)划分一次矩形区域;左子区域\(y\)轴上的数据有\((3、4、7)\),中位数为\(4\),则以\(y=4\)划分一次数据,接着以\(x\)轴上的数据划分,而\(x\)轴数据只剩下\((2,4)\),直接以\(x=4\)划分左子区域的右子区域,以\(x=2\)划分左子区域的左子区域;右子区域\(y\)轴上数据有\((1、6)\),由于划分点需要大于左子结点的,因此右子结点为\((9,6)\),即以\(y=6\)划分左子区域,接着只剩下一个点\((8,1)\),又以\(x\)轴划分,则以\(x=8\)划分。

通过以上划分,即可得以下特征空间划分图。

# kd树的构造示特征空间划分图例
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
%matplotlib inline
font = FontProperties(fname='/Library/Fonts/Heiti.ttc')

plt.scatter(2, 3, c='r', s=30)
plt.text(2, 3.5, s='$(2,3)$', ha='center')
plt.scatter(5, 4, c='r', s=30)
plt.text(5, 4.5, s='$(5,4)$', ha='center')
plt.scatter(9, 6, c='r', s=30)
plt.text(9, 6.5, s='$(9,6)$', ha='center')
plt.scatter(4, 7, c='r', s=30)
plt.text(4, 7.5, s='$(4,7)$', ha='center')
plt.scatter(8, 1, c='r', s=30)
plt.text(8, 1.5, s='$(8,1)$', ha='center')
plt.scatter(7, 2, c='r', s=30)
plt.text(7, 2.5, s='$(7,2)$', ha='center')

plt.hlines(4, 0, 7, linestyle='-', color='k')
plt.hlines(6, 7, 10, linestyle='-', color='k')

plt.vlines(2, 0, 4, linestyle='-', color='k')
plt.vlines(4, 4, 10, linestyle='-', color='k')
plt.vlines(7, 0, 10, linestyle='-', color='k')
plt.vlines(8, 0, 6, linestyle='-', color='k')

plt.xlim(0, 10)
plt.ylim(0, 10)
plt.title('kd树构造-示例图例1', fontproperties=font, fontsize=20)
plt.show()

同超矩形区域划分一样可以构造如下图所示的kd二叉树

首先以\(x\)轴作为划分轴,\(x\)轴上的数据有\((2、4、5、7、8、9)\),中位数为\(7\),根结点为\((7,2)\),左子结点\((2,3)、(4,7)、(5,4)\),右子结点\((8,1)、(9,6)\);之后以\(y\)轴作为划分轴,左子结点\(y\)轴有数据\((3、4、7)\),则左子结点的中位数为\(4\),则左子结点\((5,4)\),左子结点的左孙子结点\((2,3)\),左子结点的右孙子结点\((4,7)\);右子结点\(y\)轴有数据\((1、6)\),则有右子结点\((9,6)\),右子结点的左孙子结点\((8,1)\)。

# kd树的构造kd二叉树形成图例
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
%matplotlib inline
font = FontProperties(fname='/Library/Fonts/Heiti.ttc')

plt.scatter(20, 15, c='white', edgecolor='k', s=2000)
plt.text(20, 14.5, s='$(7,2)$', ha='center',
         fontsize=15, color='k', fontweight='bold')
plt.annotate(s='', xytext=(18.5, 13.5), xy=(13.5, 11.5),
             arrowprops=dict(arrowstyle="-", color='b'))

plt.scatter(12, 10, c='white', edgecolor='k', s=2000)
plt.text(12, 9.5, s='$(5,4)$', ha='center',
         fontsize=15, color='k', fontweight='bold')
plt.annotate(s='', xytext=(12, 8), xy=(9, 5),
             arrowprops=dict(arrowstyle="-", color='b'))

plt.scatter(8, 3, c='white', edgecolor='k', s=2000)
plt.text(8, 2.5, s='$(2,3)$', ha='center',
         fontsize=15, color='k', fontweight='bold')

plt.scatter(16, 3, c='white', edgecolor='k', s=2000)
plt.text(16, 2.5, s='$(4,7)$', ha='center',
         fontsize=15, color='k', fontweight='bold')
plt.annotate(s='', xytext=(12, 8), xy=(16, 5),
             arrowprops=dict(arrowstyle="-", color='b'))

plt.scatter(28, 10, c='white', edgecolor='k', s=2000)
plt.text(28, 9.5, s='$(9,6)$', ha='center',
         fontsize=15, color='k', fontweight='bold')
plt.annotate(s='', xytext=(21.5, 13.5), xy=(26, 11),
             arrowprops=dict(arrowstyle="-", color='b'))

plt.scatter(24, 3, c='white', edgecolor='k', s=2000)
plt.text(24, 2.5, s='$(8,1)$', ha='center',
         fontsize=15, color='k', fontweight='bold')
plt.annotate(s='', xytext=(27, 8), xy=(24.5, 5),
             arrowprops=dict(arrowstyle="-", color='b'))

plt.hlines(15, 24, 38, linestyle='--', color='r')
plt.text(39, 14.5, ha='center', color='r', s='X', fontsize=15)
plt.hlines(10, 32, 38, linestyle='--', color='b')
plt.text(39, 9.5, ha='center', color='b', s='Y', fontsize=15)
plt.hlines(3, 28, 38, linestyle='--', color='r')
plt.text(39, 2.5, ha='center', color='r', s='X', fontsize=15)

plt.xlim(-1, 40)
plt.ylim(-1, 20)
plt.title('kd树构造-示例图例2', fontproperties=font, fontsize=20)
plt.show()

3.2 kd树搜索

从根结点出发,递归的向下访问kd树,如果目标点\(x\)的\(x^{(l)}\)维的坐标小于切分点的坐标,则移动到左子结点,否则移动到右子结点,直到子结点为叶子结点为止,并假设该叶子结点为目标点的当前最近点。

递归的从该叶子结点向上回退,对每个结点上的实例都进行如下操作:

  1. 如果该实例到目标点的距离更近,则以该实例点为当前最近点
  2. 检查当前最近点父结点的另一子结点对应的区域是否有更近的点,即另一子结点的超矩形区域是否与以目标点为球心、以目标点到当前最近点的半径形成的超球体是否相交
    1. 如果相交,则另一子结点对应的超矩形区域内存在离目标点更近的点,移到另一子结点继续递归搜索
    2. 如果不相交,则向上回退
  3. 当回退到根结点时结束,目标点的最近邻点为当前最近点

上述kd树搜索的过程可以看出,由于很多实例点所在的超矩形区域和超球体不相交,压根不需要计算距离,打打节省了计算时间。

但是值得注意的是kd树的平均计算复杂度为\(O(\log{N})\),当特征维数接近训练集实例数时,kd树的搜索效率则会迅速下降,几乎接近线性扫描。

3.2.1 示例

接下来将拿上一节构造的kd树举例kd树搜索的整个过程,假设目标点为\((2,4.5)\)。

  1. 搜索目标点对应的叶子结点

    1. 从根结点\((7,2)\)开始查找,由于\((7,2)\)是由\(x=7\)划分的,并且目标点的\(x\)值为\(2\),由于\(2<7\)则到左子\((5,4)\)结点
    2. 由于\((5,4)\)是由\(y=4\)分隔超平面的,并且目标点的\(y\)值为\(4.5\),由于\(4.5>4\)则到右子\((4,7)\)叶子结点
    3. 假设\((4,7)\)叶子结点为当前最近结点
    4. 目标掉搜索到叶子结点的路径为\((7,2)\rightarrow(5,4)\rightarrow(4,7)\)
  2. 搜索目标点对应的当前最近点
    1. 当前最近结点\((4,7)\)到目标点的距离为\(3.2\),而当前最近结点的父节点\((5,4)\)到目标点的距离为\(3.0\),则当前最近结点更新为\((5,4)\)
    2. 以下图蓝色五角星目标点\((2,4.5)\)为圆心,以当前最近结点\((5,4)\)到目标点的距离\(3.0\)为半径做一个超球体,即下图的红色圆圈
    3. 可以发现红色圆圈和\(y=4\)超平面相交,因此进入\((5,4)\)的左子结点查找,即找到\((2,3)\)
    4. 计算\((2,3)\)到目标点的距离为\(1.5\),则当前最近结点更新为\((2,3)\)
    5. 以下图蓝色五角星目标点\((2,4.5)\)为圆心,以当前最近结点\((2,3)\)到目标点的距离\(1.5\)为半径做一个超球体,即下图的绿色圆圈
    6. 由于结点\((2,3)\)为叶子结点,开始沿父结点回溯,父结点\((5,4)\)已经考虑过,继续往上回溯到\((7,2)\)
    7. 绿色圆圈和\(x=7\)没有相交,又由于\((7,2)\)为根节点,搜索结束
    8. 通过上述搜索目标点最近点为\((2,3)\),最近距离为\(1.5\)

值得注意的是上述都是以最近邻展开讨论kd树的,如果想得到\(k\)个近邻,只需要最后保留\(k\)个最近邻的结点上的实例点即可,之后依据多数表决法即可获得目标点的类别;如果是回归问题,取\(k\)个近邻点的标记值的平均数或中位数即可。

# kd树的构造示特征空间划分图例
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
%matplotlib inline
font = FontProperties(fname='/Library/Fonts/Heiti.ttc')

plt.scatter(2, 3, c='r', s=30)
plt.text(2, 3.5, s='$(2,3)$', ha='center')
plt.scatter(5, 4, c='r', s=30)
plt.text(5, 4.5, s='$(5,4)$', ha='center')
plt.scatter(9, 6, c='r', s=30)
plt.text(9, 6.5, s='$(9,6)$', ha='center')
plt.scatter(4, 7, c='r', s=30)
plt.text(4, 7.5, s='$(4,7)$', ha='center')
plt.scatter(8, 1, c='r', s=30)
plt.text(8, 1.5, s='$(8,1)$', ha='center')
plt.scatter(7, 2, c='r', s=30)
plt.text(7, 2.5, s='$(7,2)$', ha='center')

plt.hlines(4, 0, 7, linestyle='-', color='k')
plt.hlines(6, 7, 10, linestyle='-', color='k')

plt.vlines(2, 0, 4, linestyle='-', color='k')
plt.vlines(4, 4, 10, linestyle='-', color='k')
plt.vlines(7, 0, 10, linestyle='-', color='k')
plt.vlines(8, 0, 6, linestyle='-', color='k')

plt.scatter(2, 4.5, s=30410, c='white', edgecolor='r')
plt.scatter(2, 4.5, s=15000, c='white', edgecolor='g')
plt.scatter(2, 4.5, s=50, c='b', marker='*')
plt.text(2, 5, s='目标点$(2,4.5)$', ha='center',fontproperties=font)
plt.scatter(2, 3, c='r', s=30)
plt.scatter(4, 7, c='r', s=30)

plt.xlim(0, 10)
plt.ylim(0, 10)
plt.title('kd树搜索-示例图例', fontproperties=font, fontsize=20)
plt.show()

四、kd树流程

4.1 输入

有\(m\)个实例\(n\)维特征的数据集
\[
T=\{(x_1,y_1),(x_2,y_2),\cdots,(x_m,y_m)\}
\]
其中\(x_i\)是实例的特征向量即\(({x_i}^{(1)},{x_i}^{(2)},\cdots,{x_i}^{(n)})\)。

4.2 输出

距离目标点最近的\(k\)个实例及\(k\)个实例到目标点的距离。

4.3 流程

  1. 构造kd树
  2. 搜索离目标点最近的\(k\)个实例点
  3. 分类问题依据多数表决法即可获得目标点的类别;如果是回归问题,取 ?? 个近邻点的标记值的平均数或中位数即可。

五、kd树优缺点

5.1 优点

  1. 简单易懂,容易实现,可以做分类也可以做回归
  2. 基于实例学习,对数据没有假设,不需要通过模型训练获得参数
  3. 较于k近邻算法计算量减少了

5.2 缺点

  1. 无论哪一种距离度量方式都需要使用到特征值之间的差值,如果某些特征值之间的差值过大会掩盖差值较小特征对预测值的影响(一般使用数据预处理中的归一化方法处理特征值)
  2. kd树的建立需要大量的内存消耗(换内存吧!!!)
  3. 相比较决策树,解释型不强(客户真的想要解释性强的模型可以换决策树试一试)

六、小结

kd树更多的使用了二叉树的思路,当然它也很好的实现了我们想要实现的,这就可以了。

如果你细心的话,可以发现在搜索的过程中,如果超球体碰到了超矩形的一点棱角就需要去搜索超矩形区域内有没有离目标点更近的点,当然有解决方法,可以使用球树(ball tree),但是其实较kd树并没有那么明显的优化,这里就不多叙述了。简而言之就是球树相较于kd树使用的是超球体切割数据集,有兴趣的可以自己思考思考(先构造一个最小球体包含所有数据,然后递归构造小球体——先选择一个离第一个球体中心最远的点A,再选择一个离点A最远的点B,那个数据离哪个点近则属于哪一个点的那一类,这样就完美把一个球体切分成两个球体)。

kd树讲完,k近邻也就告一段落了。下一篇的机器学习算法听起来可能和传统算法中的树结构有关,但它到底是不是一个树结构的机器学习算法呢?看完了就知道了——决策树。

原文地址:https://www.cnblogs.com/nickchen121/p/11686769.html

时间: 2024-11-06 11:03:51

02-17 kd树的相关文章

hdu 4347 The Closest M Points (kd树)

hdu 4347 题意: 求k维空间中离所给点最近的m个点,并按顺序输出  . 解法: kd树模板题 . 不懂kd树的可以先看看这个 . 不多说,上代码 . 1 #include <iostream> 2 #include <cstdio> 3 #include <algorithm> 4 #include <cmath> 5 #include <cstring> 6 #include <queue> 7 #include <s

数据结构(KD树):HDU 4347 The Closest M Points

The Closest M Points Time Limit: 16000/8000 MS (Java/Others)    Memory Limit: 98304/98304 K (Java/Others)Total Submission(s): 3285    Accepted Submission(s): 1201 Problem Description The course of Software Design and Development Practice is objection

KD树

什么是KD树 Kd-树是K-dimension tree的缩写,是对数据点在k维空间(如二维(x,y),三维(x,y,z),k维(x,y,z..))中划分的一种数据结构,主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索).本质上说,Kd-树就是一种平衡二叉树. 首先必须搞清楚的是,k-d树是一种空间划分树,说白了,就是把整个空间划分为特定的几个部分,然后在特定空间的部分内进行相关搜索操作.想像一个三维空间,kd树按照一定的划分规则把这个三维空间划分了多个空间,如下图所示: KD树的构建

k近邻法的C++实现:kd树

1.k近邻算法的思想 给定一个训练集,对于新的输入实例,在训练集中找到与该实例最近的k个实例,这k个实例中的多数属于某个类,就把该输入实例分为这个类. 因为要找到最近的k个实例,所以计算输入实例与训练集中实例之间的距离是关键! k近邻算法最简单的方法是线性扫描,这时要计算输入实例与每一个训练实例的距离,当训练集很大时,非常耗时,这种方法不可行,为了提高k近邻的搜索效率,常常考虑使用特殊的存储结构存储训练数据,以减少计算距离的次数,具体方法很多,这里介绍实现经典的kd树方法. 2.构造kd树 kd

初学kd树

一开始不会kd树的时候,感觉kd树一定很神,学了kd树发现kd树挺好写. kd树的每个节点有一个分割超平面,我是以深度%维数作为当前这一维的分割,比较时对于当前节点就比较这一维. 附上模板代码,求平面第k近距离(kd树裸题) 1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 #include<queue> 6 #include&l

kd树解平面最近点对

早上起来头有点疼,突然就想到能不能用kd树解平面最近点对问题,就找了道题试了一下,结果可以,虽然效率不高,但还是AC了~ 题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1007 题目要求平面上最近点对间距离的一半. 思路如下:先建立一棵树,所有点插入树中,之后为每个点查询其最近点,枚举找到最小值.注意查询的时候不要让点自己跟自己比.个人感觉,这种写法也可以达到O(nlogn)的复杂度.建树分区间的时候,按x,y中跨度大的一个来分,应该就接近O(nl

python kd树 搜索

kd树就是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构,可以运用在k近邻法中,实现快速k近邻搜索.构造kd树相当于不断地用垂直于坐标轴的超平面将k维空间切分,依次选择坐标轴对空间进行切分,选择训练实例点在选定坐标轴上的中位数为切分点.具体kd树的原理可以参考kd树的原理. 代码是参考<统计学习方法>k近邻 kd树的python实现得到 首先创建一个类,用于表示树的节点,包括:该节点的值,该节点的切分轴,左子树,右子树 class decisionnode: def __i

kd树 求k近邻 python 代码

  之前两篇随笔介绍了kd树的原理,并用python实现了kd树的构建和搜索,具体可以参考 kd树的原理 python kd树 搜索 代码 kd树常与knn算法联系在一起,knn算法通常要搜索k近邻,而不仅仅是最近邻,下面的代码将利用kd树搜索目标点的k个近邻. 首先还是创建一个类,用于保存结点的值,左右子树,以及用于划分左右子树的切分轴 class decisionnode: def __init__(self,value=None,col=None,rb=None,lb=None): sel

【hdu4347】The Closest M Points 【KD树模板】

题意 一个k维空间,给出n个点的坐标,给出t个询问,每个询问给出一个点的坐标和一个m.对于每个询问找出跟这个点最接近的m个点 分析 kd树的模板题. 1 #include <cstdio> 2 #include <cstring> 3 #include <iostream> 4 #include <algorithm> 5 #include <queue> 6 7 using namespace std; 8 typedef long long