k 近邻法
- k 近邻算法
- k 近邻模型
- k 近邻法的实现:kd 树
- 搜索 kd 树
k 近邻模型实现
- k 近邻模型实现
- kd 树搜索单个近邻点
- kd 树搜索 k 个近邻点
k 近邻法(k-nearest neighbor,k-NN)是一种基本分类与回归方法。k 近邻法假设给定一个训练数据集,其中的实例类别已定。
分类时,对新的实例,根据其 k 个最近邻的训练实例的类别,通过多数表决等方式进行预测。因此,k近邻法不具有显式的学习过程。
k 近邻法实际上利用训练数据集对特征向量空间进行划分,并作为其分类的“模型”。k 值的选择、距离度量及分类决策规则
是 k 近邻法的三个基本要素。
k 近邻算法
- 给定数据集 ,其中, 为实例的特征向量, 为实例的类别, ;实例的特征向量
1>> 根据给定的距离度量,在训练集 中找出与 最邻近的 个点,涵盖这 个点的 的邻域记作 ;
2>> 在 中根据分类决策规则(如多数表决)决定 的类别 :
式中 为指示函数。
- k 近邻法的特殊情况是 的情形,称为最近邻算法。对于输入的实例点(特征向量),最近邻法将训练数据集中与 最邻近点的类作为 的类。
k 近邻模型
- 模型由三个基本要素——距离度量、k 值的选择和分类决策规则决定。
- 特征空间中,对每个训练实例点 ,距离该点比其他点更近的所有点组成一个区域,叫作单元(cell)。
- 特征空间中两个实例点的距离是两个实例点相似程度的反映。(欧氏距离、距离、Minkowski 距离)
- 设特征空间 是 维实数向量空间 ,,,, 的 距离定义为
这里 。当 时, 称为欧氏距离
,即
当 时, 称为哈曼顿距离
,即
当 ,它是各个坐标距离的最大值,即
- k 值的选择会对 k 近邻法的结果产生重大影响。如果选择较小的 k 值,就相当于用较小的邻域中的训练实例进行预测,“学习”的近似误差(approximation error)会减小,只有与输入实例较近的(相似的)训练实例才会对预测结果起作用。但缺点是“学习”的估计误差(estimation error)会增大,预测结果会对近邻的实例点非常敏感。如果邻近的实例点恰巧是噪声,预测就会出错。
换句话说,k 值的减小就意味着整体模型变得复杂,容易发生过拟合。
如果选择较大的 k 值,就相当于用较大邻域中的训练实例进行预测。其优点是可以减少学习的估计误差。但缺点是学习的近似误差会增大。这时与输入实例较远的(不相似的)训练实例也会对预测起作用,使预测发生错误。k 值的增大就意味着整体的模型变得简单。
如果 k=N,那么无论输入实例是什么,都将简单地预测它属于在训练实例中最多的类。这时,模型过于简单,完全忽略训练实例中的大量有用信息,是不可取的。
在应用中,k 值一般取一个比较小的数值。通常采用交叉验证法来选取最优的 k 值。
- k 近邻法中的分类决策规则往往是
多数表决
,即由输入实例的 k 个邻近的训练实例中的多数类决定输入实例的类。
k 近邻法的实现:kd 树
- 实现 k 近邻法时,主要考虑的问题是如何对训练数据进行快速 k 近邻搜索。k 近邻法最简单的实现方法是线性扫描(linear scan)。这时要计算输入实例与每一个训练实例的距离。当训练集很大时,计算非常耗时,这种方法是不可行的。
- kd 树是一种对 k 维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。kd 树是二叉树,表示对 k 维空间的一个划分(partition)。构造 kd 树相当于不断地用垂直于坐标轴的超平面将 k 维空间切分,构成一系列的 k 维超矩形区域。kd 树的每个结点对应于一个 k 维超矩形区域。
- 构造 kd 树的方法如下:构造根结点,使根结点对应于 k 维空间中包含所有实例点的超矩形区域;通过下面的递归方法,不断地对 k 维空间进行切分,生成子结点。在超矩形区域(结点)上选择一个坐标轴和在此坐标轴上的一个切分点,确定一个超平面,这个超平面通过选定的切分点并垂直于选定的坐标轴,将当前超矩形区域切分为左右两个子区域(子结点);这时,实例被分到两个子区域。这个过程直到子区域内没有实例时终止(终止时的结点为叶结点)。在此过程中,将实例保存在相应的结点上。
- 通常,依次选择坐标轴对空间切分,选择训练实例点在选定坐标轴上的中位数(median)为切分点,这样得到的 kd 树是平衡的。注意,
平衡的 kd 树搜索时的效率未必是最优的。
- 构造平衡 kd 树:
给定 k 维空间数据集 ,其中 ,
1>> 开始:
构造根结点,根结点对应于包含 的 k 维空间的超矩形区域。选择 为坐标轴,以 中所有实例的 坐标的中位数为切分点,将根结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 垂直的超平面实现。
由根结点生成深度为 的左、右子结点:左子结点对应坐标 小于切分点的子区域,右子结点对应于坐标 大于切分点的子区域。
将落在切分超平面上的实例点保存在根结点。
2>> 重复:
对深度为 的结点,选择 为切分的坐标轴,,以该结点的区域中所有实例的 坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 垂直的超平面实现。
由该结点生成深度为 的左、右子结点:左子结点对应坐标 小于切分点的子区域,右子结点对应坐标 大于切分点的子区域。
将落在切分超平面上的实例点保存在该结点。
3>> 直到两个子区域没有实例存在时停止。
从而形成kd树的区域划分。
-
kd 树示例:
给定一个二维空间的数据集:。
根结点对应包含数据集 的矩形,选择 轴,6 个数据点的 坐标的中位数是 7,以平面 将空间分为左、右两个子矩形(子结点);接着,左矩形以 分为两个子矩形,右矩形以 分为两个子矩形,如此递归,得到如下特征空间划分图及 kd 树图。
搜索 kd 树
- 在 kd 树中找出包含目标点 的叶结点:从根结点出发,递归地向下访问 kd 树。若目标点 当前维的坐标小于切分点的坐标,则移动到左子结点,否则移动到右子结点。
直到子结点为叶结点为止。
- 以此叶结点为“当前最近点”。
- 递归地向上回退,在每个结点进行以下操作:
1>>
如果该结点保存的实例点比当前最近点距离目标点更近,则以该实例点为“当前最近点”。
2>>
当前最近点一定存在于该结点一个子结点对应的区域。检查该子结点的父结点的另一子结点对应的区域是否有更近的点。具体地,检查另一子结点对应的区域是否与以目标点为球心、以目标点与“当前最近点”间的距离为半径的超球体相交。如果相交,可能在另一个子结点对应的区域内存在距目标点更近的点,移动到另一个子结点。接着,递归地进行最近邻搜索;如果不相交,向上回退。
- 当回退到根结点时,搜索结束。最后的“当前最近点”即为 的最近邻点。
- kd 树更适用于训练实例数远大于空间维数时的 k 近邻搜索。当空间维数接近训练实例数时,它的效率会迅速下降,几乎接近线性扫描。
-
kd 树搜索示例:
给定一个如下图所示的 kd 树,根结点为 A,其子结点为 B,C 等。树上共存储 7 个实例点;另有一个输入目标实例点 S,求 S 的最近邻。
解:首先在 kd 树中找到包含点 S 的叶结点 D(图中的右下区域),以点 D 作为近似最近邻。真正最近邻一定在以点 S 为中心通过点 D 的圆的内部。然后返回结点 D 的父结点 B,在结点 B 的另一子结点 F 的区域内搜索最近邻。结点 F 的区域与圆不相交,不可能有最近邻点。继续返回上一级父结点 A,在结点 A 的另一子结点 C 的区域内搜索最近邻。结点 C 的区域与圆相交;该区域在圆内的实例点有点 E,点 E 比点 D 更近,成为新的最近邻近似。最后得到点 E 是点 S 的最近邻。
k 近邻模型实现
k 近邻模型实现
import math
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from collections import Counter
class Knn(object):
def __init__(self, x_train, y_train, k=3, p=2):
self.k = k
self.p = p
self.x_train = x_train
self.y_train = y_train
# 计算 Lp 距离
def lp(self, x, y, p=2):
if len(x) == len(y) and len(x) > 1:
_sum = sum([math.pow(abs(x[i] - y[i]), p) for i in range(len(x))])
return math.pow(_sum, 1/p)
else:
return 0
# 预测
def predict(self, x):
# 计算 x 到其他节点的距离
dists = [(np.linalg.norm(x - self.x_train[i], ord=self.p), self.y_train[i])
for i in range(len(x_train))]
# 排序筛选出最近的 k 个节点
nodes = sorted(dists, key=lambda x: x[0])[:self.k]
# 对最近的 k 个节点的所属类别进行计数
counter = Counter([node[-1] for node in nodes])
# 返回计数最多的一个类别
return counter.most_common(1)[0][0]
# 计算模型的准确率
def score(self, x_test, y_test):
rights = 0
for x, y in zip(x_test, y_test):
label = self.predict(x)
if label == y:
rights += 1
return rights / len(x_test)
if __name__ == '__main__':
# 获取鸢尾花数据集
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['label'] = iris.target
df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
# 生成训练样本,只取 sepal length,sepal width 作为样本特征
train = np.array(df.iloc[:100, [0, 1, -1]])
x, y = train[:, :-1], train[:, -1]
# 划分训练集与测试集
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2)
# 生成 knn 模型,并使用测试集进行验证
knn = Knn(x_train, y_train)
print('测试集验证模型准确率为:%s' % knn.score(x_test, y_test))
# 对点 [6.0, 3.0] 进行分类
print('点 [6.0, 3.0] 分类结果为: %s' % knn.predict([6.0, 3.0]))
# 绘图
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(6.0, 3.0, 'bo', label='[6.0, 3.0]')
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.legend()
运行结果
也可以使用 sklearn 实现 knn 模型
from sklearn.neighbors import KNeighborsClassifier
# KNeighborsClassifier 参数
# n_neighbors:临近点个数
# p:距离度量
# algorithm:近邻算法,可选{'auto', 'ball_tree', 'kd_tree', 'brute'}
# weights:近邻的权重
knn_sk = KNeighborsClassifier()
knn_sk.fit(x_train, y_train)
knn_sk.score(x_test, y_test)
kd 树搜索单个近邻点
from math import sqrt
from collections import namedtuple
class KdNode(object):
# 节点数据结构
def __init__(self, node, split, left, right):
self.node = node # k 维向量节点
self.split = split # 分割维度序号
self.left = left # 左树
self.right = right # 右树
class KdTree(object):
def __init__(self, data):
self.k = len(data[0]) # 数据维度
self.root = self.create_kdnode(0, data) # 生成 kd 树
def create_kdnode(self, split, data):
if not data: return None
data.sort(key=lambda x: x[split])
split_index = len(data) // 2
median = data[split_index] # 中位数分割点
split_next = (split + 1) % self.k
return KdNode(
median,
split,
self.create_kdnode(split_next, data[:split_index]),
self.create_kdnode(split_next, data[split_index + 1:])
)
# 对于构建好的 kd 树 tree,寻找离 node 最近的节点
def nearest(self, node):
k = len(node)
nearest_tuple = namedtuple("nearest_tuple", "nearest_node nearest_dist visited")
def travel(kd_node, target, nearest_dist):
# python中用float("inf")和float("-inf")表示正负无穷
if kd_node is None: return nearest_tuple([0] * k, float("inf"), 0)
visited = 1
# 分割维度
split = kd_node.split
# 分割节点
node = kd_node.node
# 如果目标点第 split 维小于分割节点的对应值,则目标离左子树更近,否则离右子树更近
if target[split] <= node[split]:
nearer_node = kd_node.left
further_node = kd_node.right
else:
nearer_node = kd_node.right
further_node = kd_node.left
# 进行遍历找到包含目标点的区域
nearest_tuple_1 = travel(nearer_node, target, nearest_dist)
nearest = nearest_tuple_1.nearest_node
dist = nearest_tuple_1.nearest_dist
visited += nearest_tuple_1.visited
# 更新最近距离
if dist < nearest_dist:
nearest_dist = dist
# 第 split 维上目标点与分割超平面的距离
temp_dist = abs(node[split] - target[split])
# 判断超球体是否与超平面相交
if temp_dist > nearest_dist:
# 相交则可以直接返回,不用继续判断
return nearest_tuple(nearest, dist, visited)
# 计算目标点与分割点的欧氏距离
temp_dist = sqrt(sum((p1 - p2) ** 2 for p1, p2 in zip(node, target)))
# 如果更近,更新最近距离
if temp_dist < nearest_dist:
nearest = node
nearest_dist = temp_dist
# 检查另一个子结点对应的区域是否有更近的点
nearest_tuple_2 = travel(further_node, target, nearest_dist)
visited += nearest_tuple_2.visited
if nearest_tuple_2.nearest_dist < nearest_dist:
nearest = nearest_tuple_2.nearest_node
nearest_dist = nearest_tuple_2.nearest_dist
return nearest_tuple(nearest, nearest_dist, visited)
# 从根节点开始递归
return travel(self.root, node, float("inf"))
if __name__ == '__main__':
data = [[2,3],[5,4],[9,6],[4,7],[8,1],[7,2]]
kd = KdTree(data)
print(kd.nearest([3, 5]))
运行结果
nearest_tuple(nearest_node=[4, 7], nearest_dist=2.23606797749979, visited=4)
kd 树搜索 k 个近邻点
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from math import sqrt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from collections import Counter
class KdNode(object):
def __init__(self, data, depth=0, lchild=None, rchild=None):
self.data = data
self.depth = depth
self.lchild = lchild
self.rchild = rchild
class KdTree(object):
def __init__(self):
self.n = 0
self.tree = None
self.nearest = None
def create(self, data_set, depth=0):
if len(data_set) > 0:
m, n = np.shape(data_set)
self.n, axis, mid = n - 1, depth % (n - 1), int(m / 2)
data_set_sorted = sorted(data_set, key=lambda x: x[axis])
node = KdNode(data_set_sorted[mid], depth)
if depth == 0: self.tree = node
node.lchild = self.create(data_set_sorted[:mid], depth+1)
node.rchild = self.create(data_set_sorted[mid+1:], depth+1)
return node
return None
# 搜索 kdtree 的前 k 个最近点
def search(self, x, k=1):
nearest = []
for i in range(k):
nearest.append([-1, None])
# 初始化 n 个点,nearest 是按照距离递减的方式
self.nearest = np.array(nearest)
def travel(node):
if node is not None:
# 当前点的维度 axis
axis = node.depth % self.n
# x 点和当前点在 axis 维度上的差
daxis = x[axis] - node.data[axis]
# 如果小于进左子树,大于进右子树
if daxis < 0:
travel(node.lchild)
else:
travel(node.rchild)
# 计算 x 点到当前点的距离 dist
dist = sqrt(sum((p1 - p2) ** 2 for p1, p2 in zip(x, node.data)))
for i, d in enumerate(self.nearest):
# 如果有比现在最近的 n 个点更近的点,更新最近的点
if d[0] < 0 or dist < d[0]:
# 插入第 i 个位置的点
self.nearest = np.insert(self.nearest, i, [dist, node], axis=0)
# 删除最后一个多出来的点
self.nearest = self.nearest[:-1]
break
# 统计距离为 -1 的个数 n
n = list(self.nearest[:, 0]).count(-1)
# self.nearest[-n-1, 0] 是当前 nearest 中已经有的最近点中,距离最大的点
# self.nearest[-n-1, 0] > abs(daxis) 代表以 x 点为圆心,self.nearest[-n-1, 0]为半径的圆
# 与 axis 相交,说明在左右子树里面可能有比 self.nearest[-n-1, 0] 更近的点
if self.nearest[-n-1, 0] > abs(daxis):
if daxis < 0:
travel(node.rchild)
else:
travel(node.lchild)
travel(self.tree)
# nodes 就是最近 k 个点
nodes = self.nearest[:, 1]
counter = Counter([node.data[-1] for node in nodes])
return self.nearest, counter.most_common(1)[0][0]
if __name__ == '__main__':
# 获取鸢尾花数据集
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['label'] = iris.target
df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
# 生成训练样本,只取 sepal length,sepal width 作为样本特征
data = np.array(df.iloc[:100, [0, 1, -1]])
# 划分训练集与测试集
train, test = train_test_split(data, test_size=0.1)
x0 = np.array([x0 for i, x0 in enumerate(train) if train[i][-1] == 0])
x1 = np.array([x1 for i, x1 in enumerate(train) if train[i][-1] == 1])
# 生成 knn 模型,并使用测试集进行验证
kdt = KdTree()
kdt.create(train)
score = 0
for x in test:
# 绘制训练点
plt.scatter(x0[:, 0], x0[:, 1], c='pink', label='[0]')
plt.scatter(x1[:, 0], x1[:, 1], c='orange', label='[1]')
plt.xlabel('sepal length')
plt.ylabel('sepal width')
# 绘制测试点
plt.scatter(x[0], x[1], c='red', marker='x')
# 设置临近点的个数
nearest, belong = kdt.search(x[:-1], 5)
if belong == x[-1]: score += 1
print("test:", x, "predict:", belong)
print("nearest:", nearest)
for near in nearest:
# k 个近邻点
plt.scatter(near[1].data[0], near[1].data[1], c='green', marker='+')
plt.legend()
plt.show()
score /= len(test)
print("score:", score)
运行结果(部分截图)
网友评论