1. K—近邻算法简介
1. K—近邻算法(KNN)的概念
K Nearest Neighbor 算法又称KNN算法,这个算法是机器学习里一个比较经典的算法,总体来说是比较简单的。
1)定义
一个样本与在特征空间中k个最相似(即样本空间中距离最近)的大多数样本属于同一个类别。
来源:KNN算法最早是由Cover和Hart提出的一种分类算法。
2)距离公式
两个样本的距离可以通过如下公式计算,又叫欧式距离:
- 二维平面上点a(x1, y1)与b(x2, y2)的欧式距离: d 12 = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 d_{12}=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2} d12=(x1−x2)2+(y1−y2)2
- 三维空间点a(x1, y1, z1)与b(x2, y2, z2)的欧氏距离: d 12 = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 + ( z 1 − z 2 ) 2 d_{12}=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2+(z_1-z_2)^2} d12=(x1−x2)2+(y1−y2)2+(z1−z2)2
- n维空间点a(x11, x12,…,x1n)与b(x21, x22,…,x2n)间的欧式距离(两个n维向量): d 12 = ∑ k = 1 n ( x 1 k − x 2 k ) 2 d_{12}=\sqrt{\sum \limits_{k=1}^n(x_{1k}-x_{2k})^2} d12=k=1∑n(x1k−x2k)2
2. 电影类型分析
假设我们现在有很多部电影。
序号 | 电影名称 | 搞笑镜头 | 拥抱镜头 | 打斗镜头 | 电影类型 |
---|---|---|---|---|---|
1 | 功夫熊猫 | 39 | 0 | 31 | 喜剧片 |
2 | 叶问3 | 3 | 2 | 65 | 动作片 |
3 | 二次曝光 | 2 | 3 | 55 | 爱情片 |
4 | 代理情人 | 8 | 34 | 17 | 爱情片 |
5 | 新步步惊心 | 8 | 34 | 17 | 爱情片 |
6 | 谍影重重 | 5 | 2 | 57 | 动作片 |
7 | 美人鱼 | 21 | 17 | 5 | 喜剧片 |
8 | 宝贝当家 | 45 | 2 | 9 | 喜剧片 |
9 | 唐人街探案 | 23 | 3 | 17 | ? |
其中9号电影不知道类别,如何去预测?我们就可以用到K近邻算法的思想,其中每一个样本都可以看做是一个n维向量,使用公式 d 12 = ∑ k = 1 n ( x 1 k − x 2 k ) 2 d_{12}=\sqrt{\sum \limits_{k=1}^n(x_{1k}-x_{2k})^2} d12=k=1∑n(x1k−x2k)2来计算9号电影和其余样本的相似程度。
序号 | 电影名称 | 搞笑镜头 | 拥抱镜头 | 打斗镜头 | 电影类型 | 与9号电影的距离 | K=5时 |
---|---|---|---|---|---|---|---|
1 | 功夫熊猫 | 39 | 0 | 31 | 喜剧片 | 21.47 | 相似 |
2 | 叶问3 | 3 | 2 | 65 | 动作片 | 52.01 | |
3 | 二次曝光 | 2 | 3 | 55 | 爱情片 | 43.42 | |
4 | 代理情人 | 8 | 34 | 17 | 爱情片 | 40.57 | 相似 |
5 | 新步步惊心 | 8 | 34 | 17 | 爱情片 | 34.44 | 相似 |
6 | 谍影重重 | 5 | 2 | 57 | 动作片 | 43.87 | |
7 | 美人鱼 | 21 | 17 | 5 | 喜剧片 | 18.55 | 相似 |
8 | 宝贝当家 | 45 | 2 | 9 | 喜剧片 | 23.43 | 相似 |
找出了K个距离最近的电影,此时K=5,就找了5个最相似的电影。
3. KNN算法流程总结
1)计算已知类别数据集中的点与当前点之间的距离
2)按照距离递增次序排序
3)选取与当前点距离最小的K个点
4)统计前K个点所在类别出现的频率
5)返回前K个点出现频率最高的类别作为当前点的预测分类
2. K—近邻算法api初步使用
sklearn.neighbors.KNeighborsClassifier(n_neighbors=5)
:参数只有一个n_neighbors
,为int
类型,默认值为5,表示查询默认使用的邻居数。
下面我们用一个案例来使用以下这个函数。
1. 步骤分析
1)获取数据集
2)数据基本处理(该案例中省略)
3)特征工程(该案例中省略)
4)机器学习
5)模型评估(该案例中省略)
2. 代码实现
# 1. 导入模块
from sklearn.neighbors import KNeighborsClassifier
# 2. 构造数据集
x = [[1], [2], [10], [20]] # 特征值
y = [0, 0, 1, 1] # 目标值
# 3. 机器学习 —— 模型训练
# 3.1 实例化api
estimator = KNeighborsClassifier(n_neighbors=1)
# 3.2 使用fit方法进行训练
estimator.fit(x, y)
# 3.3 进行预测
ret = estimator.predict([[1]])
# 打印预测结果
print(ret)
输出:
[0]
3. 距离度量
1. 欧式距离(Euclidean Distance)
1)二维平面上点a(x1, y1)与b(x2, y2)的欧式距离: d 12 = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 d_{12}=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2} d12=(x1−x2)2+(y1−y2)2
2)三维空间点a(x1, y1, z1)与b(x2, y2, z2)的欧氏距离: d 12 = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 + ( z 1 − z 2 ) 2 d_{12}=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2+(z_1-z_2)^2} d12=(x1−x2)2+(y1−y2)2+(z1−z2)2
3)n维空间点a(x11, x12,…,x1n)与b(x21, x22,…,x2n)间的欧式距离(两个n维向量): d 12 = ∑ k = 1 n ( x 1 k − x 2 k ) 2 d_{12}=\sqrt{\sum \limits_{k=1}^n(x_{1k}-x_{2k})^2} d12=k=1∑n(x1k−x2k)2
2. 曼哈顿距离(Manhattan Distance)
在曼哈顿街区,要从一个十字路口开车到另一个十字路口,驾驶距离显然不是两点间的直线距离。这个实际驾驶距离就是“曼哈顿距离”,曼哈顿距离也称为“城市街区距离”(City Block distance)。
-
二维平面两点a(x1, y1)与b(x2, y2)间的曼哈顿距离: d 12 = ∣ x 1 − x 2 ∣ + ∣ y 1 − y 2 ∣ d_{12}=|x_1-x_2|+|y_1-y_2| d12=∣x1−x2∣+∣y1−y2∣
-
n维空间点a(x11, x12,…,x1n)与b(x21, x22,…,x2n)间的曼哈顿距离: d 12 = ∑ k = 1 n ∣ x 1 k − x 2 k ∣ d_{12}=\sum \limits_{k=1}^n|x_{1k}-x_{2k}| d12=k=1∑n∣x1k−x2k∣
3. 切比雪夫距离(Chebyshev Distance)
国际象棋中,国王可以直行、横行、斜行,所以国王走一步可以移动到相邻8个方格中的任意一个。国王从格子(x1, y1)走到格子(x2, y2)最少需要多少步?这个距离就叫切比雪夫距离。
- 二维平面两点a(x1, y1)与b(x2, y2)间的切比雪夫距离: d 12 = m a x ( ∣ x 1 − x 2 ∣ , ∣ y 1 − y 2 ∣ ) d_{12}=max(|x_1-x_2|,|y_1-y_2|) d12=max(∣x1−x2∣,∣y1−y2∣)
- n维空间点a(x11, x12,…,x1n)与b(x21, x22,…,x2n)间的切比雪夫距离: d 12 = m a x ( ∣ x 1 k − x 2 k ∣ ) d_{12}=max(|x_{1k}-x_{2k}|) d12=max(∣x1k−x2k∣)
4. 闵可夫斯基距离(Minkowski Distance)
闵氏距离不是一种距离,而是一组距离的定义,是对多个距离度量公式的概括性的表述。两个n维向量a(x11, x12,…,x1n)与b(x21, x22,…,x2n)间的闵可夫斯基距离定义为:
d 12 = ∑ k = 1 n ∣ x 1 k − x 2 k ∣ p p d_{12}=\sqrt[p]{\sum \limits_{k=1}^n|x_{1k}-x_{2k}|^p} d12=pk=1∑n∣x1k−x2k∣p
其中p是一个变量参数:
- 当p=1时,就是曼哈顿距离;
- 当p=2时,就是欧式距离;
- 当p → ∞ \to\infty →∞,就是切比雪夫距离。
根据p的不同,闵氏距离可以表示某一类/种的距离。
1)注意:闵氏距离、曼哈顿距离、欧氏距离、和切比雪夫距离都存在明显的缺点:
- 二维样本(身高[单位:cm],体重[单位:kg]),现有3个样本:a(180, 50), b(190, 50), c(180, 60)。a与b的闵氏距离,等于a与c的闵氏距离。但实际上身高的10cm并不能和体重的10kg划等号。
2)闵氏距离的特点:
- 将各个分量的量纲(scale),也就是“单位”相同的看待了;
- 未考虑各个分量的分布(期望,方差等)可能是不同的。
5. 标准化欧氏距离(Standardized EuclideanDistance)
标准化欧氏距离是针对欧氏距离的缺点进行的一种改进。思路:既然数据各分量的分布不一样,那就先将各个分量都“标准化”到均值,方差相等。
s k s_k sk表示各个维度的标准差:
- 标准化欧式距离公式: d 12 = ∑ k = 1 n ( x 1 k − x 2 k s k ) 2 d_{12}=\sqrt{\sum \limits_{k=1}^n(\dfrac{x_{1k}-x_{2k}}{s_k})^2} d12=k=1∑n(skx1k−x2k)2
如果将方差的倒数看成一个权重,也可以称之为加权欧氏距离(Weighted Euclidean distance)。
6. 余弦距离(Consine Distance)
几何中,夹角余弦可用来衡量两个向量方向的差异。机器学习中,借用这一概念来衡量样本之间的差异。
- 二维空间中向量a(x1, y1)与向量B(x2, y2)的夹角余弦公式: cos θ = x 1 x 2 + y 1 y 2 x 1 2 + y 1 2 x 2 2 + y 2 2 \cos \theta=\dfrac{x_1x_2+y_1y_2}{\sqrt{x_1^2+y_1^2}\sqrt{x_2^2+y_2^2}} cosθ=x12+y12x22+y22x1x2+y1y2
- 两个n维样本点a(x11, x12,…,x1n)与b(x21, x22,…,x2n)的夹角余弦为: cos θ = ∑ k = 1 n x 1 k x 2 k x 1 2 + y 1 2 x 2 2 + y 2 2 \cos\theta=\dfrac{\sum \limits_{k=1}^nx_{1k}x_{2k}}{\sqrt{x_1^2+y_1^2}\sqrt{x_2^2+y_2^2}} cosθ=x12+y12x22+y22k=1∑nx1kx2k
夹角余弦取值范围为[-1, 1]。余弦越大表示两个向量的夹角越小,余弦越小表示两向量的夹角越大。当两个向量的方向重合时,余弦取最大值1;相反时,取最小值-1。
4. K值的选择
1. 直观解释:
序号 | 电影名称 | 搞笑镜头 | 拥抱镜头 | 打斗镜头 | 电影类型 |
---|---|---|---|---|---|
1 | 功夫熊猫 | 39 | 0 | 31 | 喜剧片 |
2 | 叶问3 | 3 | 2 | 65 | 动作片 |
3 | 二次曝光 | 2 | 3 | 55 | 爱情片 |
4 | 代理情人 | 8 | 34 | 17 | 爱情片 |
5 | 新步步惊心 | 8 | 34 | 17 | 爱情片 |
6 | 谍影重重 | 5 | 2 | 57 | 动作片 |
7 | 美人鱼 | 21 | 17 | 5 | 喜剧片 |
8 | 宝贝当家 | 45 | 2 | 9 | 喜剧片 |
9 | 唐人街探案 | 23 | 3 | 17 | ? |
- K值过小时,容易受到异常点的影响。
- K值过大会受样本均衡的影响。
就比如有一个电影和唐人街探案的距离很近,但是这个电影是异常的,这时就会得到不准确的结果;如果上述各类型的电影分布很均匀,那么当K值过大时,筛选出的相似电影可能类型分布也很均匀,这时就会出现问题。
2. 概念性解释:
1)选择较小的K值,就相当于用较小领域中的训练实例进行预测,“学习”近似误差会减小,只有与输入实例较近或相似的训练实例才会对预测结果起作用,与此同时带来的问题是“学习”的估计误差会增大,也就是说K值的减小就意味着整体模型变得复杂,容易发生过拟合;
2)选择较大的K值,就相当于用较大领域中的训练实例进行预测,其优点是可以减少学习的估计误差,但缺点是学习的近似误差会增大。这时,与输入实例较远(不相似)的训练实例也会对预测器作用,是预测发生错误,且K值的增大就意味着整体的模型变得简单,容易欠拟合;
3)K=N(N为训练样本个数),完全不足取,因为此时无论输入实例是什么,都只是简单的预测它属于在训练实例中最多的类,模型过于简单,忽略了训练实例中的大量有用信息。
在实际应用中,K一般取较小值。
5. kd树
问题导入:
实现k近邻算法时,主要考虑的问题是如何对训练数据进行快速k近邻搜索。这在特征空间的维数大及训练数据容量时尤其必要。
k近邻算法最简单的实现是线性扫描(穷举搜索),即要计算输入实例与每一个训练实例的距离。计算并存储好以后,再查找k近邻。当训练集很大时,计算非常耗时。
为了提高KNN搜索的效率,可以考虑使用特殊的结构存储训练数据,以减小计算距离的次数。
5.1 构造kd树
1. 什么是kd树?
kd树是一种对k维空间中的实例点进行存储,以便对其进行快速检索的树形数据结构。
- 本质:二叉树(一般是平衡二叉树),表示对k维空间的一个划分;
- 构造过程:不断地用垂直于坐标轴的超平面将k维空间切分,形成k维超矩形区域;
- 注意:kd树的每一个节点对应于一个k维超矩形区域;平衡的kd树搜索时未必是最优的。
2. 构造kd树
先输入k维空间数据集 T = {x1, x2, …, xN},其中 xi = (xi(1), xi(2), …, xi(k))T,再输出kd树:
1)开始:构造根节点。
- 选取方差最大的一组特征作为坐标轴,假设是 x i ( 1 ) x_i^{(1)} xi(1)对应的一组特征,以训练集中的所有数据 x ( 1 ) x^{(1)} x(1)坐标中的中位数作为切分点,将超矩形区域切分成两个子区域,以该切分点作为根节点;
- 由根节点生出深度为1的左右子节点,左节点对应坐标小于切分点,右节点对应坐标大于切分点。
2)重复
- 对深度为 j j j的节点,选择 x ( I ) x^{(I)} x(I)为切分坐标轴, I = j m o d k + 1 I = j\mod k+1 I=jmodk+1,以该节点区域中所有实例 x ( I ) x^{(I)} x(I)坐标的中位数作为切分点,将该区域划分为两个子区域;
- 生成深度为 j + 1 j+1 j+1的左右子节点。左节点对应坐标小于切分点,右节点对应坐标大于切分点。
3)结束
- 直到两个子区域没有实例时停止。
3. 上例题:以一个二维的样本空间为例
输入:训练集 T = { ( 2 , 3 ) , ( 5 , 4 ) , ( 9 , 6 ) , ( 4 , 7 ) , ( 8 , 1 ) , ( 7 , 2 ) } T=\{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)\} T={(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)}
x ( 1 ) x^{(1)} x(1)轴对应的数: 2 , 5 , 9 , 4 , 8 , 7 2,5,9,4,8,7 2,5,9,4,8,7; x ( 2 ) x^{(2)} x(2)轴对应的数: 3 , 4 , 6 , 7 , 1 , 2 3,4,6,7,1,2 3,4,6,7,1,2。
1)开始:
- 经过计算可知,x轴对应的这一串数字(这项特征)方差较大,选用x轴中的中位数5或7做切分点,这里我们选择7,切分整个区域。
2)重复:
-
以 x ( 2 ) x^{(2)} x(2)为坐标轴,选择中位数,左边区域为4,右边区域两个任取一个,这里取较大数6。故左边区域切分点为 ( 5 , 4 ) (5,4) (5,4),右边区域切分点为 ( 9 , 6 ) (9,6) (9,6);
-
划分左边区域:此时 I = 1 I=1 I=1,重新从最开始选取的轴 x ( 1 ) x^{(1)} x(1)开始切。以 x ( 1 ) x^{(1)} x(1)为坐标轴,选择中位数,此时上下两个区域都只剩一个点了,全选上作为切分点;
-
划分右边区域:同上,以 x ( 1 ) x^{(1)} x(1)为左边轴,选取切分点(只有一个)。
3)输出kd树:
5.2 搜索kd树
输入:已构造的kd树,目标点x。
输出:x的最近邻点。
1. 基本流程:
1)寻找“当前最近点”
- 从根节点出发,递归访问kd树,找出包含x的最近邻点;
- 以此叶节点为“当前最近点”。
2)回溯
- 回溯过程中,若该节点比“当前最近点”距离目标点更近,更新“当前最近点”;
- 当前最近点一定存在于该节点一个子节点对应的区域,检查子节点的父节点的另一子节点对应的区域是否有更近的点。
3)结束
- 当退回到根节点时,搜索结束,最后的“当前最近点”即为x的最近邻点。
2. 实例:
现有kd树:
1)输入目标点 x = ( 2.1 , 3.1 ) x=(2.1,3.1) x=(2.1,3.1),求最近邻点。
- 查找“当前最近邻点”:
先找到根节点 x ( 7 , 2 ) x(7,2) x(7,2),因为目标节点 ( 2.1 , 3.1 ) (2.1,3.1) (2.1,3.1)的横坐标小于7,所以先从kd树的左子树(左边区域)开始搜索;因为目标节点的纵坐标小于4,所以从下边区域开始搜索;下边区域只有一个点 ( 2 , 3 ) (2,3) (2,3),目标点 ( 2.1 , 3.1 ) (2.1,3.1) (2.1,3.1)就在点 ( 2 , 3 ) (2,3) (2,3)的右边区域中,故 ( 2 , 3 ) (2,3) (2,3)就是当前的最近邻点。
- 回溯:
以目标点为圆心,到当前最近邻点 ( 2 , 3 ) (2,3) (2,3)的距离为半径画圆(如果是多维空间,就是画一个超球面了)。先回溯到目标节点的父节点,也就是当前最近邻点 ( 2 , 3 ) (2,3) (2,3),发现圆和 ( 2 , 3 ) (2,3) (2,3)划分的左半部分平面(不含分界线)是有交集的,所以要先搜索左半部分平面;发现左半部分平面没有一个点,重新回溯到 ( 2 , 3 ) (2,3) (2,3);再回溯到当前最近邻点的父节点 ( 5 , 4 ) (5,4) (5,4),计算这个节点到目标点的距离,发现比圆的半径大,不更新数据;随后发现图中圆和 ( 5 , 4 ) (5,4) (5,4)划分的上半部分平面(不含分界线)是没有交集的,说明不可能从 ( 5 , 4 ) (5,4) (5,4)上半部分所对应的子区域中,找到更近的点;回溯到根节点,发现图中圆和根节点划分的右半部分区域也是没有交集的,所以最近邻点也不可能在根节点划分的右半部分,也不可能是根节点。
- 结束:
回溯到了根节点,停止回溯,找到最近邻点 ( 2 , 3 ) (2,3) (2,3)。
2)输入目标点 x = ( 2 , 4.5 ) x=(2,4.5) x=(2,4.5),求最近邻点。
- 查找“当前最近邻点”:
和1中的步骤一样,先找到了当前最近邻点 ( 4 , 7 ) (4,7) (4,7)。
- 回溯:
以目标点为圆心,到当前最近邻点的距离为半径画圆。先回溯到目标节点的父节点 ( 4 , 7 ) (4,7) (4,7),发现圆和 ( 4 , 7 ) (4,7) (4,7)所划分的右半部分平面是有交集的,先搜索右半部分平面;发现没有一个点,重新回溯到 ( 4 , 7 ) (4,7) (4,7);再回溯到父节点 ( 5 , 4 ) (5,4) (5,4),计算该节点到目标点的距离,发现比原来的圆半径小,更新最近邻点,重新画圆;随后发现,重新画的圆和 ( 5 , 4 ) (5,4) (5,4)划分的下半部分区域有交集,搜索下半部分区域,发现只有一个点;计算父节点的下半部分子区域中的子节点到目标点的距离,发现该距离比原来的圆半径要小,更新最近邻点为 ( 2 , 3 ) (2,3) (2,3),并重新画圆;
直接回溯到根节点,计算根节点到目标点的距离,发现比图中圆的半径要大,不更新数据;随后发现图中圆和根节点划分的右半部分区域没有交集,所以最近邻点不可能在根节点划分的右半部分,也不可能是根节点。
- 结束:
回溯到了根节点,停止回溯,找到最近邻点 ( 2 , 3 ) (2,3) (2,3)。
5.3 kd树代码实现
适用于n维向量:
import math
import numpy as np
class Node:
def __init__(self, data, lchild = None, rchild = None):
self.data = data
self.lchild = lchild
self.rchild = rchild
class KdTree:
def __int__(self):
self.kdTree = None
def create(self, dataSet, depth): # 构造kd树
if len(dataSet) > 0: # 还有实例,就往下分
m, n = np.shape(dataSet) # 求出样本的行和列
midIndex = m // 2 # 求出中位数的索引
if depth == 0: # 深度等于0时说明是根节点,轴已经选好,不用再判断
arr = list(dataSet.var(axis=0)) # 先转成list,不然无法使用index函数
max_value = max(arr) # 确定方差最大的那一列
axis = arr.index(max_value) # 确定初始划分坐标
else:
axis = depth % n # 判断以哪个轴划分数据,由于索引从0开始,所以不用+1
sortedDataSet = sorted(dataSet, key=lambda x: x[axis]) # 按照指定轴进行排序
node = Node(sortedDataSet[midIndex]) # 在树中放入节点
node.lchild = self.create(sortedDataSet[: midIndex], depth + 1) # 递归构造左子树
node.rchild = self.create(sortedDataSet[midIndex + 1 :], depth + 1) # 递归构造右子树
return node # 返回根节点
else:
return None
def preOrder(self, node): # 前序遍历二叉树
if node != None:
print(node.data)
self.preOrder(node.lchild)
self.preOrder(node.rchild)
def search(self, node, x): # 搜索kd树
self.nearestPoint = None # 保存当前最近点
self.nearestValue = math.inf # 保存当前最近距离(圆的半径)math.inf是无穷大
def travel(node, depth = 0): # 递归搜索
if node != None: # 递归终止条件
if depth == 0: # 深度等于0时说明是根节点,轴已经选好,不用再判断
arr = list(dataSet.var(axis=0)) # 先转成list,不然无法使用index函数
max_value = max(arr) # 确定方差最大的那一列
axis = arr.index(max_value) # 确定初始划分坐标
else:
axis = depth % len(x) # 判断以哪个轴划分数据,由于索引从0开始,所以不用+1
if x[axis] < node.data[axis]: # 如果数据小于节点对应坐标数据,则往左节点搜索
travel(node.lchild, depth + 1)
else:
travel(node.rchild, depth + 1)
else:
return
# 递归完毕后,开始回溯
distNodeAndX = self.dist(x, node.data) # 目标点和当前最近点的距离
if self.nearestValue > distNodeAndX: # 更新最近邻点
self.nearestPoint = node.data
self.nearestValue = distNodeAndX
if abs(x[axis] - node.data[axis]) < self.nearestValue: # 确定是否要去子节点的区域找最近邻点(圆的判断)
if x[axis] > node.data[axis]:
travel(node.rchild, depth + 1)
else:
travel(node.lchild, depth + 1)
travel(node)
return self.nearestPoint # 返回最近邻点
def dist(self, x1, x2): # 欧式距离计算
return ((x1 - x2) ** 2).sum() ** 0.5
if __name__ == '__main__':
# 准备数据
dataSet = np.array([[2, 3],
[5, 4],
[9, 6],
[4, 7],
[8, 1],
[7, 2]])
x = [2, 4.5] # 目标点
# 处理数据
kdtree = KdTree() # 初始化kd树
tree_root = kdtree.create(dataSet, 0) # 接收根节点(根节点深度为0)
# 前序遍历
kdtree.preOrder(tree_root) # 检查kd树是否正确建立
# 搜索最近邻点
print("最近邻点:", kdtree.search(tree_root, x))