Kmeans算法参考记录

来源:互联网 发布:疯狂联盟座龙升级数据 编辑:程序博客网 时间:2024/06/05 23:08

[-]

  1. 基本Kmeans算法1
  2. 注意问题
    1. 1K如何确定
      1. 与层次聚类结合2
      2. 稳定性方法3
      3. 系统演化方法3
      4. 使用canopy算法进行初始划分4
    2. 2初始质心的选取
    3. 3距离的度量
    4. 4质心的计算
    5. 5算法停止条件
    6. 6空聚类的处理
  3. 适用范围及缺陷
  4. 实现

1.基本Kmeans算法[1]

[cpp] view plain copy
  1. 选择K个点作为初始质心  
  2. repeat  
  3.     将每个点指派到最近的质心,形成K个簇  
  4.     重新计算每个簇的质心  
  5. until 簇不发生变化或达到最大迭代次数  
时间复杂度:O(tKmn),其中,t为迭代次数,K为簇的数目,m为记录数,n为维数

空间复杂度:O((m+K)n),其中,K为簇的数目,m为记录数,n为维数

2.注意问题

(1)K如何确定

        kmenas算法首先选择K个初始质心,其中K是用户指定的参数,即所期望的簇的个数。这样做的前提是我们已经知道数据集中包含多少个簇,但很多情况下,我们并不知道数据的分布情况,实际上聚类就是我们发现数据分布的一种手段,这就陷入了鸡和蛋的矛盾。如何有效的确定K值,这里大致提供几种方法,若各位有更好的方法,欢迎探讨。

1.与层次聚类结合[2]

         经常会产生较好的聚类结果的一个有趣策略是,首先采用层次凝聚算法决定结果粗的数目,并找到一个初始聚类,然后用迭代重定位来改进该聚类。

2.稳定性方法[3]

        稳定性方法对一个数据集进行2次重采样产生2个数据子集,再用相同的聚类算法对2个数据子集进行聚类,产生2个具有k个聚类的聚类结果,计算2个聚类结果的相似度的分布情况。2个聚类结果具有高的相似度说明k个聚类反映了稳定的聚类结构,其相似度可以用来估计聚类个数。采用次方法试探多个k,找到合适的k值。

3.系统演化方法[3]

         系统演化方法将一个数据集视为伪热力学系统,当数据集被划分为K个聚类时称系统处于状态K。系统由初始状态K=1出发,经过分裂过程和合并过程,系统将演化到它的稳定平衡状态Ki,其所对应的聚类结构决定了最优类数Ki。系统演化方法能提供关于所有聚类之间的相对边界距离或可分程度,它适用于明显分离的聚类结构和轻微重叠的聚类结构。

4.使用canopy算法进行初始划分[4]

          基于Canopy Method的聚类算法将聚类过程分为两个阶段
         Stage1、聚类最耗费计算的地方是计算对象相似性的时候,Canopy Method在第一阶段选择简单、计算代价较低的方法计算对象相似性,将相似的对象放在一个子集中,这个子集被叫做Canopy ,通过一系列计算得到若干Canopy,Canopy之间可以是重叠的,但不会存在某个对象不属于任何Canopy的情况,可以把这一阶段看做数据预处理;
          Stage2、在各个Canopy 内使用传统的聚类方法(如K-means),不属于同一Canopy 的对象之间不进行相似性计算。
从这个方法起码可以看出两点好处:首先,Canopy 不要太大且Canopy 之间重叠的不要太多的话会大大减少后续需要计算相似性的对象的个数;其次,类似于K-means这样的聚类方法是需要人为指出K的值的,通过Stage1得到的Canopy 个数完全可以作为这个K值,一定程度上减少了选择K的盲目性。

         其他方法如贝叶斯信息准则方法(BIC)可参看文献[5]。

