K-means聚类算法原理及c++实现

孤人 提交于 2020-01-18 03:56:56

聚类是指根据数据本身的特征对数据进行分类,不需要人工标注,是无监督学习的一种。k-means算法是聚类算法中最简单的算法之一。

k-means 算法将n个数据对象划分为 k个聚类以便使得所获得的聚类满足:同一聚类中的对象相似度较高;而不同聚类中的对象相似度较小。聚类相似度是利用各聚类中对象的均值所获得一个“中心对象”(引力中心)来进行计算的。

基于这样一个假设,我们再来导出k-means所要优化的目标函数:设我们一共有N个数据点需要分为K个cluster,而k-means要做的就是要最小化这个目标函数

                                                                      

为第k个类聚中心,

当第n个数据属于第k类时为1,否则为0。

过程如下:

1.首先从n个数据对象任意选择 k 个对象作为初始聚类中心;而对于所剩下其它对象,则根据它们与这些聚类中心的相似度(距离),分别将它们分配给与其最相似的(聚类中心所代表的)聚类;

2.然后再计算每个所获新聚类的聚类中心(该聚类中所有对象的均值);不断重复这一过程直到标准测度函数开始收敛为止。

一般都采用均方差作为标准测度函数,k个聚类具有以下特点:各聚类本身尽可能的紧凑,而各聚类之间尽可能的分开。

每一次更新聚类中心都会使目标函数减小,因此迭代最终J会达到一个极小值,不能保证是全局最小值。k-means对于噪声十分敏感。

c++实现:

class ClusterMethod
{
private:
	double **mpSample;//输入
	double **mpCenters;//储存聚类中心
	double **pDistances;//距离矩阵
	int mSampleNum;//样本数
	int mClusterNum;//聚类数
	int mFeatureNum;//每个样本特征数
	int *ClusterResult;//聚类结果
	int MaxIterationTimes;//最大迭代次数

public:
	void GetClusterd(vector<std::vector<std::vector<double> > >&v, double** feateres, int ClusterNum, int SampleNum, int FeatureNum);//外部接口


private:
	void Initialize(double** feateres, int ClusterNum, int SampleNum, int FeatureNum);//类初始化
	void k_means(vector<vector<vector<double> > >&v);//算法入口
	void k_means_Initialize();//聚类中心初始化
	void k_means_Calculate(vector<vector<vector<double> > >&v);//聚类计算
};

类内函数实现:

//param@v 保存分类结果 v[i][j][k]表示第i类第j个数据第k个特征(从0开始)
//param@feateres 输入数据 feateres[i][j]表示第i个数据第j个特征(i,j从0开始)
//param@ClusterNum 分类数
//param@SampleNum 数据数量
//param@FeatureNum 数据特征数
void ClusterMethod::GetClusterd(vector<std::vector<std::vector<double> > >&v, double** feateres, int ClusterNum, int SampleNum, int FeatureNum)
{
	Initialize(feateres, ClusterNum, SampleNum, FeatureNum);
	k_means(v);
}


//类内数据初始化
void ClusterMethod::Initialize(double** feateres, int ClusterNum, int SampleNum, int FeatureNum)
{
	mpSample = feateres;
	mFeatureNum = FeatureNum;
	mSampleNum = SampleNum;
	mClusterNum = ClusterNum;
	MaxIterationTimes = 50;

	mpCenters = new double*[mClusterNum];
	for (int i = 0; i < mClusterNum; ++i)
	{
		mpCenters[i] = new double[mFeatureNum];
	}

	pDistances = new double*[mSampleNum];
	for (int i = 0; i < mSampleNum; ++i)
	{
		pDistances[i] = new double[mClusterNum];
	}

	ClusterResult = new int[mSampleNum];
}


//算法入口
void ClusterMethod::k_means(vector<vector<vector<double> > >&v)
{
	k_means_Initialize();
	k_means_Calculate(v);
}


//初始化聚类中心
void ClusterMethod::k_means_Initialize()
{
	for (int i = 0; i < mClusterNum; ++i)
	{
		//mpCenters[i] = mpSample[i];

		for (int k = 0; k < mFeatureNum; ++k)
		{
			mpCenters[i][k] = mpSample[i][k];
		}
	}
}

上面初始化聚类中心是令数据前i(i为聚类中心个数)个点为i个聚类中心。(注意一定不能使用mpCenters[i] = mpSample[i]进行初始化,它们是指针。)

