机器学习-k近邻算法及kd树

本文介绍了k近邻算法的基本原理,包括k值选择、距离度量和决策规则。讨论了k值对模型复杂度和过拟合的影响。接着,详细阐述了线性扫描法和kd树两种实现方法,其中kd树能有效减少计算距离的次数。通过实例展示了如何构建kd树并进行最近邻搜索。最后,给出了kd树搜索目标点最近邻的过程。
摘要由CSDN通过智能技术生成

概述

k近邻法不具有显式的学习过程,实际上是利用训练数据集对特征向量空间进行划分,并作为其分类的模型。
k值的选择, 距离度量分类决策规则是k近邻算法的三个基本要素。

简单描述为:给定一个训练数据集,对新的输入实例,在训练数据集中找到与该实例最近邻的k个实例,这k个实例的多数属于某个类,就把该输入实例分为这个类。其中,当k=1时为最近邻搜索,即找到该输入实例在训练数据集中的最近实例;k>1时是范围搜索;以上描述的分类决策规则为多数表决,即由输入实例的k个近邻的训练实例中的多数类决定输入实例的类;距离度量一般采用欧式距离。
图解k近邻
图中绿色圆点即为新的输入实例,其余点为训练数据,其中共有两个分类,分别为蓝色和红色。现在要给新的绿色实例分配类别,k近邻的思想是选择与新实例最近的k个实例,这k个实例中的多数类别作为新实例的类别。当k为1,2,3时,类别为红色实例对应的类别;当k为5时,选择最近的5个实例,其中3个为蓝色,2个为红色,那么新的输入实例的类别就是蓝色。由此可见k值的选择会影响分类结果,同时新输入实例与训练数据实例点之间的距离计算距离的度量规则也是k近邻算法的关键。

k值的选择

如果k值较小,就相当于用较小的邻域中的训练实例进行预测,只有与输入实例较近的训练实例才会对预测结果起作用,预测结果会对近邻的实例点非常敏感,意味着整体模型变得复杂,容易发生过拟合。(过拟合:一味追求提高对训练数据的预测能力,所选择的模型复杂度往往比真实模型更高,指学习时选择的模型包含的参数过多,以致于这一模型对已知数据(训练数据)预测的很好,而对未知数据预测的很差)
如果k值较大,则相当于用较大邻域中的训练实例进行预测,这时与训练实例较远的训练实例也会对预测起作用,k值增大意味着整体模型变的简单。
实际应用中,k值一般取一个比较小的数据(不超过20),然后根据训练结果选择最优值。

距离度量

两个实例点的距离是两个实例点相似程度的反映。
Lp 距离的定义如下:

Lp(xi,xj)=[l=1nxlixljp]1p

$L_p距离$
当p=2时,称为欧式距离,即
L2(xi,xj)=[l=1nxlixlj2]12

当p=1时,称为曼哈顿距离,即
L1(xi,xj)=l=1nxlixlj

曼哈顿距离又称城市街角距离,图中( 12 , 12 )点对应的曼哈顿距离为1,它是各坐标轴绝对轴距之和。(想象为从街角一头走到另一个街角,不能直穿大楼,走过的距离为城市街角距离)
当p= 时,它是各个距离坐标的最大值。
我们一般使用p=2时的欧式距离;k=1是k近邻法的特殊情况,称为最近邻算法,对于输入实例点(特征向量)x,最近邻法将训练数据集中与x最近点的类作为特征向量x的类。

决策规则

一般使用多数表决,即由输入实例的k个近邻的训练实例中的多数类决定输入实例的类。

k近邻算法

输入:训练数据集T,实例特征向量x
输出:实例x所属类别y

  1. 根据给定的距离度量,在训练集T中找出与x最近邻的k个点,涵盖着k个点的x的邻域记作 Nk(x)
  2. Nk(x) 中根据分类决策规则决定x的类别y

实现k近邻算法

针对k近邻的特征点匹配有两种方法:
最容易的是线性扫描,依次计算样本集合中每个样本到输入实例点之间的距离,按距离排序,选择k个样本,这k个样本中的多数对应的类别作为新的输入实例的类别。这种方法需要计算所有样本点与新输入实例点的距离,当训练数据很大时,非常耗时,不可取。
另一种是构建数据索引,然后进行快速匹配,索引树是一种树结构索引方法,基本思想是对搜索空间进行层次划分,以减少计算距离的次数。具体方法很多,本文介绍kd树方法。

线性扫描法

