本文共 17151 字,大约阅读时间需要 57 分钟。
【导读】k近邻法(k-NN)是一种基本的分类与回归的方法。书本只讨论分类问题的k近邻法。该方法的基本思想是“物以类聚,人以群分”或“近朱者赤近墨者黑”。章节结构先叙述k近邻算法,接着讨论k近邻的模型和三个基本要素,最后讲述其中一个实现方法——kd树,介绍构造kd树和搜索kd树的算法。
关于k近邻法的回归,和分类的主要区别在于最后做预测时候的决策方式不同。k-NN做分类预测时,一般是选择多数表决法,即训练集里和预测的样本特征最近的K个样本,预测为里面有最多类别数的类别。而k-NN做回归时,一般是选择平均法,即最近的k个样本的样本输出的平均值作为回归预测值,它们的思想是一致的。对k-NN回归感兴趣的朋友可以读读这篇文章。链接:
提取码:v7w8应用 可用于常见的分类和回归情形,特别是多分类情形。应用场景有字符识别、文本分类、图像识别等领域。
优点
缺点
算法
主要有以下几个步骤:
k近邻法的特殊情况是k=1的情形,称为最近邻算法,即对于输入的实例点(特征向量)x,最近邻法将训练数据集中与x最邻近的类作为x的类。
k近邻法没有显式的学习过程,也就是不需要对训练集进行学习。k近邻法实际上利用训练数据集对特征向量空间进行划分,并作为其分类的“模型”。
维基百科这张图生动地展示了k-NN的特点(实线邻域是3近邻情况,虚线邻域是5近邻情况)
模型由三个基本要素——距离度量、k值选择和分类决策规则决定
k近邻法使用的模型实际上对应于特征空间的划分,某种意义上来说,k近邻的模型的假设空间就是样本特征空间本身。
特征空间中两个实例点的距离是两个实例点相似程度的一种数字化度量。
设特征空间 X \mathcal{X} X是n维实数向量空间 R n \mathbf{R}^{n} Rn, x i , x j ∈ X x_{i}, x_{j} \in \mathcal{X} xi,xj∈X, x i = ( x i ( 1 ) , x i ( 2 ) , ⋯ , x i ( n ) ) T x_{i}=\left(x_{i}^{(1)}, x_{i}^{(2)}, \cdots, x_{i}^{(n)}\right)^{T} xi=(xi(1),xi(2),⋯,xi(n))T, x j = ( x j ( 1 ) , x j ( 2 ) , ⋯ , x j ( n ) ) T x_{j}=\left(x_{j}^{(1)}, x_{j}^{(2)}, \cdots, x_{j}^{(n)}\right)^{\mathrm{T}} xj=(xj(1),xj(2),⋯,xj(n))T, x i , x j x_{i}, x_{j} xi,xj的 L p L_{p} Lp或闵可夫斯基距离(Minkowski Distance)距离定义为:
L p ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ p ) 1 p L_{p}\left(x_{i}, x_{j}\right)=\left(\sum_{l=1}^{n}\left|x_{i}^{(l)}-x_{j}^{(l)}\right|^{p}\right)^{\frac{1}{p}} Lp(xi,xj)=(l=1∑n∣∣∣xi(l)−xj(l)∣∣∣p)p1这里 p ⩾ 1 p \geqslant 1 p⩾1,当 p = 2 p=2 p=2时,称为欧氏距离(Euclidean distance),即
L 2 ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ 2 ) 1 2 L_{2}\left(x_{i}, x_{j}\right)=\left(\sum_{l=1}^{n}\left|x_{i}^{(l)}-x_{j}^{(l)}\right|^{2}\right)^{\frac{1}{2}} L2(xi,xj)=(l=1∑n∣∣∣xi(l)−xj(l)∣∣∣2)21 这是最常见的两点之间或多点之间的距离表示法。当 p = 1 p=1 p=1时,称为曼哈顿距离(Manhattan distance),即
L 1 ( x i , x j ) = ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ L_{1}\left(x_{i}, x_{j}\right)=\sum_{l=1}^{n}\left|x_{i}^{(l)}-x_{j}^{(l)}\right| L1(xi,xj)=l=1∑n∣∣∣xi(l)−xj(l)∣∣∣ 我们可以定义曼哈顿距离的正式意义为L1-距离或城市区块距离,也就是在欧几里得空间的固定直角坐标系上两点所形成的线段对轴产生的投影的距离总和。 通俗来讲,想象你在曼哈顿要从一个十字路口开车到另外一个十字路口,驾驶距离是两点间的直线距离吗?显然不是,除非你能穿越大楼。而实际驾驶距离就是这个“曼哈顿距离”,此即曼哈顿距离名称的来源, 同时,曼哈顿距离也称为城市街区距离(City Block distance)。当 p = ∞ p=\infty p=∞时,它是各个坐标距离的最大值,即
L ∞ ( x i , x j ) = max l ∣ x i ( l ) − x j ( l ) ∣ L_{\infty}\left(x_{i}, x_{j}\right)=\max _{l}\left|x_{i}^{(l)}-x_{j}^{(l)}\right| L∞(xi,xj)=lmax∣∣∣xi(l)−xj(l)∣∣∣ 玩过国际象棋的朋友或许知道,国王走一步能够移动到相邻的8个方格中的任意一个。那么国王从格子(x1,y1)走到格子(x2,y2)最少需要多少步?。你会发现最少步数总是max( | x2-x1 | , | y2-y1 | ) 步 。有一种类似的一种距离度量方法就是切比雪夫距离。L p L_{p} Lp距离间的关系:
【拓展推荐阅读】想要了解更多的距离度量,墙裂推荐阅读
对于k值的选择,没有一个固定的经验,一般根据样本的分布,选择一个较小的值,可以通过交叉验证选择一个合适的k值。
选择较小的k值,就相当于用较小的领域中的训练实例进行预测,训练误差会减小,只有与输入实例较近或相似的训练实例才会对预测结果起作用,与此同时带来的问题是泛化误差会增大,换句话说,K值的减小就意味着整体模型变得复杂,容易发生过拟合;
选择较大的k值,就相当于用较大领域中的训练实例进行预测,其优点是可以减少泛化误差,但缺点是训练误差会增大。这时候,与输入实例较远(不相似的)训练实例也会对预测器作用,使预测发生错误,且K值的增大就意味着整体的模型变得简单。
一个极端是k等于样本数N,则完全没有分类,此时无论输入实例是什么,都只是简单的预测它属于在训练实例中最多的类,模型过于简单。
对于分类决策规则,一般都是使用前面提到的多数表决法。
【思考】k-NN回归决策的规则是什么?与分类决策规则有什么区别吗?为什么这样选择?
一般说来,索引结构中相似性查询有两种基本的方式:
同样,针对特征点匹配也有两种方法:
课本主要讲述了kd树方法
【推荐阅读】关于R树,推荐阅读
Kd-树是K-dimension tree的缩写,是对数据点在k维空间(如二维(x,y),三维(x,y,z),k维(x1,y,z…))中划分的一种数据结构,主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索)。本质上说,Kd-树就是一种平衡二叉树。
首先必须搞清楚的是,k-d树是一种空间划分树,说白了,就是把整个空间划分为特定的几个部分,然后在特定空间的部分内进行相关搜索操作。想像一个三维空间,kd树按照一定的划分规则把这个三维空间划分了多个空间,如下图所示:
kd树构造采用的是从N个样本的n维特征中,分别计算n个特征的取值的方差,用方差最大的第k维特征 n k n_k nk来作为根节点。对于这个特征,我们选择特征 n k n_k nk的取值的中位数 n k v n_{kv} nkv对应的样本作为划分点,对于所有第k维特征的取值小于 n k v n_{kv} nkv的样本,我们划入左子树,对于第k维特征的取值大于等于 n k v n_{kv} nkv的样本,我们划入右子树,对于左子树和右子树,我们采用和刚才同样的办法来找方差最大的特征来做更节点,递归的生成kd树。
kd树的构建是一个递归过程,流程如下:
【推荐阅读】
完成了KD树建树后,接下来讨论如何利用KD树进行高效K近邻搜索:
输入:根据train set构造的kd树;目标点x
输出:x的最近邻【思考】kd树有什么优缺点,有没有改进的数据结构?
【回答】
可以看到,kd树是可用于有效寻找最近邻的良好数据结构。但是,当面对不均匀数据的数据集时,面临一些基本冲突和挑战:但是均值分裂点技术依然无法完全规避KD原生的问题,为此,学界提出了超球分界面代替超矩形分界面的改进方法。
【推荐阅读】
【思考】kd的适用场景。
【回答】kd树更适用于训练实例数远大于空间维数时的k近邻搜索。当空间维数接近训练实例数时,它的效率会迅速下降,几乎接近线性扫描。
设特征空间 x x x是 n n n维实数向量空间 , x i , x j ∈ X x_{i}, x_{j} \in \mathcal{X} xi,xj∈X, x i = ( x i ( 1 ) , x i ( 2 ) , ⋯ , x i ( n ) ) T x_{i}=\left(x_{i}^{(1)}, x_{i}^{(2)}, \cdots, x_{i}^{(n)}\right)^{\mathrm{T}} xi=(xi(1),xi(2),⋯,xi(n))T, x j = ( x j ( 1 ) , x j ( 2 ) , ⋯ , x j ( n ) ) T x_{j}=\left(x_{j}^{(1)}, x_{j}^{(2)}, \cdots, x_{j}^{(n)}\right)^{\mathrm{T}} xj=(xj(1),xj(2),⋯,xj(n))T
,则: x i x_i xi, x j x_j xj的 L p L_p Lp距离定义为:L p ( x i , x j ) = ( ∑ i = 1 n ∣ x i ( i ) − x j ( l ) ∣ p ) 1 p L_{p}\left(x_{i}, x_{j}\right)=\left(\sum_{i=1}^{n}\left|x_{i}^{(i)}-x_{j}^{(l)}\right|^{p}\right)^{\frac{1}{p}} Lp(xi,xj)=(∑i=1n∣∣∣xi(i)−xj(l)∣∣∣p)p1
import mathfrom itertools import combinations
def L(x, y, p=2): # 默认欧式距离 # x1 = [1, 1], x2 = [5,1] if len(x) == len(y) and len(x) > 1: sum = 0 for i in range(len(x)): sum += math.pow(abs(x[i] - y[i]), p) return math.pow(sum, 1 / p) else: return 0
x1 = [1, 1]x2 = [5, 1]x3 = [4, 4]
# x1, x2for i in range(1, 5): r = { '1-{}'.format(c): L(x1, c, p=i) for c in [x2, x3]} # r是字典,key是'1-c',value是L(x1, c, p=i) print(min(zip(r.values(), r.keys()))) # 比较i=1,2,3,4的情况下,c在[x2, x3]哪个L(x1, c, p=i) 最小。zip()是把后面的字典键值对应压缩转化成元组
(4.0, '1-[5, 1]')(4.0, '1-[5, 1]')(3.7797631496846193, '1-[4, 4]')(3.5676213450081633, '1-[4, 4]')
python实现,遍历所有数据点,找出 n n n个距离最近的点的分类情况,少数服从多数
import numpy as npimport pandas as pdimport matplotlib.pyplot as plt%matplotlib inlinefrom sklearn.datasets import load_irisfrom sklearn.model_selection import train_test_splitfrom collections import Counter
# datairis = load_iris()df = pd.DataFrame(iris.data, columns=iris.feature_names)df['label'] = iris.targetdf.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']# data = np.array(df.iloc[:100, [0, 1, -1]])
df
sepal length | sepal width | petal length | petal width | label | |
---|---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 | 0 |
1 | 4.9 | 3.0 | 1.4 | 0.2 | 0 |
2 | 4.7 | 3.2 | 1.3 | 0.2 | 0 |
3 | 4.6 | 3.1 | 1.5 | 0.2 | 0 |
4 | 5.0 | 3.6 | 1.4 | 0.2 | 0 |
... | ... | ... | ... | ... | ... |
145 | 6.7 | 3.0 | 5.2 | 2.3 | 2 |
146 | 6.3 | 2.5 | 5.0 | 1.9 | 2 |
147 | 6.5 | 3.0 | 5.2 | 2.0 | 2 |
148 | 6.2 | 3.4 | 5.4 | 2.3 | 2 |
149 | 5.9 | 3.0 | 5.1 | 1.8 | 2 |
150 rows × 5 columns
plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0') #前50个样本标签是0plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1') #51-100的样本标签是1plt.xlabel('sepal length')plt.ylabel('sepal width')plt.legend()
data = np.array(df.iloc[:100, [0, 1, -1]]) #取前100个样本第一第二与最后一列的值转成数组X, y = data[:,:-1], data[:,-1]X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) #测试集占的比例为0.2,默认是0.25
【参考】
class KNN: def __init__(self, X_train, y_train, n_neighbors=3, p=2): #默认3近邻,欧氏距离 """ 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): # 取出n个点 knn_list = [] for i in range(self.n): #取前n个点 dist = np.linalg.norm(X - self.X_train[i], ord=self.p) #linalg=linear(线性)+algebra(代数),norm则表示范数。 knn_list.append((dist, self.y_train[i])) for i in range(self.n, len(self.X_train)): #取n+1到100样本 max_index = knn_list.index(max(knn_list, key=lambda x: x[0])) #最大dist的索引 dist = np.linalg.norm(X - self.X_train[i], ord=self.p) if knn_list[max_index][0] > dist: #如果找到了比n近邻中最大dist的点更小的dist的点,则将其替换 knn_list[max_index] = (dist, self.y_train[i]) # 统计 knn = [k[-1] for k in knn_list] #knn是knn_list的后一个值,即标签 count_pairs = Counter(knn) #标签计数,通过字典的键值,比如本例,标签0有几个,标签1有几个# max_count = sorted(count_pairs, key=lambda x: x)[-1] max_count = sorted(count_pairs.items(), key=lambda x: x[1])[-1][0] #count_pairs.items()是dict_itmes,将原来字典类型的键值对转成列表类型的元组对。然后按着元组后一个元素(即标签出现的次数)升序排列,选最后一排(出现最多)元组的第一个元素,就是标签 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) #预测正确的比例,0——1的范围
clf = KNN(X_train, y_train)
clf.score(X_test, y_test)
1.0
test_point = [6.0, 3.0]print('Test Point: {}'.format(clf.predict(test_point)))
Test Point: 1.0
测试结构为标签1
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()
from sklearn.neighbors import KNeighborsClassifier
clf_sk = KNeighborsClassifier() #class sklearn.neighbors.KNeighborsClassifier(n_neighbors=5, *, weights='uniform', algorithm='auto', leaf_size=30, p=2, metric='minkowski', metric_params=None, n_jobs=None, **kwargs)clf_sk.fit(X_train, y_train)
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski', metric_params=None, n_jobs=None, n_neighbors=5, p=2, weights='uniform')
【参考】
clf_sk.score(X_test, y_test)
1.0
kd树是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。
kd树是二叉树,表示对 k k k维空间的一个划分(partition)。构造kd树相当于不断地用垂直于坐标轴的超平面将 k k k维空间切分,构成一系列的k维超矩形区域。kd树的每个结点对应于一个 k k k维超矩形区域。
构造kd树的方法如下:
构造根结点,使根结点对应于 k k k维空间中包含所有实例点的超矩形区域;通过下面的递归方法,不断地对 k k k维空间进行切分,生成子结点。在超矩形区域(结点)上选择一个坐标轴和在此坐标轴上的一个切分点,确定一个超平面,这个超平面通过选定的切分点并垂直于选定的坐标轴,将当前超矩形区域切分为左右两个子区域
(子结点);这时,实例被分到两个子区域。这个过程直到子区域内没有实例时终止(终止时的结点为叶结点)。在此过程中,将实例保存在相应的结点上。通常,依次选择坐标轴对空间切分,选择训练实例点在选定坐标轴上的中位数
(median)为切分点,这样得到的kd树是平衡的。注意,平衡的kd树搜索时的效率未必是最优的。输入: k k k维空间数据集 T = { x 1 , x 2 , … , x N } T=\{x_1,x_2,…,x_N\} T={ x1,x2,…,xN},
其中 x i = ( x i ( 1 ) , x i ( 2 ) , ⋯ , x i ( k ) ) T x_{i}=\left(x_{i}^{(1)}, x_{i}^{(2)}, \cdots, x_{i}^{(k)}\right)^{\mathrm{T}} xi=(xi(1),xi(2),⋯,xi(k))T , i = 1 , 2 , … , N i=1,2,…,N i=1,2,…,N;
输出:kd树。
(1)开始:构造根结点,根结点对应于包含 T T T的 k k k维空间的超矩形区域。
选择 x ( 1 ) x^{(1)} x(1)为坐标轴,以T中所有实例的 x ( 1 ) x^{(1)} x(1)坐标的中位数为切分点,将根结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 x ( 1 ) x^{(1)} x(1)垂直的超平面实现。
由根结点生成深度为1的左、右子结点:左子结点对应坐标 x ( 1 ) x^{(1)} x(1)小于切分点的子区域, 右子结点对应于坐标 x ( 1 ) x^{(1)} x(1)大于切分点的子区域。
将落在切分超平面上的实例点保存在根结点。
(2)重复:对深度为 j j j的结点,选择 x ( 1 ) x^{(1)} x(1)为切分的坐标轴, l = j ( m o d k ) + 1 l=j(modk)+1 l=j(modk)+1,以该结点的区域中所有实例的 x ( 1 ) x^{(1)} x(1)坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 x ( 1 ) x^{(1)} x(1)垂直的超平面实现。
由该结点生成深度为 j + 1 j+1 j+1的左、右子结点:左子结点对应坐标 x ( 1 ) x^{(1)} x(1)小于切分点的子区域,右子结点对应坐标 x ( 1 ) x^{(1)} x(1)大于切分点的子区域。
将落在切分超平面上的实例点保存在该结点。
(3)直到两个子区域没有实例存在时停止。从而形成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-treeclass 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 # 递归的创建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)
# 对构建好的kd树进行搜索,寻找与目标点最近的样本点:from math import sqrtfrom 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")) # 从根节点开始递归
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]
from time import process_timefrom 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)]
ret = find_nearest(kd, [3,4.5])print (ret)
Result_tuple(nearest_point=[2, 3], nearest_dist=1.8027756377319946, nodes_visited=4)
N = 400000t0 = process_time()kd2 = KdTree(random_points(3, N)) # 构建包含四十万个3维空间样本点的kd树ret2 = find_nearest(kd2, [0.1,0.5,0.8]) # 四十万个样本点中寻找离目标最近的点t1 = process_time()print ("time: ",t1-t0, "s")print (ret2)
time: 5.296875 sResult_tuple(nearest_point=[0.09655667956154679, 0.5032638969756525, 0.7944025326469046], nearest_dist=0.0073376508419597366, nodes_visited=52)
参考代码:
转载地址:http://vkgbi.baihongyu.com/