【机器学习实战之三】:C++实现K-均值(K-Means)聚类算法

聚类是一种无监督的学习,它将相似的对象归到同一个簇中。它有点像全自动分类(类别体系是自动构建的)。聚类方法几乎可以应用于所有对象,簇内的对象越相似,聚类的效果越好。本文要介绍一种称为K-均值(K-means)聚类的算法。之所以称之为K-均值是因为它可以发现k个不同的簇,且每个簇的中心采用簇中所含值的均值计算而成。

在介绍K-均值之前,先讨论一席簇识别(cluster identification)。簇识别给出聚类结果的含义。假定有一些数据,现在将相似数据归到一起,簇识别会告诉我们这些簇到底都是些什么。聚类与分类的最大不同在于,分类的目标事先已经知道,而聚类则不一样。因为其产生的结果与分类相同,只是类别没有预先定义,聚类有时也被称为无监督分类(unsupervised
classification)。

聚类分析试图将相似对象归入同一簇,将不相似对象归到不同簇。相似这一概念取决于所选择的的相似度计算方法。

一、Kmeans算法

优点:容易实现

缺点:可能收敛到局部最小值,在大规模数据集上收敛较慢

使用数据类型:数值型数据

K-均值是发现给定数据集的k个簇的算法。簇个数k是用户给定的(当然如何选择k使得聚类更准确是一个难题),每一个簇通过其质心(centroid),即簇中所有点的中心来描述。

K-均值算法工作流程是这样的。首先,随机确定k个初始点作为质心。然后将数据集中的每个点分配到一个簇中,具体来讲,为每个点找距其最近的质心,并将其分配给该质心所对应的簇。这一步完成之后,每个簇的质心更新为该簇所有点的平均值。

伪代码:

创建k个点作为起始质心(经常是随机选择)
当任意一个点的簇分配结果发生改变时
	对数据集中的每个数据点
		对每个质心
			计算质心与数据点之间的距离
		将数据点分配到距其最近的簇
	对每一个簇,计算簇中所有点的均值并将均值作为质心

二、K-均值聚类的一般流程

(1)收集数据:使用任意方法

(2)准备数据:需要数值型数据来计算距离,也可以将标称型数据映射为二值型数据再用于距离计算。

(3)分析数据:使用任意方法

(4)训练算法:不适用于无监督学习,即无监督学习没有训练过程

(5)测试算法:应用聚类算法、观察结果。可以使用量化的误差指标如误差平方和来评价算法的结果。

(6)使用算法:可以用于所希望的任何应用。通常情况下,簇质心可以代表整个簇的数据来做出决策。

三、算法设计和实现

首先看一下类的定义:

template<typename T>
class KMEANS
{
private:
	vector< vector<T> > dataSet;//the data set
	vector< T > mmin,mmax;
	int colLen,rowLen;//colLen:the dimension of vector;rowLen:the number of vectors
	int k;
	vector< vector<T> > centroids;
	typedef struct MinMax
	{
		T Min;
		T Max;
		MinMax(T min , T max):Min(min),Max(max) {}
	}tMinMax;
	typedef struct Node
	{
		int minIndex; //the index of each node
		double minDist;
		Node(int idx,double dist):minIndex(idx),minDist(dist) {}
	}tNode;
	vector<tNode>  clusterAssment;

	/*split line into numbers*/
	void split(char *buffer , vector<T> &vec);
	tMinMax getMinMax(int idx);
	void setCentroids(tMinMax &tminmax , int idx);
	void updateCentroids();
	void initClusterAssment();
	double distEclud(vector<T> &v1 , vector<T> &v2);

public:
	KMEANS(int k);
	void loadDataSet(char *filename);
	void randCent();
	void print();
	void kmeans();
};

数据成员:

+vector< vector<T> > dataSet:训练数据,即所有点的集合,是一个二维的矩阵。

+colLen:向量的维度;rowLen:向量的个数

+k:KMeans中对应的k值

+vector< vector<T> > centroids:k个簇的中心点

+tMinMax:存有最大最小值的结构体

