本内容将介绍机器学习中的
k
k
k 近邻法(
k
k
k-NN) 的原理及暴力和
k
d
kd
kd 树实现。
一、 k k k 近邻算法介绍
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 值的选择和分类决策规则决定。
2.1 距离度量
特征空间中两个实例点的距离是两个实例点相似程度的反映。
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)。
2.2 k k k 值的选择
k
k
k 值的选择会对
k
k
k 近邻法的结果产生重大影响。
如果选择较小的
k
k
k 值,就相当于用较小的邻域中的训练实例进行预测,训练误差会减小,只有与输入实例较近的(相似的)训练实例才会对预测结果起作用。但缺点是泛化误差会增大,预测结果会对近邻的实例点非常敏感。如果邻近的实例点恰巧是噪声,预测就会出错。
如果选择较大的
k
k
k 值,就相当于用较大的邻域中的训练实例进行预测。其优点是可以减少泛化误差。但缺点是训练会增大。这时与输入实例较远的(不相似的)训练实例也会对预测起作用,是预测发生错误。
如果
k
k
k = N,那么无论输入什么实例,都将简单地预测它属于在训练实例中最多的类。这是不可取的。
在应用中,
k
k
k 值一般取一个较小的数值(通常为小于 20 的整数)。通常采用交叉验证法来选取最优的
k
k
k 值。
2.3 分类决策规则
k
k
k 近邻法中的分类决策规则通常采用多数表决法,即由输入实例的
k
k
k 个邻近的训练实例中的多数类决定实例的类。
三、 k k k 近邻法的实现
实现 k k k 近邻法时,主要考虑的问题是如何对训练数据进行快速 k k k 近邻搜索。
3.1 暴力实现
首先计算输入实例与训练集中所有实例的距离;然后采用线性扫描方法找出最小的 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 树实现。
3.2 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 维超矩形区域。
3.2.1 构造 k d kd kd 树
构造
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 树:
图:特征空间划分
图:
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
3.2.2 搜索 k d kd kd 树
下面以最近邻为例进行介绍,同样的方法可以应用到 k k k 近邻。在 k d kd kd 树中搜索最近邻步骤:
- 在 k d kd kd 树中找到包含目标点的叶结点,并将此叶结点保存的实例点作为“当前最近点”。具体查找方法:从根结点出发,递归地向下访问 k d kd kd 树,若目标点当前维的值小于切分点的值,则移动到左子结点,否则移入右子结点,直到子结点为叶结点为止。
- 向上返回到父结点,如果父结点保存的实例点比“当前最近点”距离目标点更近,则将父结点保存的实例点作为“当前最近点”。以目标点为圆心,以目标点到“当前最近点”的距离为半径,得到一个超球体,检查父结点的另一个子结点对应的超矩形区域是否与此超球体相交。如果相交,就在这个子结点中查找是否存在更近的实例点,如果有,就更新“当前最近点”。
- 不断执行步骤 2,直到返回到父结点结束。
通过上面的描述,我们可以看到,利用
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