1. 程式人生 > >k-means(k均值聚類)演算法介紹及實現(c++)

k-means(k均值聚類)演算法介紹及實現(c++)

基本介紹:

k-means 演算法接受輸入量 k ;然後將n個數據物件劃分為 k個聚類以便使得所獲得的聚類滿足:同一聚類中的物件相似度較高;而不同聚類中的物件相似度較小。聚類相似度是利用各聚類中物件的均值所獲得一個“中心物件”(引力中心)來進行計算的。

工作過程:

  k-means 演算法的工作過程說明如下:首先從n個數據物件任意選擇 k 個物件作為初始聚類中心;而對於所剩下其它物件,則根據它們與這些聚類中心的相似度(距離),分別將它們分配給與其最相似的(聚類中心所代表的)聚類;然後再計算每個所獲新聚類的聚類中心(該聚類中所有物件的均值);不斷重複這一過程直到標準測度函式開始收斂為止。一般都採用均方差作為標準測度函式. k個聚類具有以下特點:各聚類本身儘可能的緊湊,而各聚類之間儘可能的分開。

(1) 從 n個數據物件任意選擇 k 個物件作為初始聚類中心;

  (2) 迴圈(3)到(4)直到每個聚類不再發生變化為止   (3) 根據每個聚類物件的均值(中心物件),計算每個物件與這些中心物件的距離;並根據最小距離重新對相應物件進行劃分;   (4) 重新計算每個(有變化)聚類的均值(中心物件)   k-means 演算法接受輸入量 k ;然後將n個數據物件劃分為 k個聚類以便使得所獲得的聚類滿足:同一聚類中的物件相似度較高;而不同聚類中的物件相似度較小。聚類相似度是利用各聚類中物件的均值所獲得一個“中心物件”(引力中心)來進行計算的。   k-means 演算法的工作過程說明如下:首先從n個數據物件任意選擇 k 個物件作為初始聚類中心;而對於所剩下其它物件,則根據它們與這些聚類中心的相似度(距離),分別將它們分配給與其最相似的(聚類中心所代表的)聚類;然後再計算每個所獲新聚類的聚類中心(該聚類中所有物件的均值);不斷重複這一過程直到標準測度函式開始收斂為止。一般都採用均方差作為標準測度函式. k個聚類具有以下特點:各聚類本身儘可能的緊湊,而各聚類之間儘可能的分開。   計算複雜度:   O(nkt), 其中t是迭代次數。   k-means演算法是一種基於樣本間相似性度量的間接聚類方法,屬於非監督學習方法。此演算法以k為引數,把n 個物件分為k個簇,以使簇內具有較高的相似度,而且簇間的相似度較低。相似度的計算根據一個簇中物件的平均值(被看作簇的重心)來進行。此演算法首先隨機選擇k個物件,每個物件代表一個聚類的質心。對於其餘的每一個物件,根據該物件與各聚類質心之間的距離,把它分配到與之最相似的聚類中。然後,計算每個聚類的新質心。重複上述過程,直到準則函式會聚。k-means演算法是一種較典型的逐點修改迭代的動態聚類演算法,其要點是以誤差平方和為準則函式。逐點修改類中心:一個象元樣本按某一原則,歸屬於某一組類後,就要重新計算這個組類的均值,並且以新的均值作為凝聚中心點進行下一次象元素聚類;逐批修改類中心:在全部象元樣本按某一組的類中心分類之後,再計算修改各類的均值,作為下一次分類的凝聚中心點。
/*kmeans演算法實現(此處只考慮元組只有兩個屬性的情況)
*@File:k_means.cpp
*@Author:Cai0538
*@Create:2011-12-10
*@Last Modified:2011-12-10
*/
#include <iostream>
#include <fstream>
#include <vector>
#include <math.h>
#define k 3
using namespace std;
//存放元組的屬性資訊
struct Tuple{
	float attr1;
	float attr2;
};
//計算兩個元組間的歐幾裡距離
float getDistXY(Tuple t1, Tuple t2) 
{
	return sqrt((t1.attr1 - t2.attr1) * (t1.attr1 - t2.attr1) + (t1.attr2 - t2.attr2) * (t1.attr2 - t2.attr2));
}