+tNode:标识着每个点所属的簇类别以及距离值

+vector<tNode> clusterAssment:代表每一个向量所属的类别(簇)。

成员函数:

+void loadDataSet( char *filename ):读入文件信息并初始化dataSet以及colLen、rowLen。

+void split(char *buffer , vector<T> &vec):将一行切分成多个数值放入容器中。

+void randCent():随机生成k个中心点

+void kmeans():kmeans的核心函数,计算k个中心点以及每个点的归类。

+double distEclud(vector<T> &v1 , vector<T> &v2):计算两个向量的欧式距离。

+void print():打印结果

在这里贴一下kmeans的核心函数kmeans()

template<typename T>
void KMEANS<T>::kmeans()
{
	initClusterAssment();
	bool clusterChanged = true;
	//the termination condition can also be the loops less than	some number such as 1000
	while( clusterChanged )
	{
		clusterChanged = false;
		//step one : find the nearest centroid of each point
		cout<<"find the nearest centroid of each point : "<<endl;
		for(int i=0;i<rowLen;i++)
		{
			int minIndex = -1;
			double minDist = INT_MAX;
			//distance between dataSet[i] and all centroids
			for(int j=0;j<k;j++)
			{
				double distJI = distEclud( centroids[j],dataSet[i] );
				if( distJI < minDist )
				{
					minDist = distJI;
					minIndex = j;
				}
			}
			//update the cluster which the dataSet[i] belongs to...
			if( clusterAssment[i].minIndex != minIndex )
			{
				clusterChanged = true;
				clusterAssment[i].minIndex = minIndex;
				clusterAssment[i].minDist = minDist ;
			}
		}

		//step two : update the centroids
		cout<<"update the centroids:"<<endl;
		for(int cent=0;cent<k;cent++)
		{
			vector<T> vec(colLen,0);
			int cnt = 0;
			for(int i=0;i<rowLen;i++)
			{
				if( clusterAssment[i].minIndex == cent )
				{
					++cnt;
					//sum of two vectors
					for(int j=0;j<colLen;j++)
					{
						vec[j] += dataSet[i].at(j);
					}
				}
			}

			//mean of the vector and update the centroids[cent]
			for(int i=0;i<colLen;i++)
			{
				if( cnt!=0 )	vec[i] /= cnt;
				centroids[cent].at(i) = vec[i];
			}
		}//for
		print();//update the centroids
	}//while

#if 0
	typename vector<tNode> :: iterator it = clusterAssment.begin();
	while( it!=clusterAssment.end() )
	{
		cout<<(*it).minIndex<<"\t"<<(*it).minDist<<endl;
		it++;
	}
#endif
}

做一个简单的解释:

line 4:首先初始化clusterAssment的值,每个元素为一个结构体,一个域值为簇的索引值;一个域值为存储误差。

line 5:创建一个标志变量clusterChanged,如果该值为true,说明有点改变了类的归属,那么继续迭代,知道没有任何一个点改变了簇的归属为止。当然还有一种方法来作为循环的结束条件:循环的次数,例如循环在1000次以内计算。

line 12~33:第一步:找到每个点距离最近的中心点。line17~25 计算dataSet的第i个向量距离所有簇中心centroids的距离。line27~32 如果簇中心有所变化然后更新。

line 37~60:第二步:将同一类的向量相加并且求平均然后更新簇中心点centroids的值。

输入数据集testSet.txthttp://yunpan.cn/cyRYWWeXKa8bU(提取码:f2ad))为:

1.658985	4.285136
-3.453687	3.424321
4.838138	-1.151539
-5.379713	-3.362104
0.972564	2.924086
-3.567919	1.531611
0.450614	-3.302219
-3.487105	-1.724432
2.668759	1.594842
-3.156485	3.191137
3.165506	-3.999838
-2.786837	-3.099354
4.208187	2.984927
-2.123337	2.943366
0.704199	-0.479481
-0.392370	-3.963704
2.831667	1.574018
-0.790153	3.343144
2.943496	-3.357075
-3.195883	-2.283926
2.336445	2.875106

