K近邻法
KNN算法流程
K近邻法思想非常简单:给定测试样本?基于某种距离度量找出训练集中与其最靠近的k 个训练样本,然后基于这k 个" 邻居"的信息来进行预测. 通常, 在分类任务中可使用"投票法" 即选择这k 个样本中出现最多的类别标记作为预
测结果;在回归任务中时使用"平均法" ,即将这k 个样本的实值输出标记的平均值作为预测结果;还可基于距离远近进行加权平均或加权投票,距离越近的样本权重越大
这里直接给出其算法流程:
KNN没有显式的训练过程,它是"懒惰学习" (lazy learning) 的著名代表,此类学习技术在训练阶段仅仅是把样本保存起来,训练时间开销为零,待收到测试样本后再进行处理;相应的,那些在训练阶段就对样本进行学习处理的方
法,称为"急切学习" (eager learning)
KD-tree
KD-tree介绍
KNN最简单的实现方法是线性扫描,这时需要计算输入实例与每一个训练实例的距离,当训练集很大或者问题是高维问题时,计算非常耗时。为了提高KNN搜索的效率,可以考虑用特殊的结构存储训练数据,以减少计算距离的次数,具体方法很多,下面介绍其中的KD-tree方法。KD-tree是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构,KD-tree是二叉树,表示对k维空间的一个划分。构造KD-tree相当于不断地用垂直于坐标轴的超平面将k维空间划分,构成一系列的k维超矩形区域,KD-tree的每一个结点对应于一个k维超矩形区域
KD-tree可以看作是二叉搜索树(BST)在高维的推广,1D-tree其实就是BST
KD-tree的构造流程
KD-tree的构造流程如下:
上图展示的流程是李航《统计学习方法》中给出的,其中选择切分的坐标轴是按顺序依次循环的方式选取的,只是一种简单的执行方式,但不一定能得到一个好的平衡KD-tree(类似平衡二叉搜索树的平衡的含义:尽可能让每个结点的左子树和右子树划分得到的数据量相当,这样就能使得树有较低的深度,从而使得搜索k个距离最近的点时能有较低的复杂度),所以实际往往选择切分点时并不是按顺序循环,而是每次选方差最大的维度作为切分维度(这样能够较好的从切分点处分开,且分开后两边的数据量相当,还能保证自下而上搜索k个距离最近的点时,越到上方越不可能再合并考查另一端结点,见下面的KD-tree搜索算法),可以更好地二分,保持最大程度的区分度,达到平衡KD-tree,好处是最后找k近邻时复杂度更低
KD-tree搜索算法
构建好了KD-tree,就可以在KD-tree上执行搜索了,这里我们的目的是找一个测试实例在原始训练数据集中的k个距离最近的点,下面给出找最邻近的算法,找k邻接的算法是类似的(可以用一个最大堆来实现,最大堆里面先一直添加数据集中的点到该测试实例的距离,直到达到k个,此时最大堆的堆顶元素就是距离最大的第k个元素,以后新来的点到测试实例的距离若小于堆顶元素,则将最大堆的堆顶元素弹出,加入新的点到测试实例的距离,并调整元素位置以维持最大堆的条件,如果新来的点到测试实例的距离大于等于堆顶元素,则不需要执行操作,同样一直遍历到根节点结束)
KD-tree构造和搜索算法的Python实现
import heapq
class KdTreeNode:
def __init__(self, data=None, split=0, median=None, left=None, right=None, parent=None):
self.data = data
self.split = split
self.median = median
self.left = left
self.right = right
self.parent = parent
class KdTree:
def __init__(self, all_data, dimension, min_split=1):
self.min_split = min_split
self.dimension = dimension
self.size = len(all_data)
self.root = self.__build_kdtree(all_data)
def __build_kdtree(self, all_data):
if len(all_data) == 0: return None
if len(all_data) <= self.min_split: return self.KdTreeNode(all_data, 0, all_data[0], None, None)
split = self.__get_max_variance_dimension(all_data)
left_data, median, right_data = self.__split(all_data, split)
node = self.KdTreeNode([], split, median,
self.__build_kdtree(left_data),
self.__build_kdtree(right_data))
if node.left is not None: node.left.parent = node
if node.right is not None: node.right.parent = node
return node
def __get_max_variance_dimension(self, all_data):
num_of_data = len(all_data)
means = [0.0] * self.dimension
variances = [0.0] * self.dimension
for data in all_data:
for i in range(self.dimension):
means[i] += data[i]
means = [mean / num_of_data for mean in means]
for data in all_data:
for i in range(self.dimension):
variances[i] += (data[i] - means[i]) ** 2
return max((variance, i) for i, variance in enumerate(variances))[1]
def __split(self, all_data, dim):
length = len(all_data)
def min_k(data, begin, end, k):
# partition
pivot = data[begin][:]
left = begin
right = end
while left < right:
while left < right and data[right][dim] >= pivot[dim]:
right -= 1
data[left] = data[right]
while left < right and data[left][dim] <= pivot[dim]:
left += 1
data[right] = data[left]
data[left] = pivot
if left - begin + 1 == k:
return
elif left - begin + 1 < k:
return min_k(data, left + 1, end, k - (left - begin + 1))
else:
return min_k(data, begin, left - 1, k)
mid = round(length / 2)
min_k(all_data, 0, length - 1, mid)
return all_data[0:mid], all_data[mid - 1], all_data[mid:]
def data_distance(self, data_1, data_2):
dis = 0.0
for i in range(self.dimension):
dis += (data_1[i] - data_2[i]) ** 2
return dis ** (1 / 2)
def search_k_nearest(self, data, k=1):
result = []
min_radius = float('inf')
if self.root is None:
return result
def helper(root):
nonlocal result
nonlocal min_radius
if self.size == 0:
return
# leaf node
if root.left is None and root.right is None:
for src_data in root.data:
dis = self.data_distance(data, src_data)
if dis <= min_radius:
if len(result) == k:
heapq.heappop(result)
heapq.heappush(result, (-dis, src_data))
if len(result) == k:
min_radius = -result[0][0] # update threshold
else:
if data[root.split] <= root.median[root.split]:
helper(root.left)
if data[root.split] - root.median[root.split] >= -min_radius:
helper(root.right) # cycle cross rectangle
else:
helper(root.right)
if data[root.split] - root.median[root.split] <= min_radius:
helper(root.left)
helper(self.root)
result.sort(key=(lambda x: -x[0]))
result = [(data, -dis) for dis, data in result]
return result