分类 :kNN(k nearest neighbour)最近邻算法(Python)

kNN算法概述

kNN算法是比较好理解,也比较容易编写的分类算法。

简单地说,kNN算法采用测量不同特征值之间的距离方法进行分类。

我们可以假设在一个N维空间中有很多个点,然后这些点被分为几个类。相同类的点,肯定是聚集在一起的,它们之间的距离相比于和其他类的点来说,非常近。如果现在有个新的点,我们不知道它的类别,但我们知道了它的坐标,那只要计算它和已存在的所有点的距离,然后以最近的k个点的多数类作为它的类别,则完成了它的分类。这个k就是kNN中的k值。

举个例子:我们知道地球是有经纬度的,中国人肯定绝大多数都集中在中国的土地上,美国人也一样多数都集中在自己的土地上。如果现在给我们某个人的坐标,让我们给它分类,判断他是哪国人。我们计算了他和世界上每个人的距离,然后取离他最近的k个人中最多国别的国别作为他的国别。这样我们就完成了他的国别分类。(当然也有可能一个外国人正好来中国游玩,我们错误的将他分类为中国人了,这个只是举例,不要在意这些细节啦 ^_^)

 

所以kNN算法无非就是计算一个未知点与所有已经点的距离,然后根据最近的k个点类别来判断它的类别。简单,粗暴,实用。

 

kNN算法的重点

既然我们已经了解kNN算法了,那我们应该也大概了解到这个算法的重点是什么了

(1)怎么度量邻近度

我们首先想到的肯定是点和点之间距离。但除了距离,其实我们也可以考虑两个点之间的相似度,越相似,就代表两个点距离越近。同理,我们也可以考虑相异度,越相异,就代表两个点距离越远。其实距离的度量就是相异性度量的其中一种。

(2)k值怎么取

k值的选取关乎整个分类器的性能。如果k值取得过小,容易受噪点的影响而导致分类错误。而k值取得过大,又容易分类不清,混淆了其他类别的点。

(3)数据的预处理

拿到数据,我们不能直接就开始套用算法,而是需要先规范数据。例如我们想通过一个人的年龄和工资来进行分类,很明显工资的数值远大于年龄,如果我们不对它进行一个统一的规范,必然工资这个特征会左右我们的分类,而让年龄这个特征无效化,这不是我们想看到的。

 

邻近度的度量

临近度的度量,主要考虑相似性和相异性的度量。

一般的,我们把相似度定义为s,常常在0(不相似)和1(完全相似)之间取值。而相异度d有时在0(不相异)和1(完全相异)之间取值,有时也在0和∞之间取值。

当相似度(相异度)落在区间[0,1]之间时,我们可以定义d = 1 - s(或 s = 1 - d)。另一种简单的方法是定义相似度为负的相异度(或相反)。

 

通常,具有若干属性的对象之间的邻近度用单个属性的邻近度的组合来定义,下图是单个属性的对象之间的邻近度。

 

下面我们讨论更复杂的涉及多个属性的对象之间的邻近性度量

1、距离

一维、二维、三维或高纬空间中两个点xy之间的欧几里得距离(Euclidean distance)$d$由如下公式定义:

$$d(\mathbf{x},\mathbf{y})=\sqrt{\sum_{k=1}^{n}(x_{k}-y_{k})^{2}}$$ 

其中,$n$是维数,而$x_{k}$和$y_{k}$分别是xy的第$k$个属性值(分量)。

欧几里得距离是最常用的距离公式。距离对特征都是区间或比率的对象非常有效。

 

2、二元数据的相似性度量

两个仅包含二元属性的对象之间的相似性度量也称为相似系数(similarity coefficient),并且通常在0和1直接取值,值为1表明两个对象完全相似,而值为0表明对象一点也不相似。

xy是两个对象,都由n个二元属性组成。这样的两个对象(即两个二元向量)的比较可生成如下四个量(频率):

$f_{00}=\mathbf{x}取0并且\mathbf{y}取0的属性个数$

$f_{01}=\mathbf{x}取0并且\mathbf{y}取1的属性个数$

$f_{10}=\mathbf{x}取1并且\mathbf{y}取0的属性个数$

$f_{11}=\mathbf{x}取1并且\mathbf{y}取1的属性个数$

