KNN算法是机器学习里面常用的一种分类算法,假设一个样本空间被分为几类,然后给定一个待分类所有的特征数
据,通过计算距离该数据的最近的K个样本来判断这个数据属于哪一类。如果距离待分类属性最近的K个类大多数都
属于某一个特定的类,那么这个待分类的数据也就属于这个类。
Contents
1. KNN算法介绍
2. KNN算法的C++实现
1. KNN算法介绍
K Nearest Neighbor算法,简称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