邻近算法


KNN算法的决策过程

         k-Nearest Neighbor algorithm
  右图中,绿色圆要被决定赋予哪个类,是红色三角形还是蓝色四方形?如果K=3,由于红色三角形所占比例为2/3,绿色圆将被赋予红色三角形那个类,如果K=5,由于蓝色四方形比例为3/5,因此绿色圆被赋予蓝色四方形类。
  K最近邻(k-Nearest Neighbor,KNN)分类算法,是一个理论上比较成熟的方法,也是最简单的机器学习算法之一。该方法的思路是:如果一个样本在特征空间中的k个最相 似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。KNN算法中,所选择的邻居都是已经正确分类的对象。该方法在定类决 策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。KNN方法虽然从原理上也依赖于极限定理,但在类别决策时,只与极少量的相邻样本有关。由于KNN方法主要靠周围有限的邻近的样本,而不是靠判别类域的方 法来确定所属类别的,因此对于类域的交叉或重叠较多的待分样本集来说,KNN方法较其他方法更为适合。
  KNN算法不仅可以用于分类,还可以用于回归。通过找出一个样本的k个最近邻居,将这些邻居的属性的平均值赋给该样本,就可以得到该样本的属性。更有用的方法是将不同距离的邻居对该样本产生的影响给予不同的权值(weight),如权值与距离成正比。
  该算法在分类时有个主要的不足是,当样本不平衡时,如一个类的样本容量很大,而其他类样本容量 很小时,有可能导致当输入一个新样本时,该样本的K个邻居中大容量类的样本占多数。因此可以采用权值的方法(和该样本距离小的邻居权值大)来改进。该方法 的另一个不足之处是计算量较大,因为对每一个待分类的文本都要计算它到全体已知样本的距离,才能求得它的K个最近邻点。目前常用的解决方法是事先对已知样 本点进行剪辑,事先去除对分类作用不大的样本。该算法比较适用于样本容量比较大的类域的自动分类,而那些样本容量较小的类域采用这种算法比较容易产生误 分。

#include <iostream>
#include <string>
#include <vector>
#include <set>
#include <map>
#include <fstream>
#include <sstream>
#include <cassert>
#include <cmath>
using namespace std;

//样例结构体,所属类型和特征向量
struct sample
{
	string type;
	vector<double> features;
};

// 类型和距离结构体,未用到
struct typeDistance
{
	string type;
	double distance;
};

bool operator < (const typeDistance& lhs, const typeDistance& rhs)
{
	return lhs.distance < rhs.distance;
}

// 读取训练样本
// 训练样本的格式是:每行代表一个样例
// 每行的第一个元素是类型名,后面的是样例的特征向量
// 例如:
/*
a    1 2 3 4 5
b    5 4 3 2 1
c    3 3 3 3 3
d    -3 -3 -3 -3 -3
a    1 2 3 4 4
b    4 4 3 2 1
c    3 3 3 2 4
d    0 0 1 1 -2
*/
void readTrain(vector<sample>& train, const string& file)
{
	ifstream fin(file.c_str());
	if (!fin)
	{
		cerr << "File error!" << endl;
		exit(1);
	}
	string line;
	double d = 0.0;
	while (getline(fin, line))
	{
		istringstream sin(line);
		sample ts;
		sin >> ts.type;
		while (sin >> d)
		{
			ts.features.push_back(d);
		}
		train.push_back(ts);
	}
	fin.close();
}

// 读取测试样本
// 每行代表一个样例
// 每一行是一个样例的特征向量
// 例如:
/*
1 2 3 2 4
2 3 4 2 1
8 7 2 3 5
-3 -2 2 4 0
-4 -4 -4 -4 -4
1 2 3 4 4
4 4 3 2 1
3 3 3 2 4
0 0 1 1 -2
*/
void readTest(vector<sample>& test, const string& file)
{
	ifstream fin(file.c_str());
	if (!fin)
	{
		cerr << "File error!" << endl;
		exit(1);
	}
	double d = 0.0;
	string line;
	while (getline(fin, line))
	{
		istringstream sin(line);
		sample ts;
		while (sin >> d)
		{
			ts.features.push_back(d);
		}
		test.push_back(ts);
	}
	fin.close();
}

