前言

k邻近算法(k-nearest)是一种判别模型,解决分类问题和回归问题,以分类问题为主,在此我们也主要介绍分类问题中的k近邻算法。

k近邻算法的输入为实例的特征向量,对应予特征空间中的点;输出为实例的类别,可以取多类,(前面我们介绍的三种方法主要是解决二分类问题)。

k近邻算法假设给定一个训练数据集,其中的实例类别已定。分类时,对新的实例,根据其k个最近邻的训练实例的类别,通过多数表决等决策方法进行类别预测。因此,k近邻算法不具有显示的学习过程。

k近邻算法实际上利用训练数据集对特征向量空间进行划分,并作为其分类的“模型”。

k值的选择、距离度量以及分类决策规则是k近邻算法的三个基本要素。

算法概述

k近邻算法还是比较容易理解的,在此我们不做具体解释,后面我们只对一些细节进行说明。

直接贴图: 


统计学习方法---KNN(K近邻)_K近邻

模型

k近邻模型实质上是一个空间划分模型。根据训练样本自身的特征,通过距离公式计算,将训练数据集组成空间整体划分成M个字空间(M为类别数)。利用测试集进行测试评估模型的好快,以调整k的选择或者距离方法的选择。在此,经常使用交叉验证的方法。

距离度量

特征空间中两个实例点的距离是两个实例点相似程度的反映。在此我们介绍一些我们经常用到的一些距离公式:

直接上图: 


统计学习方法---KNN(K近邻)_感知机学习算法_02

统计学习方法---KNN(K近邻)_K近邻_03

上图表明,不同的距离计算方法,对于同一个点(远点)计算得到的最近邻点是不同的,下面的例子也可以证明:


统计学习方法---KNN(K近邻)_knn_04

k值的选择

k值的选择会对k近邻算法的结果产生重大影响。

如果k值选择的较小,则会有:

1)优点:“学习”的近似误差会减小,只与输入实例较近的实例才会对预测结果起作用。
2)缺点:“学习”的估计误差会增大,预测结果会对近邻的实例点非常敏感,因为k值较小,邻近点比较少(也就是说能够投票的人比较少),所以敏感(投票结果只倚重这几个人,如果这几个人都是一派人士,最终结果可想而知了)。如果邻近的实例点恰巧是噪音,预测就会出错。

k值的减小意味着整体模型变得比较复杂(划分较多),容易发生过拟合。

如果选择的k值较大,则会有:

1)优点:可以减少学习的估计误差,因为能够投票的人数较多,更具有说服力,所以投票结果会比较符合实际情况(预测误差较小)。
2)缺点:增大了近似误差。因为投票的人多了,每个人都会有投票误差,所以总误差就会增大。这时候,与输入实例比较远的(不相似的)训练实例也会对预测结果起作用,使得预测发生错误。

k值的增大意味着模型变得简单。如果k=N,那么无论输入什么,都将简单的预测它属于在训练实例中最多的类,(此时,我想到一个词,人多势众)。这个时候,模型过于简单,完全忽略了实例中大量的有用信息,只考虑了类别数量,这是不可取的。

分类决策规则

k近邻算法中的分类决策规则往往是多数表决,即由输入实例的k个近邻的训练实例中的多数类决定输入实例的类别。

统计学习方法---KNN(K近邻)_感知机学习算法_05

K近邻算法的实现---kd树

实现k近邻算法的时候,主要考虑的问题是如何对训练数据进行快速k近邻搜索,这点在特征空间的维数大以及训练数据容量大的时候,很重要!

k近邻算法最简单的实现方法是线性扫描(linear scan),这时候需要计算输入实例与每一个训练实例的距离,当训练集很大的时候,计算非常耗时,这种方法是不可行的。

为了提高k近邻搜索的效率,可以考虑使用特殊的结构来存储训练数据,以减少计算距离的次数,具体的方法很多,kd树便是其中的一种,其思路还是比较简单易理解的。我不打算在此复述。

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

直接贴图: 

统计学习方法---KNN(K近邻)_knn_06


统计学习方法---KNN(K近邻)_K近邻_07

给出实例,加深理解:

统计学习方法---KNN(K近邻)_python_08


统计学习方法---KNN(K近邻)_knn_09

最终形成的kd树为: 


统计学习方法---KNN(K近邻)_python_10

kd搜索

根据上一部分构造出的kd树,进行搜索获得k近邻节点,利用kd树可以省去对大部分数据点的搜索,不需要一一计算进行比较,从而减少计算量,提高算法效率。

以最近邻为例(k=1),给定一个目标点,搜索其最近邻。首先找到包含目标点的叶节点(最小的包含目标点的超矩形区域);然后从该叶节点出发,依次回退到父节点;不断查找与目标节点最邻近的节点,当确定不可能存在更近的节点时终止。这样搜索就被限制在空间的局部区域上,效率大为提高。

在此直接贴图: 

统计学习方法---KNN(K近邻)_K近邻_11


统计学习方法---KNN(K近邻)_python_12

给出例子,加深理解:

统计学习方法---KNN(K近邻)_感知机学习算法_13

    KNN算法实现

K近邻算法是一种思想极其简单,而分类效果比较优秀的分类算法,最重要的是该算法是很多高级机器学习算分基础,并且在后面我们将要学习的集成算法中,k近邻也经常被用来做基础分类器。它的基本思想我们已经在上节介绍过了,在此我们不在赘述,本节主要讲一下有关它的拓展知识以及实现。

模型:所有的空间划分,判别模型
策略:距离最近的k个邻居
方法:多数表决(注意,这里没有可计算的优化方法,可能我也没有说清楚,自己体会一下)
训练过程:交叉验证的方法,来调整k值得选择,使得最终的预测估计误差最小。

拓展

  KNN算法思想简单容易实现,但是它是一种没有优化(因为分类决策为多数投票)的暴力方法(线性搜索方法),所以当数据量比较大的时候,算法效率容易达到瓶颈。例如,样本个数为N,特征维数位D的时候,该算法的时间复杂度为O(D*N)。所以,通常情况下,KNN的实现会把训练数据建成Kd-tree(K-dimensional tree,kd-tree搜索方法),构建过程很快,甚至不用计算D为欧氏距离,而搜索速度高达O(D*log(N))。但是建立kd-tree搜索方法也有一个缺点是:当D维度过高的时候,会产生所谓的“维度灾难”,最终的效率会降低到与暴力法一样。 
kd树更适合用于训练实例树远大于空间维数时的k近邻搜索。当空间维数接近于训练实例数的时候,它的效率会迅速下降,几乎接近于线性扫描。

  当维度D>20以后,最好使用更高效率的ball-tree, 其时间复杂度为仍为O(D*log(N))。

人们经过长期的实践发现,KNN算法适用于样本分类边界不规则的情况。由于KNN主要依靠周围有限的邻近样本,而不是靠判别类域的方法来确定所属类别,因此对于类域的交叉或重叠较多的待分样本集来说,KNN算法比其他方法要更有效。

该算法在分类时有个主要的不足是,当样本不平衡的时候,如果一个类的样本容量很大,而其他类样本容量很小的时候,有可能导致当输入一个新样本的时候,该样本的K个邻居中大容量类的样本占多数。因此可以采用权值的方法(和该样本距离小的邻居取值大)来改进。 
  该方法的另一个不足之处是计算量比较大,因为对每一个待分类的文本都要计算它到全体已知样本的距离(当然了我们也提到过多次kd-tree的优化算法,可以避免这种情况),才能求得它的K个最近邻。目前常用的解决方法是事先对已知样本点进行剪辑,事先去除对分类作用不大的样本。该算法比较适用于样本容量较大的类域的自动分类,而那些样本容量较小的类域采用这种方法比较容易产生误分。

算法描述

总的来说就是我们已经存在了一个带标签的数据库,然后输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似(最近邻)的分类标签。一般来说,只选择样本数据库中前k个最相似的数据。最后,选择k个最相似数据中出现次数最多的分类。其算法描述如下:

1)计算已知类别数据集中的点与当前点之间的距离;

2)按照距离递增次序排序;

3)选取与当前点距离最小的k个点;