(2)初始质心的选取

          选择适当的初始质心是基本kmeans算法的关键步骤。常见的方法是随机的选取初始质心,但是这样簇的质量常常很差。处理选取初始质心问题的一种常用技术是:多次运行,每次使用一组不同的随机初始质心,然后选取具有最小SSE(误差的平方和)的簇集。这种策略简单,但是效果可能不好,这取决于数据集和寻找的簇的个数。
          第二种有效的方法是,取一个样本,并使用层次聚类技术对它聚类。从层次聚类中提取K个簇,并用这些簇的质心作为初始质心。该方法通常很有效,但仅对下列情况有效:(1)样本相对较小,例如数百到数千(层次聚类开销较大);(2)K相对于样本大小较小
           第三种选择初始质心的方法,随机地选择第一个点,或取所有点的质心作为第一个点。然后,对于每个后继初始质心,选择离已经选取过的初始质心最远的点。使用这种方法,确保了选择的初始质心不仅是随机的,而且是散开的。但是,这种方法可能选中离群点。此外,求离当前初始质心集最远的点开销也非常大。为了克服这个问题,通常该方法用于点样本。由于离群点很少(多了就不是离群点了),它们多半不会在随机样本中出现。计算量也大幅减少。
          第四种方法就是上面提到的canopy算法。

(3)距离的度量

          常用的距离度量方法包括:欧几里得距离和余弦相似度。两者都是评定个体间差异的大小的。欧几里得距离度量会受指标不同单位刻度的影响,所以一般需要先进行标准化,同时距离越大,个体间差异越大;空间向量余弦夹角的相似度度量不会受指标刻度的影响,余弦值落于区间[-1,1],值越大,差异越小。但是针对具体应用,什么情况下使用欧氏距离,什么情况下使用余弦相似度?
          从几何意义上来说,n维向量空间的一条线段作为底边和原点组成的三角形,其顶角大小是不确定的。也就是说对于两条空间向量,即使两点距离一定,他们的夹角余弦值也可以随意变化。感性的认识,当两用户评分趋势一致时,但是评分值差距很大,余弦相似度倾向给出更优解。举个极端的例子,两用户只对两件商品评分,向量分别为(3,3)和(5,5),这两位用户的认知其实是一样的,但是欧式距离给出的解显然没有余弦值合理。[6]

(4)质心的计算

         对于距离度量不管是采用欧式距离还是采用余弦相似度,簇的质心都是其均值,即向量各维取平均即可。对于距离对量采用其它方式时,这个还没研究过。

(5)算法停止条件

         一般是目标函数达到最优或者达到最大的迭代次数即可终止。对于不同的距离度量,目标函数往往不同。当采用欧式距离时,目标函数一般为最小化对象到其簇质心的距离的平方和,如下:

         当采用余弦相似度时,目标函数一般为最大化对象到其簇质心的余弦相似度和,如下:

(6)空聚类的处理

           如果所有的点在指派步骤都未分配到某个簇,就会得到空簇。如果这种情况发生,则需要某种策略来选择一个替补质心,否则的话,平方误差将会偏大。一种方法是选择一个距离当前任何质心最远的点。这将消除当前对总平方误差影响最大的点。另一种方法是从具有最大SSE的簇中选择一个替补的质心。这将分裂簇并降低聚类的总SSE。如果有多个空簇,则该过程重复多次。另外,编程实现时,要注意空簇可能导致的程序bug。

3.适用范围及缺陷

           Kmenas算法试图找到使平凡误差准则函数最小的簇。当潜在的簇形状是凸面的,簇与簇之间区别较明显,且簇大小相近时,其聚类结果较理想。前面提到,该算法时间复杂度为O(tKmn),与样本数量线性相关,所以,对于处理大数据集合,该算法非常高效,且伸缩性较好。但该算法除了要事先确定簇数K和对初始聚类中心敏感外,经常以局部最优结束,同时对“噪声”和孤立点敏感,并且该方法不适于发现非凸面形状的簇或大小差别很大的簇。
