k近邻法(k-nearest neighbor,k-NN)是一种基本分类与回归方法。k近邻法的输入为实例的特征向量,对应于特征空间的点;输出为实例的类别,可以取多类。k近邻法假设给定一个训练数据集,其中的实例类别已定.分类时,对新的实例,根据其k个最近邻的训练实例的类别,通过多数表决等方式进行预测。
1、介绍
kNN是一种基本分类与回归方法。
- kNN是机器学习中被分析的最透彻的算法之一。
- 多数表决规则等价于0-1损失函数下的经验风险最小化,支持多分类, 有别于前面的感知机算法
- kNN的k和KDTree的k含义不同,书上这部分有注释说明
- KDTree是一种存储k维空间数据的树结构,KDTree是平衡二叉树
- KNN应用的一个实践问题是如何建立高效的索引。建立空间索引的方法在点云数据处理中也有广泛的应用,KDTree和八叉树在3D点云数据组织中应用比较广
- 书中的KDTree搜索实现的时候针对了一种 k = 1 k=1 k=1的特殊的情况,实际是最近邻搜索
- KDTree的搜索问题分为k近邻查找和范围查找,一个是已知 k k k,求点集范围,一个是已知范围,求里面有k个点。范围查找问题在维度高的时候复杂度非常高,不太推荐用KDTree做范围查找。
- K近邻问题在杭电ACM里面有收录,HUD4347
- 图像的特征点匹配,数据库查询,图像检索本质上都是同一个问题–相似性检索问题。Facebook开源了一个高效的相似性检索工具Faiss,用于有效的相似性搜索和稠密矢量聚类。
- 这一章有个经典的图,在很多文章和教材上都能看到,就是第一个图3.1。这个图画出了1NN算法在实例空间上的决策面形状。这种类型的图经常被称为在训练集上的的Voronoi图,也叫Thiessen Polygons。可以通过检索Delaunay三角剖分和Voronoi划分进一步了解。
- 在scipy.spatial.KDTree中有KDTree的实现,KDTree在创建马赛克照片的时候可以用到。
2、k近邻算法
k k k近邻法是基本且简单的分类与回归方法。 k k k近邻法的基本做法是:对给定的训练实例点和输入实例点,首先确定输入实例点的 k k k个最近邻训练实例点,然后利用这 k k k个训练实例点的类的多数来预测输入实例点的类。
k近邻模型
算法
-
输入: T = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , … , ( x N , y N ) } , x i ∈ X ⊆ R n , y i ∈ Y = { c 1 , c 2 , … , c k } T=\{(x_1,y_1),(x_2,y_2),\dots,(x_N,y_N)\}, x_i\in \mathcal{X}\sube{\bf{R}^n}, y_i\in\mathcal{Y}=\{c_1,c_2,\dots, c_k\} T={(x1,y1),(x2,y2),…,(xN,yN)},xi∈X⊆Rn,yi∈Y={c1,c2,…,ck}; 实例特征向量 x x x
-
输出: 实例所属的 y y y
步骤:
-
根据指定的距离度量,在 T T T中查找 x x x的最近邻的 k k k个点,覆盖这 k k k个点的 x x x的邻域定义为 N k ( x ) N_k(x) Nk(x)
-
在 N k ( x ) N_k(x) Nk(x)中应用分类决策规则决定 x x x的类别 y y y
y = arg max c j ∑ x i ∈ N k ( x ) I ( y i = c j ) , i = 1 , 2 , … , N , j = 1 , 2 , … , K y=\arg\max_{c_j}\sum_{x_i\in N_k(x)}I(y_i=c_j), i=1,2,\dots,N, j=1,2,\dots,K y=argcjmaxxi∈Nk(x)∑I(yi=cj),i=1,2,…,N,j=1,2,…,K
这里提到了 k k k近邻模型的三要素,即算法描述中黑体标注的部分, 注意这里的三要素和前面说的统计学习方法的三要素不同。
距离度量
特征空间中的两个实例点的距离是两个实例点相似程度的反映。距离越近(数值越小), 相似度越大。
这里用到了 L p L_p Lp距离,
- p = 1 p=1 p=1 对应 曼哈顿距离
- p = 2 p=2 p=2 对应 欧氏距离
- 任意 p p p 对应 闵可夫斯基距离
L p ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ p ) 1 p L_p(x_i, x_j)=\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
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pprint
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from collections import Counter
def distance(x, y, p=2):
"""mathcalculate type of P distance between two points.
input:
x: N*M shape array.
y: 1*M shape array.
p: type of distance
output:
N*1 shape of distance between x and y.
"""
try:
dis = np.power(np.sum(np.power(np.abs((x - y)), p), 1), 1/p)
except:
dis = np.power(np.sum(np.power(np.abs((x - y)), p)), 1/p)
return dis
# load toy data
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']
df.head()
# plot dataset
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.xlabel('sepal length')
plt.ylabel('sepal width')
plt.legend()
# X, y
data = np.array(df.iloc[:100, [0, 1, -1]])
X, y = data[:,:-1], data[:,-1]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
class KNN:
"""
KNN implementation of violence to mathcalculate.
"""
def __init__(self, X_train, y_train, n_neighbors=1, p=2):
"""
n_neighbors: k
p: type of distance
"""
self.k = n_neighbors
self.p = p
self.X_train = X_train
self.y_train = y_train
def predict(self, X):
diss = distance(self.X_train, X, self.p)
diss_idx = np.argsort(diss) # return sorted index
top_k_idx = diss_idx[:self.k]
top_k_diss = diss[top_k_idx]
top_k_points = self.X_train[top_k_idx]
top_k_diss = diss[top_k_idx]
top_k_y = self.y_train[top_k_idx]
counter = Counter(top_k_y)
label = counter.most_common()[0][0]
return label, top_k_points, top_k_diss
def score(self, X_test, y_test):
right_count = 0
for X, y in zip(X_test, y_test):
label = self.predict(X)[0]
if label == y:
right_count += 1
return right_count / len(X_test)
clf = KNN(X_train, y_train)
# test on testset
clf.score(X_test, y_test) # 1.0
# test a single point
test_point = [6, 2.7]
clf.predict(test_point) # (1.0, array([[6. , 2.7]]), array([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(test_point[0], test_point[1], 'bo', label='test_point')
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.legend()
k k k值选择
- 关于 k k k大小对预测结果的影响,书中给的参考文献是ESL,这本书还有个先导书叫ISL。
- 通过交叉验证选取最优 k k k,算是超参数
- 二分类问题, k k k选择奇数有助于避免平票
分类决策规则
Majority Voting Rule
误分类率
1 k ∑ x i ∈ N k ( x ) I ( y i ≠ c i ) = 1 − 1 k ∑ x i ∈ N k ( x ) I ( y i = c i ) \frac{1}{k}\sum_{x_i\in N_k(x)}{I(y_i\ne c_i)}=1-\frac{1}{k}\sum_{x_i\in N_k(x)}{I(y_i= c_i)} k1xi∈Nk(x)∑I(yi=ci)=1−k1xi∈Nk(x)∑I(yi=ci)
如果分类损失函数是0-1损失,误分类率最低即经验风险最小。
3、实现
kNN在实现的时候,要考虑多维数据的存储,这里会用到树结构。
构造KDTree
KDTree的构建是一个递归的过程
注意: KDTree左边的点比父节点小,右边的点比父节点大。
这里面有提到,KDTree搜索时效率未必是最优的,这个和样本分布有关系。随机分布样本KDTree搜索(这里应该是最近邻搜索)的平均计算复杂度是 O ( log N ) O(\log N) O(logN),空间维数 K K K接近训练样本数 N N N时,搜索效率急速下降,几乎 O ( N ) O(N) O(N)
看维度,如果维度比较高,搜索效率很低。当然,在考虑维度的同时也要考虑样本的规模。
考虑个例子
[[1, 1],
[2, 1],
[3, 1],
[4, 1],
[5, 1],
[6, 1],
[100, 1],
[1000, 1]]
这个数据,如果找[100, 1]
kd树
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树搜索时的效率未必是最优的。
构造平衡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树的区域划分。
KDTree搜索
scikit-learn
from sklearn.neighbors import KNeighborsClassifier
clf_sk = KNeighborsClassifier()
clf_sk.fit(X_train, y_train)
Out:
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树
# 算法3.2 平衡kd树
class KdTree:
"""
build kdtree recursively along axis, split on median point.
k: k dimensions
method: alternate/variance, 坐标轴轮替或最大方差轴
"""
def __init__(self, k=2, method='alternate'):
self.k = k
self.method = method
def build(self, points, depth=0):
n = len(points)
if n <= 0:
return None
if self.method == 'alternate':
axis = depth % self.k
elif self.method == 'variance':
axis = np.argmax(np.var(points, axis=0), axis=0)
sorted_points = sorted(points, key=lambda point: point[axis])
return {
'point': sorted_points[n // 2],
'left': self.build(sorted_points[:n//2], depth+1),
'right': self.build(sorted_points[n//2+1:], depth+1)
}
查找kd树
class SearchKdTree:
"""
search closest point
"""
def __init__(self, k=2):
self.k = k
def __closer_distance(self, pivot, p1, p2):
if p1 is None:
return p2
if p2 is None:
return p1
d1 = distance(pivot, p1)
d2 = distance(pivot, p2)
if d1 < d2:
return p1
else:
return p2
def fit(self, root, point, depth=0):
if root is None:
return None
axis = depth % self.k
next_branch = None
opposite_branch = None
if point[axis] < root['point'][axis]:
next_branch = root['left']
opposite_branch = root['right']
else:
next_branch = root['right']
opposite_branch = root['left']
best = self.__closer_distance(point,
self.fit(next_branch,
point,
depth+1),
root['point'])
if distance(point, best) > abs(point[axis] - root['point'][axis]):
best = self.__closer_distance(point,
self.fit(opposite_branch,
point,
depth+1),
best)
return best
# test
point = [3.,4.5]
search = SearchKdTree()
best = search.fit(tree1, point, depth=0)
print(best) # [2 3]
# force computing
def force(points, point):
dis = np.power(np.sum(np.power(np.abs((points - point)), 2), 1), 1/2)
idx = np.argmin(dis, axis=0)
return points[idx]
print(force(data, point)) # [2 3]
force和kd tree之间的测试时间成本
from time import time
# 生成样本点
N = 500000
K = 5
points = np.random.randint(15, size=(N, K))
print('points shape:{}'.format(points.shape)) # points shape:(500000, 5)
# 建立树
kd_tree = KdTree(k=K, method='alternate')
tree = kd_tree.build(points)
# 生成测试点
#test_point = np.random.randint(10, size=(K))
test_point = [14., 3.5, 10.6, 7.2, 9.2]
print('test point: {}'.format(test_point)) # test point: [14.0, 3.5, 10.6, 7.2, 9.2]
#搜索点
start = time()
seah = SearchKdTree()
best = seah.fit(tree, test_point, depth=0)
end = time()
dist = distance(test_point, best)
print('best point:{}, distance:{}, time cost:{}'.format(best, dist, end - start)) # best point:[14 3 11 7 9], distance:0.7000000000000001, time cost:5.568157196044922
# force computing
start = time()
best = force(points, test_point)
end = time()
dist = distance(test_point, best)
print('best point:{}, distance:{}, time cost:{}'.format(best, dist, end - start)) # best point:[14 4 11 7 9], distance:0.7000000000000001, time cost:0.09840011596679688
看上去,相比于在大量的数据点中寻找与目标最近的点,kd树不需要一个个查找,O(n)的复杂的,看上去确实效率提高了。但是!对于numpy来说,numpy的矩阵运算是常量时间复杂度O(1),效率极高。在内存许可的情况下,可以快速查找出符合要求的点, 速度比kd树还快, 从上面的时间消耗可以看出。
4、k近邻法-习题
习题3.1
在二维空间中给出实例点,画出 k k k为1和2时的 k k k近邻法构成的空间划分,并对其进行比较,体会 k k k值选择与模型复杂度及预测准确率的关系。
import numpy as np
from sklearn.neighbors import KNeighborsClassifier
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
data = np.array([[5, 12, 1], [6, 21, 0], [14, 5, 0], [16, 10, 0], [13, 19, 0],
[13, 32, 1], [17, 27, 1], [18, 24, 1], [20, 20,
0], [23, 14, 1],
[23, 25, 1], [23, 31, 1], [26, 8, 0], [30, 17, 1],
[30, 26, 1], [34, 8, 0], [34, 19, 1], [37, 28, 1]])
X_train = data[:, 0:2]
y_train = data[:, 2]
models = (KNeighborsClassifier(n_neighbors=1, n_jobs=-1),
KNeighborsClassifier(n_neighbors=2, n_jobs=-1))
models = (clf.fit(X_train, y_train) for clf in models)
titles = ('K Neighbors with k=1', 'K Neighbors with k=2')
fig = plt.figure(figsize=(15, 5))
plt.subplots_adjust(wspace=0.4, hspace=0.4)
X0, X1 = X_train[:, 0], X_train[:, 1]
x_min, x_max = X0.min() - 1, X0.max() + 1
y_min, y_max = X1.min() - 1, X1.max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.2),
np.arange(y_min, y_max, 0.2))
for clf, title, ax in zip(models, titles, fig.subplots(1, 2).flatten()):
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
colors = ('red', 'green', 'lightgreen', 'gray', 'cyan')
cmap = ListedColormap(colors[:len(np.unique(Z))])
ax.contourf(xx, yy, Z, cmap=cmap, alpha=0.5)
ax.scatter(X0, X1, c=y_train, s=50, edgecolors='k', cmap=cmap, alpha=0.5)
ax.set_title(title)
plt.show()
习题3.2
利用例题3.2构造的 k d kd kd树求点 x = ( 3 , 4.5 ) T x=(3,4.5)^T x=(3,4.5)T的最近邻点。
import numpy as np
from sklearn.neighbors import KDTree
train_data = np.array([(2, 3), (5, 4), (9, 6), (4, 7), (8, 1), (7, 2)])
tree = KDTree(train_data, leaf_size=2)
dist, ind = tree.query(np.array([(3, 4.5)]), k=1)
x1 = train_data[ind[0]][0][0]
x2 = train_data[ind[0]][0][1]
print("x点的最近邻点是({0}, {1})".format(x1, x2))
Out:
x点的最近邻点是(2, 3)
习题3.3
参照算法3.3,写出输出为 x x x的 k k k近邻的算法。
解答:
算法:用kd树的
k
k
k近邻搜索
输入:已构造的kd树;目标点
x
x
x;
输出:
x
x
x的最近邻
在
k
d
kd
kd树中找出包含目标点
x
x
x的叶结点:从根结点出发,递归地向下访问树。若目标点
x
x
x当前维的坐标小于切分点的坐标,则移动到左子结点,否则移动到右子结点,直到子结点为叶结点为止;
如果“当前
k
k
k近邻点集”元素数量小于
k
k
k或者叶节点距离小于“当前
k
k
k近邻点集”中最远点距离,那么将叶节点插入“当前k近邻点集”;
递归地向上回退,在每个结点进行以下操作:
(a)如果“当前
k
k
k近邻点集”元素数量小于
k
k
k或者当前节点距离小于“当前
k
k
k近邻点集”中最远点距离,那么将该节点插入“当前
k
k
k近邻点集”。
(b)检查另一子结点对应的区域是否与以目标点为球心、以目标点与于“当前
k
k
k近邻点集”中最远点间的距离为半径的超球体相交。如果相交,可能在另一个子结点对应的区域内存在距目标点更近的点,移动到另一个子结点,接着,递归地进行最近邻搜索;如果不相交,向上回退;
当回退到根结点时,搜索结束,最后的“当前
k
k
k近邻点集”即为
x
x
x的最近邻点。
# 构建kd树,搜索待预测点所属区域
from collections import namedtuple
import numpy as np
# 建立节点类
class Node(namedtuple("Node", "location left_child right_child")):
def __repr__(self):
return str(tuple(self))
# kd tree类
class KdTree():
def __init__(self, k=1):
self.k = k
self.kdtree = None
# 构建kd tree
def _fit(self, X, depth=0):
try:
k = self.k
except IndexError as e:
return None
# 这里可以展开,通过方差选择axis
axis = depth % k
X = X[X[:, axis].argsort()]
median = X.shape[0] // 2
try:
X[median]
except IndexError:
return None
return Node(location=X[median],
left_child=self._fit(X[:median], depth + 1),
right_child=self._fit(X[median + 1:], depth + 1))
def _search(self, point, tree=None, depth=0, best=None):
if tree is None:
return best
k = self.k
# 更新 branch
if point[0][depth % k] < tree.location[depth % k]:
next_branch = tree.left_child
else:
next_branch = tree.right_child
if not next_branch is None:
best = next_branch.location
return self._search(point,
tree=next_branch,
depth=depth + 1,
best=best)
def fit(self, X):
self.kdtree = self._fit(X)
return self.kdtree
def predict(self, X):
res = self._search(X, self.kdtree)
return res
KNN = KdTree()
X_train = np.array([[2, 3], [5, 4], [9, 6], [4, 7], [8, 1], [7, 2]])
KNN.fit(X_train)
X_new = np.array([[3, 4.5]])
res = KNN.predict(X_new)
x1 = res[0]
x2 = res[1]
print("x点的最近邻点是({0}, {1})".format(x1, x2))
Out:
x点的最近邻点是(2, 3)