//根據質心,決定當前元組屬於哪個簇
int clusterOfTuple(Tuple means[],Tuple tuple){
	float dist=getDistXY(means[0],tuple);
	float tmp;
	int label=0;//標示屬於哪一個簇
	for(int i=1;i<k;i++){
		tmp=getDistXY(means[i],tuple);
		if(tmp<dist) {dist=tmp;label=i;}
	}
	return label;	
}
//獲得給定簇集的平方誤差
float getVar(vector<Tuple> clusters[],Tuple means[]){
	float var = 0;
	for (int i = 0; i < k; i++)
	{
		vector<Tuple> t = clusters[i];
		for (int j = 0; j< t.size(); j++)
		{
			var += getDistXY(t[j],means[i]);
		}
	}
	//cout<<"sum:"<<sum<<endl;
	return var;

}
//獲得當前簇的均值(質心)
Tuple getMeans(vector<Tuple> cluster){
	
	int num = cluster.size();
	double meansX = 0, meansY = 0;
	Tuple t;
	for (int i = 0; i < num; i++)
	{
		meansX += cluster[i].attr1;
		meansY += cluster[i].attr2;
	}
	t.attr1 = meansX / num;
	t.attr2 = meansY / num;
	return t;
	//cout<<"sum:"<<sum<<endl;
	

}
void KMeans(vector<Tuple> tuples){
	vector<Tuple> clusters[k];
	Tuple means[k];
	int i=0;
	//預設一開始將前K個元組的值作為k個簇的質心(均值)
	for(i=0;i<k;i++){
		means[i].attr1=tuples[i].attr1;
		means[i].attr2=tuples[i].attr2;
	}
	int lable=0;
	//根據預設的質心給簇賦值
	for(i=0;i!=tuples.size();++i){
		lable=clusterOfTuple(means,tuples[i]);
		clusters[lable].push_back(tuples[i]);
	}
	//輸出剛開始的簇
	for(lable=0;lable<3;lable++){
		cout<<"第"<<lable+1<<"個簇:"<<endl;
		vector<Tuple> t = clusters[lable];
		for (i = 0; i< t.size(); i++)
		{
			cout<<"("<<t[i].attr1<<","<<t[i].attr2<<")"<<"   ";
		}	
		cout<<endl;
	}
	float oldVar=-1;
	float newVar=getVar(clusters,means);
	while(abs(newVar - oldVar) >= 1) //當新舊函式值相差不到1即準則函式值不發生明顯變化時,演算法終止
	{
		
		for (i = 0; i < k; i++) //更新每個簇的中心點
		{
			means[i] = getMeans(clusters[i]);
			//cout<<"means["<<i<<"]:"<<means[i].attr1<<"  "<<means[i].attr2<<endl;
		}
		oldVar = newVar;
		newVar = getVar(clusters,means); //計算新的準則函式值
		for (i = 0; i < k; i++) //清空每個簇
		{
			clusters[i].clear();
		}
		//根據新的質心獲得新的簇
		for(i=0;i!=tuples.size();++i){
			lable=clusterOfTuple(means,tuples[i]);
			clusters[lable].push_back(tuples[i]);
		}
		//輸出當前的簇
		for(lable=0;lable<3;lable++){
			cout<<"第"<<lable+1<<"個簇:"<<endl;
			vector<Tuple> t = clusters[lable];
			for (i = 0; i< t.size(); i++)
			{
				cout<<"("<<t[i].attr1<<","<<t[i].attr2<<")"<<"   ";
			}	
			cout<<endl;
		}
	}
	
}
int main(){

	char fname[256];
	cout<<"請輸入存放資料的檔名: ";
	cin>>fname;
	cout<<endl;
	ifstream infile;
	infile.open(fname,ios::in);
	if(!infile){
		cout<<"不能開啟輸入的檔案"<<fname<<endl;
		return 0;
	}
	int count=0;
	vector<Tuple> tuples;
	Tuple tuple;
	//從檔案流中讀入資料
	while(!infile.eof()){
		count++;
		if(count%2==1) infile>>tuple.attr1;
		else {
			infile>>tuple.attr2;
			tuples.push_back(tuple);
		}
	}
	//int k;
	//cout<<"請輸入期望的簇的個數:"
	//cin>>k;
	//cout<<endl;

	//輸出檔案中的元組資訊
	for(vector<Tuple>::size_type ix=0;ix!=tuples.size();++ix)
		cout<<"("<<tuples[ix].attr1<<","<<tuples[ix].attr2<<")"<<"    ";
    cout<<endl;
	KMeans(tuples);
	return 0;
}

大家要執行該程式,要滿足如下條件:

輸入:

執行程式時,會提示輸出要讀入資料所在的檔案,比如在程式所在的當前目錄下的檔案data.txt。

data.txt中的資料格式可參考下面:

1  1
2  1
1  2
2  2
4  3
5  3
4  4
5  4

將如此格式的資料貼上在data.txt中即可。

輸出檔案後,回車即可看到相應的輸出結果。