K-Means算法和实现代码

来源:互联网 发布:js中如何格式化日期 编辑:程序博客网 时间:2024/04/30 23:02

在数据挖掘中,K-Means算法是一种cluster analysis的算法,其主要是来计算数据聚集的算法,主要通过不断地取离种子点最近均值的算法。

问题

K-Means算法主要解决的问题如下图所示。我们可以看到,在图的左边有一些点,我们用肉眼可以看出来有四个点群,但是我们怎么通过计算机程序找出这几个点群来呢?于是就出现了我们的K-Means算法

K-Means要解决的问题

算法概要

这个算法其实很简单,如下图所示: 

K-Means 算法概要

从上图中,我们可以看到,A,B,C,D,E是五个在图中点。而灰色的点是我们的种子点,也就是我们用来找点群的点。有两个种子点,所以K=2。

然后,K-Means的算法如下:

  1. 随机在图中取K(这里K=2)个种子点。
  2. 然后对图中的所有点求到这K个种子点的距离,假如点Pi离种子点Si最近,那么Pi属于Si点群。(上图中,我们可以看到A,B属于上面的种子点,C,D,E属于下面中部的种子点)
  3. 接下来,我们要移动种子点到属于他的“点群”的中心。(见图上的第三步)
  4. 然后重复第2)和第3)步,直到,种子点没有移动(我们可以看到图中的第四步上面的种子点聚合了A,B,C,下面的种子点聚合了D,E)。

这个算法很简单,但是有些细节我要提一下,求距离的公式我不说了,大家有初中毕业水平的人都应该知道怎么算的。我重点想说一下“求点群中心的算法”。

求点群中心的算法

一般来说,求点群中心点的算法你可以很简的使用各个点的X/Y坐标的平均值。不过,我这里想告诉大家另三个求中心点的的公式:

1)Minkowski Distance公式——λ可以随意取值,可以是负数,也可以是正数,或是无穷大。

2)Euclidean Distance公式——也就是第一个公式λ=2的情况

3)CityBlock Distance公式——也就是第一个公式λ=1的情况

这三个公式的求中心点有一些不一样的地方,我们看下图(对于第一个λ在0-1之间)。

(1)Minkowski Distance     (2)Euclidean Distance    (3) CityBlock Distance

上面这几个图的大意是他们是怎么个逼近中心的,第一个图以星形的方式,第二个图以同心圆的方式,第三个图以菱形的方式。

K-Means的演示

如果你以”K Means Demo“为关键字到Google里查你可以查到很多演示。这里推荐一个演示:http://home.dei.polimi.it/matteucc/Clustering/tutorial_html/AppletKM.html

操作是,鼠标左键是初始化点,右键初始化“种子点”,然后勾选“Show History”可以看到一步一步的迭代。

注:这个演示的链接也有一个不错的K Means Tutorial。

K-Means++算法

K-Means主要有两个最重大的缺陷——都和初始值有关:

  • K是事先给定的,这个K值的选定是非常难以估计的。很多时候,事先并不知道给定的数据集应该分成多少个类别才最合适。(ISODATA算法通过类的自动合并和分裂,得到较为合理的类型数目K)
  • K-Means算法需要用初始随机种子点来搞,这个随机种子点太重要,不同的随机种子点会有得到完全不同的结果。(K-Means++算法可以用来解决这个问题,其可以有效地选择初始点)

我在这里重点说一下K-Means++算法步骤:

  1. 先从我们的数据库随机挑个随机点当“种子点”。
  2. 对于每个点,我们都计算其和最近的一个“种子点”的距离D(x)并保存在一个数组里,然后把这些距离加起来得到Sum(D(x))。
  3. 然后,再取一个随机值,用权重的方式来取计算下一个“种子点”。这个算法的实现是,先取一个能落在Sum(D(x))中的随机值Random,然后用Random -= D(x),直到其<=0,此时的点就是下一个“种子点”。
  4. 重复第(2)和第(3)步直到所有的K个种子点都被选出来。
  5. 进行K-Means算法。

相关的代码你可以在这里找到“implement the K-means++ algorithm”(墙)另,Apache的通用数据学库也实现了这一算法

K-Means算法应用

