聚类算法(一)—— k-means算法以及其改进算法

      聚类算法是一种无监督学习,它把数据分成若干类,同一类中的数据的相似性应尽可能地大,不同类中的数据的差异性应尽可能地大。聚类算法可分为“软聚类”和“硬聚类”,对于“硬聚类”,样本中的每一个点都是 100%确定分到某一个类别;而“软聚类”是指样本点以一定的概率被分配到一个类别中。提到聚类算法,很容易想到 K-means 算法,即 K-均值。这种方法很好理解,也很好实现。本文以 k-means 为引子,不断引出 K-means 的各种改进版本,如K-means ++ 、kernel K-means等。

      K-means 的步骤可以分为:

(1)、随机选取 K 个点,作为 K 类的聚类中心,用 Ki 表示

(2)、遍历所有的数据点 Pj,通过计算距离,找到距离 Pj 最近的聚类中心点 Ki。此时可以说第 j 个数据属于第 i 类

(3)、分别计算第 i 类的所有数据的中心点,作为该类的新的聚类中心点。

(4)、重复进行(2)(3)步骤。直到每一类的聚类中心不再发生变化

下面是 K -means 的代码:

#include<iostream>
#include<sstream>
#include<fstream>
#include<string>
#include<vector>
#include<ctime>
#include<cstdlib>  //srand() 在里面
#include<limits>

using namespace std;

typedef struct Point
{
	float x;
	float y;
	int cluster;
	Point(){};
	Point(float a,float b,int c)
	{
		x = a;
		y = b;
		cluster = c;
	}
}point;


float stringToFloat(string str)
{
	stringstream sf;  // 在<sstream>头文件中,进行流的输入输出操作。传入参数和目标对象的类型能够被自动推导出来
	float f_num = 0;
	sf << str;  // 将 str 输入到流中
	sf >> f_num;  // 将字符串 str 转换为 int 类型的变量
	return f_num;
}

vector<point> openFile(const char* dataset)   // 打开文件
{
	fstream file;
	file.open(dataset,ios::in);
	vector<point> data;
	while (!file.eof())
	{
		string temp;
		file >> temp;
		int split = temp.find(',', 0);// 从 0 开始找‘,’如果找到了就返回位置
		point p(stringToFloat(temp.substr(0, split)), stringToFloat(temp.substr(split + 1, temp.length() - 1)), 0);
		data.push_back(p);
	
	}
	file.close();
	return data;
}

float squarDistance(point a, point b)
{
	return (a.x - b.x) *(a.x - b.x) + (a.y - b.y)*(a.y - b.y);
}

void k_means(vector<point> dataset, int k)
{
	vector<point> centroid;
	int n = 1; //n 表示所属的类别
	int len = dataset.size();
	srand((int) time(0));

	// 随机选择 centroids
	while (n <= k)   // 质心点的个数为 1 ~k.每一次循环选择一个随机点 cp 作为中心
	{
		int cen = (float)rand() / (RAND_MAX + 1)*len;  // RAND_MAX代表rand() 的最大值。那么rand() / (RAND_MAX + 1)的范围就是[0,1)。所以cen范围为[0,len)
		point cp(dataset[cen].x, dataset[cen].y, n);  
		centroid.push_back(cp);
		n++;
	}

	//for (int i = 0; i < k; i++)
	//{
	//	cout << "x:" << centroid[i].x << "\ty:" << centroid[i].y << "\tc: " << centroid[i].cluster << endl;  // 一开始随机选择的每一类的聚类中心点
	//}

	// cluster
	int time = 0;  // 记录迭代次数
	int oSSE = INT_MAX;  // 确保第一次能够进入到循环中
	int nSSE = 0;    // nSSE 为每一个点距离其所属聚类中心的距离平方和 的和


	while (abs(oSSE - nSSE) >= 0.1)  // 前后两次的 nSSE 的差值,如果小于 0.1(这是要根据数据集适当调整的),停止迭代。也就是聚类中心不再发生变化
	{
		time++;

		oSSE = nSSE;
		nSSE = 0;

		// 遍历所有的点,更新每一个点所属的聚类群.len 为点的个数
		for (int i = 0; i < len; i++)
		{
			n = 1;
			float shortest = INT_MAX;
			int cur = dataset[i].cluster; // 当前的点所属的类别
			while (n <= k)   // 判断属于 k 类中的哪一类
			{
				float temp = squarDistance(dataset[i],centroid[n-1]);  // 第 i 个点和聚类中心的距离平方和
				if (temp < shortest)   
				{
					shortest = temp;
					cur = n;     //循环 k 次,找到当前的点与每个聚类中心的距离的最小值,,然后,将这个聚类中心作为该数据点的所属聚类
				}
				n++;
			}
			dataset[i].cluster = cur;
		}

		// 更新聚类中心(聚类点的聚类类别是 1~k,但是对应的下标都是 0~k - 1)
		int *point_num_percluster = new int[k];  // 记录每类聚类点的个数
		for (int i = 0; i < k; i++)
			point_num_percluster[i] = 0;
		for (int i = 0; i < k; i++)
		{
			centroid[i] = point(0, 0, i + 1);  // 初始化聚类中心点。之前的第一次是随机选的,此时需要重新计算了。
		}

		for (int i = 0; i < len; i++)
		{
			centroid[dataset[i].cluster - 1].x += dataset[i].x;  // 将每一类的聚类点的坐标进行累加
			centroid[dataset[i].cluster - 1].y += dataset[i].y;
			point_num_percluster[dataset[i].cluster - 1]++;    // 这是记录每类聚类点的个数
		}

		for (int i = 0; i < k; i++)
		{
			centroid[i].x /= point_num_percluster[i];
			centroid[i].y /= point_num_percluster[i];
		}

		for (int i = 0; i < k; i++)  // 输出每一类的聚类中心点
		{
			cout << "x : " << centroid[i].x << "\ty : " << centroid[i].y << "\tc: " << centroid[i].cluster << endl;
		}

		for (int i = 0; i < len; i++)
		{
			nSSE += squarDistance(centroid[dataset[i].cluster - 1],dataset[i]); // 计算聚类中心点与每一个点的距离的平方和的差
		}
	}  // while (abs(oSSE - nSSE) >= 1) 循环结束

	fstream clustering;
	clustering.open("clustering.txt", ios::out); // 将聚类后的结果写入clustering.txt
	for (int i = 0; i < len; i++)
	{
		clustering << dataset[i].x << "," << dataset[i].y << "," << dataset[i].cluster << "\n";
	}
	clustering.close();

	cout << "迭代次数: " << time << endl;
}