简单匹配系数(Simple Matching Coefficient,SMC)一种常用的相似性系数是简单匹配系数,定义如下:

$$SMC=\frac{f_{11}+f_{00}}{f_{01}+f_{10}+f_{11}+f_{00}}$$

该度量对出现和不出现都进行计数。因此,SMC可以在一个仅包含是非题的测验中用来发现回答问题相似的学生。

Jaccard系数(Jaccard Coefficient)假定xy是两个数据对象,代表一个事务矩阵的两行(两个事务)。如果每个非对称的二元属性对应于商店的一种商品,则1表示该商品被购买,而0表示该商品未被购买。由于未被顾客购买的商品数远大于被其购买的商品数,因而像SMC这样的相似性度量将会判定所有的事务都是类似的。这样,常常使用Jaccard系数来处理仅包含非对称的二元属性的对象。Jaccard系数通常用符号J表示,由如下等式定义:

$$J=\frac{匹配的个数}{不设计0-0匹配的属性个数}=\frac{f_{11}}{f_{01}+f_{10}+f_{11}}$$

 

3、余弦相似度

文档的相似性度量不仅应当像Jaccard度量一样需要忽略0-0匹配,而且还必须能够处理非二元向量。下面定义的余弦相似度(cosine similarity)就是文档相似性最常用的度量之一。如果xy是两个文档向量,则

$$\cos(\mathbf{x},\mathbf{y})=\frac{\mathbf{x}\cdot\mathbf{y}}{|| \mathbf{x} || || \mathbf{y} ||}$$

其中,“▪”表示向量点积,$\mathbf{x}\cdot\mathbf{y}=\sum_{k=1}^{n}x_{k}y_{k}$,$||\mathbf{x}||$是向量x的长度,$||\mathbf{x}||=\sqrt{\sum_{k=1}^{n}x_{k}^{2}}=\sqrt{\mathbf{x}\cdot\mathbf{x}}$

余弦相似度公式还可以写为:

$$\cos(\mathbf{x},\mathbf{y})=\frac{\mathbf{x}}{||\mathbf{x}||}\cdot\frac{\mathbf{y}}{||\mathbf{y}||}=\mathbf{x}^{'}\cdot\mathbf{y}^{'}$$

xy被它们的长度除,将它们规范化成具有长度1。这意味在计算相似度时,余弦相似度不考虑两个数据对象的量值。(当量值是重要的时,欧几里得距离可能是一种更好的选择)

余弦相似度为1,则xy之间夹角为0度,xy是相同的;如果余弦相似度为0,则xy之间的夹角为90度,并且它们不包含任何相同的词。

 

4、广义Jaccard系数

 广义Jaccard系数可以用于文档数据,并在二元属性情况下归约为Jaccard系数。该系数用EJ表示:

$$EJ(\mathbf{x},\mathbf{y})=\frac{\mathbf{x}\cdot\mathbf{y}}{||\mathbf{x}||^{2}+||\mathbf{y}||^{2}-\mathbf{x}\cdot\mathbf{y}}$$

 

知道度量的方法后,我们还要考虑实际的邻近度计算问题

1、距离度量的标准化和相关性

距离度量的一个重要问题是当属性具有不同的值域时如何处理(这种情况通常称作“变量具有不同的尺度”)。前面,使用欧几里得距离,基于年龄和收入两个属性来度量人之间的距离。除非这两个属性是标准化的,否则两个人之间的距离将被收入所左右。

一个相关的问题是,除值域不同外,当某些属性之间还相关时,如何计算距离。当属性相关、具有不同的值域(不同的方差)、并且数据分布近似高斯(正态)分布时,欧几里得距离的拓广,Mahalanobis距离是有用。

$$mahalanobis(\mathbf{x},\mathbf{y})=(\mathbf{x}-\mathbf{y})\sum^{-1}(\mathbf{x}-\mathbf{y})^{T}$$

其中$\sum^{-1}$是数据协方差矩阵的逆。注意,协方差矩阵$\sum$是这样的矩阵,它的第$ij$个元素是第$i$个和第$j$个属性的协方差。

计算Mahalanobis距离的费用昂贵,但是对于其属性相关的对象来说是值得的。如果属性相对来说不相关,只是具有不同的值域,则只需要对变量进行标准化就足够了。

