K近邻法
- 1、概述
- 2、判别过程
- 3、kNN的三要素
- 3.1 距离度量
- 3.2 K值的选择
- 3.3 分类决策规则
- 4、k近邻算法及代码实(python)
- 4.1 kNN算法
- 4.2 k近邻法的实现—kd树
- 3.2.1 kd树的构造
- 4.2.2 搜索kd树
- 5、总结
1、概述
K近邻法(KNN)是一种基本的分类与回归方法。分类时,对于新的实例,根据与它最接近的k个训练实例的类别,通过多数表决等方式,进行预测。对于给定的训练集,当k值,距离度量和分类决策规则(统称三要素)确定后,基于k近邻法的模型就已经确定了。所以,它实际上利用训练集对特征向量空间进行划分,并没有显示的学习过程。k近邻法,符合我们基本的认知,即 “物以类聚,人以群分” ,一件事物的类别通常与它附近的事物具有相似性。
2、判别过程
给定一个训练数据集,对新的输入实例,在训练数据集中找到与该实例最邻近的k个实例,这k个实例的多数属于某个类(多数表决argmax),就把该输入实例分为这个类。
优点:易于实现,无需估计参数,无需训练,支持增量学习,能对超多边形的复杂决策空间建模
缺点:计算量较大,分析速度慢(因为要扫描全部训练样本并计算距离)
3、kNN的三要素
KNN模型由三个基本要素——距离度量、K值的选择和分类决策规则决定。根据选择的距离度量(如曼哈顿距离或欧氏距离),可计算测试实例与训练集中的每个实例点的距离,根据k值选择k个最近邻点,最后根据分类决策规则将测试实例分类。
如图1,根据欧氏距离,选择k=4个离测试实例最近的训练实例(红圈处),再根据多数表决的分类决策规则,即这4个实例多数属于“-类”,可推断测试实例为“-类”。
3.1 距离度量
特征空间中的两个实例点的距离是两个实例点相似程度的反映。KNN模型使用的距离是欧式距离,但也可以是其他距离,如更一般的Lp距离或Minkowski距离。
设特征空间X是n维实数向量空间,,
其中,
当时,它是各个坐标距离的最大值,即
3.2 K值的选择
k值的选择会对k近邻法的结果产生重大影响。在应用中,k值一般取一个比较小的数值,通常采用交叉验证法来选取最优的k值。
- 如果选择较小的k值,“学习”的近似误差(approximation error)会减小,只有与输入实例相近的(相似的)训练实例才会对预测结果起作用。但是“学习”的估计误差(estimation error)会增大,预测结果会对近邻的实例点非常敏感。如果邻近的实例点恰巧是噪声,预测就会出错。换句话说,k值的减小就意味着整体模型变得复杂,容易发生过拟合。
- 如果选择较大的k值,就相当于用较大邻域中的训练实例进行预测。其优点是可以减少学习的估计误差。但缺点是学习的近似误差会增大。这时与输入实例较远的(不相似的)训练实例也会对预测起作用,使预测发生错误。k值的增大意味着整体的模型变得简单,容易发生欠拟合。
- 如果k=N(N是训练样本个数),那么无论输入实例是什么,都将简单的预测它属于在训练实例中最多的类。这时,模型过于简单,完全忽略训练实例中的大量有用信息,是不可取的。
3.3 分类决策规则
k近邻法中的分类决策规则往往是多数表决,即由输入实例的k个邻近的训练实例中的多数类,决定输入实例的类。
4、k近邻算法及代码实(python)
4.1 kNN算法
输入:训练集
其中,为实例的特征向量,为实例的类别,i=1,2,…,N。
输出:实例x所属的类别y
① 根据给定的距离度量,在训练集T中找出与x最近邻的k个点,涵盖这k个点的x的领域记作
② 在中根据分类决策规则(如多数表决)决定x的类别y:
其中,为指示函数,即当时I为1,否则I为0。
【代码实现】
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
from collections import Counter
class KNN:
def __init__(self, X_train, y_train, n_neighbors=3, p=2):
"""
parameter: n_neighbors 临近点个数
parameter: p 距离度量
"""
self.n = n_neighbors
self.p = p
self.X_train = X_train
self.y_train = y_train
def predict(self, X):#判别/预测输入X属于哪一类
# 取出n个点,先算前n个样本与待预测样本X的L2距离,并添加到列表中
knn_list = []
for i in range(self.n):
dist = np.linalg.norm(X - self.X_train[i], ord=self.p)#Lp计算X与训练数据集中各个样本的Lp距离
knn_list.append((dist, self.y_train[i]))#将距离以及对应的类别添加到列表
#计算其余样本与待预测样本X的L2距离,若距离比列表中最大者小,则替换其最大者
for i in range(self.n, len(self.X_train)):
max_index = knn_list.index(max(knn_list, key=lambda x: x[0]))#按照距离最大者取相应索引
dist = np.linalg.norm(X - self.X_train[i], ord=self.p)#L2范数
if knn_list[max_index][0] > dist:
knn_list[max_index] = (dist, self.y_train[i])#替换最大者
# 统计
knn = [k[-1] for k in knn_list]#提取knn_list中的最后一列即类别
count_pairs = Counter(knn)#求数组Knn中每个数字出现了几次,具体用法见下面
# max_count = sorted(count_pairs, key=lambda x: x)[-1]
max_count = sorted(count_pairs.items(), key=lambda x: x[1])[-1][0]#计算类别最多者
return max_count
#模型得分,用于测试集看模型得分
def score(self, X_test, y_test):
right_count = 0
n = 10
for X, y in zip(X_test, y_test):
label = self.predict(X)
if label == y:
right_count += 1
return right_count / len(X_test)
案例一:使用KNN算法预测一个电影是爱情片还是动作片
下面表格是我们已有的数据集合,也就是训练样本集。这个数据集有两个特征,即打斗镜头,接吻镜头。试预测打斗镜头为101,接吻镜头为20的电影属于哪个类型。
电影名称 | 打斗镜头 | 接吻镜头 | 电影类型 |
电影1 | 1 | 101 | 爱情片 |
电影2 | 5 | 89 | 爱情片 |
电影3 | 108 | 5 | 动作片 |
电影4 | 115 | 8 | 动作片 |
1、创建数据集
对于这些少量数据集的,我们可以定义创建数据集的函数:
def createDataSet():
#四组二维特征
X_train = np.array([[1,101],[5,89],[108,5],[115,8]])
#四组特征的标签
Y_train= ['爱情片','爱情片','动作片','动作片']
return X_train, Y_train
2、利用上述创建的模块预测
X_train, Y_train =createDataSet()
a=KNN(X_train, Y_train)
test = [101,20]
print('Test Point: {}'.format(a.predict(test)))
Test Point: 动作片
案例二:利用常用验证模型的鸢尾花数据集建立模型,并预测鸢尾花属于哪一类别
1、加载数据集,为各列命名
# data
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['label'] = iris.target#将特征名iris.target转换为label
df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
df#查看数据集
2、选取前两个特征作为分类依据,划分训练集与测试集
data = np.array(df.iloc[:100, [0, 1, -1]])#选取前一百个样本中的第一、第二特征、便签为训练数据集
X, y = data[:,:-1], data[:,-1]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
数据可视图,查看数据分布
plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0')
plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1')
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.legend()
3、进行预测
clf = KNN(X_train, y_train)
clf.score(X_test, y_test)
test_point = [6.0, 3.0]
print('Test Point: {}'.format(clf.predict(test_point)))
Test Point: 1.0
4、可以将待预测的点画在训练集上的散点图上
plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0')
plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1')
plt.plot(test_point[0], test_point[1], 'bo', label='test_point')
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.legend()
除此之外, Scikit learn 也简称sklearn,提供了众多的机器学习方法
- Classification 分类
- Regression 回归
- Clustering 非监督分类
- Dimensionality reduction 数据降维
- Model Selection 模型选择
- Preprocessing 数据与处理
使用sklearn可以很方便地让我们实现一个机器学习算法。一个复杂度算法的实现,使用sklearn可能只需要调用几行API即可。所以学习sklearn,可以有效减少我们特定任务的实现周期。
- Sklearn实现k-近邻算法简介
官网地址
KNneighborsClassifier参数说明:
- n_neighbors: 默认为5,就是k-NN的k的值,选取最近的k个点。
- weights: 默认是uniform,参数可以是uniform、distance,也可以是用户自己定义的函数。uniform是均等的权重,就说所有的邻近点的权重都是相等的。distance是不均等的权重,距离近的点比距离远的点的影响大。用户自定义的函数,接收距离的数组,返回一组维数相同的权重。
- algorithm:快速k近邻搜索算法,默认参数为auto,可以理解为算法自己决定合适的搜索算法。除此之外,用户也可以自己指定搜索算法ball_tree、kd_tree、brute方法进行搜索,brute是蛮力搜索,也就是线性扫描,当训练集很大时,计算非常耗时。kd_tree,构造kd树存储数据以便对其进行快速检索的树形数据结构,kd树也就是数据结构中的二叉树。以中值切分构造的树,每个结点是一个超矩形,在维数小于20时效率高。ball tree是为了克服kd树高纬失效而发明的,其构造过程是以质心C和半径r分割样本空间,每个节点是一个超球体。
- leaf_size:默认是30,这个是构造的kd树和ball树的大小。这个值的设置会影响树构建的速度和搜索速度,同样也影响着存储树所需的内存大小。需要根据问题的性质选择最优的大小。
- metric: 用于距离度量,默认度量是minkowski,也就是p=2的欧氏距离(欧几里德度量)。
p:距离度量公式。在上小结,我们使用欧氏距离公式进行距离度量。除此之外,还有其他的度量方法,例如曼哈顿距离。这个参数默认为2,也就是默认使用欧式距离公式进行距离度量。也可以设置为1,使用曼哈顿距离公式进行距离度量。 - metric_params: 距离公式的其他关键参数,这个可以不管,使用默认的None即可。
- n_jobs: 并行处理设置。默认为1,临近点搜索并行工作数。如果为-1,那么CPU的所有cores都用于并行工作。
KNeighborsClassifier提供了以一些方法供我们使用,如下图所示。
下面用sklearn中KNN模块实现上述的预测
clf_sk = KNeighborsClassifier()
clf_sk.fit(X_train, y_train)
clf_sk.score(X_test, y_test)
test_point = [6.0, 3.0]
clf_sk.predict([test_point])
array([1.])
4.2 k近邻法的实现—kd树
实现k近邻法问题是如何对训练数据进行快速k近邻搜索,这点在特征空间的维数大及训练数据容量大时尤其必要。
k近邻法最简单的实现方法是线性扫描(linear scan),这时要计算输入实例与每一个训练实例的距离,当训练集很大时,计算非常耗时,这种方法是不行的。为了提高k近邻法搜索的效率,可以考虑使用特殊的结构存储训练数据,以减少计算距离的次数。 具体方法很多,下面介绍其中的kd树方法(kd树是存储k维空间数据的树结构,这里的k与k近邻法的k意义不同)。
3.2.1 kd树的构造
kd树是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。 kd树是二叉树,表示对k维空间的一个划分。构造kd树相当于不断利用垂直于坐标轴的超平面将k维空间划分,构成一些列的k维超矩形区域。设想一个最简单的情况(当k=1时),kd树就退化为二叉搜索树,我们可以以O(logn)的时间复杂度查找数据。
构造kd树的方法如下:构造根节点,使根结点对应于k维空间中包含所有实例点的超矩形区域;通过下面的递归方法,不断地对k维空间进行划分,生成子节点。在超矩形区域(结点)上选择一个坐标轴和在此坐标轴上的一个切分点,确定一个超平面,这个超平面通过选定的切分点并垂直于选定的坐标轴,将当前超矩形区域划分为左右两个子区域(子结点);这时,实例被分到两个子区域。重复此过程直到子区域没有实例时终止。在此过程中,将实例保存在相应的结点上。
这里有两个需要注意的问题:a. 如何选定坐标轴(维度);b. 如何选定切分点。对于问题a,我们通常选择数据最为分散的维度,即方差最大的维度,有时为了简单也可以循环地选择维度(j(mod k) + 1,其中j为结点的深度)。对于问题b,我们可以选择中位数作为切分点,这样得到的kd树是平衡的,但搜索效率未必是最优的。
构造kd树算法过程
构造平衡kd树
输入:k维空间数据集T={},
其中,i=1,2,⋯,N;
输出:kd树。
(1)开始:构造根结点,根结点对应于包含T的k维空间的超矩形区域。
- 选择以为坐标轴,以T中所有的实例的坐标的中位数为切分点,将根结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴垂直的超平面实现。
- 由根结点生成深度为1的左、右子结点:左子结点对应左边小于切分点的子区域,右子结点对应于坐标大于切分点的子区域。
- 将落在切分超平面上的实例点保存在根结点。
(2)重复:
- 对深度为j的结点,选择为切分的坐标轴,,以该结点的区域中的所有实例的坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴垂直的超平面实现。
- 由该结点生成的深度为j+1的左、右子结点:左子结点对应坐标小于切分点的子区域,右子结点对应坐标大于切分点的子区域。
- 将落在切分超平面上的实例点保存在该结点。
(3)直到两个子区域没有实例存在时停止,从而形成kd树的区域划分。
为了便于理解,下面结合实例进行讲述kd树的构造过程。给定一个二维空间的数据集:T = { (2, 3), (5, 4), (9, 6), (4, 7) ,(8, 1), (7, 2) }。
- 根结点对应包含数据集T的矩形,选择轴(采用循环选择维度的方法,因为根结点的深度为0,根据公式算出此时的维度为1,即x轴),6个数据点的轴坐标的中位数为7(事实上5也可以,因为有偶数个数字,中位数本应为6,这里没有选6是由于结点必须选择一个存在的实例),以=7将空间划分为左右两个子矩形({ (2, 3), (5, 4), (4, 7) }和{ (8, 1), (9, 6) });
接着左矩形以=4分为两个子矩形({ (2, 3) }和{ (4, 7) }),右矩形以=6分为两个子矩形({ (8, 1) }和{})。如此递归,最后得到如图2-1所示的特征空间和如图2-2所示的kd树。
【代码实现】
# kd-tree每个结点中主要包含的数据结构如下
class KdNode(object):
def __init__(self, dom_elt, split, left, right):
self.dom_elt = dom_elt # k维向量节点(k维空间中的一个样本点)
self.split = split # 整数(进行分割维度的序号)
self.left = left # 该结点分割超平面左子空间构成的kd-tree
self.right = right # 该结点分割超平面右子空间构成的kd-tree
class KdTree(object):
def __init__(self, data):
k = len(data[0]) # 数据维度
def CreateNode(split, data_set): # 按第split维划分数据集exset创建KdNode
if not data_set: # 数据集为空
return None
# key参数的值为一个函数,此函数只有一个参数且返回一个值用来进行比较
# operator模块提供的itemgetter函数用于获取对象的哪些维的数据,参数为需要获取的数据在对象中的序号
#data_set.sort(key=itemgetter(split)) # 按要进行分割的那一维数据排序
data_set.sort(key=lambda x: x[split])
split_pos = len(data_set) // 2 # //为Python中的整数除法
median = data_set[split_pos] # 中位数分割点
split_next = (split + 1) % k # cycle coordinates 下一分割维度序号$l=j(modk)+1$
# 递归的创建kd树
return KdNode(
median,
split,
CreateNode(split_next, data_set[:split_pos]), # 创建左子树
CreateNode(split_next, data_set[split_pos + 1:])) # 创建右子树
self.root = CreateNode(0, data) # 从第0维分量开始构建kd树,返回根节点
# KDTree的前序遍历
def preorder(root):
print(root.dom_elt)
if root.left: # 节点不为空
preorder(root.left)
if root.right:
preorder(root.right)
将此算法用于上述案列
data = [[2,3],[5,4],[9,6],[4,7],[8,1],[7,2]]
kd = KdTree(data)
preorder(kd.root)
[7, 2]
[5, 4]
[2, 3]
[4, 7]
[9, 6]
[8, 1]
4.2.2 搜索kd树
利用kd树可以省去对大部分数据点的搜索,从而减少搜索的即使是算量。这里以最近邻(k=1)为例加以叙述,同样的方法可以应用到k近邻。
算法(用kd树的最近邻搜索)
输入:已构造的kd树,目标点x;
输出:x的最近邻
(1)在kd树中找到包含目标点x的叶结点:从根结点出发,递归地向下访问kd树。若目标点x当前维的坐标小于切分点的坐标,则移动到左子结点,否则移动到右子结点。直到子结点为叶节点为止。
(2)以此叶节点为“当前最近点”
(3)递归地向上回退,在每个结点进行以下操作:
(a)如果该结点保存的实例点比当前最近点距离目标点更近,则以该实例点为“当前最近点”
(b)当前最近点一定存在于该结点一个子结点对应的区域。检查该子结点对应的区域是否与以目标点为球心,以目标点与“当前最近点”间的距离为半径的超球体相交。
- 如果相交,可能在另一个子结点对应的区域内存在距目标更近的点,移动到另一个子结点,接着,递归地进行最近邻搜索:
- 如果不相交,向上回退
(4)当回退到根结点时,搜索结束。最后的“当前最近点”即为x的最近邻点。
【代码实现】
# 对构建好的kd树进行搜索,寻找与目标点最近的样本点:
from math import sqrt
from collections import namedtuple
# 定义一个namedtuple,分别存放最近坐标点、最近距离和访问过的节点数,具体用法见下面
result = namedtuple("Result_tuple",
"nearest_point nearest_dist nodes_visited")
def find_nearest(tree, point):
k = len(point) # 数据维度
def travel(kd_node, target, max_dist):
if kd_node is None:
return result([0] * k, float("inf"),
0) # python中用float("inf")和float("-inf")表示正负无穷
nodes_visited = 1
s = kd_node.split # 进行分割的维度
pivot = kd_node.dom_elt # 进行分割的“轴”
if target[s] <= pivot[s]: # 如果目标点第s维小于分割轴的对应值(目标离左子树更近)
nearer_node = kd_node.left # 下一个访问节点为左子树根节点
further_node = kd_node.right # 同时记录下右子树
else: # 目标离右子树更近
nearer_node = kd_node.right # 下一个访问节点为右子树根节点
further_node = kd_node.left
temp1 = travel(nearer_node, target, max_dist) # 进行遍历找到包含目标点的区域
nearest = temp1.nearest_point # 以此叶结点作为“当前最近点”
dist = temp1.nearest_dist # 更新最近距离
nodes_visited += temp1.nodes_visited
if dist < max_dist:
max_dist = dist # 最近点将在以目标点为球心,max_dist为半径的超球体内
temp_dist = abs(pivot[s] - target[s]) # 第s维上目标点与分割超平面的距离
if max_dist < temp_dist: # 判断超球体是否与超平面相交
return result(nearest, dist, nodes_visited) # 不相交则可以直接返回,不用继续判断
#----------------------------------------------------------------------
# 计算目标点与分割点的欧氏距离
temp_dist = sqrt(sum((p1 - p2)**2 for p1, p2 in zip(pivot, target)))
if temp_dist < dist: # 如果“更近”
nearest = pivot # 更新最近点
dist = temp_dist # 更新最近距离
max_dist = dist # 更新超球体半径
# 检查另一个子结点对应的区域是否有更近的点
temp2 = travel(further_node, target, max_dist)
nodes_visited += temp2.nodes_visited
if temp2.nearest_dist < dist: # 如果另一个子结点内存在更近距离
nearest = temp2.nearest_point # 更新最近点
dist = temp2.nearest_dist # 更新最近距离
return result(nearest, dist, nodes_visited)
return travel(tree.root, point, float("inf")) # 从根节点开始递归
案例:利用上面构造的kd树求点x=(3,4.5)的最近邻点。
ret = find_nearest(kd, [3,4.5])
print (ret)
Result_tuple(nearest_point=[2, 3], nearest_dist=1.8027756377319946, nodes_visited=4)
如果实例点是随机分布的,kd树搜索的平均计算复杂度是,这里N是训练实例数。kd树更适合用于训练实例数远大于空间维数时的k近邻搜索,当空间维数接近训练实例数时,它的效率会迅速下降,几乎接近线性扫描。
利用kd树k近邻算法总代码:
'''
构造Kd树
'''
# kd-tree每个结点中主要包含的数据结构如下
class KdNode(object):
def __init__(self, dom_elt, split, left, right):
self.dom_elt = dom_elt # k维向量节点(k维空间中的一个样本点)
self.split = split # 整数(进行分割维度的序号)
self.left = left # 该结点分割超平面左子空间构成的kd-tree
self.right = right # 该结点分割超平面右子空间构成的kd-tree
class KdTree(object):
def __init__(self, data):
k = len(data[0]) # 数据维度
def CreateNode(split, data_set): # 按第split维划分数据集exset创建KdNode
if not data_set: # 数据集为空
return None
# key参数的值为一个函数,此函数只有一个参数且返回一个值用来进行比较
# operator模块提供的itemgetter函数用于获取对象的哪些维的数据,参数为需要获取的数据在对象中的序号
#data_set.sort(key=itemgetter(split)) # 按要进行分割的那一维数据排序
data_set.sort(key=lambda x: x[split])
split_pos = len(data_set) // 2 # //为Python中的整数除法
median = data_set[split_pos] # 中位数分割点
split_next = (split + 1) % k # cycle coordinates 下一分割维度序号$l=j(modk)+1$
# 递归的创建kd树
return KdNode(
median,
split,
CreateNode(split_next, data_set[:split_pos]), # 创建左子树
CreateNode(split_next, data_set[split_pos + 1:])) # 创建右子树
self.root = CreateNode(0, data) # 从第0维分量开始构建kd树,返回根节点
# KDTree的前序遍历,非必要部分,主要是为了查看kd树
def preorder(root):
print(root.dom_elt)
if root.left: # 节点不为空
preorder(root.left)
if root.right:
preorder(root.right)
'''
搜索Kd树,查找最近邻邻点(k=1)
'''
# 对构建好的kd树进行搜索,寻找与目标点最近的样本点:
from math import sqrt
from collections import namedtuple
# 定义一个namedtuple,分别存放最近坐标点、最近距离和访问过的节点数,具体用法见下面
result = namedtuple("Result_tuple",
"nearest_point nearest_dist nodes_visited")
def find_nearest(tree, point):
k = len(point) # 数据维度
def travel(kd_node, target, max_dist):
if kd_node is None:
return result([0] * k, float("inf"),
0) # python中用float("inf")和float("-inf")表示正负无穷
nodes_visited = 1
s = kd_node.split # 进行分割的维度
pivot = kd_node.dom_elt # 进行分割的“轴”
if target[s] <= pivot[s]: # 如果目标点第s维小于分割轴的对应值(目标离左子树更近)
nearer_node = kd_node.left # 下一个访问节点为左子树根节点
further_node = kd_node.right # 同时记录下右子树
else: # 目标离右子树更近
nearer_node = kd_node.right # 下一个访问节点为右子树根节点
further_node = kd_node.left
temp1 = travel(nearer_node, target, max_dist) # 进行遍历找到包含目标点的区域
nearest = temp1.nearest_point # 以此叶结点作为“当前最近点”
dist = temp1.nearest_dist # 更新最近距离
nodes_visited += temp1.nodes_visited
if dist < max_dist:
max_dist = dist # 最近点将在以目标点为球心,max_dist为半径的超球体内
temp_dist = abs(pivot[s] - target[s]) # 第s维上目标点与分割超平面的距离
if max_dist < temp_dist: # 判断超球体是否与超平面相交
return result(nearest, dist, nodes_visited) # 不相交则可以直接返回,不用继续判断
#----------------------------------------------------------------------
# 计算目标点与分割点的欧氏距离
temp_dist = sqrt(sum((p1 - p2)**2 for p1, p2 in zip(pivot, target)))
if temp_dist < dist: # 如果“更近”
nearest = pivot # 更新最近点
dist = temp_dist # 更新最近距离
max_dist = dist # 更新超球体半径
# 检查另一个子结点对应的区域是否有更近的点
temp2 = travel(further_node, target, max_dist)
nodes_visited += temp2.nodes_visited
if temp2.nearest_dist < dist: # 如果另一个子结点内存在更近距离
nearest = temp2.nearest_point # 更新最近点
dist = temp2.nearest_dist # 更新最近距离
return result(nearest, dist, nodes_visited)
return travel(tree.root, point, float("inf")) # 从根节点开始递归
kd树更适合用于训练实例数远大于空间维数时的k近邻搜索,下面看一下大量训练数据下,查找最近邻点所耗费时间
from time import clock
from random import random
# 产生一个k维随机向量,每维分量值在0~1之间
def random_point(k):
return [random() for _ in range(k)]
# 产生n个k维随机向量
def random_points(k, n):
return [random_point(k) for _ in range(n)]
N = 400000
t0 = clock()
kd2 = KdTree(random_points(3, N)) # 构建包含四十万个3维空间样本点的kd树
ret2 = find_nearest(kd2, [0.1,0.5,0.8]) # 四十万个样本点中寻找离目标最近的点
t1 = clock()
print ("time: ",t1-t0, "s")
print (ret2)
time: 7.038252100001046 s
Result_tuple(nearest_point=[0.10284308148070764, 0.5056170533243651, 0.7922743068211113], nearest_dist=0.009965978900691678, nodes_visited=59)
时间还是挺快的,说明kd树搜索适合大量训练数据集。
5、总结
优点
- 简单好用,容易理解,精度高,理论成熟,既可以用来做分类也可以用来做回归;
- 可用于数值型数据和离散型数据;
- 训练时间复杂度为O(n);无数据输入假定;
- 对异常值不敏感。
缺点:
- 计算复杂性高;空间复杂性高;
- 样本不平衡问题(即有些类别的样本数量很多,而其它样本的数量很少);
- 一般数值很大的时候不用这个,计算量太大。但是单个样本又不能太少,否则容易发生误分。
- 最大的缺点是无法给出数据的内在含义。
- 两点间距离公式不能提供足够的信息
- 维度灾难
另:
- Counter 函数用法
from collections import Counter
a = [1,4,2,3,2,3,4,2]
b = Counter(a) #求数组中每个数字出现了几次
print(b)
print( b[2]) #计算每个元素出现了几次
Counter({2: 3, 4: 2, 3: 2, 1: 1})
3
- collections.namedtuple()函数实际上是一个返回 Python 中标准元组类型子类的一个工厂方法。需要传递一个类型名和你需要的字段给它,然后它就会返回一个类,你可以初始化这个类。
>>> from collections import namedtuple
>>> Student = namedtuple('Student', ('name', 'age', 'sex'))
>>> tom = Student('Tom', '12', 'meal') # 按位置指定其中各项
>>> lisa = Student(name='Lisa', age=12, sex='female') # 采用关键字来指定
>>> tom.name # 通过属性名称访问
'Tom'
>>> tom.sex
'meal'
>>> lisa.name
'Lisa'
>>> lisa.age
12
>>> lisa[0]
'Lisa'
>>> lisa[1]
12
>>> [ i for i in lisa ]
['Lisa', 12, 'female']
>>> len(lisa)
3
参考资料:
1、李航《统计学习方法》
2、周志华《机器学习》
3、Peter Harrington《Machine Learing in Action》
4、https://github.com/fengdu78/lihang-code/blob/master/%E7%AC%AC03%E7%AB%A0%20k%E8%BF%91%E9%82%BB%E6%B3%95/3.KNearestNeighbors.ipynb