4)确定前k个点所在类别的出现频率;

5)返回前k个点出现频率最高的类别作为当前点的预测分类。
#算法一 调用sklearn中的方法
# -*- encoding:utf-8-*-

'''
调用sklearn中的方法,并且使用sklearn中自己的数据
@author:Ada
'''
print __doc__
import numpy as np
from sklearn import neighbors,datasets

#下载数据
#64维 1797个样本
datas=datasets.load_digits()
totalNum=len(datas.data)#1797
#print totalNum
#print len(datas.data[0])

#分割数据集
#选出80%样本作为训练集,剩余的20%作为测试集
trainNum=int(0.8*totalNum)
trainX=datas.data[0:trainNum]
trainY=datas.target[0:trainNum]
testX=datas.data[trainNum:]
testY=datas.target[trainNum:]

#设置k值,一般情况下k<20
n_neighbors=10

#建立分类器
clf=neighbors.KNeighborsClassifier(n_neighbors=n_neighbors,weights='uniform',algorithm='auto')
#训练分类器
clf.fit(trainX,trainY)

#利用训练好的分类器进行预测
answer=clf.predict(testX)

print "误分率为:%.2f%%"%((1-np.sum(answer==testY)/float(len(testY)))*100)

#说明:
#KNeighborsClassifier可以设置3种算法:‘brute’,‘kd_tree’,‘ball_tree’。
#如果不知道用哪个好,设置‘auto’让KNeighborsClassifier自己根据输入去决定。

关于kd-tree和ball-tree基础参见​​这里​

#方法二:自己实现一个线性KNN算法
from numpy import *
import operator

#create a dataset which coantains 4 samples with 2 classes

def createDataSet():
#create a matrix:each row as a sample
group=array([[1.5,1.4],[1.6,1.5],[0.1,0.2],[0.0,0.1]])# each sample has two features
labels=['A','A','B','B']#four samples with two labels
return group,labels

#calssify using KNN
def KNNClassify(newInput,dataSet,labels,k):
numSamples=dataSet.shape[0]#shape[0] stands for the num of row

#Step 1:calculate Euclidean distance
#tile(A,reps):construce an array by repeating A reps times
# eg:
#A=[[1.2,1.0]]
#diff =tile(A,(4,3))#也就是说把A看成一个整体,把A赋值成4行3列
# diff:
#[[ 1.2 1. 1.2 1. 1.2 1. ]
# [ 1.2 1. 1.2 1. 1.2 1. ]
# [ 1.2 1. 1.2 1. 1.2 1. ]
# [ 1.2 1. 1.2 1. 1.2 1. ]]
#the following copy numSamples rows for dataSet
diff =tile(newInput,(numSamples,1))-dataSet#计算测试集与每个训练集的差值
squareDiff=diff**2#差的平方
squareDist=sum(squareDiff,axis=1)#按行求和,也就是对每个样本进行操作计算,平方和
distance=squareDist**0.5

#Step 2: sort the distance by asce
#argsort() returns the distances that would sort an array in a ascending order
sortedDistance=argsort(distance)

classCount={}#定义一个字典(对应到每个样本)
for i in xrange(k):
#Step 3:choose the min k distance
voteLabel=labels[sortedDistance[i]]

# Step 4:count the times labels occur
# when the key voteLabel is not in dictionary classCount ,get()
# will return 0
classCount[voteLabel]=classCount.get(voteLabel,0)+1

# Step 5:the max voted class will return
maxCount=0
for key,value in classCount.items():
if value>maxCount:
maxCount=value
maxIndex=key

return maxIndex


if __name__ == '__main__':
dataSet,labels=createDataSet()
testData=array([1.2,1.0])
k=3
predictlabel=KNNClassify(testData,dataSet,labels,k)
print '测试数据为:',testData,'预测结果类别为:',predictlabel

testData=array([0.1,0.3])
predictlabel=KNNClassify(testData,dataSet,labels,k)
print '测试数据为:',testData,'预测结果类别为:',predictlabel

实验结果为:

测试数据为: [ 1.2  1. ] 预测结果类别为: A
测试数据为: [ 0.1 0.3] 预测结果类别为: B