前言
一、什么是k近邻算法?
k近邻法是一种基本分类与回归方法。给定一个训练数据集,其中的实例类别已定,对新的输入实例,在训练数据集中找到与该实例最近邻的k个实例,这k个实例的多数属于某个类,就把该输入实例分为这个类。
KNN使用的模型实际上对应于特征空间的划分,没有显式的训练过程。
二、KNN三要素
- 距离度量
特征空间中两个实例点的距离是两个实例点相似程度的反映。设输入实例 x ∈ R n x\in \mathbb{R}^n x∈Rn, x i x_i xi和 x j x_j xj的 L p L_p Lp距离定义为:
L p ( x i , x j ) = ( ∑ l = 1 n ∣ x i l − x j l ∣ p ) ( 1 p ) L_p(x_i,x_j)=(\sum_{l=1}^n|x_i^{l}-x_j^{l}|^p)^{(\frac{1}{p})} Lp(xi,xj)=(l=1∑n∣xil−xjl∣p)(p1)
当p为1时成为曼哈顿距离,当p为2是为欧氏距离,当p为正无穷大时为各个坐标轴距离的最大值。不同的p值可能导致最近邻点的选取不同。 - K 值
较小的的k值,就相当于用较小的领域中的训练实例进行预测,近似误差会减小,只有与输入实例较近的训练实例才会对预测结果起作用。但是误差估计会增大,预测结果会对近邻的实例点非常敏感。k值的减小意味着模型变得复杂,容易发生过拟合。
较大的k值回减小学习的估计误差,但是学习的近似误差会变大。这时与输入实例较远的训练实例也会对最终判断起影响。k值的增大意味着模型变简单。
在应用中,k值一般取一个比较小的数,通常采取交叉验证法来选取最优的k值。 - 分类决策规则
一般采用多数表决规则。
三、kd树
kd树是一种对k维空间中的实例点进行存储以便对其进行快速检索的二叉树结构,表示对l维空间的一个划分。构造kd树相当于不断地用垂直于坐标轴的超平面将l维空间切分,构成一系列的l维超矩形区域。kd树的每个结点对应于一个k维超矩形区域。
构造平衡kd树的算法
输入:k维空间数据集
T
=
{
x
1
,
x
2
,
.
.
,
x
N
}
T=\{x_1,x_2,..,x_N\}
T={x1,x2,..,xN},其中
x
i
=
(
x
i
(
0
)
,
x
i
(
1
)
,
.
.
.
,
x
i
(
l
−
1
)
)
x_i=(x_i^{(0)},x_i^{(1)},...,x_i^{(l-1)})
xi=(xi(0),xi(1),...,xi(l−1))为
l
l
l维向量。
输出:kd树。
1.开始:构造根结点,根结点对应于包含T的l维空间的超矩形区域。
选择
x
(
0
)
x^{(0)}
x(0)为坐标轴,以T中所有实例在该坐标轴上的中位数为切分点,将根结点对应的超矩形区域切分为两个子区域。切分由通过切分点并于坐标轴
x
(
0
)
x^{(0)}
x(0)垂直的超平面实现。由根结点生成深度为1的左、右子结点,分别对应坐标轴
x
(
0
)
x^{(0)}
x(0)小于和大于切分点的子区域,将落在切分超平面上的实例点保存在根结点
2.对深度为j的结点,选择 x j m o d l x^{j\bmod l} xjmodl为切分坐标轴(也可选择方差最大的坐标轴),重复1所述的切分过程。
3.直到两个子区域没有实例存在时停止,从而形成kd树的区域划分。
kd树的最近邻搜索
输入:kd树,目标点x;
输出:x的最近邻;
- 在kd树中从根结点出发,递归向下查找包含目标点x的叶结点;
- 以此叶结点为当前最近点;
- 递归向上回退,在每个结点进行以下操作:
- 如果该结点保存到实例点比当前最近点距离目标点更近,则以该实例点位当前最近点。
- 检查该结点的另一子结点是否与以目标点为球心,以目标点与当前最近点间的距离为半径的超球体相交。如果相交,则移动到另一子结点进行递归搜索,,否则向上回退。
- 当回退到根结点时,搜索结束,当前最近点即为x的最近邻点。
四、代码实现
代码如下(示例):
"""
k近邻法的实现:kd树
"""
import numpy as np
from sklearn.datasets import load_digits
import heapq
from tqdm import tqdm
class Node(object):
'''
树节点
'''
def __init__(self, data, left, right, dim):
self.data = data
self.left = left
self.right = right
self.dim = dim # 该节点划分数据的维度
class KDTree(object):
'''
KDTree的构造和搜索
'''
def __init__(self, datas):
self.root = self.build_tree(datas, 0, datas.shape[1]-1)
def build_tree(self, datas, cur_dim, max_dim):
'''
构建kd树
:param datas: 特征数据
:param cur_dim: 当前节点的划分维度
:param max_dim: 最大划分维度
:return:
'''
if len(datas) == 0:
return
datas = datas[np.argsort(datas[:, cur_dim])] # 按照当前维度的特征对实例进行排序
mid = datas.shape[0] // 2
return Node(datas[mid], self.build_tree(datas[:mid], (cur_dim+1) % max_dim, max_dim),
self.build_tree(datas[mid+1:], (cur_dim+1) % max_dim, max_dim), cur_dim)
def search(self, x, k, lp_dis):
'''
在kd树中查找与实例x距离最近的k个实例点
'''
top_k = [(-np.inf, None)] * k
def visit(node):
if node is None:
return
dis_with_axis = x[node.dim] - node.data[node.dim] # 待查点与切分轴的距离
visit(node.left if dis_with_axis < 0 else node.right)
dis_with_node = lp_dis(x.reshape((-1,)), node.data.reshape((-1,))[:-1])
heapq.heappushpop(top_k, (-dis_with_node, node.data[-1]))
# 如果当前已知的最近的k个点中最远的点距离查询点的距离(-top_k[0][0])大于查询点到当前节点分割超平面的距离(abs(
# dis_with_axis)),那么我们可以断定,在当前节点的另一个子树中可能存在距离查询点更近的点
if -top_k[0][0] > abs(dis_with_axis):
visit(node.right if dis_with_axis < 0 else node.right)
visit(self.root)
top_k = [int(x[1]) for x in heapq.nlargest(k, top_k)]
return top_k
class KNN(object):
'''
Knn算法的具体实现
'''
def __init__(self, k, train_x, train_y, p=2):
self.train_x = train_x
self.train_y = train_y
self.k = k
self.p = p
self.kdtree = None
def lp_distance(self, x1, x2):
'''
L_p距离
'''
distance = np.sum(np.abs(x1 - x2)**self.p, -1)**(1/self.p)
return distance
def vote(self, top_k):
'''
多数表决
'''
count = {}
max_freq, predict_y = 0, 0
for key in top_k:
count[key] = count.get(key, 0) + 1
if count[key] > max_freq:
max_freq = count[key]
predict_y = key
return predict_y
def kdtree_search(self, test_x):
'''
kdtree搜索
'''
if self.kdtree is None:
self.kdtree = KDTree(np.concatenate([self.train_x, self.train_y.reshape((-1, 1))], -1))
predict_ys = []
for x in tqdm(test_x):
top_k = self.kdtree.search(x, self.k, self.lp_distance)
predict_y = self.vote(top_k)
predict_ys.append(predict_y)
predict_ys = np.array(predict_ys)
return predict_ys
if __name__ == '__main__':
digits = load_digits()
features = digits.data
target = digits.target
shuffle_indices = np.random.permutation(features.shape[0])
features = features[shuffle_indices]
target = target[shuffle_indices]
train_count = int(len(features)*0.8)
train_x, test_x = features[:train_count], features[train_count:]
train_y, test_y = target[:train_count], target[train_count:]
k = 5
p = 2
knn = KNN(k, train_x, train_y, p)
predict_y = knn.kdtree_search(test_x)
accuracy = (predict_y == test_y).sum() / test_y.shape[0]
print('Accuracy:%.4f' % accuracy)