机器学习之k-近邻

1. K近邻定义

k近邻算法,也成为KNN算法,是一种基本分类与回归算法。它在基本实现上,使用的是多数表决的惰性学习过程。也就是它实际上是基于记忆的学习方法。它并没有学出一个什么判别模型,其实也没有像贝叶斯那样算出一个新东西,而是简单的统计距离目标点最近的K个节点里数目最多的标签赋予目标点。就是这么一个简单的算法。我们这里给出一个最朴素的K近邻算法: 
K近邻算法 
输入:训练数据集T=(x1,y1),(x2,y2),...(xN,yN)T=(x1,y1),(x2,y2),...(xN,yN) 
输出:实例x所属的类y 
算法步骤: 
(1)根据给定的距离度量,在训练集T中找出与x最近邻的k个点,涵盖这k个点的x的邻域记作Nk(x)Nk(x) 
(2)在Nk(x)Nk(x)中根据分类决策规则,如多数表决决定x的类别y。

1.k近邻模型

k近邻模型的核心就是使用一种距离度量,获得距离目标点最近的k个点,根据分类决策规则,决定目标点的分类。

就是这么三句话,决定了k近邻模型的三个基本要素——距离度量、k值的选择、分类决策规则。

2. 距离度量

一个点和一个点之间的距离,无论是什么计算方式,基本上离不开LpLp距离。我们熟知的欧式距离,则是L2L2范式,也就是p=2的情况,而另一个很熟悉的距离曼哈顿距离,则是L1L1范式。LpLp距离的定义如下: 

Lp(xi,xj)=(l=1n|x(l)ix(l)j|p)1pLp(xi,xj)=(∑l=1n|xi(l)−xj(l)|p)1p

当然,如果p→∞的时候,就叫做切比雪夫距离了。 
除了这个闵可夫斯基距离集合外,还有另外的距离评估体系,例如马氏距离、巴氏距离、汉明距离,这些都是和概率论中的统计学度量标准相关。而像夹角余弦、杰卡德相似系数、皮尔逊系数等都是和相似度有关的。

因此,简单说来,各种“距离”的应用场景简单概括为,空间:欧氏距离,路径:曼哈顿距离,国际象棋国王:切比雪夫距离,以上三种的统一形式:闵可夫斯基距离,加权:标准化欧氏距离,排除量纲和依存:马氏距离,向量差距:夹角余弦,编码差别:汉明距离,集合近似度:杰卡德类似系数与距离,相关:相关系数与相关距离。

这其实只是个度量标准而已,应当根据数据特征选择相应的度量标准。

3. k值的选择

k值的选择也很有必要,因为k的选择小了,则近似误差会减小,但估计误差会增大;相反k的选择大了,则近似误差会增大,估计误差会减小。这一点,我们会在近似误差与估计误差那一部分进一步讲解。

大部分情况是依靠经验来选择,多试几次。

3.1.估计误差

估计误差我们应该在初中或者高中物理的时候就已经学过了,也许只是忘记了而已。估计误差主要包含四个部分:系统误差、随机误差、过失误差、精密度和精确度。

就我们K近邻来讲,如果K值比较小,那么例如像噪点,错误的数据,不恰当的度量标准以及数据本身的缺陷等,都会很大程度上影响最终的结果,而如果K值比较大,那么以上缺陷就会尽可能的平均,从而减小对最终结果的影响。

3.2.近似误差

近似误差与估计误差的描述对象不同,估计误差度量的是预测结果与最优结果的相近程度,而近似误差是度量与最优误差之间的相似程度。就K近邻算法来讲,K值越小,那么与目标点相近的点的标签对于其目标点的影响也就越大,其标签的一致性就越高,这样近似误差就会变小。

3.3.两者区别与联系