完整代码kmeans.cc:

#include<iostream>
#include<vector>
#include<map>
#include<cstdlib>
#include<algorithm>
#include<fstream>
#include<stdio.h>
#include<string.h>
#include<string>
#include<time.h>  //for srand
#include<limits.h> //for INT_MIN INT_MAX

using namespace std;

template<typename T>
class KMEANS
{
private:
	vector< vector<T> > dataSet;//the data set
	vector< T > mmin,mmax;
	int colLen,rowLen;//colLen:the dimension of vector;rowLen:the number of vectors
	int k;
	vector< vector<T> > centroids;
	typedef struct MinMax
	{
		T Min;
		T Max;
		MinMax(T min , T max):Min(min),Max(max) {}
	}tMinMax;
	typedef struct Node
	{
		int minIndex; //the index of each node
		double minDist;
		Node(int idx,double dist):minIndex(idx),minDist(dist) {}
	}tNode;
	vector<tNode>  clusterAssment;

	/*split line into numbers*/
	void split(char *buffer , vector<T> &vec);
	tMinMax getMinMax(int idx);
	void setCentroids(tMinMax &tminmax , int idx);
	void initClusterAssment();
	double distEclud(vector<T> &v1 , vector<T> &v2);

public:
	KMEANS(int k);
	void loadDataSet(char *filename);
	void randCent();
	void print();
	void kmeans();
};

template<typename T>
void KMEANS<T>::initClusterAssment()
{
	tNode node(-1,-1);
	for(int i=0;i<rowLen;i++)
	{
		clusterAssment.push_back(node);
	}
}

template<typename T>
void KMEANS<T>::kmeans()
{
	initClusterAssment();
	bool clusterChanged = true;
	//the termination condition can also be the loops less than	some number such as 1000
	while( clusterChanged )
	{
		clusterChanged = false;
		//step one : find the nearest centroid of each point
		cout<<"find the nearest centroid of each point : "<<endl;
		for(int i=0;i<rowLen;i++)
		{
			int minIndex = -1;
			double minDist = INT_MAX;
			for(int j=0;j<k;j++)
			{
				double distJI = distEclud( centroids[j],dataSet[i] );
				if( distJI < minDist )
				{
					minDist = distJI;
					minIndex = j;
				}
			}
			if( clusterAssment[i].minIndex != minIndex )
			{
				clusterChanged = true;
				clusterAssment[i].minIndex = minIndex;
				clusterAssment[i].minDist = minDist ;
			}
		}

		//step two : update the centroids
		cout<<"update the centroids:"<<endl;
		for(int cent=0;cent<k;cent++)
		{
			vector<T> vec(colLen,0);
			int cnt = 0;
			for(int i=0;i<rowLen;i++)
			{
				if( clusterAssment[i].minIndex == cent )
				{
					++cnt;
					//sum of two vectors
					for(int j=0;j<colLen;j++)
					{
						vec[j] += dataSet[i].at(j);
					}
				}
			}

			//mean of the vector and update the centroids[cent]
			for(int i=0;i<colLen;i++)
			{
				if( cnt!=0 )	vec[i] /= cnt;
				centroids[cent].at(i) = vec[i];
			}
		}//for
		print();//update the centroids
	}//while

#if 0
	typename vector<tNode> :: iterator it = clusterAssment.begin();
	while( it!=clusterAssment.end() )
	{
		cout<<(*it).minIndex<<"\t"<<(*it).minDist<<endl;
		it++;
	}
#endif
}

template<typename T>
KMEANS<T>::KMEANS(int k)
{
	this->k = k;
}

template<typename T>
void KMEANS<T>::setCentroids(tMinMax &tminmax,int idx)
{
	T rangeIdx = tminmax.Max - tminmax.Min;
	for(int i=0;i<k;i++)
	{
		/* generate float data between 0 and 1 */
		centroids[i].at(idx) = tminmax.Min + rangeIdx *  ( rand() /  (double)RAND_MAX  ) ;
	}
}