一般采用$d^{'}=(d-d_{min})/(d_{max}-d_{min})$来变化欧几米得距离的特征值域。

 

2、组合异种属性的相似度

前面的相似度定义所基于的方法都假定所有属性具有相同类型。当属性具有不同类型时,就需要更一般的方法。直截了当的方法是使用上文的表分别计算出每个属性之间的相似度,然后使用一种导致0和1之间相似度的方法组合这些相似度。总相似度一般定义为所有属性相似度的平均值。

不幸的是,如果某些属性是非对称属性,这种方法效果不好。处理该问题的最简单方法是:如果两个对象在非对称属性上的值都是0,则在计算对象相似度时忽略它们。类似的方法也能很好地处理遗漏值。

概括地说,下面的算法可以有效地计算具有不同类型属性的两个对象xy之间的相似度。修改该过程可以很轻松地处理相异度。

算法:

1:对于第$k$个属性,计算相似度$s_{k}(\mathbf{x},\mathbf{y})$,在区间[0,1]中

2:对于第$k$个属性,定义一个指示变量$\delta_{k}$,如下:

     $\delta_{k}=0$,如果第$k$个属性是非对称属性,并且两个对象在该属性上的值都是0,或者如果一个对象的第$k$个属性具有遗漏值

     $\delta_{k}=0$,否则

3:使用如下公式计算两个对象之间的总相似度:

$$similarity(\mathbf{x},\mathbf{y})=\frac{\sum_{k=1}^{n}\delta_{k}s_{k}(\mathbf{x},\mathbf{y})}{\sum_{k=1}^{n}\delta_{k}}$$

 

3、使用权值

在前面的大部分讨论中,所有的属性在计算临近度时都会被同等对待。但是,当某些属性对临近度的定义比其他属性更重要时,我们并不希望这种同等对待的方式。为了处理这种情况,可以通过对每个属性的贡献加权来修改临近度公式。

如果权$w_{k}$的和为1,则上面的公式变成:

$$similarity(\mathbf{x},\mathbf{y})=\frac{\sum_{k=1}^{n}w_{k}\delta_{k}s_{k}(\mathbf{x},\mathbf{y})}{\sum_{k=1}^{n}\delta_{k}}$$

欧几里得距离的定义也可以修改为:

$$d(\mathbf{x},\mathbf{y})=(\sum_{k=1}^{n}w_{k}|x_{k}-y_{k}|^{2})^{1/2}$$

 

算法代码

 算法伪码:

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

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

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

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

(5)返回前k个点出现频率最高的类别作为当前点的预测分类。

 

具体代码

以下代码都是博主根据自己的理解写的,因为才开始学习不久,如有代码的错误和冗余,请见谅,并同时欢迎指出,谢谢!

博主主要是根据Pandas和Numpy库来编写的,阅读的同学可能需要有一点这方面的基础。Pandas和Numpy库都是处理数据分析的最佳库,要想学好数据分析,还是需要好好学习这两个库的。

算法采用的是欧几里得距离,采用$d^{'}=(d-d_{min})/(d_{max}-d_{min})$来规范特征值

# -*- coding: utf-8 -*-

"""kNN最近邻算法最重要的三点:
   (1)确定k值。k值过小,对噪声非常敏感;k值过大,容易误分类
   (2)采用适当的临近性度量。对于不同的类型的数据,应考虑不同的度量方法。除了距离外,也可以考虑相似性。
   (3)数据预处理。需要规范数据,使数据度量范围一致。
"""

import pandas as pd
import numpy as np

class kNN:
    def __init__(self,X,y=None,test='YES'):
        """参数X为训练样本集,支持list,array和DataFrame;
        
           参数y为类标号,支持list,array,Series
           默认参数y为空值,表示类标号字段没有单独列出来,而是存储在数据集X中的最后一个字段;
           参数y不为空值时,数据集X中不能含有字段y
           
           参数test默认为'YES',表是将原训练集拆分为测试集和新的训练集 
        """
        if isinstance(X,pd.core.frame.DataFrame) != True:  #将数据集转换为DataFrame格式
            self.X = pd.DataFrame(X)
        else:
            self.X = X
        if y is None:                                      #将特征和类别分开
            self.y = self.X.iloc[:,-1]
            self.X = self.X.iloc[:,:-1]
            self.max_data = np.max(self.X,axis=0)          #获取每个特征的最大值,为下面规范数据用
            self.min_data = np.min(self.X,axis=0)          #获取每个特征的最小值,为下面规范数据用
            max_set = np.zeros_like(self.X); max_set[:] = self.max_data  #以每个特征的最大值,构建一个与训练集结构一样的数据集
            min_set = np.zeros_like(self.X); min_set[:] = self.min_data  #以每个特征的最小值,构建一个与训练集结构一样的数据集
            self.X = (self.X - min_set)/(max_set - min_set)  #规范训练集
        else:
            self.max_data = np.max(self.X,axis=0)
            self.min_data = np.min(self.X,axis=0)
            max_set = np.zeros_like(self.X); max_set[:] = self.max_data
            min_set = np.zeros_like(self.X); min_set[:] = self.min_data
            self.X = (self.X - min_set)/(max_set - min_set)
            if isinstance(y,pd.core.series.Series) != True:
                self.y = pd.Series(y)
            else:
                self.y = y      
        if test == 'YES':        #如果test为'YES',将原训练集拆分为测试集和新的训练集
            self.test = 'YES'    #设置self.test,后面knn函数判断测试数据需不需要再规范
            allCount = len(self.X)
            dataSet = [i for i in range(allCount)]
            testSet = []
            for i in range(int(allCount*(1/5))):
                randomnum = dataSet[int(np.random.uniform(0,len(dataSet)))]
                testSet.append(randomnum)
                dataSet.remove(randomnum)
            self.X,self.testSet_X = self.X.iloc[dataSet],self.X.iloc[testSet]
            self.y,self.testSet_y = self.y.iloc[dataSet],self.y.iloc[testSet]
        else:
            self.test = 'NO'
    
def getDistances(self,point): #计算训练集每个点与计算点的欧几米得距离 points = np.zeros_like(self.X) #获得与训练集X一样结构的0集 points[:] = point minusSquare = (self.X - points)**2 EuclideanDistances = np.sqrt(minusSquare.sum(axis=1)) #训练集每个点与特殊点的欧几米得距离 return EuclideanDistances
def getClass(self,point,k): #根据距离最近的k个点判断计算点所属类别 distances = self.getDistances(point) argsort = distances.argsort(axis=0) #根据数值大小,进行索引排序 classList = list(self.y.iloc[argsort[0:k]]) classCount = {} for i in classList: if i not in classCount: classCount[i] = 1 else: classCount[i] += 1 maxCount = 0 maxkey = 'x' for key in classCount.keys(): if classCount[key] > maxCount: maxCount = classCount[key] maxkey = key return maxkey
def knn(self,testData,k): #kNN计算,返回测试集的类别 if self.test == 'NO': #如果self.test == 'NO',需要规范测试数据(参照上面__init__) testData = pd.DataFrame(testData) max_set = np.zeros_like(testData); max_set[:] = self.max_data min_set = np.zeros_like(testData); min_set[:] = self.min_data testData = (testData - min_set)/(max_set - min_set) #规范测试集 if testData.shape == (len(testData),1): #判断testData是否是一行记录 label = self.getClass(testData.iloc[0],k) return label #一行记录直接返回类型 else: labels = [] for i in range(len(testData)): point = testData.iloc[i,:] label = self.getClass(point,k) labels.append(label) return labels #多行记录则返回类型的列表
def errorRate(self,knn_class,real_class): #计算kNN错误率,knn_class为算法计算的类别,real_class为真实的类别 error = 0 allCount = len(real_class) real_class = list(real_class) for i in range(allCount): if knn_class[i] != real_class[i]: error += 1 return error/allCount

 

下面利用sklearn库里的iris数据(sklearn是数据挖掘算法库),进行上述代码测试

 

from sklearn import datasets
sets = datasets.load_iris()  #载入iris数据集
X = sets.data  #特征值数据集
y = sets.target  #类别数据集
myknn = kNN(X,y)
knn_class = myknn.knn(myknn.testSet_X,4)
errorRate = myknn.errorRate(knn_class,myknn.testSet_y)

 

 

kNN算法到此结束。如果发现有什么问题,欢迎大家指出。

 

 

posted @ 2018-01-07 13:42  浮生未stay  阅读(16023)  评论(0编辑  收藏  举报