总而言之,近似误差指的是目标点对于其原样本点的可信度,误差越小,对于原样本点的信任度越高,也就是说,目标点可能只需要对最近的点确认一次就可以标注自己的标签,而无需去询问其他目标点。而估计误差则是原模型本身的真实性,也就是说,该模型所表现出的分类特性,是不是就是真实的分类特性,比如有噪点影响,有错误数据记录,或者本身数据分布就不是很好,都会是影响估计误差的因素,而询问的点越多,那么这些坏点对于目标点的标签影响就越小。

这就像是你向别人征求意见,你对于别人意见的采纳率越高,则别人意见的近似误差越小。而别人意见越符合实际情况,则估计误差越小。这么说应该有个大致的理解了吧。

4. 分类决策规则

k近邻的分类决策规则是最为常见的简单多数规则,也就是在最近的K个点中,哪个标签数目最多,就把目标点的标签归于哪一类。

实际上,也是可行的,也是唯一可行的分类决策规则。无论是全体一致规则(一票否决制)还是绝对多数规则,都不能在任何时候对目标点做出确切的预测,更不用提少数原则这种不靠谱的决策规则了。

3. K近邻的实现kd树

我们通过上述描述,应该清楚了一个K近邻算法的基本运作思想,由于没有训练过程,没有预测模型,使得K近邻算法的计算量十分巨大,因为它需要把所有的样本点都和目标点进行一次距离度量,很难适应大规模的数据样本。那么kd树就应运而生了。

1. kd树定义

kd树,指的是k-dimensional tree,是一种分割K维数据空间的数据结构,主要用于多维空间关键数据的搜索。kd树是二进制空间分割树的特殊情况。

索引结构中相似性查询有两种基本的方式:一种是范围查询,另一种是K近邻查询。范围查询就是给定查询点和查询距离的阈值,从数据集中找出所有与查询点距离小于阈值的数据;K近邻查询是给定查询点及正整数K,从数据集中找到距离查询点最近的K个数据,当K=1时,就是最近邻查询。

而对于这类问题,解决办法有两类:一类是线性扫描法,即将数据集中的点与查询点逐一进行距离比较,也就是穷举,缺点很明显,就是没有利用数据集本身蕴含的任何结构信息,搜索效率较低,第二类是建立数据索引,然后再进行快速匹配。因为实际数据一般都会呈现出簇状的聚类形态,通过设计有效的索引结构可以大大加快检索的速度。索引树属于第二类,其基本思想就是对搜索空间进行层次划分。根据划分的空间是否有混叠可以分为Clipping和Overlapping两种。前者划分空间没有重叠,其代表就是k-d树;后者划分空间相互有交叠,其代表为R树。

但是要说明的是kd树的朴素用法,只能解决最近邻算法,对于K近邻算法还需要改进后才能够使用,这里我们放到最后再讲。

2. kd树的构造算法

构造kd树的方法根据不同的决策规则分为很多种,但最终都是平衡二叉树。具体方法如下: 
1. 构造根节点,使根节点对应用于K维空间中包含所有实例点的超矩形区域。 
2. 通过下面的递归方法,不断切分K维空间,生成子节点: 
1) 在超矩形区域上选择一个坐标轴和该坐标上的一个切分点,确定一个超平面。 
2)以经过该点且垂直于该坐标轴做一个超平面,该超平面将当前的超矩形区域切分成左右两个子区域,实例被分到两个子区域。 
3.该过程直到子区域内无实例时终止(终止时的节点为子节点)。 
在此过程中将实例集合保存在相应的节点上。

在这个方法中,有两个部分时可以进行调节的,第一个部分就是选取的维度的顺序,另一个部分就是选取分割点的度量标准。

在第一部分,我们可以使用顺序采样,即从第1维,第2维,第n维一直到分割完毕为止。也可以使用最大方差所在的维度,也可以使用维度主次优先级为顺序,以此等等。

在第二部分,我们可以使用的是所在维度的中位数作为切分点,也可以使用中值作为切分点,以此等等。

这里,我们使用的是最朴素的方法,维度采用顺序采样,切分点选取中位数作为切分点,来描述一下kd树构造算法。

