Weka算法Clusterers-Xmeans源码分析(一)

<p></p><p><span style="font-size:18px">上几篇博客都是分析的分类器算法(有监督学习),这次就分析一个聚类算法(无监督学习)。</span></p><p><span style="font-size:18px"></span></p><p><span style="font-size:18px">一、算法</span></p><p><span style="font-size:18px">Xmeans算法基本就是大名鼎鼎的K-means算法,然后Weka做了一点“小”改进,使之能自动确定聚类数量,那么首先就说一下K-means算法。顺便说一下Weka原生的Kmeans算法是SimpleKMeans聚类器。</span></p><p><span style="font-size:18px">K-means算法是属于典型的简单但有有效的算法,具有非常直观的美感,其过程如下:</span></p><p><span style="font-size:18px">输入:聚类数量K,以及数据集data</span></p><p><span style="font-size:18px">1、随机选取K个点作为聚类中心</span></p><p><span style="font-size:18px">2、对于数据集中每个用例,找出离其最近的聚类中心i,将这个用例归到第i类。</span></p><p><span style="font-size:18px">3、对于每个分类,重新计算聚类中心</span></p><p><span style="font-size:18px">4、重复2和3,直到达到迭代退出的条件。</span></p><p><span style="font-size:18px">K-means的时间复杂度是O(snk),其中s是迭代次数,和退出迭代的条件选取有关,n是数据集数量,k是聚类的数量,可以看出,在聚类数量要求不多的情况下,算法还是比较高效的。</span></p><p><span style="font-size:18px">但K-means的缺点以下两个:</span></p><p><span style="font-size:18px">1、不稳定,最后聚类结果和初始的聚类中心之间有很大关系。</span></p><p><span style="font-size:18px">2、只能处理连续值,无法处理离散值。</span></p><p><span style="font-size:18px">针对1,产生了K-means的扩展K-means++算法,针对2,则有K-modes算法以及K-prototype算法,有兴趣的读者可以去搜一下,这里不展开说了。</span></p><p><span style="font-size:18px">K-means算法的关键有以下几点:</span></p><p><span style="font-size:18px">1、如何计算各用例之间的“距离”</span></p><p><span style="font-size:18px">2、所谓的“迭代退出条件”是什么</span></p><p><span style="font-size:18px">3、如何确定聚类中心</span></p><p><span style="font-size:18px">4、在实现过程中有没有一些用来提高效率的trick</span></p><p><span style="font-size:18px">本篇博客在分析源码时将着重去解决以上4个问题。</span></p><p><span style="font-size:18px"></span></p><p><span style="font-size:18px">二、源码</span></p><p><span style="font-size:18px">weka.clusterers.Xmeans继承自RandomizableClusterer类(从名字来猜测是不稳定的聚类器,其可以传入一个随机数种子),而后者又继承自AbstractClusterer(含有两个关键的虚方法buildClusterer和clusterInstance),因此我们着重分析Xmeans对buildClusterer和clusterInstance的实现</span></p><p><span style="font-size:18px">Xmeans方法只能处理连续型数值、日期、以及MissingValue,可以从getCapabilities中看到。</span></p><p><span style="font-size:18px"></span></p><p><span style="font-size:18px">1、buildCLusterer</span></p><p><span style="font-size:18px">该方法接受Instances作为参数,作用是训练聚类模型。</span></p><p><span style="font-size:18px"></span></p><pre name="code" class="java"> public void buildClusterer(Instances data) throws Exception {

    // 先测一下这个data的属性是否能处理。
    getCapabilities().testWithFail(data);
    //这两个是最小聚类数量和最大聚类数量
    if (m_MinNumClusters > m_MaxNumClusters) {
      throw new Exception("XMeans: min number of clusters "
          + "can't be greater than max number of clusters!");
    }

    m_NumSplits = 0;
    m_NumSplitsDone = 0;
    m_NumSplitsStillDone = 0;

    // 替换掉MissingValue,如果是数值型,则替换为平均值,如果是枚举型,则替换为出现最多的那个值
    // 这里可以算预处理数据时的一个小技巧
    m_ReplaceMissingFilter = new ReplaceMissingValues();
    m_ReplaceMissingFilter.setInputFormat(data);
    m_Instances = Filter.useFilter(data, m_ReplaceMissingFilter);
    
    // 设定一个随机种子
    Random random0 = new Random(m_Seed);

    // 聚类数量从最小聚类数量开始,这个值默认是2
    m_NumClusters =  m_MinNumClusters;

    //这里是默认的算距离的方法,可以传入自定义的函数,默认使用欧式距离。
    if (m_DistanceF == null) {
      m_DistanceF = new EuclideanDistance();
    }
    //这两个函数都没实现,不知道放这里的用意是什么
    m_DistanceF.setInstances(m_Instances);
    checkInstances();
    
    //测试相关,暂时忽略
    if (m_DebugVectorsFile.exists() && m_DebugVectorsFile.isFile())
      initDebugVectorsInput();

    // allInstList存放所有Instances的下标
    int[] allInstList = new int[m_Instances.numInstances()]; 
    for (int i = 0; i < m_Instances.numInstances(); i++) {
      allInstList[i] = i;
    }
    
    // 只是拷贝一个表头
    m_Model = new Instances(m_Instances, 0);

    // 确定聚类中心
    if (m_CenterInput != null) {
      //聚类中心可以从文件读取,注意m_ClusterCenters本身是一个Instances对象,但这里似乎没有判断这个m_ClusterCenters和m_Model(也就是传入的训练集)是否同构
      m_ClusterCenters = new Instances(m_CenterInput);
      m_NumClusters = m_ClusterCenters.numInstances();//如果传入了聚类中心文件,那么就更新一下聚类中心数量
    }
    else
      // 随机选取聚类中心,有放回的随机抽样。
      m_ClusterCenters = makeCentersRandomly(random0,
					     m_Instances, m_NumClusters);
    PFD(D_FOLLOWSPLIT, "\n*** Starting centers ");//这个是debug函数,忽略
    for (int k = 0; k < m_ClusterCenters.numInstances(); k++) {
      PFD(D_FOLLOWSPLIT, "Center " + k + ": " + m_ClusterCenters.instance(k));
    }

    PrCentersFD(D_PRINTCENTERS);//打日志的函数,忽略

    boolean finished = false;
    Instances children; 

    // 是否使用KDTree,简单说一下KDTree,如果给定一堆点X,又给定一个点A,A离X中最近的那个点,传统的做法遍历整个X集合,找出最近的,时间复杂度为O(n),构建KDTree之后(本质是在空间上建立索引),时间复杂度可以将为O(logn)
    if (m_UseKDTree)
      m_KDTree.setInstances(m_Instances);
  
    // 迭代次数
    m_IterationCount = 0;

    /**
     * 训练过程由两次迭代组成,外层迭代进行聚类中心的分裂,内层迭代对每个实例进行划分并算出新的聚类中心,外层迭代的退出条件有两个
     * 1. finished为true(finished为true的条件后面会说到)
     * 2. 达到最大迭代次数
     * 注意,m_ClusterCenters有可能已经比m_MaxClusters大了,因为可能是从文件读入的聚类中心,这种情况下迭代也会进行一次,因为finish是在循环结束时判断的
     */
    while (!finished &&
           !stopIteration(m_IterationCount, m_MaxIterations)) {
      PFD(D_FOLLOWSPLIT, "\nBeginning of main loop - centers:");
      PrCentersFD(D_FOLLOWSPLIT);
      PFD(D_ITERCOUNT, "\n*** 1. Improve-Params " + m_IterationCount + 
	  ". time");
      m_IterationCount++;

      // converged代表两次内层迭代,所产生的聚类结果是否一样
      boolean converged = false;

      // 这是一个一维数组,记录每个实例被分到了哪个聚类中心
      m_ClusterAssignments = initAssignments(m_Instances.numInstances());
      // 这个二维数组存放每个聚类中心都有那些实例,很奇怪的是weka全都是用数组,而没用list这样的数据结构,估计是从效率方面进行考虑。
      int[][] instOfCent = new int[m_ClusterCenters.numInstances()][];

      // 内层迭代的计数器
      int kMeansIteration = 0;

      // 打日志忽略
      PFD(D_FOLLOWSPLIT, "\nConverge in K-Means:");
      //进行内层迭代,内层迭代退出的条件也有两个,第一个是迭代次数达到最大,第二个是两次循环的聚类结果一样
      while (!converged && 
	     !stopKMeansIteration(kMeansIteration, m_MaxKMeans)) {
	
	kMeansIteration++;
	converged = true;
	
        // 把实例分给相应的聚类中心,这里对converged进行了赋值,但后面有覆盖了所以这个赋值没有意义。这个函数比较麻烦但没有什么算法思想,就不展开分析了,KDTree结构或许会在后面的博客去分析其实现。
        converged = assignToCenters(m_UseKDTree ? m_KDTree : null,
				    m_ClusterCenters, 
				    instOfCent,
				    allInstList, 
				    m_ClusterAssignments,
				    kMeansIteration);
	
	PFD(D_FOLLOWSPLIT, "\nMain loop - Assign - centers:");//打日志忽略
	PrCentersFD(D_FOLLOWSPLIT);//打日志忽略
	// 重新算聚类中心,如果两次聚类中心一样,就返回true,两次聚类中心一样,和两次的聚类结果一样是完全等价的。聚类中心的计算方法是算数平均值。
        converged = recomputeCenters(m_ClusterCenters, // 聚类中心
				     instOfCent,       // 这些聚类中心的实例
				     m_Model);         // 表头
      PFD(D_FOLLOWSPLIT, "\nMain loop - Recompute - centers:");
      PrCentersFD(D_FOLLOWSPLIT);
      }
      PFD(D_FOLLOWSPLIT, "");
      PFD(D_FOLLOWSPLIT, "End of Part: 1. Improve-Params - conventional K-means");


      //计算每个聚类中心的偏差,m_Mle是个数组,存储各聚类中实例到聚类中心的距离之和
      m_Mle = distortion(instOfCent, m_ClusterCenters);
      //bic是“贝叶斯失真规则”,越小说明模型对数据拟合越好,百度百科连接http://baike.baidu.com/view/1425589.htm?fr=aladdin#2。反正越小越好
      m_Bic = calculateBIC(instOfCent, m_ClusterCenters, m_Mle);
      PFD(D_FOLLOWSPLIT, "m_Bic " + m_Bic);

      int currNumCent = m_ClusterCenters.numInstances();
      //新的聚类中心,可以遇见到,每个原聚类中心都要进行分裂,因为容量是currNumCent*2
      Instances splitCenters = new Instances(m_ClusterCenters, 
					     currNumCent * 2);
      
      // 
      double[] pbic = new double [currNumCent];
      double[] cbic = new double [currNumCent];
            
      // 对中心进行分裂
      for (int i = 0; i < currNumCent 
	   // 原备注说加了下一行可以提高速度,我也不是很懂
	   //	     && currNumCent + numSplits <= m_MaxNumClusters
           ; 
	   i++) {
	
	PFD(D_FOLLOWSPLIT, "\nsplit center " + i +
		      " " + m_ClusterCenters.instance(i));
	Instance currCenter = m_ClusterCenters.instance(i);
	int[] currInstList = instOfCent[i];
	int currNumInst = instOfCent[i].length;//代表这个聚类中有几个实例
	
	// 如果目前的实例小于等于2,就直接复制自己一份,每个聚类中心必须分裂,当然如果两个instance,每个点都当做聚类中心也可以,但直接dummy自己也不影响最后结果。
	if (currNumInst <= 2) {
	  pbic[i] = Double.MAX_VALUE;
	  cbic[i] = 0.0;
	  // add center itself as dummy
	  splitCenters.add(currCenter);
	  splitCenters.add(currCenter);
	  continue;
	}
	
	//m_Mle[i]代表聚类i上的距离误差和,除以分类数得到平均误差,但这个误差并不是方差,这个变量的名字有点误导性。。。。
	double variance = m_Mle[i] / (double)currNumInst;
        //通过某种方式分裂成两个中心,这个分裂过程还是挺有意思的,主流程之后会详细分析
	children = splitCenter(random0, currCenter, variance, m_Model);
	
	// 准备用这个聚类上的所有数据,根据这两个新的聚类中心,再做一次聚类
	int[] oneCentAssignments = initAssignments(currNumInst);
	int[][] instOfChCent = new int [2][]; // todo maybe split didn't work
	
	// 标志记录两次迭代是否一样,下面循环逻辑和之前的聚类过程基本一样
	converged = false;
	int kMeansForChildrenIteration = 0;
	PFD(D_FOLLOWSPLIT, "\nConverge, K-Means for children: " + i);
	while (!converged && 
          !stopKMeansIteration(kMeansForChildrenIteration, 
			       m_MaxKMeansForChildren)) {
	  kMeansForChildrenIteration++;
	  
	  converged =
	    assignToCenters(children, instOfChCent,
			    currInstList, oneCentAssignments);

	  if (!converged) {       
	    recomputeCentersFast(children, instOfChCent, m_Model);//这个和recomputeCenters唯一的区别就是不算converged
	  }
	} 

	
	splitCenters.add(children.instance(0));
	splitCenters.add(children.instance(1));

	PFD(D_FOLLOWSPLIT, "\nconverged cildren ");
	PFD(D_FOLLOWSPLIT, " " + children.instance(0));
	PFD(D_FOLLOWSPLIT, " " + children.instance(1));

	// 分别计算父聚类中心和子聚类中心(2个)的BIC
	pbic[i] = calculateBIC(currInstList, currCenter,  m_Mle[i], m_Model);
	double[] chMLE = distortion(instOfChCent, children);
	cbic[i] = calculateBIC(instOfChCent, children, chMLE);

      } //对于每个聚类中心都做上述操作,循环结束

      // 这个函数根据之前算出的BIC,计算出新的聚类中心,具体怎么选的后面会再跟进去详细说。
      Instances newClusterCenters = null;
      newClusterCenters = newCentersAfterSplit(pbic, cbic, m_CutOffFactor,
                                                 splitCenters);

      int newNumClusters = newClusterCenters.numInstances();
      if (newNumClusters != m_NumClusters) {
	//如果新的聚类中心数量和老的不相等,进入这个if。
	PFD(D_FOLLOWSPLIT, "Compare with non-split");

	int[] newClusterAssignments = 
	  initAssignments(m_Instances.numInstances());
	
	int[][] newInstOfCent = new int[newClusterCenters.numInstances()][];
	//把所有instance放到新的聚类中心上。
	converged = assignToCenters(m_UseKDTree ? m_KDTree : null,
				    newClusterCenters, 
				    newInstOfCent,
				    allInstList, 
				    newClusterAssignments,
				    m_IterationCount);
	
	double[] newMle = distortion(newInstOfCent, newClusterCenters);
	double newBic = calculateBIC(newInstOfCent, newClusterCenters, newMle);//算一算新的bic
	PFD(D_FOLLOWSPLIT, "newBic " + newBic);
	if (newBic > m_Bic) {//如果新的bic比旧的大,说明新的聚类效果好,则用新的替换老的
          PFD(D_FOLLOWSPLIT, "*** decide for new clusters");
	  m_Bic = newBic;
	  m_ClusterCenters = newClusterCenters;
	  m_ClusterAssignments = newClusterAssignments;
	} else {
          PFD(D_FOLLOWSPLIT, "*** keep old clusters");
        }
      }

      newNumClusters = m_ClusterCenters.numInstances();
      if ((newNumClusters >= m_MaxNumClusters) 
	  || (newNumClusters == m_NumClusters)) {
	finished = true;//置finish条件,当达到最大分类数量,或者没有任何分裂的时候,就置为true
      }
      m_NumClusters = newNumClusters;
    }
    
    if (m_ClusterCenters.numInstances() > 0 && m_CenterOutput != null) {
      m_CenterOutput.println(m_ClusterCenters.toString());//输出模型用的,忽略
      m_CenterOutput.close();
      m_CenterOutput = null;
    }    
  }


(未完待续)


你可能感兴趣的:(源码,算法,机器学习,weka,kmeans)