本内容将介绍机器学习中的
近邻法(
-NN) 的原理及暴力和
树实现。
一、 近邻算法介绍
近邻法(K-nearest neighbor,
-NN)1968 年由 Cover 和 Hart 提出,是一种基本分类与回归方法。使用
近邻法进行分类预测时,对新的实例,根据其
个最近邻的训练实例的类别,通过多数表决法等方式进行预测(这
个实例的多数属于某个类,就把该新实例分为这个类)。因此,
近邻法不具有显式的学习过程。
近邻法实际上利用训练数据集对特征向量空间进行划分,并作为其分类的“模型”。
近邻法进行分类和回归预测的主要区别:进行预测时的决策方式不同。进行分类预测时,通常采用多数表决法;而进行回归预测时,通常采用平均法。由于两者区别不大,所以本内容仅介绍使用
近邻进行分类预测。
二、 近邻模型
近邻法使用的模型实际上对应于对特征空间的划分。模型由三个基本要素——距离度量、 值的选择和分类决策规则决定。
2.1 距离度量
特征空间中两个实例点的距离是两个实例点相似程度的反映。
近邻模型的特征空间一般是
维实数向量空间
。通常使用的距离是欧式距离,但也可以选择其他距离,如更一般的
距离(
distance)或 Minkowski 距离(Minkowski distance)。
设特征空间
是
维实数向量空间
,
,
,
,
的
距离定义为
这里
。当
时,称为欧式距离(Euclidean distance);当
时,称为曼哈顿距离(Manhattan distance)。
2.2 值的选择
值的选择会对
近邻法的结果产生重大影响。
如果选择较小的
值,就相当于用较小的邻域中的训练实例进行预测,训练误差会减小,只有与输入实例较近的(相似的)训练实例才会对预测结果起作用。但缺点是泛化误差会增大,预测结果会对近邻的实例点非常敏感。如果邻近的实例点恰巧是噪声,预测就会出错。
如果选择较大的
值,就相当于用较大的邻域中的训练实例进行预测。其优点是可以减少泛化误差。但缺点是训练会增大。这时与输入实例较远的(不相似的)训练实例也会对预测起作用,是预测发生错误。
如果
= N,那么无论输入什么实例,都将简单地预测它属于在训练实例中最多的类。这是不可取的。
在应用中,
值一般取一个较小的数值(通常为小于 20 的整数)。通常采用交叉验证法来选取最优的
值。
2.3 分类决策规则
近邻法中的分类决策规则通常采用多数表决法,即由输入实例的
个邻近的训练实例中的多数类决定实例的类。
三、 近邻法的实现
实现 近邻法时,主要考虑的问题是如何对训练数据进行快速 近邻搜索。
3.1 暴力实现
首先计算输入实例与训练集中所有实例的距离;然后采用线性扫描方法找出最小的 个距离;最后采用多数表决法进行预测。这种方法非常简单,当训练集样很小时,可以采用。但是当训练集很大时,计算会非常耗时,不能采用。
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)
为了提高 近邻搜索的效率,需要考虑使用特殊的结构存储训练数据。实现的方法有很多,下面将介绍其中的一种, 树实现。
3.2 树实现
树是一种对 维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。 树是二叉树,表示对 维空间的一个划分(partition)。构造 树相当于不断地用垂直于坐标轴的超平面将 维空间划分,构成一系列的 维超矩形区域。 树的每个结点对应于一个 维超矩形区域。
3.2.1 构造 树
构造
树的方法:构造根结点,使根结点对应于
维空间中包含所有实例点的超矩形区域;通过下面的递归方法,不断地对
维空间进行切分,生成子结点;当子结点内没有实例时结束。
具体递归方法:在超矩形区域(父结点)上选择一个坐标轴和在此坐标轴上的一个切分点,确定一个超平面(这个超平面通过选定的切分点并垂直于选定的坐标轴);通过这个超平面将当前超矩形区域分为左右两个子区域(子结点),左子结点对应小于切分点的子区域,右子结点对应大于切分点的子区域,落在超平面上的实例点保存在父结点。
上面的选定坐标轴,实际上就是选定一个实例特征。坐标轴选择方法:通常是轮流选择所有特征,也可以选择当前超矩形区域中方差最大的特征。切分点选择方法:选择当前超矩形区域中所有实例在选定特征上的中位数(将实例按照选定特征值大小进行排序,出在中间位置的特征值或者中间两个的平均值)。
假如我们存在这样一个二维空间的数据集:
,构造
树的具体步骤:根结点对应包含数据集
的矩形,选择
轴,中位数为 7,即确定分割平面为
。
分入左子区域,
分入右子区域。用同样的方法分别划分左右子区域,最终得到如下的特征空间划分和
树:
图:特征空间划分
图:
树
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 搜索 树
下面以最近邻为例进行介绍,同样的方法可以应用到 近邻。在 树中搜索最近邻步骤:
- 在 树中找到包含目标点的叶结点,并将此叶结点保存的实例点作为“当前最近点”。具体查找方法:从根结点出发,递归地向下访问 树,若目标点当前维的值小于切分点的值,则移动到左子结点,否则移入右子结点,直到子结点为叶结点为止。
- 向上返回到父结点,如果父结点保存的实例点比“当前最近点”距离目标点更近,则将父结点保存的实例点作为“当前最近点”。以目标点为圆心,以目标点到“当前最近点”的距离为半径,得到一个超球体,检查父结点的另一个子结点对应的超矩形区域是否与此超球体相交。如果相交,就在这个子结点中查找是否存在更近的实例点,如果有,就更新“当前最近点”。
- 不断执行步骤 2,直到返回到父结点结束。
通过上面的描述,我们可以看到,利用
树可以省去对大部分实例点的搜索,从而减少搜索的计算量。
如果实例点是随机分布的,
树搜索的平均计算复杂度是
,这是
是训练实例树。
树更适用于训练是隶属远大于空间维数时的
近邻搜索。当空间维数接近训练实例树时,它的效率会迅速下降,几乎接近线性扫描。
参考:
[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