//get the min and max value of the idx column
template<typename T>
typename KMEANS<T>::tMinMax KMEANS<T>::getMinMax(int idx)
{
    T min , max ;
	dataSet[0].at(idx) > dataSet[1].at(idx) ? ( max = dataSet[0].at(idx),min = dataSet[1].at(idx) ) : ( max = dataSet[1].at(idx),min = dataSet[0].at(idx) ) ;

	for(int i=2;i<rowLen;i++)
	{
		if( dataSet[i].at(idx) < min )	min = dataSet[i].at(idx);
		else if( dataSet[i].at(idx) > max ) max = dataSet[i].at(idx);
		else continue;
	}

	tMinMax tminmax(min,max);
	return tminmax;
}

template<typename T>
void KMEANS<T>::randCent()
{
	//init centroids
	vector<T> vec(colLen,0);
	for(int i=0;i<k;i++)
	{
		centroids.push_back(vec);
	}

	//set values by column
	srand( time(NULL) );
	for(int j=0;j<colLen;j++)
	{
	    tMinMax tminmax = getMinMax(j);
		setCentroids(tminmax,j);
	}
}

template<typename T>
double KMEANS<T>::distEclud(vector<T> &v1 , vector<T> &v2)
{
	T sum = 0;
	int size = v1.size();
	for(int i=0;i<size;i++)
	{
		sum += (v1[i] - v2[i])*(v1[i] - v2[i]);
	}
	return sum;
}

template<typename T>
void KMEANS<T>::split(char *buffer , vector<T> &vec)
{
	char *p = strtok(buffer," \t");
	while(p!=NULL)
	{
		vec.push_back( atof(p) );
		p = strtok( NULL," " );
	}
}

template<typename T>
void KMEANS<T>::print()
{
	ofstream fout;
	fout.open("res.txt");
	if(!fout)
	{
		cout<<"file res.txt open failed"<<endl;
		exit(0);
	}

#if 0
	typename vector< vector<T> > :: iterator it = centroids.begin();
	while( it!=centroids.end() )
	{
		typename vector<T> :: iterator it2 = (*it).begin();
		while( it2 != (*it).end() )
		{
			//fout<<*it2<<"\t";
			cout<<*it2<<"\t";
			it2++;
		}
		//fout<<endl;
		cout<<endl;
		it++;
	}
#endif

	typename vector< vector<T> > :: iterator it = dataSet.begin();
	typename vector< tNode > :: iterator itt = clusterAssment.begin();
	for(int i=0;i<rowLen;i++)
	{
		typename vector<T> :: iterator it2 = (*it).begin();
		while( it2!=(*it).end() )
		{
			fout<<*it2<<"\t";
			it2++;
		}
		fout<<(*itt).minIndex<<endl;
		itt++;
		it++;
	}

}

template<typename T>
void KMEANS<T>:: loadDataSet(char *filename)
{
	FILE *pFile;
	pFile = fopen(filename,"r");
	if( !pFile )
	{
		printf("open file %s failed...\n",filename);
		exit(0);
	}

	//init dataSet
	char *buffer = new char[100];
	vector<T> temp;
	while( fgets(buffer,100,pFile) )
	{
		temp.clear();
		split(buffer,temp);
		dataSet.push_back(temp);
	}

	//init colLen,rowLen
	colLen = dataSet[0].size();
	rowLen = dataSet.size();
}

int main( int argc , char *argv[])
{
	if(argc!=3)
	{
		cout<<"Usage : ./a.out filename k"<<endl;
		exit(0);
	}

	char *filename = argv[1];
	int k = atoi(argv[2]);
	KMEANS<double> kms(k);
	kms.loadDataSet(filename);
	kms.randCent();
	kms.kmeans();

	return 0;
}

makefile:

target:
	g++ kmeans.cc
	./a.out testSet.txt 4

得到了结果文件res.txt,那么下面用python来画一下图,看看聚类的效果。

res.txt文件:

1.65898	4.28514	2
-3.45369	3.42432	1
4.83814	-1.15154	3
-5.37971	-3.3621	0
0.972564	2.92409	2
-3.56792	1.53161	1
0.450614	-3.30222	3
-3.48711	-1.72443	0
2.66876	1.59484	2
-3.15649	3.19114	1
3.16551	-3.99984	3
-2.78684	-3.09935	0
4.20819	2.98493	2
-2.12334	2.94337	1
0.704199	-0.479481	3
-0.39237	-3.9637	3
2.83167	1.57402	2
-0.790153	3.34314	1
2.9435	-3.35708	3
-3.19588	-2.28393	0
2.33644	2.87511	2
-1.78635	2.55425	1
2.1901	-1.90602	3
-3.40337	-2.77829	0
1.77812	3.88083	2
-1.68835	2.23027	1
2.59298	-2.05437	3
-4.00726	-3.20707	0
2.25773	3.38756	2
-2.67901	0.785119	1
。。。。。

四、结果验证

将res.txt对应的值画成图,需要用到python的一些包( python2.7.3+NumPy+matplotlib:http://yunpan.cn/cyRSixGj49HVq(提取码:80be) ):

import matplotlib.pyplot as plt

if __name__ == '__main__':
    fp = open('res.txt', 'r')
    colors = ['ro', 'go', 'bo', 'mo', 'co']
    plt.figure()
    for line in fp:
        a = line.strip('\n').split()
        xi = float(a[0])
        yi = float(a[1])
        kind = int(a[2])
        plt.plot(xi, yi, colors[kind], markersize=5)
    fp.close()
    plt.savefig('res.png')

效果截图如下:

当选择k=2:

当选择k=3:

当选择k=4:

可以看到当k=4的时候聚类的效果还是很好的。当然如何做才能提高聚类的效果又是另一个话题,以后有机会再探讨。

Author:忆之独秀

Email:[email protected]

注明出处:http://blog.csdn.net/lavorange/article/details/28854929

时间: 2024-08-29 03:28:35

【机器学习实战之三】:C++实现K-均值(K-Means)聚类算法的相关文章

《机器学习实战》读书笔记2:K-近邻(kNN)算法

声明:文章是读书笔记,所以必然有大部分内容出自<机器学习实战>.外加个人的理解,另外修改了部分代码,并添加了注释 1.什么是K-近邻算法? 简单地说,k-近邻算法采用测量不同特征值之间距离的方法进行分类.不恰当但是形象地可以表述为近朱者赤,近墨者黑.它有如下特点: 优点:精度高.对异常值不敏感.无数据输入假定 缺点:计算复杂度高.空间复杂度高 适用数据范围:数值型和标称型 2.K-近邻算法的工作原理: 存在一个样本数据集合,也称作训练样本集,并且样本集中的每个数据都存在标签,即我们知道样本集中

机器学习:weka中添加自己的分类和聚类算法

不管是实验室研究机器学习算法或是公司研发,都有需要自己改进算法的时候,下面就说说怎么在weka里增加改进的机器学习算法. 一 添加分类算法的流程 1 编写的分类器必须继承 Classifier或是Classifier的子类:下面用比较简单的zeroR举例说明: 2 复写接口 buildClassifier,其是主要的方法之一,功能是构造分类器,训练模型: 3 复写接口 classifyInstance,功能是预测一个标签的概率:或实现distributeForInstance,功能是对得到所有的

《机器学习实战》代码实现学习一 使用K-近邻算法改进约会网站的配对效果(数据准备)

1.数据准备:从文本文件中解析数据 文本文件datingTestSet2.txt网盘地址为: https://pan.baidu.com/s/19HNwo1TSWjWhbRwsyL-itg 提取码为:mz11 约会数据由1000行,主要包含一下三种特征: 每年获得的飞行常客里程数 玩视频游戏所耗时间百分比 每周消费的冰淇淋公升数 但是在把这些特征输入到分类器之前,必须将待处理数据格式改变为分类器可以接受的格式,在kNN.py中创建名为file2matrix的函数,以此来处理输入格式问题,该函数的

机器学习实战笔记-利用K均值聚类算法对未标注数据分组

聚类是一种无监督的学习,它将相似的对象归到同一个簇中.它有点像全自动分类.聚类方法几乎可以应用于所有对象,簇内的对象越相似,聚类的效果越好 簇识别给出聚类结果的含义.假定有一些数据,现在将相似数据归到一起,簇识别会告诉我们这些簇到底都是些什么.聚类与分类的最大不同在于,分类的目标事先巳知,而聚类则不一样.因为其产生的结果与分类相同,而只是类别没有预先定义,聚类有时也被称为无监督分类(unsupervised classification ). 聚类分析试图将相似对象归人同一簇,将不相似对象归到不

K均值聚类--利用k-means算法分析NBA近四年球队实力

分类作为一种监督学习方法,要求必须事先明确知道各个类别的信息,并且断言所有待分类项都有一个类别与之对应.但是很多时候上述条件得不到满足,尤其是在处理海量数据的时候,如果通过预处理使得数据满足分类算法的要求,则代价非常大,这时候可以考虑使用聚类算法.聚类属于无监督学习,相比于分类,聚类不依赖预定义的类和类标号的训练实例.本文首先介绍聚类的基础--距离与相异度,然后介绍一种常见的聚类算法--k-means算法,并利用k-means算法分析NBA近四年球队实力.因为本人比较喜欢观看NBA比赛,所以用这

k均值聚类

目录 一.k均值简介 二.应用简介 三.算法 四.选择合适的K 五.具体实例 一.k均值简介 K均值聚类是一种无监督学习,对未标记的数据(即没有定义类别或组的数据)进行分类. 该算法的目标是在数据中找到由变量K标记的组.该算法迭代地工作基于所提供的特征,将每个数据点分配给K个组中的一个. 基于特征相似性对数据点进行聚类. K均值聚类算法的结果是: 1.K簇的质心,可用于标记新数据 2.训练数据的标签(每个数据点分配给一个集群) 二.应用简介 K均值聚类算法用于查找未在数据中明确标记的组.这可用于

《机器学习实战》--KNN

代码来自<机器学习实战>https://github.com/wzy6642/Machine-Learning-in-Action-Python3 K-近邻算法(KNN) 介绍 简单地说,k-近邻算法采用测量不同特征值之间的距离方法进行分类. 优点:精度高.对异常值不敏感,无数据输入假定. 缺点:计算复杂度高.空间复杂度高,无法给出数据的内在含义. 使用数据范围:数值型.标称型. 分类函数的伪代码: 对未知类别属性的数据集中的每个点依次执行以下操作: (1)计算已知类别数据集中的点与当前点之间

k-means均值聚类算法(转)

4.1.摘要 在前面的文章中,介绍了三种常见的分类算法.分类作为一种监督学习方法,要求必须事先明确知道各个类别的信息,并且断言所有待分类项都有一个类别与之对应.但是很多时候上述条件得不到满足,尤其是在处理海量数据的时候,如果通过预处理使得数据满足分类算法的要求,则代价非常大,这时候可以考虑使用聚类算法.聚类属于无监督学习,相比于分类,聚类不依赖预定义的类和类标号的训练实例.本文首先介绍聚类的基础——距离与相异度,然后介绍一种常见的聚类算法——k均值和k中心点聚类,最后会举一个实例:应用聚类方法试

机器学习六--K-means聚类算法

想想常见的分类算法有决策树.Logistic回归.SVM.贝叶斯等.分类作为一种监督学习方法,要求必须事先明确知道各个类别的信息,并且断言所有待分类项都有一个类别与之对应.但是很多时候上述条件得不到满足,尤其是在处理海量数据的时候,如果通过预处理使得数据满足分类算法的要求,则代价非常大,想想如果给你50个G这么大的文本,里面已经分好词,这时需要将其按照给定的几十个关键字进行划分归类,监督学习的方法确实有点困难,而且也不划算,前期工作做得太多了. 这时候可以考虑使用聚类算法,我们只需要知道这几十个