package com.kmeans.fjsh.algorithm;import java.util.ArrayList;  import java.util.Random;    /**  * K均值聚类算法  */  public class Kmeans {    private int k;// 分成多少簇      private int m;// 迭代次数      private int dataSetLength;// 数据集元素个数,即数据集的长度      private ArrayList<float[]> dataSet;// 数据集链表      private ArrayList<float[]> center;// 中心链表      private ArrayList<ArrayList<float[]>> cluster; // 簇      private ArrayList<Float> jc;// 误差平方和,k越接近dataSetLength,误差越小      private Random random;      /**      * 设置需分组的原始数据集      *       * @param dataSet      */     public void setDataSet(ArrayList<float[]> dataSet) {          this.dataSet = dataSet;      }    /**      * 获取结果分组      *       * @return 结果集      */        public ArrayList<ArrayList<float[]>> getCluster() {          return cluster;      }        /**      * 构造函数,传入需要分成的簇数量      *       * @param k      *            簇数量,若k<=0时,设置为1,若k大于数据源的长度时,置为数据源的长度      */      public Kmeans(int k) {          if (k <= 0) {              k = 1;          }          this.k = k;      }        /**      * 初始化      */      private void init() {          m = 0;          random = new Random();          if (dataSet == null || dataSet.size() == 0) {              initDataSet();          }          dataSetLength = dataSet.size();          if (k > dataSetLength) {              k = dataSetLength;          }          center = initCenters();          cluster = initCluster();          jc = new ArrayList<Float>();      }        /**      * 如果调用者未初始化数据集,则采用内部测试数据集      */      private void initDataSet() {          dataSet = new ArrayList<float[]>();          // 其中{6,3}是一样的,所以长度为15的数据集分成14簇和15簇的误差都为0          float[][] dataSetArray = new float[][] { { 8, 2 }, { 3, 4 }, { 2, 5 },                  { 4, 2 }, { 7, 3 }, { 6, 2 }, { 4, 7 }, { 6, 3 }, { 5, 3 },                  { 6, 3 }, { 6, 9 }, { 1, 6 }, { 3, 9 }, { 4, 1 }, { 8, 6 } };            for (int i = 0; i < dataSetArray.length; i++) {              dataSet.add(dataSetArray[i]);          }      }  /**     * 初始化中心数据链表,分成多少簇就有多少个中心点      *       * @return 中心点集      */      private ArrayList<float[]> initCenters() {          ArrayList<float[]> center = new ArrayList<float[]>();          int[] randoms = new int[k];          boolean flag;          int temp = random.nextInt(dataSetLength);          randoms[0] = temp;          for (int i = 1; i < k; i++) {              flag = true;              while (flag) {                  temp = random.nextInt(dataSetLength);                  int j = 0;                  // 不清楚for循环导致j无法加1                  // for(j=0;j<i;++j)                  // {                  // if(temp==randoms[j]);                  // {                  // break;                  // }                  // }                  while (j < i) {                      if (temp == randoms[j]) {                          break;                      }                      j++;                  }                  if (j == i) {                      flag = false;                  }              }              randoms[i] = temp;          }            // 测试随机数生成情况          // for(int i=0;i<k;i++)          // {          // System.out.println("test1:randoms["+i+"]="+randoms[i]);          // }            // System.out.println();          for (int i = 0; i < k; i++) {              center.add(dataSet.get(randoms[i]));// 生成初始化中心链表          }          return center;      }        /**      * 初始化簇集合      *       * @return 一个分为k簇的空数据的簇集合      */      private ArrayList<ArrayList<float[]>> initCluster() {          ArrayList<ArrayList<float[]>> cluster = new ArrayList<ArrayList<float[]>>();          for (int i = 0; i < k; i++) {              cluster.add(new ArrayList<float[]>());          }            return cluster;      }        /**      * 计算两个点之间的距离      *       * @param element      *            点1      * @param center      *            点2      * @return 距离      */      private float distance(float[] element, float[] center) {          float distance = 0.0f;          float x = element[0] - center[0];          float y = element[1] - center[1];          float z = x * x + y * y;          distance = (float) Math.sqrt(z);            return distance;      }        /**      * 获取距离集合中最小距离的位置      *       * @param distance      *            距离数组      * @return 最小距离在距离数组中的位置      */      private int minDistance(float[] distance) {          float minDistance = distance[0];          int minLocation = 0;          for (int i = 1; i < distance.length; i++) {              if (distance[i] < minDistance) {                  minDistance = distance[i];                  minLocation = i;              } else if (distance[i] == minDistance) // 如果相等,随机返回一个位置              {                  if (random.nextInt(10) < 5) {                      minLocation = i;                  }              }          }            return minLocation;      }        /**      * 核心,将当前元素放到最小距离中心相关的簇中      */      private void clusterSet() {          float[] distance = new float[k];          for (int i = 0; i < dataSetLength; i++) {              for (int j = 0; j < k; j++) {                  distance[j] = distance(dataSet.get(i), center.get(j));                  // System.out.println("test2:"+"dataSet["+i+"],center["+j+"],distance="+distance[j]);                }              int minLocation = minDistance(distance);              // System.out.println("test3:"+"dataSet["+i+"],minLocation="+minLocation);              // System.out.println();                cluster.get(minLocation).add(dataSet.get(i));// 核心,将当前元素放到最小距离中心相关的簇中            }      }        /**      * 求两点误差平方的方法      *       * @param element      *            点1      * @param center      *            点2      * @return 误差平方      */      private float errorSquare(float[] element, float[] center) {          float x = element[0] - center[0];          float y = element[1] - center[1];            float errSquare = x * x + y * y;            return errSquare;      }        /**      * 计算误差平方和准则函数方法      */      private void countRule() {          float jcF = 0;          for (int i = 0; i < cluster.size(); i++) {              for (int j = 0; j < cluster.get(i).size(); j++) {                  jcF += errorSquare(cluster.get(i).get(j), center.get(i));                }          }          jc.add(jcF);      }        /**      * 设置新的簇中心方法      */      private void setNewCenter() {          for (int i = 0; i < k; i++) {              int n = cluster.get(i).size();              if (n != 0) {                  float[] newCenter = { 0, 0 };                  for (int j = 0; j < n; j++) {                      newCenter[0] += cluster.get(i).get(j)[0];                      newCenter[1] += cluster.get(i).get(j)[1];                  }                  // 设置一个平均值                  newCenter[0] = newCenter[0] / n;                  newCenter[1] = newCenter[1] / n;                  center.set(i, newCenter);              }          }      }        /**      * 打印数据,测试用      *       * @param dataArray      *            数据集      * @param dataArrayName      *            数据集名称      */      public void printDataArray(ArrayList<float[]> dataArray,              String dataArrayName) {          for (int i = 0; i < dataArray.size(); i++) {              System.out.println("print:" + dataArrayName + "[" + i + "]={"                      + dataArray.get(i)[0] + "," + dataArray.get(i)[1] + "}");          }          System.out.println("===================================");      }        /**      * Kmeans算法核心过程方法      */      private void kmeans() {          init();          // printDataArray(dataSet,"initDataSet");          // printDataArray(center,"initCenter");            // 循环分组,直到误差不变为止          while (true) {              clusterSet();              // for(int i=0;i<cluster.size();i++)              // {              // printDataArray(cluster.get(i),"cluster["+i+"]");              // }                countRule();                // System.out.println("count:"+"jc["+m+"]="+jc.get(m));                // System.out.println();              // 误差不变了,分组完成              if (m != 0) {                  if (jc.get(m) - jc.get(m - 1) == 0) {                      break;                  }              }                setNewCenter();              // printDataArray(center,"newCenter");              m++;              cluster.clear();              cluster = initCluster();          }            // System.out.println("note:the times of repeat:m="+m);//输出迭代次数      }        /**      * 执行算法      */      public void execute() {          long startTime = System.currentTimeMillis();          System.out.println("kmeans begins");          kmeans();          long endTime = System.currentTimeMillis();          System.out.println("kmeans running time=" + (endTime - startTime)                  + "ms");          System.out.println("kmeans ends");          System.out.println();      }  }  

package com.kmeans.fjsh.algorithm;import java.util.ArrayList; public class KmeansTest {      public  static void main(String[] args)      {          //初始化一个Kmean对象,将k置为10          Kmeans k=new Kmeans(10);          ArrayList<float[]> dataSet=new ArrayList<float[]>();                    dataSet.add(new float[]{1,2});          dataSet.add(new float[]{3,3});          dataSet.add(new float[]{3,4});          dataSet.add(new float[]{5,6});          dataSet.add(new float[]{8,9});          dataSet.add(new float[]{4,5});          dataSet.add(new float[]{6,4});          dataSet.add(new float[]{3,9});          dataSet.add(new float[]{5,9});          dataSet.add(new float[]{4,2});          dataSet.add(new float[]{1,9});          dataSet.add(new float[]{7,8});          //设置原始数据集          k.setDataSet(dataSet);          //执行算法          k.execute();          //得到聚类结果          ArrayList<ArrayList<float[]>> cluster=k.getCluster();          //查看结果          for(int i=0;i<cluster.size();i++)          {              k.printDataArray(cluster.get(i), "cluster["+i+"]");          }                }  }  


仅作参考,具体细化需要根据具体情况处理
0 0
原创粉丝点击