本内容将介绍机器学习中的 k k k 近邻法( k k k-NN) 的原理及暴力和 k d kd kd 树实现。
k k k 近邻法(K-nearest neighbor, k k k-NN)1968 年由 Cover 和 Hart 提出,是一种基本分类与回归方法。使用 k k k 近邻法进行分类预测时,对新的实例,根据其 k k k 个最近邻的训练实例的类别,通过多数表决法等方式进行预测(这 k k k 个实例的多数属于某个类,就把该新实例分为这个类)。因此, k k k 近邻法不具有显式的学习过程。 k k k 近邻法实际上利用训练数据集对特征向量空间进行划分,并作为其分类的“模型”。
k k k 近邻法进行分类和回归预测的主要区别:进行预测时的决策方式不同。进行分类预测时,通常采用多数表决法;而进行回归预测时,通常采用平均法。由于两者区别不大,所以本内容仅介绍使用 k k k 近邻进行分类预测。
k k k 近邻法使用的模型实际上对应于对特征空间的划分。模型由三个基本要素——距离度量、 k k k 值的选择和分类决策规则决定。
特征空间中两个实例点的距离是两个实例点相似程度的反映。 k k k 近邻模型的特征空间一般是 n n n 维实数向量空间 R n \mathbf R^n Rn。通常使用的距离是欧式距离,但也可以选择其他距离,如更一般的 L p L_p Lp 距离( L p L_p Lp distance)或 Minkowski 距离(Minkowski distance)。
设特征空间 X \mathcal X X 是 n n n 维实数向量空间 R n \mathbf R^n Rn, x i , x j ∈ X x_i,x_j\in\mathbf X xi,xj∈X, x i = ( x i ( 1 ) , x i ( 2 ) , ⋯   , x i ( n ) ) T x_i=(x_i^{(1)},x_i^{(2)},\cdots,x_i^{(n)})^T xi=(xi(1),xi(2),⋯,xi(n))T, x i = ( x j ( 1 ) , x j ( 2 ) , ⋯   , x j ( n ) ) T x_i=(x_j^{(1)},x_j^{(2)},\cdots,x_j^{(n)})^T xi=(xj(1),xj(2),⋯,xj(n))T, x i , x j x_i,x_j xi,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)=\Big(\sum_{l=1}^{n}|x_i^{(l)}-x_j^{(l)}|^p\Big)^{\frac{1}{p}} Lp(xi,xj)=(l=1∑n∣xi(l)−xj(l)∣p)p1
这里 p ≥ 1 p\geq1 p≥1。当 p = 2 p=2 p=2 时,称为欧式距离(Euclidean distance);当 p = 1 p=1 p=1 时,称为曼哈顿距离(Manhattan distance)。
k k k 值的选择会对 k k k 近邻法的结果产生重大影响。
如果选择较小的 k k k 值,就相当于用较小的邻域中的训练实例进行预测,训练误差会减小,只有与输入实例较近的(相似的)训练实例才会对预测结果起作用。但缺点是泛化误差会增大,预测结果会对近邻的实例点非常敏感。如果邻近的实例点恰巧是噪声,预测就会出错。
如果选择较大的 k k k 值,就相当于用较大的邻域中的训练实例进行预测。其优点是可以减少泛化误差。但缺点是训练会增大。这时与输入实例较远的(不相似的)训练实例也会对预测起作用,是预测发生错误。
如果 k k k = N,那么无论输入什么实例,都将简单地预测它属于在训练实例中最多的类。这是不可取的。
在应用中, k k k 值一般取一个较小的数值(通常为小于 20 的整数)。通常采用交叉验证法来选取最优的 k k k 值。
k k k 近邻法中的分类决策规则通常采用多数表决法,即由输入实例的 k k k 个邻近的训练实例中的多数类决定实例的类。
实现 k k k 近邻法时,主要考虑的问题是如何对训练数据进行快速 k k k 近邻搜索。
首先计算输入实例与训练集中所有实例的距离;然后采用线性扫描方法找出最小的 k k k 个距离;最后采用多数表决法进行预测。这种方法非常简单,当训练集样很小时,可以采用。但是当训练集很大时,计算会非常耗时,不能采用。
Python 代码实现如下:
import numpy as np
import operator
class KNNClassification:
def __init__(self, method):
self.method = method
self.train_data_set = None
pass
# 加入测试数据集
def fit(self, train_data_set):
self.train_data_set = train_data_set
pass
# 对测试实例进行预测
def predict(self, k, test_data):
# 计算两点之间的欧式距离
def euclidean_dist(vec01, vec02):
mat_vec01 = np.mat(vec01)
mat_vec02 = np.mat(vec02)
return np.sqrt((mat_vec01-mat_vec02)*(mat_vec01-mat_vec02).T)
# 查找出最近的 k 个近邻
def get_k_nearest_neighbors(train_data_set, k, test_data):
distances = []
# 计算测试实例与训练集中的所有实例的欧式距离
for x in range(len(train_data_set)):
dist = euclidean_dist(test_data[0], train_data_set[x][0])
distances.append((train_data_set[x], dist))
# 进行排序
distances.sort(key=operator.itemgetter(1))
neighbors = []
for x in range(k):
neighbors.append(distances[x][0])
return neighbors
# 从 k 个近邻中返回数量最大的type
def get_type(neighbors):
class_votes = {}
for x in range(len(neighbors)):
type = neighbors[x][-1]
if type in class_votes:
class_votes[type] += 1
else:
class_votes[type] = 1
# 排序,对字典 sorted_votes 中的第二个字符进行排序,即对 value 排序
sorted_votes = sorted(class_votes.items(), key=operator.itemgetter(1), reverse=True)
return sorted_votes[0][0]
if self.train_data_set is None:
return None
neighbors = get_k_nearest_neighbors(self.train_data_set, k, test_data)
return get_type(neighbors)
if __name__ == "__main__":
data_set = np.array([[[2, 3], 1],
[[5, 4], 1],
[[9, 6], 0],
[[4, 7], 1],
[[8, 1], 0],
[[7, 2], 0]])
# 实例化一个 KNN 分类器
KNN = KNNClassification('BruteForce')
KNN.fit(data_set)
# 进行预测
predict_type = KNN.predict(3, [[6, 6], 1])
print(predict_type)
predict_type = KNN.predict(3, [[10, 4], 0])
print(predict_type)
为了提高 k k k 近邻搜索的效率,需要考虑使用特殊的结构存储训练数据。实现的方法有很多,下面将介绍其中的一种, k d kd kd 树实现。
k d kd kd 树是一种对 k k k 维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。 k d kd kd 树是二叉树,表示对 k k k 维空间的一个划分(partition)。构造 k d kd kd 树相当于不断地用垂直于坐标轴的超平面将 k k k 维空间划分,构成一系列的 k k k 维超矩形区域。 k d kd kd 树的每个结点对应于一个 k k k 维超矩形区域。
构造 k d kd kd 树的方法:构造根结点,使根结点对应于 k k k 维空间中包含所有实例点的超矩形区域;通过下面的递归方法,不断地对 k k k 维空间进行切分,生成子结点;当子结点内没有实例时结束。
具体递归方法:在超矩形区域(父结点)上选择一个坐标轴和在此坐标轴上的一个切分点,确定一个超平面(这个超平面通过选定的切分点并垂直于选定的坐标轴);通过这个超平面将当前超矩形区域分为左右两个子区域(子结点),左子结点对应小于切分点的子区域,右子结点对应大于切分点的子区域,落在超平面上的实例点保存在父结点。
上面的选定坐标轴,实际上就是选定一个实例特征。坐标轴选择方法:通常是轮流选择所有特征,也可以选择当前超矩形区域中方差最大的特征。切分点选择方法:选择当前超矩形区域中所有实例在选定特征上的中位数(将实例按照选定特征值大小进行排序,出在中间位置的特征值或者中间两个的平均值)。
假如我们存在这样一个二维空间的数据集: T = { ( 2 , 3 ) T , ( 5 , 4 ) T , ( 9 , 6 ) T , ( 4 , 7 ) T , ( 8 , 1 ) T , ( 7 , 2 ) T } T=\{(2,3)^T,(5,4)^T,(9,6)^T,(4,7)^T,(8,1)^T,(7,2)^T\} T={(2,3)T,(5,4)T,(9,6)T,(4,7)T,(8,1)T,(7,2)T},构造 k d kd kd 树的具体步骤:根结点对应包含数据集 T T T 的矩形,选择 x x x 轴,中位数为 7,即确定分割平面为 x = 7 x=7 x=7。 { ( 2 , 3 ) , ( 4 , 7 ) , ( 5 , 4 ) } \{(2,3),(4,7),(5,4)\} {(2,3),(4,7),(5,4)} 分入左子区域, { ( 8 , 1 ) , ( 9 , 6 ) } \{(8,1),(9,6)\} {(8,1),(9,6)} 分入右子区域。用同样的方法分别划分左右子区域,最终得到如下的特征空间划分和 k d kd kd 树:
图:特征空间划分
Python 代码实现如下:
class KdNode:
def __init__(self, sample, order, parent):
self.sample = sample # 保存在该结点中的实例点
self.order = order
self.parent = parent
self.left = None # 左子结点
self.right = None # 右子结点
pass
def set_child(self, left, right):
self.left = left
self.right = right
class KdTree:
def __init__(self, date_set):
self.root = self.create_kd_tree(date_set)
pass
# 创建 kd 树
def create_kd_tree(self, data_set):
return self.create_node(data_set, 0, None)
# 创建 kd 树的结点
def create_node(self, data_set, order, parent):
if len(data_set) == 0:
return None
# 对数据集进行排序
data_set = sorted(data_set, key=lambda x: x[order])
# 获取中位数位置
split_pos = len(data_set) // 2
median = data_set[split_pos]
order_next = (order+1) % (len(data_set[0]))
node = KdNode(median, order, parent)
left_child = self.create_node(data_set[:split_pos], order_next, node)
right_child = self.create_node(data_set[split_pos+1:], order_next, node)
node.set_child(left_child, right_child)
return node
下面以最近邻为例进行介绍,同样的方法可以应用到 k k k 近邻。在 k d kd kd 树中搜索最近邻步骤:
通过上面的描述,我们可以看到,利用 k d kd kd 树可以省去对大部分实例点的搜索,从而减少搜索的计算量。
如果实例点是随机分布的, k d kd kd 树搜索的平均计算复杂度是 O ( l o g N ) O(logN) O(logN),这是 N N N 是训练实例树。 k d kd kd 树更适用于训练是隶属远大于空间维数时的 k k k 近邻搜索。当空间维数接近训练实例树时,它的效率会迅速下降,几乎接近线性扫描。
参考:
[1] 李航《统计学习方法》
[2] https://www.cnblogs.com/pinard/p/6061661.html
[3] https://www.cnblogs.com/21207-iHome/p/6084670.html
[4] https://blog.csdn.net/gamer_gyt/article/details/51232210