// 计算欧氏距离
double euclideanDistance(const vector<double>& v1, const vector<double>& v2)
{
	assert(v1.size() == v2.size());
	double ret = 0.0;
	/*
	size_type由string类类型和vector类类型定义的类型,用以保存任意string对象或vector对象的长度,标准库类型将size_type定义为unsigned类型
	*/
	for (vector<double>::size_type i = 0; i != v1.size(); ++i)
	{
		ret += (v1[i] - v2[i]) * (v1[i] - v2[i]);
	}
	return sqrt(ret);
}

// 初始化距离矩阵
// 该矩阵是根据训练样本和测试样本而得
// 矩阵的行数为测试样本的数目,列数为训练样本的数目
// 每一行为一个测试样本到各个训练样本之间的欧式距离组成的数组
void initDistanceMatrix(vector<vector<double> >& dm, const vector<sample>& train, const vector<sample>& test)
{
	for (vector<sample>::size_type i = 0; i != test.size(); ++i)
	{
		vector<double> vd;
		for (vector<sample>::size_type j = 0; j != train.size(); ++j)
		{
			vd.push_back(euclideanDistance(test[i].features, train[j].features));
		}
		dm.push_back(vd);
	}
}

// K-近邻法的实现
// 设定不同的 k 值,给每个测试样例予以一个类型
// 距离和权重成反比
void knnProcess(vector<sample>& test, const vector<sample>& train, const vector<vector<double> >& dm, unsigned int k)
{
	for (vector<sample>::size_type i = 0; i != test.size(); ++i)
	{
		multimap<double, string> dts;  //保存与测试样本i距离最近的k个点
		for (vector<double>::size_type j = 0; j != dm[i].size(); ++j)
		{
			if (dts.size() < k) //把前面k个插入dts中
			{
				dts.insert(make_pair(dm[i][j], train[j].type)); //插入时会自动排序,按dts中的double排序,最小的排在最后
			}
			else
			{
				multimap<double, string>::iterator it = dts.end();
				--it;
				if (dm[i][j] < it->first) //把当前测试样本i到当前训练样本之间的欧氏距离与dts中最小距离比较,若更小就更新dts
				{
					dts.erase(it);
					dts.insert(make_pair(dm[i][j], train[j].type));
				}
			}
		}
		map<string, double> tds;
		string type = "";
		double weight = 0.0;
		//下面for循环主要是求出与测试样本i最邻近的k个样本点中大多数属于的类别,即将其作为测试样本点i的类别
		for (multimap<double, string>::const_iterator cit = dts.begin(); cit != dts.end(); ++cit)
		{
			// 不考虑权重的情况,在 k 个样例中只要出现就加 1
			// ++tds[cit->second];

			// 这里是考虑距离与权重的关系,距离越大权重越小
			tds[cit->second] += 1.0 / cit->first;
			if (tds[cit->second] > weight)
			{
				weight = tds[cit->second];
				type = cit->second;  //保存一下类别
			}
		}
		test[i].type = type;
	}
}

// 输出结果
// 输出的格式和训练样本的格式一样
// 每行表示一个样例,第一个元素是该样例的类型,后面是该样例的特征向量
// 例如:
/*
a    1 2 3 2 4 
b    2 3 4 2 1 
b    8 7 2 3 5 
a    -3 -2 2 4 0 
d    -4 -4 -4 -4 -4 
a    1 2 3 4 4 
b    4 4 3 2 1 
c    3 3 3 2 4 
d    0 0 1 1 -2 
*/
void writeTest(const vector<sample>& test, const string& file)
{
	ofstream fout(file.c_str());
	if (!fout)
	{
		cerr << "File error!" << endl;
		exit(1);
	}
	for (vector<sample>::size_type i = 0; i != test.size(); ++i)
	{
		fout << test[i].type << '\t';
		for (vector<double>::size_type j = 0; j != test[i].features.size(); ++j)
		{
			fout << test[i].features[j] << ' ';
		}
		fout << endl;
	}
}

// 封装
void knn(const string& file1, const string& file2, const string& file3, int k)
{
	vector<sample> train, test;
	readTrain(train, file1.c_str());
	readTest(test, file2.c_str());
	vector<vector<double> > dm;
	initDistanceMatrix(dm, train, test);
	knnProcess(test, train, dm, k);
	writeTest(test, file3.c_str());
}

// 测试
int main()
{
	knn("train.txt", "test.txt", "result.txt", 5);
	return 0;
}


Logo

永洪科技,致力于打造全球领先的数据技术厂商,具备从数据应用方案咨询、BI、AIGC智能分析、数字孪生、数据资产、数据治理、数据实施的端到端大数据价值服务能力。

更多推荐