看到这里,你会说,K-Means算法看来很简单,而且好像就是在玩坐标点,没什么真实用处。而且,这个算法缺陷很多,还不如人工呢。是的,前面的例子只是玩二维坐标点,的确没什么意思。但是你想一下下面的几个问题:

1)如果不是二维的,是多维的,如5维的,那么,就只能用计算机来计算了。

2)二维坐标点的X,Y 坐标,其实是一种向量,是一种数学抽象。现实世界中很多属性是可以抽象成向量的,比如,我们的年龄,我们的喜好,我们的商品,等等,能抽象成向量的目的就是可以让计算机知道某两个属性间的距离。如:我们认为,18岁的人离24岁的人的距离要比离12岁的距离要近,鞋子这个商品离衣服这个商品的距离要比电脑要近,等等。

只要能把现实世界的物体的属性抽象成向量,就可以用K-Means算法来归类了

K-Means++算法

K-Means主要有两个最重大的缺陷——都和初始值有关:

  • K是事先给定的,这个K值的选定是非常难以估计的。很多时候,事先并不知道给定的数据集应该分成多少个类别才最合适。(ISODATA算法通过类的自动合并和分裂,得到较为合理的类型数目K)
  • K-Means算法需要用初始随机种子点来搞,这个随机种子点太重要,不同的随机种子点会有得到完全不同的结果。(K-Means++算法可以用来解决这个问题,其可以有效地选择初始点)

我在这里重点说一下K-Means++算法步骤:

  1. 先从我们的数据库随机挑个随机点当“种子点”。
  2. 对于每个点,我们都计算其和最近的一个“种子点”的距离D(x)并保存在一个数组里,然后把这些距离加起来得到Sum(D(x))。
  3. 然后,再取一个随机值,用权重的方式来取计算下一个“种子点”。这个算法的实现是,先取一个能落在Sum(D(x))中的随机值Random,然后用Random -= D(x),直到其<=0,此时的点就是下一个“种子点”。
  4. 重复第(2)和第(3)步直到所有的K个种子点都被选出来。
  5. 进行K-Means算法。

相关的代码你可以在这里找到“implement the K-means++ algorithm”(墙)另,Apache的通用数据学库也实现了这一算法

K-Means算法应用

看到这里,你会说,K-Means算法看来很简单,而且好像就是在玩坐标点,没什么真实用处。而且,这个算法缺陷很多,还不如人工呢。是的,前面的例子只是玩二维坐标点,的确没什么意思。但是你想一下下面的几个问题:

1)如果不是二维的,是多维的,如5维的,那么,就只能用计算机来计算了。

2)二维坐标点的X,Y 坐标,其实是一种向量,是一种数学抽象。现实世界中很多属性是可以抽象成向量的,比如,我们的年龄,我们的喜好,我们的商品,等等,能抽象成向量的目的就是可以让计算机知道某两个属性间的距离。如:我们认为,18岁的人离24岁的人的距离要比离12岁的距离要近,鞋子这个商品离衣服这个商品的距离要比电脑要近,等等。

只要能把现实世界的物体的属性抽象成向量,就可以用K-Means算法来归类了

#include <iostream>    

#include <math.h>    

#include <vector>    
#define _NUM 3 //预定义划分簇的数目    using namespace std;   /**  
    特征对象,表示一个元组,一个元组有两个数值属性  **/     
struct Tuple   {   
    int attr1; 

    int attr2;   

};   

/** 获取两个特征对象之间的距离,在此以欧基米德距离作为距离度量标准  **/   
double getDistXY(Tuple t1, Tuple t2)    {   
    return sqrt((t1.attr1 - t2.attr1) * (t1.attr1 - t2.attr1) + (t1.attr2 - t2.attr2) * (t1.attr2 - t2.attr2));   

}   

/** 计算簇的中心点,在此以簇中所有对象的平均距离来计算中心点  **/   
Tuple getMeansC(vector<Tuple> c)

{   
    int num = c.size();   
    double meansX = 0, meansY = 0; 

    Tuple t;   
    for (int i = 0; i < num; i++)       {   
        meansX += c[i].attr1; 

        meansY += c[i].attr2;       

    }   
    t.attr1 = meansX / num; 

    t.attr2 = meansY / num;

    return t;   

}  

 /** 获取算法的准则函数值,当准则函数收敛时算法停止  **/   