数据集来源于《机器学习实战》“使用k近邻算法改进约会网站的配对效果”,数据集下载链接: https://pan.baidu.com/s/1boAADC7 密码: me77
数据描述:共四列数据,分别代表每年获得的飞行常客里程数、每周消费的冰淇淋公升数、玩视频游戏所耗的时间百分比,最后一列为该实例对应的类别,分别为不喜欢、魅力一般、极具魅力。(我想不通男人的魅力和冰淇淋有啥关系)

数据导入
import numpy as np
from operator import itemgetter

def get_train_data(filename):
    return np.array([label.strip().split() for label in open(filename)])

# 将数据以字符串类型读入numpy数组
train_data = get_train_data('datingTestSet.txt')
# 提取训练数据及对应的类别
train_data, labels = train_data[:,:3].astype(float), train_data[:,3]
# 将类别转化为数值类型,每个数值代表一个类别
index_labels = list(set(labels))
labels = [index_labels.index(label) for label in labels.tolist()]
特征值归一化

输出训练数据集train_data为:

array([['40920', '8.326976', '0.953952', 'largeDoses'],
       ['14488', '7.153469', '1.673904', 'smallDoses'],
       ['26052', '1.441871', '0.805124', 'didntLike'],
       ..., 
       ['26575', '10.650102', '0.866627', 'largeDoses'],
       ['48111', '9.134528', '0.728045', 'largeDoses'],
       ['43757', '7.882601', '1.332446', 'largeDoses']], 
      dtype='|S10')

第一列是每年的飞行常客里程数,采用欧式距离度量方法计算两个实例点之间的距离时,差值最大的飞行常客里程数对计算结果的影响最大:

(4092014488)2+(8.3269767.153469)2+(0.9539521.673904)2

而这三种特征是同样重要的,因此我们采用以下公式将任意取值范围的特征值全部转化为0到1之间的值:
new_value = (old_value - min) / (max - min)

def autoNorm(dataSet):
    min_value = dataSet.min(0) # 0代表axis=0,获取每列最小值
    max_value = dataSet.max(0)
    ranges = max_value - min_value
    m = dataSet.shape[0]
    norm_dataSet = dataSet - np.tile(min_value, (m, 1)) # np.tile()复制第一个参数为第二个参数给出的形状
    norm_dataSet /= np.tile(ranges, (m, 1)) # numpy的/代表矩阵对应元素相除
    return norm_dataSet, ranges, min_value

获得归一化后的数据集:

norm_mat, ranges, min_value = autoNorm(train_data)

输出norm_mat如下所示:

array([[ 0.44832535,  0.39805139,  0.56233353],
       [ 0.15873259,  0.34195467,  0.98724416],
       [ 0.28542943,  0.06892523,  0.47449629],
       ..., 
       [ 0.29115949,  0.50910294,  0.51079493],
       [ 0.52711097,  0.43665451,  0.4290048 ],
       [ 0.47940793,  0.3768091 ,  0.78571804]])
分类
# inX: 被分类的输入向量
# labels:数据集中每个样本的分类,和dataSet对应
# k: 选择最近邻个数
def classify0(inX, dataSet, labels, k):
    # 计算当前输入向量与训练集中所有点之间的距离, 并按距离依次排序
    diff_mat = np.tile(inX, (dataSet.shape[0], 1)) - dataSet
    distances = ((diff_mat**2).sum(axis=1))**0.5
    sorted_distances_index = distances.argsort() # 从小到大排序, 返回数组对应值的索引
    # 选取k个点
    class_count = {}
    for i in range(k):
        vote_label = labels[sorted_distances_index[i]]
        class_count[vote_label] = class_count.get(vote_label, 0) + 1
    # 确定前k个点的对应类别出现频率, 并按频率排序
    sorted_class_count = sorted(class_count.items(), key=itemgetter(1), reverse=True)
    # 返回最近的k个实例中出现次数最多的类别
    return sorted_class_count[0][0]
测试

测试上述分类器的准确率:

def datingClassTest(norm_data_set, labels):
    # 选取训练集个数的10%作为测试数据,其余90%作为已知样本点
    num_test_vecs = int(norm_data_set.shape[0] * 0.1)
    error_count = 0
    for i in range(num_test_vecs):
        classifier_result = classify0(norm_data_set[i,:], norm_data_set[num_test_vecs:, :], labels, 3)
        if classifier_result != labels[i]: error_count += 1
    return float(error_count) / num_test_vecs

