【统计学习方法】第3章 k近邻法

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)}xiXRn,yiY={c1,c2,,ck}; 实例特征向量 x x x

  • 输出: 实例所属的 y y y

步骤:

  1. 根据指定的距离度量,在 T T T中查找 x x x最近邻的 k k k个点,覆盖这 k k k个点的 x x x的邻域定义为 N k ( x ) N_k(x) Nk(x)

  2. 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=argcjmaxxiNk(x)I(yi=cj),i=1,2,,N,j=1,2,,K

这里提到了 k k k近邻模型的三要素,即算法描述中黑体标注的部分, 注意这里的三要素和前面说的统计学习方法的三要素不同。

距离度量

特征空间中的两个实例点的距离是两个实例点相似程度的反映。距离越近(数值越小), 相似度越大

这里用到了 L p L_p Lp距离,

  1. p = 1 p=1 p=1 对应 曼哈顿距离
  2. p = 2 p=2 p=2 对应 欧氏距离
  3. 任意 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=1nxi(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值选择
  1. 关于 k k k大小对预测结果的影响,书中给的参考文献是ESL,这本书还有个先导书叫ISL。
  2. 通过交叉验证选取最优 k k k,算是超参数
  3. 二分类问题, 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)} k1xiNk(x)I(yi=ci)=1k1xiNk(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{x1x2,,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 i1,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 lj(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搜索

对应图3.5
B
A
C
F
D
G
E

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)
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值