double getE(vector<Tuple> classes[], Tuple means[])   {   
    double sum = 0;   
    for (int i = 0; i < _NUM; i++)       {   
        vector<Tuple> v = classes[i]; 

        for (int j = 0; j< v.size(); j++)           {   
            sum += (v[j].attr1 - means[i].attr1) * (v[j].attr1 - means[i].attr1) + (v[j].attr2 - means[i].attr2) *(v[j].attr2 - means[i].attr2); 

         } 

    }   
    cout<<"sum:"<<sum<<endl; 

    return sum;   

}  

 /**   对当前的特征对象,查找与其最临近的簇,最临近即到簇中心点的距离最短  **/   
int searchMinC(Tuple t, Tuple means[_NUM])   {   
    int c = 0;   
    int d = (t.attr1 - means[0].attr1) * (t.attr1 - means[0].attr1) + (t.attr2 - means[0].attr2) * (t.attr2 - means[0].attr2); 

    for (int i = 1; i < _NUM; i++)       {   
        int temp = (t.attr1 - means[i].attr1) * (t.attr1 - means[i].attr1) + (t.attr2 - means[i].attr2) * (t.attr2 - means[i].attr2); 

       if (temp < d) 

        {   
            c = i;   
            d = temp; 

         } 

    }   
    return c;   

}  

 /**   k-Means算法  **/   
void kMeans(vector<Tuple> init)   {   
    vector<Tuple> classes[_NUM]; //定义簇数组,共需划分_NUM个簇

    int c;   
    Tuple means[_NUM]; //定义中心点数组,每个簇对应一个中心点

    double newE, oldE = -1; //定义准则函数值    
    for (int i = 0; i < _NUM; i++) //对每个簇初始赋予一个特征对象        {   
        cin >> c;   
        classes[i].push_back(init[c - 1]);   
        means[i] = getMeansC(classes[i]); //计算当前每个簇的中心点    
        cout<<"means["<<i<<"]:"<<means[i].attr1<<"  "<<means[i].attr2<<endl; 

    }   
    newE = getE(classes, means); //计算当前准则函数值 

    cout<<"newE:"<<newE<<" oldE:"<<oldE<<endl;   

    for (i = 0; i < _NUM; i++) //清空每个簇        {   
        classes[i].clear();  

    }   
    while(abs(newE - oldE) >= 1) //当新旧函数值相差不到1即准则函数值不发生明显变化时,算法终止  

    {   
        for (int j = 0; j < init.size(); j++) //遍历所有特征对象,将其加入到离它最近的簇    

        {   
            int toC = searchMinC(init[j], means);      

            classes[toC].push_back(init[j]);    

        }   

        cout<<"--------------------"<<endl;    
        for (i = 0; i < _NUM; i++) //打印出当前每个簇的特征对象            {   
            vector<Tuple> temp = classes[i];      

            cout<<"类"<<i+1<<":"<<endl;      

            for (j = 0; j < temp.size(); j++) 

            {   
                cout<<temp[j].attr1<<" "<<temp[j].attr2<<endl;    

            }    

        }   
        cout<<"--------------------"<<endl;   
        for (i = 0; i < _NUM; i++) //更新每个簇的中心点            {   
            means[i] = getMeansC(classes[i]);   
            cout<<"means["<<i<<"]:"<<means[i].attr1<<"  "<<means[i].attr2<<endl;   

        }   
        oldE = newE;   
        newE = getE(classes, means); //计算新的准则函数值   

        for (i = 0; i < _NUM; i++) //清空每个簇            {   
            classes[i].clear();   

        }   

    }

   }  

 /**程序入口  **/   
void main(int args, char * arg[])   {   
    int n1, n2;   
    vector<Tuple> init; //保存所有输入的特征对象    
    while ((cin >> n1 >> n2) && n1 != -1 && n2 != -1) //输入特征对象        {   
        Tuple p;     

        p.attr1 = n1;    

        p.attr2 = n2;   
        init.push_back(p);  

     }   
    kMeans(init); //调用k-Means算法进行聚类分析 

 }   

0 0