K—NN的学习和实践

 什么是KNN算法呢?顾名思义,就是K-Nearest neighbors Algorithms的简称。我们可能都知道最近邻算法,它就是KNN算法在k=1时的特例,也就是寻找最近的邻居。我们从名字可以知道我们要寻找邻居,但是为什么要寻找邻居,如何选取邻居,选取多少邻居,怎么样去寻找我们想要的邻居,以及如何利用邻居来解决分类问题这是KNN算法需要解决的几大问题,好了闲话不多说,进入正题。

   首先我要说的是为什么我们要寻找邻居啊,古话说的好,人以类聚,物以群分,要想知道一个人怎么样,去看看他的朋友就知道了,其实这个过程就蕴含了KNN的算法核心思想,我们如果要判断一个样本点的类别,去看看和它相似的样本点的类别就行了, If it walks like a duck, quacks like a duck, then it is probably a duck,如图1所示:

  好了,在深入了解KNN之前有必要了解一下分类算法的大致情况以及其完整定义。图2所示的是一般的分类模型建立的步骤,分类一般分为两种:

  积极学习法 (决策树归纳):先根据训练集构造出分类模型,根据分类模型对测试集分类。

  消极学习 (基于实例的学习法):推迟建模, 当给定训练元组时,简单地存储训练数据 (或稍加处理,一直等到给定一个测试元组。

   消极学习法在提供训练元组时只做少量工作,而在分类或预测时做更多的工作。KNN就是一种简单的消极学习分类方法,它开始并不建立模型,而只是对于给定的训练实例点和输入实例点,基于给定的邻居度量方式以及结合经验选取合适的k值,计算并且查找出给定输入实例点的k个最近邻训练实例点,然后基于某种给定的策略,利用这k个训练实例点的类来预测输入实例点的类别。算法的过程如图3所示:


     

   了解了KNN的主体思想以后,接下来我们就来逐一的探讨和回答我在第一章所提出的四个问题,第一个就是如何度量邻居之间的相识度,也就是如何选取邻居的问题,我们知道相似性的度量方式在很大程度上决定了选取邻居的准确性,也决定了分类的效果,因为判定一个样本点的类别是要利用到它的邻居的,如果邻居都没选好,准确性就无从谈起。因此我们需要用一个量来定量的描述邻居之间的距离,也可以形象的表述为邻居之间的相似度,具体的距离度量方式有很多,不同的场合使用哪种需要根据不同问题具体探讨,具体的我就不罗嗦,在这篇博文http://www.cnblogs.com/v-July-v/archive/2012/11/20/3125419.html中有详细的阐述。以下给出了使用三种距离(欧式距离,曼哈顿距离,还有切比雪夫距离)的对glass数据集测试的例子,测试结果如图4所示:红线指的是实验使用的距离度量方式,黄线指的是实验的结果,可以看出使用曼哈顿距离分类效果明显好于其他两种。

   在给定了度量方式以后,我们自然而然会遇到一个问题就是到底要找多少个邻居才合适了,如图5所示 ,X是待分类样本,‘,’和‘-’是样本类别属性,如果K选大了的话,可能求出来的k最近邻集合可能包含了太多隶属于其它类别的样本点,最极端的就是k取训练集的大小,此时无论输入实例是什么,都只是简单的预测它属于在训练实例中最多的累,模型过于简单,忽略了训练实例中大量有用信息。如果K选小了的话,结果对噪音样本点很敏感。那么到底如何选取K值,其实我在前面也说了,其实完全靠经验或者交叉验证(一部分样本做训练

集,一部分做测试集)的方法,就是是K值初始取一个比较小的数值,之后不段来调整K值的大小来时的分类最优,得到的K值就是我们要的,但是这个K值也只是对这个样本集是最优的。一般采用k为奇数,跟投票表决一样,避免因两种票数相等而难以决策。下面我们可以通过交叉验证的方式求出最合适的K值,对iris数据(UCI Machine Learning Repository下载)用kNN算法进行分类,通过交叉验证(10次)的方式,对k取不同值时进行了实验,实验结果如图5所示,其中红线指的是实验选用的K值,黄线指的是实验的结果,我们发现在我所选取的k值中,当k=17时效果最好,在k=1时,即用最近邻来进行分类的效果也不错,实验结果呈现一个抛物线,与我们之前分析的结果相吻合。

    好了,到这一步工作已经做了一半了,接下来就是如何去寻找这k个邻居了,因为对每一个待测样本点来说,我们都要对整个样本集逐一的计算其与待测点的距离,计算并存储好以后,接下来就是查找K近邻,这是最简单,也是最笨的方法,计算量太大了。因此KNN的一大缺点需要存储全部训练样本,以及繁重的距离计算量,有没有简单的一点的方法可以避免这种重复的运算啊,改进的方案有两个,一个是对样本集进行组织与整理,分群分层,尽可能将计算压缩到在接近测试样本邻域的小范围内,避免盲目地与训练样本集中每个样本进行距离计算。另一个就是在原有样本集中挑选出对分类计算有效的样说本,使样本总数合理地减少,以同时达到既减少计算量,又减少存储量的双重效果。KD树方法采用的就是第一个思路,关于KD树及其扩展可以参看博文http://www.cnblogs.com/v-July-v/archive/2012/11/20/3125419.html,它对其进行了详细的阐述,我就不啰嗦了。我想补充的是压缩近邻算法,它采用的思路是第二种方案,利用现有样本集,逐渐生成一个新的样本集,使该样本集在保留最少量样本的条件下,仍能对原有样本的全部用最近邻法正确分类,那么该样本集也就能对待识别样本进行分类,并保持正常识别率。它的步骤如下:

   首先定义两个存储器,一个用来存放即将生成的样本集,称为Store;另一存储器则存放原样本集,称为Grabbag。其算法是:

1.   初始化Store是空集,原样本集存入Grabbag;从Grabbag中任意选择一样本放入Store中作为新样本集的第一个样本。

2.   样本集生成。在Grabbag中取出第i个样本用Store中的当前样本集按最近邻法分类。若分类错误,则将该样本从Grabbag转入Store中,若分类正确,则将该样本放回Grabbag中。

3.   结束过程。若Grabbag中所有样本在执行第二步时没有发生转入Store的现象,或Grabbag已成空集,则算法终止,否则转入第二步。 

   当然解决的方案很多,还有比如剪辑近邻法,快速搜索近邻法等等很多,就不一一介绍了。下面测试了一下不同最近邻搜索算法(线性扫描,kd树,Ball树,Cover树)所花费的时间,如表1所示:

    到这一步基本上是万事俱备,只欠东风啦。K近邻(通俗的来说就是某人的k个最要好的朋友都找出来啦)都求出来啦,接下来就是要朋友们利用手中的投票器为其投票啦。一般的做法就是一人一票制,少数服从多数的选举原则,但是当和我测试对象离的近的数量少,而离得远的数量多时,这种方法可能就要出错啦,那咋办呢,看过歌唱选秀节目的人应该清楚,评审分为两种,一种是大众评审一人一票,一种是专家评审,一人可能有很多票,我们也可以借鉴这个思想,为每个邻居赋予一定的投票权重,通过它们与测试对象距离的远近来相应的分配投票的权重,最简单的就是取两者距离之间的倒数,距离越小,越相似,权重越大,将权重累加,最后选择累加值最高类别属性作为该待测样本点的类别。我用不同的权重方式对UCI中的glass数据集进行测试,图7显示的是直接不采用权重的实验结果,图8显示的是权重为距离的倒数,图9显示的是权重为1减去归一化后的距离,红线指的是实验使用的权重赋值方式,“0”指的是不采用权重,“0 -I”指的是取距离倒数,“0-F”指的是1减去归一化后的距离,深红线指的是实验的结果,我们可以看出采用了权重的总体上来说比不使用权重要好。

    至此关于KNN算法的描述就到此结束了。可以看出算法的思想是十分简单的,我们自然而然的就会想这个算法的准确率到底是多少,有没有啥科学的证明,其实最初的近邻法是由CoverHart1968年提出的,随后得到理论上深入的分析与研究,是非参数法中最重要的方法之一,它在论文Nearest Neighbor Pattern Classification中给出了算法准确率的相信描述。最近邻法的错误率是高于贝叶斯错误率的,   其中代表的是贝叶斯误差率,由于一般情况下P*很小,因此又可粗略表示成:对于kNN来说,当样本数量N→∞的条件下,k-近邻法的错误率要低于最近邻法,具体如图10所示:

    最后来一下小小的总结,KNN是典型的非参数法,是一个非常简单,性能优秀的分类算法,人们正在从不同角度提出改进KNN算法,推动了KNN算法的研究工作,使KNN算法的研究得到了快速的发展。

实践:

算法步骤:

step.1---初始化距离为最大值

step.2---计算未知样本和每个训练样本的距离dist

step.3---得到目前K个最临近样本中的最大距离maxdist

step.4---如果dist小于maxdist,则将该训练样本作为K-最近邻样本

step.5---重复步骤2、3、4,直到未知样本和所有训练样本的距离都算完

step.6---统计K-最近邻样本中每个类标号出现的次数

step.7---选择出现频率最大的类标号作为未知样本的类标号

 

 

KNN的matlab简单实现代码

function target=KNN(in,out,test,k)
% in:       training samples data,n*d matrix
% out: training samples' class label,n*1
% test:     testing data
% target:   class label given by knn
% k:        the number of neighbors
ClassLabel=unique(out);
c=length(ClassLabel);
n=size(in,1);
% target=zeros(size(test,1),1);
dist=zeros(size(in,1),1);
for j=1:size(test,1)
    cnt=zeros(c,1);
    for i=1:n
        dist(i)=norm(in(i,:)-test(j,:));
    end
    [d,index]=sort(dist);
    for i=1:k
        ind=find(ClassLabel==out(index(i)));
        cnt(ind)=cnt(ind)+1;
    end
    [m,ind]=max(cnt);
    target(j)=ClassLabel(ind);
end


C++ 实现 :

//    KNN.cpp     K-最近邻分类算法
//

#include <stdlib.h>
#include <stdio.h>
#include <memory.h>
#include <string.h>
#include <iostream>
#include <math.h>
#include <fstream>
using namespace std;

//
//    宏定义
//

#define  ATTR_NUM  4                        //属性数目
#define  MAX_SIZE_OF_TRAINING_SET  1000      //训练数据集的最大大小
#define  MAX_SIZE_OF_TEST_SET      100       //测试数据集的最大大小
#define  MAX_VALUE  10000.0                  //属性最大值
#define  K  7
//结构体
struct dataVector {
 int ID;                      //ID号
 char classLabel[15];             //分类标号
 double attributes[ATTR_NUM]; //属性 
};
struct distanceStruct {
 int ID;                      //ID号
 double distance;             //距离
 char classLabel[15];             //分类标号
};


//
//    全局变量
//

struct dataVector gTrainingSet[MAX_SIZE_OF_TRAINING_SET]; //训练数据集
struct dataVector gTestSet[MAX_SIZE_OF_TEST_SET];         //测试数据集
struct distanceStruct gNearestDistance[K];                //K个最近邻距离
int curTrainingSetSize=0;                                 //训练数据集的大小
int curTestSetSize=0;                                     //测试数据集的大小

//
//    求 vector1=(x1,x2,...,xn)和vector2=(y1,y2,...,yn)的欧几里德距离
//

double Distance(struct dataVector vector1,struct dataVector vector2)
{
 double dist,sum=0.0;
 for(int i=0;i<ATTR_NUM;i++)
 {
  sum+=(vector1.attributes[i]-vector2.attributes[i])*(vector1.attributes[i]-vector2.attributes[i]);
 }
 dist=sqrt(sum);
 return dist;
}

//
//    得到gNearestDistance中的最大距离,返回下标
//

int GetMaxDistance()
{
 int maxNo=0;
 for(int i=1;i<K;i++)
 {
  if(gNearestDistance[i].distance>gNearestDistance[maxNo].distance) maxNo = i;
 }
    return maxNo;
}

//
//    对未知样本Sample分类
//

char* Classify(struct dataVector Sample)
{
 double dist=0;
 int maxid=0,freq[K],i,tmpfreq=1;;
 char *curClassLable=gNearestDistance[0].classLabel;
 memset(freq,1,sizeof(freq));
 //step.1---初始化距离为最大值
 for(i=0;i<K;i++)
 {
  gNearestDistance[i].distance=MAX_VALUE;
 }
 //step.2---计算K-最近邻距离
 for(i=0;i<curTrainingSetSize;i++)
 {
  //step.2.1---计算未知样本和每个训练样本的距离
  dist=Distance(gTrainingSet[i],Sample);
  //step.2.2---得到gNearestDistance中的最大距离
  maxid=GetMaxDistance();
  //step.2.3---如果距离小于gNearestDistance中的最大距离,则将该样本作为K-最近邻样本
  if(dist<gNearestDistance[maxid].distance) 
  {
   gNearestDistance[maxid].ID=gTrainingSet[i].ID;
   gNearestDistance[maxid].distance=dist;
   strcpy(gNearestDistance[maxid].classLabel,gTrainingSet[i].classLabel);
  }
 }
 //step.3---统计每个类出现的次数
 for(i=0;i<K;i++)  
 {
  for(int j=0;j<K;j++)
  {
   if((i!=j)&&(strcmp(gNearestDistance[i].classLabel,gNearestDistance[j].classLabel)==0))
   {
    freq[i]+=1;
   }
  }
 }
 //step.4---选择出现频率最大的类标号
 for(i=0;i<K;i++)
 {
  if(freq[i]>tmpfreq)  
  {
   tmpfreq=freq[i];
    curClassLable=gNearestDistance[i].classLabel;
  }
 }
 return curClassLable;
}

//
//    主函数
//

void main()
{   
 char c; 
    char *classLabel="";
 int i,j, rowNo=0,TruePositive=0,FalsePositive=0;
 ifstream filein("iris.data");
 FILE *fp;
 if(filein.fail()){cout<<"Can't open data.txt"<<endl; return;}
 //step.1---读文件 
 while(!filein.eof()) 
 {
  rowNo++;//第一组数据rowNo=1
  if(curTrainingSetSize>=MAX_SIZE_OF_TRAINING_SET) 
  {
   cout<<"The training set has "<<MAX_SIZE_OF_TRAINING_SET<<" examples!"<<endl<<endl;
   break ;
  }  
  //rowNo%3!=0的100组数据作为训练数据集
  if(rowNo%3!=0)
  {   
   gTrainingSet[curTrainingSetSize].ID=rowNo;
   for(int i = 0;i < ATTR_NUM;i++) 
   {     
    filein>>gTrainingSet[curTrainingSetSize].attributes[i];
    filein>>c;
   }   
   filein>>gTrainingSet[curTrainingSetSize].classLabel;
   curTrainingSetSize++;
   
  }
  //剩下rowNo%3==0的50组做测试数据集
  else if(rowNo%3==0)
  {
   gTestSet[curTestSetSize].ID=rowNo;
   for(int i = 0;i < ATTR_NUM;i++) 
   {    
    filein>>gTestSet[curTestSetSize].attributes[i];
    filein>>c;
   }  
   filein>>gTestSet[curTestSetSize].classLabel;
   curTestSetSize++;
  }
 }
 filein.close();
 //step.2---KNN算法进行分类,并将结果写到文件iris_OutPut.txt
 fp=fopen("iris_OutPut.txt","w+t");
 //用KNN算法进行分类
 fprintf(fp,"************************************程序说明***************************************\n");
 fprintf(fp,"** 采用KNN算法对iris.data分类。为了操作方便,对各组数据添加rowNo属性,第一组rowNo=1!\n");
 fprintf(fp,"** 共有150组数据,选择rowNo模3不等于0的100组作为训练数据集,剩下的50组做测试数据集\n");
 fprintf(fp,"***********************************************************************************\n\n");
 fprintf(fp,"************************************实验结果***************************************\n\n");
 for(i=0;i<curTestSetSize;i++)
 {
        fprintf(fp,"************************************第%d组数据**************************************\n",i+1);
  classLabel =Classify(gTestSet[i]);
     if(strcmp(classLabel,gTestSet[i].classLabel)==0)//相等时,分类正确
  {
   TruePositive++;
  }
  cout<<"rowNo: ";
  cout<<gTestSet[i].ID<<"    \t";
  cout<<"KNN分类结果:      ";

  cout<<classLabel<<"(正确类标号: ";
  cout<<gTestSet[i].classLabel<<")\n";
  fprintf(fp,"rowNo:  %3d   \t  KNN分类结果:  %s ( 正确类标号:  %s )\n",gTestSet[i].ID,classLabel,gTestSet[i].classLabel);
  if(strcmp(classLabel,gTestSet[i].classLabel)!=0)//不等时,分类错误
  {
  // cout<<"   ***分类错误***\n";
   fprintf(fp,"                                                                      ***分类错误***\n");
  }
  fprintf(fp,"%d-最临近数据:\n",K);
  for(j=0;j<K;j++)
  {
  // cout<<gNearestDistance[j].ID<<"\t"<<gNearestDistance[j].distance<<"\t"<<gNearestDistance[j].classLabel[15]<<endl;
   fprintf(fp,"rowNo:  %3d   \t   Distance:  %f   \tClassLable:    %s\n",gNearestDistance[j].ID,gNearestDistance[j].distance,gNearestDistance[j].classLabel);
  }
  fprintf(fp,"\n"); 
 }
    FalsePositive=curTestSetSize-TruePositive;
 fprintf(fp,"***********************************结果分析**************************************\n",i);
 fprintf(fp,"TP(True positive): %d\nFP(False positive): %d\naccuracy: %f\n",TruePositive,FalsePositive,double(TruePositive)/(curTestSetSize-1));
 fclose(fp);
    return;
}

 


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值