测试结果为0.7

kd树法

kd树是二叉树,表示对k维空间的一个划分,构造kd树相当于不断地用垂直于坐标轴的超平面将k维空间切分,构成一系列的k维超矩形区域。

kd树就是把整个空间划分为特定的几个部分,然后在特定空间的部分内进行相关搜索操作。通常依次选择坐标轴对空间进行切分,选择训练实例点在选定坐标轴上的中位数为切分点,这样得到的kd树是平衡的,但平衡kd树的搜索效率未必是最优的。

构建kd树

输入:k维空间数据集T
输出:kd树
以数据集T={(2,3), (5,4), (9,6), (4,7), (8,1), (7,2)}为例构造平衡kd树。
划分空间为:
特征空间划分

首先选择切分坐标轴,计算数据集所有维度上的方差,选择最大值所在维度为split域,6个实例点在 x0 x1 维度上的方差分别为39,28.63,因此split域为 x0 。(数据方差最大表明沿坐标轴方向上数据点分散的比较开,这个方向上进行数据分割可以获得最好的分辨率)。 x0 轴上的中位数为7,x=7的超平面将数据集划分为两部分, x0 <7作为根结点(7,2)的左子树,其余为右子树。
接着再次选择切分坐标轴 xb b=(split + 1) % k,当前split为0,k代表数据维度,此处为2,因此选择 x1 作为切分坐标轴, x1 =4将左矩形划分为两个子矩形; x1 =6将右矩形划分为两个子矩形。
再次选择切分坐标轴 xb 时,b=(1+1)%2=0,切分坐标轴为 x0
如此递归得到如下所示kd树:
kd树实例
树结点:

class KDNode(object):
    def __init__(self, dom_elt, split, left_node, right_node):
        self.dom_elt = dom_elt # 保存结点数据
        self.split = split # 进行分割的维度
        self.left_node = left_node
        self.right_node = right_node

构造kd树:

class KDTree(object):

    def __init__(self, data_set):
        # 取数据维度
        k = data_set.shape[1]

        def create_node(split, data_set):
            if data_set.shape[0] == 0: return None
            # 按某列排序,并返回排序后原数组的索引
            sorted_index = np.argsort(data_set[:,split], axis=0)
            new_data_set = data_set[sorted_index]
            # 取中位数所在索引
            split_index = new_data_set.shape[0] // 2 # //为整除
            median_data = new_data_set[split_index]
            next_split = (split + 1) % k # 选择切分坐标轴
            return KDNode(median_data, split, create_node(next_split, new_data_set[:split_index]), create_node(next_split, new_data_set[split_index + 1:]))

        # 确定初始split域
        # 计算每个维度的数据的方差
        var_result = np.var(data_set, axis=0)
        split_num = list(var_result).index(var_result.max())
        self.root_node = create_node(split_num, data_set)
kd树的最近邻搜索

利用kd树可以省去大部分数据点的搜索,这里以最近邻(k=1)为例,同样的方法可以应用到k近邻范围搜索。
输入:已构造的kd树,目标点x
输出:x的最近邻

  1. 找出包含目标点x的叶结点:从根结点出发,递归地向下访问,若目标点x当前维的坐标小于被访问结点的切分点的坐标,则移动到左子结点,否则移动到右子结点,直到叶结点,以此叶结点为当前最近点
  2. 从叶结点开始递归地向上回退,在每个结点处进行以下操作:
    a. 若该实例点比当前最近点与目标点的距离更近,则以该实例点为当前最近点
    b. 检查该结点的另一子结点对应的区域是否与以目标点为球心、以目标点与当前最近点之间的距离为半径的超球体相交。若相交,则移动到另一子结点递归进行最近邻搜索;若不相交则向上回退。
  3. 回退到根结点时,搜索结束,当前最近点为目标点x的最近邻点。

根据上述构造的kd树,给定目标点(3, 4.5),搜索目标点的最近邻。

