经典机器学习模型(二)KNN模型
1 KNN算法简介
1.1 初识KNN
KNN算法可以说是机器学习中最简单的是一种分类、回归方法,它甚至连一个显性的模型结构表达都没有。
假如给定了一个训练数据集 T T T,里面包含了 N N N个样本,每一个实例的标签都给定。当给定一个新的实例 x x x时,我们就可以在训练数据集 T T T里面寻找与 x x x最近的 k k k个实例,然后根据这 k k k个实例的标签来决定新实例的 x x x值。
-
对于
分类问题
,采用多数表决。哪一种类别所占比例最大,那么新的实例点它就属于哪个类别。 -
对于
回归问题
,可以取这些邻居的标签,计算平均值。 -
如下图,k=3时,绿色圆点属于红色三角形类别,k=5时,绿色圆点属于蓝色正方形类别
-
如果换一种距离定义方式,可能这里绿圆点所属的类别也会发生变化。所以,
多少个邻居,怎么计算距离,如何通过邻居的情况反映目标点的信息
,都是非常关键的问题。
1.2 KNN算法及其三要素
KNN算法如下:
距离度量、 k k k个邻居、和分类决策规则,这三个就是k近邻法的三要素。任意一者的变化,都可能导致实例 x x x所属的类别发生变化。
1.2.1 距离度量
特征空间中任意的 x i 和 x j 都是一个 n 维向量,下式中 x ( l ) 代表的是第 l 个特征, p 决定了不同的范数距离。 L p ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ p ) 1 p , p > = 1 特征空间中任意的x_i和x_j都是一个n维向量,下式中x^{(l)}代表的是第l个特征,p决定了不同的范数距离。 \\ L_p(x_i,x_j)=(\sum\limits_{l=1}^n|x_i^{(l)}-x_j^{(l)}|^p)^{\frac{1}{p}},p>=1 特征空间中任意的xi和xj都是一个n维向量,下式中x(l)代表的是第l个特征,p决定了不同的范数距离。Lp(xi,xj)=(l=1∑n∣xi(l)−xj(l)∣p)p1,p>=1
- 当p=2的时候,是我们最熟悉的欧式距离
L 2 ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ 2 ) 1 2 = ( x i ( 1 ) − x j ( 1 ) ) 2 + ( x i ( 2 ) − x j ( 2 ) ) 2 + . . . + ( x i ( n ) − x j ( n ) ) 2 L_2(x_i,x_j)=(\sum\limits_{l=1}^n|x_i^{(l)}-x_j^{(l)}|^2)^{\frac{1}{2}} \\ =\sqrt{(x_i^{(1)}-x_j^{(1)})^2+(x_i^{(2)}-x_j^{(2)})^2+...+(x_i^{(n)}-x_j^{(n)})^2} \\ L2(xi,xj)=(l=1∑n∣xi(l)−xj(l)∣2)21=(xi(1)−xj(1))2+(xi(2)−xj(2))2+...+(xi(n)−xj(n))2
- 当p=1的时候,称为曼哈顿距离,也就是计算所有特征下的绝对距离,再求和。
L 1 ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ ) = ∣ x i ( 1 ) − x j ( 1 ) ∣ + ∣ x i ( 2 ) − x j ( 2 ) ∣ + . . . + ∣ x i ( n ) − x j ( n ) ∣ L_1(x_i,x_j)=(\sum\limits_{l=1}^n|x_i^{(l)}-x_j^{(l)}|) \\ =|x_i^{(1)}-x_j^{(1)}|+|x_i^{(2)}-x_j^{(2)}|+...+|x_i^{(n)}-x_j^{(n)}| \\ L1(xi,xj)=(l=1∑n∣xi(l)−xj(l)∣)=∣xi(1)−xj(1)∣+∣xi(2)−xj(2)∣+...+∣xi(n)−xj(n)∣
- 当p为无穷的时候,称为切比雪夫距离,也就是找到所有特征下的绝对距离的最大值。
L ∞ ( x i , x j ) = max l ∣ x i ( l ) − x j ( l ) ∣ L_\infty(x_i,x_j)=\max \limits_{l}|x_i^{(l)}-x_j^{(l)}| \\ L∞(xi,xj)=lmax∣xi(l)−xj(l)∣
举例如下:
x 0 = ( 0 , 0 ) T x_0=(0,0)^T x0=(0,0)T ,求出下图蓝色、红色和黄色分别代表的 L P L_P LP距离。
- L 2 ( x i , x 0 ) = = ( x i ( 1 ) − 0 ) 2 + ( x i ( 2 ) − 0 ) 2 = ( x i ( 1 ) − 0 ) 2 + ( x i ( 2 ) − 0 ) 2 = 1 L_2(x_i,x_0)= =\sqrt{(x_i^{(1)}-0)^2+(x_i^{(2)}-0)^2} = (x_i^{(1)}-0)^2+(x_i^{(2)}-0)^2 = 1 L2(xi,x0)==(xi(1)−0)2+(xi(2)−0)2=(xi(1)−0)2+(xi(2)−0)2=1,代表一个圆,因此红色线为L_2,即欧式距离。
- L 1 ( x i , x 0 ) = = ∣ x i ( 1 ) − 0 ∣ + ∣ x i ( 2 ) − 0 ∣ = 1 L_1(x_i,x_0)= =|x_i^{(1)}-0|+|x_i^{(2)}-0|=1 L1(xi,x0)==∣xi(1)−0∣+∣xi(2)−0∣=1,而AB + BO=1,可知蓝色线为L_1,即曼哈顿距离。曼哈顿距离的命名来源于曼哈顿岛上的街道网格系统,因为只能沿着水平和垂直方向移动,所以通过网格线移动的最短路径就是曼哈顿距离。
- 黄色线代表切比雪夫距离。
- 需要注意的是
不同的距离度量,所确定的最近邻点也是不同的
。
1.2.2 k值的选择
我们已经知道了,当 k k k值不同时,所属的分类也会不同。
- 如果选择的 k k k值比较小,相当于就在一个比较小的领域里面对训练集内的实例进行预测,所以近似误差小,但是如果新增一个实例时,超过了范围则会导致估计误差增大,这就是只对训练集友好,对新的实例点不友好的情况——过拟合;
- 与之相对, k k k值较大时,就会出现欠拟合;
- k k k值可通过交叉验证选择, k k k值一般低于训练集样本量的平方根。
1.2.3 分类决策规则
k 近邻法中的分类决策规则往往是多数表决,即由输入实例的k 个邻近的训练实例中的多数类
决定输入实例的类。
1.3 利用python简单实现KNN
import numpy as np
from collections import Counter
class KNN:
def __init__(self, X_train, y_train, n_neighbors=3, p=2):
"""
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):
# 1、从训练集中取n个
knn_list = []
for i in range(self.n):
dist = np.linalg.norm(X - self.X_train[i], ord=self.p)
knn_list.append((dist, self.y_train[i]))
# 2、遍历未取的训练集,如果所取的knn_list中最大距离 > 目前遍历点的距离,就用现在的点进行替换
for i in range(self.n, len(self.X_train)):
max_index = knn_list.index(max(knn_list, key=lambda x: x[0]))
dist = np.linalg.norm(X - self.X_train[i], ord=self.p)
if knn_list[max_index][0] > dist:
knn_list[max_index] = (dist, self.y_train[i])
# 3、将取出的n个最近邻的标签值进行统计
knn = [k[-1] for k in knn_list]
count_pairs = Counter(knn)
# 取出值最多的标签
max_count = sorted(count_pairs.items(), key=lambda x: x[1])[-1][0]
return max_count
def score(self, X_test, y_test):
right_count = 0
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)
if __name__ == '__main__':
# 求x的类别
x = [1, 1]
x1 = [15, 10]
x2 = [4, 4]
x3 = [10, 2]
x4 = [2, 1]
X_train = np.array([x1, x2, x3, x4])
y_train = np.array([0, 1, 0, 1])
knn = KNN(X_train=X_train, y_train=y_train, n_neighbors=3)
print(knn.predict(np.array(x)))
2 K近邻法的实现:kd树
-
我们现在已经知道了KNN算法,并且用python进行了简单实现。在上面的实现过程中,实现方法是线性扫描(linear scan),计算了输入实例与每1个训练实例的距离。当训练集很大时,计算非常耗时,这种方法是不可行的。
-
当训练集很大时,我们可以用一个更快速的计算办法——kd树。
-
kd 从根本上来看,
是一个二叉树结构
,根据这个结构对 k k k维空间进行不断的划分,每一个节点就代表了 k k k维超矩形区域。既然是二叉树,可以想到在建树的时候需要用到递归法
。
2.1 利用递归法构造kd树
2.1.1 构造kd树的算法描述
2.1.2 举例构造kd树
为了更加理解上面算法,就用《统计学习方法》书中的例题,构建一棵二维的kd树,手动走下算法流程。
数据集为 T = ( 2 , 3 ) , ( 5 , 4 ) , ( 9 , 6 ) , ( 4 , 7 ) , ( 8 , 1 ) , ( 7 , 2 ) T={(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)} T=(2,3),(5,4),(9,6),(4,7),(8,1),(7,2),利用此训练数据集构造一棵kd树。
1、开始:构造根节点
因为该训练数据集的维度为2,那么任选一个特征即可。不妨选择 x ( 1 ) x^{(1)} x(1)为坐标轴(即第一个特征维度),将 x ( 1 ) x^{(1)} x(1)中的数据按照从小到大排序,分别是:2,4,5,7,8,9。
中位数不妨选7,即以(7,2)为根节点,切分整个区域
,那么左区域的坐标小于7,右节点坐标大于7。
2、剩余特征的选取和切割
左边区域点为(5,4),(2,3),(4,7),我们先递归的创建左子树
。
此时,按照第2个特征进行排序,即3、4、7,中位数为4,切分坐标为(5, 4),将左区域再次划分为上、下两个区域。
继续递归,下面区域小于4,按照第1个特征进行排序,只有1个点了(2, 3),切割完无实例点结束。
上面区域大于4,仍旧按照第1个特征进行排序,也只有1个点了(4, 7),切割完无实例点结束,左子树构造完毕。
同理可以递归构造右子树,构造完如下。
2.1.3 构造kd树的python实现
# 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-tree
class KdTree(object):
def __init__(self, data):
k = len(data[0]) # 数据维度
def CreateNode(split, data_set): # 按第split维划分数据集data_set创建KdNode
if not data_set: # 数据集为空
return None
# 按照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 # 下一个分割点
print(f'median = {median}, split = {split + 1}')
# 递归的创建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)
if __name__ == '__main__':
data = [
[2, 3],
[5, 4],
[9, 6],
[4, 7],
[8, 1],
[7, 2]
]
tree = KdTree(data)
preorder(tree.root)
median = [7, 2], split = 1
median = [5, 4], split = 2
median = [2, 3], split = 1
median = [4, 7], split = 1
median = [9, 6], split = 2
median = [8, 1], split = 1
[7, 2]
[5, 4]
[2, 3]
[4, 7]
[9, 6]
[8, 1]
2.2 搜索kd树
2.2.1 最近邻搜索算法
2.2.2 举例理解最近邻搜索算法
还是上面的例题。我们目标点(2,4.5)的最近邻。
-
1.寻找当前最近点:
- 从根节点开始,(2, 4.5) 在根节点 (7, 2)的左子区域内,继续到(5, 4) 所确定的上子区域内,继续到 (4,7)的左子区域中,(4, 7)就是当前最近邻点。
-
2.回溯:
- 我们以 (2, 4.5) 为圆心,以 (2, 4.5) 到 (4,7)两点之间的距离为半径画一个圆,这个区域内有两个节点,分别是 (2, 3) 和 (5, 4),通过计算 (2, 4.5) 到这两点的距离,得出到 (2, 3)的距离最近,那么 (2, 3) 就是最近邻点。
- 接着,再以 (2, 4.5) 为圆心以 (2, 4.5)到(2, 3) 两点之间的距离为半径画一个圆,此时圆里没有其他的节点,说明可以确认 (2, 3)就是(2, 4.5) 的最近邻点。
2.2.3 使用python完成最近邻搜索算法
注意,这里并没有实现k近邻的算法,k近邻可以使用sklearn中的API。
# 对构建好的kd树进行搜索,寻找与目标点最近的样本点:
from math import sqrt
from 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:
# python中用float("inf")和float("-inf")表示正负无穷
return result([0] * k, float("inf"), 0)
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")) # 从根节点开始递归
if __name__ == '__main__':
data = [
[2, 3],
[5, 4],
[9, 6],
[4, 7],
[8, 1],
[7, 2]
]
tree = KdTree(data)
# preorder(tree.root)
# print(find_nearest(tree, point=[2.1, 3.1]))
print(find_nearest(tree, point=[2, 4.5]))
Result_tuple(nearest_point=[2, 3], nearest_dist=0.14142135623730964, nodes_visited=3)