KNN算法是机器学习里面常用的一种分类算法,假设一个样本空间被分为几类,然后给定一个待分类所有的特征数

据,通过计算距离该数据的最近的K个样本来判断这个数据属于哪一类。如果距离待分类属性最近的K个类大多数都

属于某一个特定的类,那么这个待分类的数据也就属于这个类。

 

Contents

 

   1. KNN算法介绍

   2. KNN算法的C++实现

 

 

1. KNN算法介绍

 

   K Nearest Neighbor算法,简称KNN算法,它是一种比较简单的机器学习算法,它的原理也比较简单。下面用

   网上最经典的一张图来说明,如下图

 

                          

KNN算法_数据

 

   如图所示,假设现在需要对中间绿色的圆进行分类,假设我们寻找距离这个绿色圆最近的3个样本,即K=3,那么

   可以看出这个绿色圆属于红色三角形所在的类;如果K=5,因为蓝色方框最多,所以此时绿色圆属于蓝色方框所在

   的类。从而得到当K值取值不同时,得到的分类结果也可能不一样,所以很多时候K值得选取很关键,这就是KNN的

   核心思想。如果类别个数为偶数,那么K通常会设置为一个奇数;如果类别个数为奇数,K通常设置为偶数,这样就

   能保证不会有平局的发生。

 

 

2. KNN算法的C++实现

 

   在求最邻近的K个样本数据时,可以采用高效的K-D树,以后再详细讲,现在先通过暴力方法求解。代码如下

 

   KNN.h

#ifndef _KNN_H_
#define _KNN_H_
 
class KNN{
 
public:
    KNN(double **DataSet, int nFeatures, int nSamples);   //获取样本数据集等数据
    ~KNN();
    double Predict(double *data, int K);   //根据新的数据预测分类
 
private:
    void getMeans();      //获取样本属性的均值
    void getStdDev();     //获取样本属性的标准差,即standard deviation
    void getNormal();     //对样本数据进行归一化
    double getDistance(double *x, double *y);  //计算两个样本属性之间的距离
 
private:
    double **DataSet;     //样本训练数据
    double *means;        //样本的每一个属性的均值
    double *stddev;       //样本的每一个属性的标准差
    int nFeatures;        //每个样本的维度,实际上特征个数为nFeatures - 1
    int nSamples;         //样本的个数
};
 
#endif //_KNN_H_

KNN.cpp

#include <math.h>
#include <map>
 
#include "KNN.h"
 
using namespace std;
 
KNN::KNN(double **DataSet, int nFeatures, int nSamples)
{
    this->DataSet = DataSet;
    this->nFeatures = nFeatures;
    this->nSamples = nSamples;
 
    getMeans();
    getStdDev();
    getNormal();
}
 
KNN::~KNN()
{
    delete[] means;
    delete[] stddev;
}
 
//获取样本每个特征的均值
void KNN::getMeans()
{
    means = new double[nFeatures - 1];
    for(int i = 0; i < nFeatures - 1; i++)
    {
        double ans = 0;
        for(int j = 0; j < nSamples; j++)
            ans += DataSet[j][i];
        means[i] = ans / nSamples;
    }
}
 
//获取样本每个特征的标准差
void KNN::getStdDev()
{
    stddev = new double[nFeatures - 1];
    for(int i = 0; i < nFeatures - 1; i++)
    {
        double ans = 0;
        for(int j = 0; j < nSamples; j++)
            ans += (DataSet[j][i] - means[i]) * (DataSet[j][i] - means[i]);
        stddev[i] = sqrt(ans / nSamples);
    }
}
 
//利用Z-score标准化对数据进行归一化
void KNN::getNormal()
{
    for(int i = 0; i < nSamples; i++)
    {
        for(int j = 0; j < nFeatures - 1; j++)
            DataSet[i][j] = (DataSet[i][j] - means[i]) / stddev[i];
    }
}
 
//计算两个特征之间的距离,采用欧式距离度量法
double KNN::getDistance(double *x, double *y)
{
    double ans = 0;
    for(int i = 0; i < nFeatures - 1; i++)
        ans += (x[i] - y[i]) * (x[i] - y[i]);
    return sqrt(ans);
}
 
//对新样本进行分类预测
double KNN::Predict(double *data, int K)
{
    //对输入的新数据特征进行归一化
    for(int i = 0; i < nFeatures - 1; i++)
        data[i] = (data[i] - means[i]) / stddev[i];
 
    //存放K邻近的样本下标及距离值
    map<int, double> kmins;    
    //寻找距离data的最近的K个样本
    for(int i = 0; i < nSamples; i++)   
    {
        double dist = getDistance(data, DataSet[i]);
        if(kmins.size() < K)
            kmins.insert(map<int, double>::value_type(i, dist));
        else
        {
            //指向kmins中当前距离最大元素对应的位置
            map<int, double>::iterator max = kmins.begin();
            for(map<int, double>::iterator it = kmins.begin(); it != kmins.end(); it++)
            {
                if(it->second > max->second)
                    max = it;
            }
            if(dist < max->second)
            {
                kmins.erase(max);
                kmins.insert(map<int, double>::value_type(i, dist));
            }
        }
    }
 
    //统计这K个样本分别在分类中出现的次数
    map<double, int> votes;
    for(map<int, double>::iterator it = kmins.begin(); it != kmins.end(); it++)
    {
        double tmp = DataSet[it->first][nFeatures - 1];
        map<double, int>::iterator voteIt = votes.find(tmp);
        if(voteIt != votes.end())
            voteIt->second++;
        else
            votes.insert(map<double, int>::value_type(tmp, 1));
    }
 
    //计算这K个样本所属最多的类
    map<double, int>::iterator maxVote = votes.begin();
    for(map<double, int>::iterator it = votes.begin(); it != votes.end(); it++)
    {
        if(it->second > maxVote->second)
            maxVote = it;
    }
    data[nFeatures - 1] = maxVote->first;
    return maxVote->first;
}

main.cpp

#include <iostream>
#include <string.h>
#include <stdio.h>
 
#include "KNN.h"
 
using namespace std;
 
double Array[14][5] = {
{0, 0, 0, 0, 0},
{0, 0, 0, 1, 0},
{1, 0, 0, 0, 1},
{2, 1, 0, 0, 1},
{2, 2, 1, 0, 1},
{2, 2, 1, 1, 0},
{1, 2, 1, 1, 1},
{0, 1, 0, 0, 0},
{0, 2, 1, 0, 1},
{2, 1, 1, 0, 1},
{0, 1, 1, 1, 1},
{1, 1, 0, 1, 1},
{1, 0, 1, 0, 1},
{2, 1, 0, 1, 0}
};
 
int main()
{
    double **DataSet = new double*[14];
    for(int i = 0; i < 14; i++)
        DataSet[i] = new double[5];
 
    for(int i = 0; i < 14; i++)
        for(int j = 0; j < 5; j++)
            DataSet[i][j] = Array[i][j];
    KNN knn(DataSet, 5, 14);
    double data[5] = {2, 2, 0, 1, 0};
    cout << knn.Predict(data, 3) << endl;
 
    for(int i = 0; i < 14; i++)
        delete[] DataSet[i];
    delete[] DataSet;
    return 0;
}

可以看出,每预测一个新样本的所属类别时,都会对整体样本进行遍历,可以看出KNN的效率实际上是十分低下的,

后面会用K-D树进行优化。

 

在Julia语言中,有KNN的开发包,链接为:https://github.com/johnmyleswhite/kNN.jl