寻找目标点(3, 4.5)的最近邻,首先从根结点(7, 2)出发,切分坐标轴为 x0 ,3<7,移动到左子结点(5, 4),切分坐标轴为 x1 ,4.5>4,移动到右子结点(4, 7),该结点为包含目标点的叶结点,以该叶结点为当前最近点,目标点的最近邻一定在以目标点为中心并通过当前最近点的超球体内部
然后从叶结点出发,返回当前结点的父结点(5, 4):
该父结点比当前最近点(4, 7)更接近目标点,因此更新当前最近点为(5, 4);该父结点的另一子结点(2, 3)对应的区域与以目标点为中心、以当前最近点与目标点的距离为半径的超球体相交,移动到子结点(2 ,3)。
实例点(2, 3)比当前最近点(5, 4)距离目标点更近,更新当前最近点为(2, 3),该结点不存在子结点,因此向上回退到根结点(7, 2)。
根结点不比当前最近点距离目标点更近;且根结点的另一子结点(9, 6)对应的区域不与以目标点为中心、以目标点与当前最近点(2, 3)的距离为半径的超球体相交。回溯结束,返回当前最近点(2, 3)。
另外,(2, 3)与绿色超球体相交、(9, 6)不与蓝色超球体相交的依据是:
实例点(2, 3)的切分坐标轴为 xo ,由图可知 x0 =2的超平面与绿色超球体相交;而实例点(9, 6)的切分坐标轴为 x1 x1 =6的超平面与蓝色超球体不相交。

from collections import namedtuple

class KDNode(object):
    def __init__(self, dom_elt, split, left_node, right_node):
        self.dom_elt = dom_elt # 保存结点数据
        self.split = split # 进行分割的维度
        self.left_node = left_node
        self.right_node = right_node

find_result = namedtuple('Result_tuple', 'nearest_point nearest_dist nodes_visited')

class KDTree(object):

    def __init__(self, data_set):
        # 取数据维度
        k = data_set.shape[1]

        def create_node(split, data_set):
            if data_set.shape[0] == 0: return None
            # 按某列排序,并返回排序后原数组的索引
            sorted_index = np.argsort(data_set[:,split], axis=0)
            new_data_set = data_set[sorted_index]
            # 取中位数所在索引
            split_index = new_data_set.shape[0] // 2
            median_data = new_data_set[split_index]
            next_split = (split + 1) % k
            return KDNode(median_data, split, create_node(next_split, new_data_set[:split_index]), create_node(next_split, new_data_set[split_index + 1:]))

        # 确定split域
        var_result = np.var(data_set, axis=0)
        split_num = list(var_result).index(var_result.max())
        self.root_node = create_node(split_num, data_set)


    # KD树的最近邻搜索 (k=1)
    def find(self, target_point):
        # 取数据维度 (不同于k近邻的k)
        k = len(target_point)

        def travel(kd_node, target_point, min_dist):
            if kd_node is None: return find_result([0]*k, float('inf'), 0)            
            # 找到叶结点
            nodes_visited = 1
            split = kd_node.split # 进行分割的维度
            pivot = kd_node.dom_elt # 当前结点的数据
            if target_point[split] <= pivot[split]: 
                nearer_node = kd_node.left_node
                further_node = kd_node.right_node
            else:
                nearer_node = kd_node.right_node
                further_node = kd_node.left_node
            # 从叶结点回退,直到根结点
            nearest_point, nearest_dist, return_visited1 = travel(nearer_node, target_point, min_dist)
            nodes_visited += return_visited1
            if nearest_dist < min_dist:
                min_dist = nearest_dist
            if min_dist < abs(pivot[split] - target_point[split]): # 超球体与超平面是否相交
                return find_result(nearest_point, nearest_dist, nodes_visited) # 不相交直接向上回退
            # 相交则转到另一子结点
            temp_dist = np.sqrt(np.sum((pivot - target_point)**2)) # 计算欧式距离
            if temp_dist < nearest_dist:
                nearest_point = pivot
                nearest_dist = temp_dist
                min_dist = temp_dist
            # 检查另一个子结点是否为更近的点
            nearest_point2, nearest_dist2, return_visited2 =travel(further_node, target_point, min_dist)
            nodes_visited += return_visited2
            if nearest_dist2 < temp_dist:
                nearest_point = nearest_point2
                nearest_dist = nearest_dist2
            return find_result(nearest_point, nearest_dist, nodes_visited)
        return travel(self.root_node, target_point, float('inf'))

构造kd树并执行最近邻搜索:

kdtree =KDTree(np.array([[2,3], [5,4], [9,6], [4,7], [8,1], [7,2]]))
kdtree.find(np.array([3, 4.5]))

输出如下:

Result_tuple(nearest_point=array([2, 3]), nearst_dist=1.8027756377319946, nodes_visited=4)

参考文章:
http://www.cnblogs.com/21207-iHome/p/6084670.html
http://blog.csdn.net/likika2012/article/details/39619687

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值