赞
踩
KNN算法可以说是机器学习中最简单的是一种分类、回归方法,它甚至连一个显性的模型结构表达都没有。
假如给定了一个训练数据集 T T T,里面包含了 N N N个样本,每一个实例的标签都给定。当给定一个新的实例 x x x时,我们就可以在训练数据集 T T T里面寻找与 x x x最近的 k k k个实例,然后根据这 k k k个实例的标签来决定新实例的 x x x值。
对于分类问题
,采用多数表决。哪一种类别所占比例最大,那么新的实例点它就属于哪个类别。
对于回归问题
,可以取这些邻居的标签,计算平均值。
如下图,k=3时,绿色圆点属于红色三角形类别,k=5时,绿色圆点属于蓝色正方形类别
如果换一种距离定义方式,可能这里绿圆点所属的类别也会发生变化。所以,多少个邻居,怎么计算距离,如何通过邻居的情况反映目标点的信息
,都是非常关键的问题。
KNN算法如下:
距离度量、 k k k个邻居、和分类决策规则,这三个就是k近邻法的三要素。任意一者的变化,都可能导致实例 x x x所属的类别发生变化。
特征空间中任意的 x i 和 x j 都是一个 n 维向量,下式中 x ( l ) 代表的是第 l 个特征, p 决定了不同的范数距离。 L p ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ p ) 1 p , p > = 1 特征空间中任意的x_i和x_j都是一个n维向量,下式中x^{(l)}代表的是第l个特征,p决定了不同的范数距离。 \\ L_p(x_i,x_j)=(\sum\limits_{l=1}^n|x_i^{(l)}-x_j^{(l)}|^p)^{\frac{1}{p}},p>=1 特征空间中任意的xi和xj都是一个n维向量,下式中x(l)代表的是第l个特征,p决定了不同的范数距离。Lp(xi,xj)=(l=1∑n∣xi(l)−xj(l)∣p)p1,p>=1
L 2 ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ 2 ) 1 2 = ( x i ( 1 ) − x j ( 1 ) ) 2 + ( x i ( 2 ) − x j ( 2 ) ) 2 + . . . + ( x i ( n ) − x j ( n ) ) 2 L_2(x_i,x_j)=(\sum\limits_{l=1}^n|x_i^{(l)}-x_j^{(l)}|^2)^{\frac{1}{2}} \\ =\sqrt{(x_i^{(1)}-x_j^{(1)})^2+(x_i^{(2)}-x_j^{(2)})^2+...+(x_i^{(n)}-x_j^{(n)})^2} \\ L2(xi,xj)=(l=1∑n∣xi(l)−xj(l)∣2)21=(xi(1)−xj(1))2+(xi(2)−xj(2))2+...+(xi(n)−xj(n))2
L 1 ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ ) = ∣ x i ( 1 ) − x j ( 1 ) ∣ + ∣ x i ( 2 ) − x j ( 2 ) ∣ + . . . + ∣ x i ( n ) − x j ( n ) ∣ L_1(x_i,x_j)=(\sum\limits_{l=1}^n|x_i^{(l)}-x_j^{(l)}|) \\ =|x_i^{(1)}-x_j^{(1)}|+|x_i^{(2)}-x_j^{(2)}|+...+|x_i^{(n)}-x_j^{(n)}| \\ L1(xi,xj)=(l=1∑n∣xi(l)−xj(l)∣)=∣xi(1)−xj(1)∣+∣xi(2)−xj(2)∣+...+∣xi(n)−xj(n)∣
L ∞ ( x i , x j ) = max l ∣ x i ( l ) − x j ( l ) ∣ L_\infty(x_i,x_j)=\max \limits_{l}|x_i^{(l)}-x_j^{(l)}| \\ L∞(xi,xj)=lmax∣xi(l)−xj(l)∣
举例如下:
x 0 = ( 0 , 0 ) T x_0=(0,0)^T x0=(0,0)T ,求出下图蓝色、红色和黄色分别代表的 L P L_P LP距离。
不同的距离度量,所确定的最近邻点也是不同的
。我们已经知道了,当 k k k值不同时,所属的分类也会不同。
k 近邻法中的分类决策规则往往是多数表决,即由输入实例的k 个邻近的训练实例中的多数类
决定输入实例的类。
import numpy as np 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): # 1、从训练集中取n个 knn_list = [] for i in range(self.n): dist = np.linalg.norm(X - self.X_train[i], ord=self.p) knn_list.append((dist, self.y_train[i])) # 2、遍历未取的训练集,如果所取的knn_list中最大距离 > 目前遍历点的距离,就用现在的点进行替换 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) if knn_list[max_index][0] > dist: knn_list[max_index] = (dist, self.y_train[i]) # 3、将取出的n个最近邻的标签值进行统计 knn = [k[-1] for k in knn_list] count_pairs = Counter(knn) # 取出值最多的标签 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 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) if __name__ == '__main__': # 求x的类别 x = [1, 1] x1 = [15, 10] x2 = [4, 4] x3 = [10, 2] x4 = [2, 1] X_train = np.array([x1, x2, x3, x4]) y_train = np.array([0, 1, 0, 1]) knn = KNN(X_train=X_train, y_train=y_train, n_neighbors=3) print(knn.predict(np.array(x)))
我们现在已经知道了KNN算法,并且用python进行了简单实现。在上面的实现过程中,实现方法是线性扫描(linear scan),计算了输入实例与每1个训练实例的距离。当训练集很大时,计算非常耗时,这种方法是不可行的。
当训练集很大时,我们可以用一个更快速的计算办法——kd树。
kd 从根本上来看,是一个二叉树结构
,根据这个结构对
k
k
k维空间进行不断的划分,每一个节点就代表了
k
k
k维超矩形区域。既然是二叉树,可以想到在建树的时候需要用到递归法
。
为了更加理解上面算法,就用《统计学习方法》书中的例题,构建一棵二维的kd树,手动走下算法流程。
数据集为 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),利用此训练数据集构造一棵kd树。
1、开始:构造根节点
因为该训练数据集的维度为2,那么任选一个特征即可。不妨选择 x ( 1 ) x^{(1)} x(1)为坐标轴(即第一个特征维度),将 x ( 1 ) x^{(1)} x(1)中的数据按照从小到大排序,分别是:2,4,5,7,8,9。
中位数不妨选7,即以(7,2)为根节点,切分整个区域
,那么左区域的坐标小于7,右节点坐标大于7。
2、剩余特征的选取和切割
左边区域点为(5,4),(2,3),(4,7),我们先递归的创建左子树
。
此时,按照第2个特征进行排序,即3、4、7,中位数为4,切分坐标为(5, 4),将左区域再次划分为上、下两个区域。
继续递归,下面区域小于4,按照第1个特征进行排序,只有1个点了(2, 3),切割完无实例点结束。
上面区域大于4,仍旧按照第1个特征进行排序,也只有1个点了(4, 7),切割完无实例点结束,左子树构造完毕。
同理可以递归构造右子树,构造完如下。
# 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维划分数据集data_set创建KdNode if not data_set: # 数据集为空 return None # 按照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 # 下一个分割点 print(f'median = {median}, split = {split + 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) if __name__ == '__main__': data = [ [2, 3], [5, 4], [9, 6], [4, 7], [8, 1], [7, 2] ] tree = KdTree(data) preorder(tree.root)
median = [7, 2], split = 1
median = [5, 4], split = 2
median = [2, 3], split = 1
median = [4, 7], split = 1
median = [9, 6], split = 2
median = [8, 1], split = 1
[7, 2]
[5, 4]
[2, 3]
[4, 7]
[9, 6]
[8, 1]
还是上面的例题。我们目标点(2,4.5)的最近邻。
1.寻找当前最近点:
2.回溯:
注意,这里并没有实现k近邻的算法,k近邻可以使用sklearn中的API。
# 对构建好的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: # python中用float("inf")和float("-inf")表示正负无穷 return result([0] * k, float("inf"), 0) 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")) # 从根节点开始递归 if __name__ == '__main__': data = [ [2, 3], [5, 4], [9, 6], [4, 7], [8, 1], [7, 2] ] tree = KdTree(data) # preorder(tree.root) # print(find_nearest(tree, point=[2.1, 3.1])) print(find_nearest(tree, point=[2, 4.5]))
Result_tuple(nearest_point=[2, 3], nearest_dist=0.14142135623730964, nodes_visited=3)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。