也可以随机选取i个数据令为聚类中心,这样同样的数据多次的运行结果就可能不同。因为k-means结果不一定达到全局最小点,最简单的解决方法就是多次运行(这里指整个函数的重复运行,与聚类时的迭代次数不同)取目标函数最小时候的聚类结果。如果像前面那种每次都选前i个数据初始化聚类中心,多次运行将不能解决局部最小点问题。

聚类和更新聚类中心的实现如下:

//聚类过程
void ClusterMethod::k_means_Calculate(vector<vector<vector<double> > >&v)
{

	double J = DBL_MAX;//目标函数
	int time = MaxIterationTimes;

	while (time)

	{
		double now_J = 0;//上次更新距离中心后的目标函数
		--time;
                
                //距离初始化
		for (int i = 0; i < mSampleNum; ++i)
		{
			for (int j = 0; j < mClusterNum; ++j)
			{
				pDistances[i][j] = 0;

			}
		}
                //计算欧式距离
		for (int i = 0; i < mSampleNum; ++i)
		{
			for (int j = 0; j < mClusterNum; ++j)
			{
				for (int k = 0; k < mFeatureNum; ++k)
				{
					pDistances[i][j] += abs(pow(mpSample[i][k], 2) - pow(mpCenters[j][k], 2));
				}
				now_J += pDistances[i][j];
			}
		}
	
		if (J - now_J < 0.01)//目标函数不再变化结束循环
		{	
			break;
		}
		J = now_J;

                //a存放临时分类结果
		vector<vector<vector<double> > > a(mClusterNum);
		for (int i = 0; i < mSampleNum; ++i)
		{
			
			double min = DBL_MAX;
			for (int j = 0; j < mClusterNum; ++j)
			{
				if (pDistances[i][j] < min)
				{
					min = pDistances[i][j];
					ClusterResult[i] = j;
				}
			}

			vector<double> vec(mFeatureNum);
			for (int k = 0; k < mFeatureNum; ++k)
			{
				vec[k] = mpSample[i][k];
			}
			a[ClusterResult[i]].push_back(vec);
		//	v[ClusterResult[i]].push_back(vec);这里不能这样给v输入数据,因为v没有初始化大小
		}
		v = a;

		//计算新的聚类中心
		for (int j = 0; j < mClusterNum; ++j)
		{
			for (int k = 0; k < mFeatureNum; ++k)
			{

				mpCenters[j][k] = 0;
			}
		}


		for (int j = 0; j < mClusterNum; ++j)
		{
			for (int k = 0; k < mFeatureNum; ++k)
			{
				for (int s = 0; s < v[j].size(); ++s)
				{
					mpCenters[j][k] += v[j][s][k];
				}
				if (v[j].size() != 0)
				{
					mpCenters[j][k] /= v[j].size();
				}
			}
		}
	}

        //输出聚类中心
	for (int j = 0; j < mClusterNum; ++j)
	{
		for (int k = 0; k < mFeatureNum; ++k)
		{
			cout << mpCenters[j][k] << " ";
		}
		cout << endl;
	}
}

生成随机数据函数:

//param@datanum 数据数量
//param@featurenum 每个数据特征数
double** createdata(int datanum, int featurenum)
{
	srand((int)time(0));
	double** data = new  double*[datanum];
	for (int i = 0; i < datanum; ++i)
	{
		data[i] = new double[featurenum];
	}
	cout << "输入数据:" << endl;
	for (int i = 0; i < datanum ; ++i)
	{
		for (int j = 0; j < featurenum; ++j)
		{
			data[i][j] = ((int)rand() % 30) / 10.0;
			cout << data[i][j] << " ";
		}
		cout << endl;
	}

	return data;
}

主函数:

int main()
{
	vector<std::vector<std::vector<double> > >v;
	double** data = createdata(10, 2);
	ClusterMethod a;
	a.GetClusterd(v, data, 3, 10, 2);
	for (int i = 0; i < v.size(); ++i)
	{
		cout << "第" << i+1 << "类" << endl;
		for (int j = 0; j < v[i].size(); ++j)
		{
			for (int k = 0; k < v[i][j].size(); ++k)
			{
				cout << v[i][j][k] << " ";
			}
			cout << endl;
		}	
	}
}

 运行结果如下:

 

算法改进

k-means更新聚类中心时使用每类的均值,因此对噪声较为敏感。

k-medoids将前面的均值改为中位数,这样可以避免个别过大或过小的点对聚类中心的影响。

说到中位数,我就想起了唐僧师徒。。。不对,想起了之前写的选择算法,不需要对所有数据进行排序,运行速度更快。

但这也不是说k-medoids就一定比k-means好,要知道,数据量较大时,算法的时间也是关键。求均值所需时间比求中位数小。因此,孰优孰劣需要看具体需求。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!