kd树构造算法 
输入:k维空间数据集T = x1,x2,,xNx1,x2,…,xN,其中,xi=(x(1)i,x(2)i,,x(k)i)i=1,2,,Nxi=(xi(1),xi(2),…,xi(k)),i=1,2,…,N 
输出:kd树 
开始:

  1. 构造根节点(根节点对应于包含T的K维空间的超矩形区域) 
    选择x(1)x(1)为坐标轴,以T中所有实例的x(1)x(1)坐标的中位数为切分点,这样,经过该切分点且垂直与x(1)x(1)的超平面就将超矩形区域切分成2个子区域。保存这个切分点为根节点。

  2. 重复如下步骤: 
    对深度为j的节点选择x(l)x(l)为切分的坐标轴,l=j(modk)+1l=j(modk)+1 ,以该节点区域中所有实例的x(l)x(l)坐标的中位数为切分点,将该节点对应的超平面切分成两个子区域。切分由通过切分点并与坐标轴x(l)x(l)垂直的超平面实现。保存这个切分点为一般节点。

  3. 直到两个子区域没有实例存在时停止。

    具体的例子大家可以看一下书,我们的重点不在于此。对于书上的例子,一句话概括为:偶数层以第一维为二分检索树,奇数层以第二维为二分检索树。对于n个实例的k维数据来说,建立kd-tree的时间复杂度为O(k×n×logn)。

    而对于kd树的插入删除等不在我们这堂课的讨论范围内,大家可以课后自己查阅资料,因为这方面比较复杂。

    对于kd树的插入来说,就相当于一个随时改变比较维度的二叉检索树:在偶数层比较x坐标值,而在奇数层比较y坐标值。当我们到达了树的底部,(也就是当一个空指针出现),我们也就找到了结点将要插入的位置。

    这一部分感兴趣的同学可以更深入的研究一下。

3. kd树的检索算法

既然已经建成了kd树了,那么kd树的检索算法就被提上议程,这次,我们使用的就是针对上面的kd树构建算法而写出的kd树检索算法:

kd树最近邻搜索算法 
输入:已构造的kd树:目标点x; 
输出:x的最近邻。

(1)在kd树中找出包含目标点x的叶结点:从根结点出发,递归的向下访问kd树。若目标点x当前维的坐标小于切分点的坐标,则移动到左子结点,否则移动到右子节点,直到子节点为叶结点为止。 
(2)以此叶节点为“当前最近点”。 
(3)递归的向上回退,在每个结点进行以下操作: 
(a) 如果该结点保存的实例点比当前最近点距离目标点更近,则以该实例点为“当前最近点”。 
(b) 当前最近点一定存在于该结点一个子结点对应的区域。检查该子结点的另一子结点对应的区域是否有更近的点。 
具体的,检查另一子结点对应的区域是否与以目标点为球心,以目标点与“当前最近点”间的距离为半径的超球体相交。 
如果相交,可能在另一个子结点对应的区域内存在距离目标点更近的点,移动到另一个子结点,接着,递归地进行最近邻搜索。 
如果不相交,向上回退。 
(4)当回退到根结点时,搜索结束,最后的“当前最近点”即为x的最近邻点。

这样,其实只要把握住了这么个几个部分,首先,是当前最近点的初始值的确定,第二,如何回溯比较,第三,如何搜索可能存在的解。

总而言之,若实例点随机分布,则KD树搜索的时间复杂度为O(logN),N为训练实例数。就具体而言,KD树更适用于训练实例数远大于空间维度的K近邻搜索。一般是20维以下的,效果比较好。

4. 关于K近邻算法的若干其他问题

关于K近邻算法还有很多更加深入的问题,我们在下面进行简要的讨论,在以后有时间时,我们会针对某些具体的问题,做出专题讲解。

1. K近邻算法与K-means聚类算法的区别

K近邻算法和K均值聚类算法看起来十分相似,不过还是有一些区别的,我们这里给出一张表:

KNNK-Means
KNN是分类算法K-Means是聚类算法
KNN是监督学习K-Means非监督学习
没有明显的前期训练过程有明显的前期训练过程
K的含义指的是判断依据来源个数K的含义是集合的分类数目

而这两者都用到了NN算法,一般使用kd树来实现。

2. 如何使用kd树来进行K近邻查找

这道题是课后习题第三题,网上说通过维护一个包含 k 个最近邻结点的队列来实现,就我个人想法而言,实际上,主要是看这些队列里的值从何而来。我认为,这些较近的点是来源于那些最近邻点可能存在的点的集合中,也就是最近邻点的父节点以及跨越了超球面的那些区域内的点及他们的父节点。

也就是说,只需要改动这么几个步骤,就是在与最近邻点相比较的所有节点都可以存入到k近邻队列中,然后队列未满时,其最短距离为∞,队列已满时,其最短距离为队列中最长的结点的距离。一旦新来的结点小于这个最长距离,则删除最长结点,插入新来的结点,并且更新队列的最短距离。

一般来讲,如果k比较小,那么常规kd树检索已经足够查到K个近邻点。但是如果发生了没有找全k个最小的,可以在另一半的树中查找剩下的近邻点。

三、K-近邻算法实现

from sklearn import neighbors
from sklearn import datasets

knn = neighbors.KNeighborsClassifier()
iris = datasets.load_iris()

# print(iris)
knn.fit(iris.data,iris.target)
prediction = knn.predict([[0.1,0.2,0.3,0.4]])
print(prediction)
[0]
import csv
import numpy as np
import math
import operator

# 加载数据集
def loadDataset(filename, split, trainingSet = [], testSet = []):
    with open(filename,"rt") as csvfile:
        lines = csv.reader(csvfile)
        dataSet = list(lines)
        for x in range(len(dataSet) - 1):
            for y in range(4):
                dataSet[x][y] = float(dataSet[x][y])
            if np.random.random() < split:
                trainingSet.append(dataSet[x])
            else:
                testSet.append(dataSet[x])

# 计算距离
def euclideanDistance(instance1, instance2, length):
    distance = 0
    for x in range(length):
        distance += pow((instance1[x] - instance2[x]) , 2)
    return math.sqrt(distance)

# 返回最近的 k 个实例
def getNeighbors(trainingSet, testInstance, k):
    distance = []
    length = len(testInstance) - 1
    for x in range(len(trainingSet)):
        distance.append((trainingSet[x],euclideanDistance(trainingSet[x],testInstance,length)))
    distance.sort(key=operator.itemgetter(1))
    neighbors = []
    for x in range(k):
        neighbors.append(distance[k][0])
    return neighbors

# 统计投票的多少,返回票数最多的类别
def getResponse(neighbors):
    classVotes = {}
    for x in range(len(neighbors)):
        response = neighbors[x][-1]
        if response in classVotes:
            classVotes[response] += 1
        else:
            classVotes[response] = 1
    sortedVotes = sorted(classVotes.items(),key=operator.itemgetter(1),reverse=True)
    return sortedVotes[0][0]

# 预测正确率
def getAccuracy(testSet,predictions):
    correct = 0
    for x in range(len(testSet)):
        if testSet[x][-1] == predictions[x]:
            correct += 1
    return (correct/float(len(testSet))) * 100.0

def main():
    split = 0.67
    trainingSet = []
    testSet = []
    loadDataset('irisdata.txt',split,trainingSet,testSet)
    print('traingingSet 的数目:'+ str(len(trainingSet))+ 'testSet 的数目:' + str(len(testSet)))
    predictions = []
    k = 3
    for x in range(len(testSet)):
        neighbors = getNeighbors(trainingSet, testSet[x], k)
        result = getResponse(neighbors)
        predictions.append(result)
        print ('>predicted=' + repr(result) + ', actual=' + repr(testSet[x][-1]))
    accuracy = getAccuracy(testSet,predictions)
    print('预测正确率:' + repr(accuracy)+'%')

if __name__ == '__main__':
    main()

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页