int main(int argc, char** argv)
{
	cout << "start running...." << endl;
	vector<point> dataset = openFile("dataset3.txt");
	cout << "read successfully!" << endl;

	k_means(dataset, 3);  // 比如这 20 个数据,如果分成 2 类,只有10个数据被分类了。如果被分成 5 类,会有5个数据至少被分到两个类别中了
	return 0;
}

程序中所用的数据集,是我自己设定的。数据集我选取了(0,0)到(3,3)内的正方形的点集和(8,8)到(10,10)内的正方形的点集。数据集即聚类结果如下:

《聚类算法(一)—— k-means算法以及其改进算法》

 

K-means 存在以下几个缺点:

(1)、对 K 值敏感。也就是说,K 的选择会较大程度上影响分类效果。在聚类之前,我们需要预先设定 K 的大小,但是我们很难确定分成几类是最佳的,比如上面的数据集中,显然分为 2 类,即K = 2 最好,但是当数据量很大时,我们预先无法判断。

(2)、对离群点和噪声点敏感。如果在上述数据集中添加一个(100,100)这个点。很显然,如果 K = 2,其余20个点是一类,(100,100)自成一类。如果 K = 3,(100,100)也是自成一类,剩下的数据分成两类。但是实际上,(100,100)自成一类没有什么必要,并且,如果数据量再加一点且分的类别比较多的话,(100,100)这个点会极大的影响其他点的分类。

(3)、初始聚类中心的选择。K-means 是随机选择 K 个点作为初始的聚类中心。但是,我们可以对这个随机性进行一点约束,使迭代速度更快。举个例子,如果上面的数据集我随机选择了(0,0)和(1,0)两个点,或者选择了(1.5,1.5)和(9,9)两个点,即可以加快迭代速度,也可以避免陷入局部最优。

(4)、只能聚凸的数据集。所谓的凸数据集,是指集合内的每一对点,连接两个点的直线段上的每个点也在该集合内。但是有研究表明,若采用 Bregman 距离,则可显著增强此类算法对更多类型簇结构的适用性。

 

K-means 的改进

针对(1):(1)中存在的问题主要是 K 的值必须认为预先设定,并且在整个算法执行过程中无法更改。此时,可以利用 ISODATA 算法:当属于某个类别的样本数过少,就将这个类别剔除;当属于这个类别的样本过多、分散程度很大的时候,就将这个类别分为两个子类,此时 K 也就会 + 1了

可以参考:李芳. K-Means算法的k值自适应优化方法研究[D]. 安徽大学, 2015

               https://www.cnblogs.com/yixuan-xu/p/6272208.html

一些说明:虽然有很多启发式用于自动确定 k 的值,但是实际应用中,最常用的仍然是基于不同的 K 值,多次运行取平均值(周志华 《机器学习》书 P218)

针对(2):针对离群点和噪声点,我们可以使用一些算法,比如 RANSAC 、LOF 等剔除离群点。此外,基于 K-means 的改进算法有 k-medoids 和 k-medians

针对(3):K-means ++ 不再随机选择 K 个聚类中心:假设已经选取了 m 个聚类中心( 0 < m < K),m = 1时,随机选择一个聚类中心点;在选取第 m+1 个点的时候,距离当前 m 个聚类中心点的中心越远的点,越会以更高的概率被选为第 m+1 个聚类中心。这种方法在一定程度上可以让“随机”选择的聚类中心点的分布更均匀。此外还有 canopy 算法等。

针对(4):K-means 是使用欧式距离来测量,显然,这种度量方式并不适合于所有的数据集。换句话说,K-means 比较适合聚那些球状的簇。参照 SVM 中核函数的思想,将样本映射到另外一个特征空间,就可以改善聚类效果。代表算法是;kernel K-means。

 

扩展:

1、核函数的选取也是一个很麻烦的工作,此时,可以考虑换方法!聚类不再以距离来度量,而是基于密度!参见另一篇博客:基于密度的聚类方法

2、k-means 算法可以看做高斯混合聚类在混合成分方差相等、且每个样本仅仅指派给一个混合成分时的特例。关于高斯混合聚类,参见另一篇博客:基于高斯混合分布的聚类

 

    原文作者:聚类算法
    原文地址: https://blog.csdn.net/dengheCSDN/article/details/82744181
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