K-means聚类算法初探
1.1 算法初探
K-means是一种基于距离的迭代式算法。它将n个观察实例分类到k个聚类中,以使得每个观察实例距离它所在的聚类的中心点比其他的聚类中心点的距离更小。
其中,距离的计算方式可以是欧式距离(2-norm distance),或者是曼哈顿距离(Manhattan distance,1-norm distance)或者其他。这里我们使用欧式距离。
要将每个观测实例都归类到距离它最近的聚类中心点,我们需要找到这些聚类中心点的具体位置。然而,要确定聚类中心点的位置,我们必须知道该聚类中包含了哪些观测实例。这似乎是一个“先有蛋还是先有鸡”的问题。从理论上来说,这是一个NP-Hard问题[3]。
但是,我们可以通过启发式的算法来近似地解决问题。找到一个全局最优的方案是NP-hard问题,但是,降低问题的难度,如果能够找到多个局部最优方案,然后通过一种评价其聚类结果优劣的方式,把其中最优的解决方案作为结果,我们就可以得到一个不错的聚类结果[2]。
这就是为什么我们说K-means算法是一个迭代式的算法。算法[2]的过程如下:
1)所有的观测实例中随机抽取出k个观测点,作为聚类中心点,然后遍历其余的观测点找到距离各自最近的聚类中心点,将其加入到该聚类中。这样,我们就有了一个初始的聚类结果,这是一次迭代的过程。
2)我们每个聚类中心都至少有一个观测实例,这样,我们可以求出每个聚类的中心点(means),作为新的聚类中心,然后再遍历所有的观测点,找到距离其最近的中心点,加入到该聚类中。然后继续运行2)。
3)如此往复2),直到前后两次迭代得到的聚类中心点一模一样。
这样,算法就稳定了,这样得到的k个聚类中心,和距离它们最近的观测点构成k个聚类,就是我们要的结果。
实验证明,算法试可以收敛的[2]。
计算聚类的中心点有三种方法如下:
1)Minkowski Distance 公式 —— λ 可以随意取值,可以是负数,也可以是正数,或是无穷大。
公式(1)
2)Euclidean Distance 公式 —— 也就是第一个公式 λ=2 的情况
公式(2)
3)CityBlock Distance 公式 —— 也就是第一个公式 λ=1 的情况
公式(3)
这三个公式的求中心点有一些不一样的地方,我们看下图(对于第一个 λ 在 0-1之间)。
(1)Minkowski Distance (2)Euclidean Distance (3) CityBlock Distance
上面这几个图的大意是他们是怎么个逼近中心的,第一个图以星形的方式,第二个图以同心圆的方式,第三个图以菱形的方式。
那么,如何评价一个聚类结果呢?我们计算所有观测点距离它对应的聚类中心的距离的平方和即可,我们称这个评价函数为evaluate(C)。它越小,说明聚类越好。
1.2 K-means的问题及解决方案
K-means算法非常简单,然而却也有许多问题。
1)首先,算法只能找到局部最优的聚类,而不是全局最优的聚类。而且算法的结果非常依赖于初始随机选择的聚类中心的位置。我们通过多次运行算法,使用不同的随机生成的聚类中心点运行算法,然后对各自结果C通过evaluate(C)函数进行评估,选择多次结果中evaluate(C)值最小的那一个。
2)关于初始k值选择的问题。首先的想法是,从一个起始值开始,到一个最大值,每一个值运行k-means算法聚类,通过一个评价函数计算出最好的一次聚类结果,这个k就是最优的k。我们首先想到了上面用到的evaluate(C)。然而,k越大,聚类中心越多,显然每个观测点距离其中心的距离的平方和会越小,这在实践中也得到了验证。第四节中的实验结果分析中将详细讨论这个问题。
3)关于性能问题。原始的算法,每一次迭代都要计算每一个观测点与所有聚类中心的距离。有没有方法能够提高效率呢?是有的,可以使用k-d tree或者ball tree这种数据结构来提高算法的效率。特定条件下,对于一定区域内的观测点,无需遍历每一个观测点,就可以把这个区域内所有的点放到距离最近的一个聚类中去。这将在第三节中详细地介绍。
基本Kmeans算法
1.选择K个点作为初始质心
2.repeat
3. 将每个点指派到最近的质心,形成K个簇
4. 重新计算每个簇的质心
5.until 簇不发生变化或达到最大迭代次数
时间复杂度:O(tKmn),其中,t为迭代次数,K为簇的数目,m为记录数,n为维数
空间复杂度:O((m+K)n),其中,K为簇的数目,m为记录数,n为维数
#include <iostream>
#include <sstream>
#include <fstream>
#include <vector>
#include <math.h>
#include <stdlib.h>
#define k 3//簇的数目
using namespace std;
//存放元组的属性信息
typedef vector<double> Tuple;//存储每条数据记录
int dataNum;//数据集中数据记录数目
int dimNum;//每条记录的维数
//计算两个元组间的欧几里距离
double getDistXY(const Tuple& t1, const Tuple& t2)
{
double sum = 0;
for(int i=1; i<=dimNum; ++i)
{
sum += (t1[i]-t2[i]) * (t1[i]-t2[i]);
}
return sqrt(sum);
}
//根据质心,决定当前元组属于哪个簇
int clusterOfTuple(Tuple means[],const Tuple& tuple){
double dist=getDistXY(means[0],tuple);
double 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;
}
//获得给定簇集的平方误差
double getVar(vector<Tuple> clusters[],Tuple means[]){
double 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(const vector<Tuple>& cluster){
int num = cluster.size();
Tuple t(dimNum+1, 0);
for (int i = 0; i < num; i++)
{
for(int j=1; j<=dimNum; ++j)
{
t[j] += cluster[i][j];
}
}
for(int j=1; j<=dimNum; ++j)
t[j] /= num;
return t;
//cout<<"sum:"<<sum<<endl;
}
void print(const vector<Tuple> clusters[])
{
for(int lable=0; lable<k; lable++)
{
cout<<"第"<<lable+1<<"个簇:"<<endl;
vector<Tuple> t = clusters[lable];
for(int i=0; i<t.size(); i++)
{
cout<<i+1<<".(";
for(int j=0; j<=dimNum; ++j)
{
cout<<t[i][j]<<", ";
}
cout<<")\n";
}
}
}
void KMeans(vector<Tuple>& tuples){
vector<Tuple> clusters[k];//k个簇
Tuple means[k];//k个中心点
int i=0;
//一开始随机选取k条记录的值作为k个簇的质心(均值)
srand((unsigned int)time(NULL));
for(i=0;i<k;){
int iToSelect = rand()%tuples.size();
if(means[iToSelect].size() == 0)
{
for(int j=0; j<=dimNum; ++j)
{
means[i].push_back(tuples[iToSelect][j]);
}
++i;
}
}
int lable=0;
//根据默认的质心给簇赋值
for(i=0;i!=tuples.size();++i){
lable=clusterOfTuple(means,tuples[i]);
clusters[lable].push_back(tuples[i]);
}
double oldVar=-1;
double newVar=getVar(clusters,means);
cout<<"初始的的整体误差平方和为:"<<newVar<<endl;
int t = 0;
while(abs(newVar - oldVar) >= 1) //当新旧函数值相差不到1即准则函数值不发生明显变化时,算法终止
{
cout<<"第 "<<++t<<" 次迭代开始:"<<endl;
for (i = 0; i < k; i++) //更新每个簇的中心点
{
means[i] = getMeans(clusters[i]);
}
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]);
}
cout<<"此次迭代之后的整体误差平方和为:"<<newVar<<endl;
}
cout<<"The result is:\n";
print(clusters);
}
int main(){
char fname[256];
cout<<"请输入存放数据的文件名: ";
cin>>fname;
cout<<endl<<" 请依次输入: 维数 样本数目"<<endl;
cout<<endl<<" 维数dimNum: ";
cin>>dimNum;
cout<<endl<<" 样本数目dataNum: ";
cin>>dataNum;
ifstream infile(fname);
if(!infile){
cout<<"不能打开输入的文件"<<fname<<endl;
return 0;
}
vector<Tuple> tuples;
//从文件流中读入数据
for(int i=0; i<dataNum && !infile.eof(); ++i)
{
string str;
getline(infile, str);
istringstream istr(str);
Tuple tuple(dimNum+1, 0);//第一个位置存放记录编号,第2到dimNum+1个位置存放实际元素
tuple[0] = i+1;
for(int j=1; j<=dimNum; ++j)
{
istr>>tuple[j];
}
tuples.push_back(tuple);
}
cout<<endl<<"开始聚类"<<endl;
KMeans(tuples);
return 0;
}
参考:http://blog.csdn.net/qll125596718/article/details/8243404/
http://blog.csdn.net/skyline0623/article/details/8154911