Cluster:Hierarchical Clustering

Cluster:Hierarchical Clustering

 

上次学习了K-Means算法之后,本次继续学习另外一种Clustering算法:Hierarchical Clustering算法。Hierarchical Clustering分簇技术在Clustering方法中也是很重要的,其历史比较久远,和K-means一样。尽管如此,这两种算法仍然广泛使用,算是Clustering算法的基本思路了。

 

按照Clustering形式的不同,Hierarchical Clustering有两种思路:

Agglomerative:聚合思路。就是将每个点都初始化为单个簇,然后在后续迭代过程中逐渐合并类似的簇,直到最后整体成为单簇。

Divisive:拆分思路。将整体数据点看做单簇,在后续迭代过程中每次选择某种标准去拆分其中选择出来的一个簇,知道所有的簇中都只有单个数据点为止。

 

这是两种截然不同的思路,前者是Down-Top,后者是Top-Down。后者拆分的思路在K-means中的Bisecting K-means算法类似,只是初始条件和收敛条件不一样,迭代过程时一致的。不过按照某种标准拆分簇这个做法可能难度比较大,也不好操作,导致拆分思路的Hierarchical Clustering算法研究不多(我不知道是不是这个原因,仅是猜想)。所以本文也只介绍聚合思路(Agglomerative)的方法讨论。

 

在Hierarchical Clustering算法开始之前,我们先介绍一个很重要的概念:Dendrogram。wiki地址见这里,http://en.wikipedia.org/wiki/Dendrogram,表示系统树图,实际上可以看做一棵树,能够很好的表示出Hierarchical Clustering算法的结果。

 

对于Agglomerative Hierarchical Clustering算法的思路在上面提到过,思路比较简单:Cluster:Hierarchical Clustering

 中间提到的Proximity Matrix就是距离的一种度量,对于单个数据作为单个簇的数据度量没有问题。当簇中有很多个数据时,就需要一种距离的度量来衡量簇之间的Distance。注意簇之间的Distance这个概念,和数据点之间的Distance是有区别的。在Hierarchical Clustering使用的距离度量(称为Linkage)形式有以下这么几种:

Single:又被成为Nearest-Neighbour,是取两个集合中最近的两个点作为这两个集合的距离。

Complete:是Single的反面,取两个集合中距离最远的两个点作为两个集合的距离。

Group:将两个集合中的点两两距离加权求和取平均值,相对得到合适的结果。Cluster:Hierarchical Clustering

 unweighted:在UPGMA(Unweighted Pair-Group Method with Arithmetic)中使用这个权重,这里有个算法详细解释了UPGMA的实现过程。http://www.southampton.ac.uk/~re1u06/teaching/upgma/,这个教程理解透彻,对于理解Hierarchical Clustering很关键。

centroid:和k-means类似,注意这个是集合之间的linkage。

ward:这个距离也可以看做是一种目标函数,在迭代过程中计算簇内的方差。wiki参考这里:http://en.wikipedia.org/wiki/Ward%27s_method,在wiki上提到了一种Lance-Williams算法度量簇见的相似性或者距离,将前面提到的各种度量标准全部统一起来,感兴趣的同学可以看下。 

对于上面提到的度量,Single会将结果扩散,如两个Cluster比较远,但是个别点比较接近就被合并,结果会比较松散。Complete则相反,由于个别点比较远,导致两个比较接近的簇会抗拒合并到底。Group是相对温和的做法,只是计算量相对比较大。

 

时间和空间分析:

在空间上,Agglomerative Hierarchical Clustering需要保存数据点之间的距离矩阵,所以需要m*m个存储空间,其中m表示数据点数量;由于距离矩阵是对称的,只需要保存m*m/2空间;所以总体的空间复杂度是O(m*m)。

在时间复杂度上,需要计算数据点之间的距离,需要O(m*m)的时间复杂度;并且每次迭代只能产生合并两个簇,所以需要m-1词迭代,所以总的时间复杂度是O(m*m*m)。根据这里http://www.southampton.ac.uk/~re1u06/teaching/upgma/的算法流程,实际上不需要每次迭代过程中更新全部的距离矩阵,只需要更新O(m-i+1)个数据,即只更新对应簇相对于其他簇的距离,对于没有距离变化的簇不用更新距离。所以计算后总体的时间复杂度为O(m*m*logm)。 

Hierarchical Clustering算法的复杂度和K-means相比要复杂许多,尽管其描述不麻烦。计算量大,并且是贪心运算,不能保证全局最优。所以其使用范围,或者说实际用途,并不如Flat Clustering算法广泛。

 

Hierarchical Clustering需要注意的有这么几个问题:

1:没有全局目标函数

在Hierarchical Clustering中,每次迭代的过程都是局部(Local)最优,对于最终分簇结果缺少目标函数度量。缺少目标函数能够避免解决组合优化问题,但是这并不是个优点。我们在分簇算法中,对于最终的分簇结果有个最终的衡量标准,确定我们的分簇结果是否正确,如K-Means算法的SSE标准。在Hierarchical Clustering算法中,就没有标准对最终的分簇结果进行衡量。

 

2:对于簇内数据量的处理能力

簇内数据量的不同也是一种分簇的内在隐含变量,常规的Hierarchical Clustering算法将数据点设定为权重相同。实际上应该将这个因素纳入考虑范围,前面提到的UPGMA算法就将簇内数据量考虑,实际上在Hierarchical Clustering中,如果没有特殊的考虑,将数据点纳入考虑的UPGMA算法总是比较受欢迎的。

 

3:簇合并问题

在每次簇合并过程中,都会从当前距离矩阵中选择距离最小的簇进行合并,该合并动作一旦进行,后面就不可逆转了。这种操作会阻止局部最优成为全局最优,这个实际上贪心算法的弊病。

对于簇合并使用的贪心算法,也有一些方法来解除这个限制:如将簇分支移位,跳出局部最优;对簇中的部分数据量使用其它Clustering技术,如K-Means算法等。这些方法都是改变局部数据特性,增加数据跳出局部最优的可能,以期望达到全局最优。

 

优点:

1:潜在的使用场景,如分类学、或者本身就需要这种按照层次进行分类的地方就有不错的效果。

2:这种层次聚类在某些场景下会有不错的效果,研究证明(这一点我也说不准是哪些研究)。

缺点:

1:计算量大

2:目标函数不明,不容易对分类效果量化比较

3:簇合并后不可逆转,将局部最优作为全局最优解。

 

这些缺点并不影响我们对这种层次聚类的使用,有时候还会有出其不意的效果,我们先看下Hierarchical Clustering算法的过程:

var HierarchicalClustering = function(distance, linkage, threshold) {
         this.distance = distance;
         this.linkage = linkage;
         this.threshold = threshold == undefined ? Infinity : threshold;
    }
    
    HierarchicalClustering.prototype = {
        cluster: function(items, snapshotPeriod, snapshotCb) {
            this.clusters = [];
            this.dists = []; // distances between each pair of clusters
            this.mins = []; // closest cluster for each cluster
            this.index = []; // keep a hash of all clusters by key
            
            for (var i = 0; i < items.length; i++) {
                var cluster = {
                    value: items[i],
                    key: i,
                    index: i,
                    size: 1
                };
                this.clusters[i] = cluster;
                this.index[i] = cluster;
                this.dists[i] = [];
                this.mins[i] = 0;
        }
        
        for (var i = 0; i < this.clusters.length; i++) {
            for (var j = 0; j <= i; j++) {
                var dist = (i == j) ? Infinity : 
                this.distance(this.clusters[i].value, this.clusters[j].value);
                this.dists[i][j] = dist;
                this.dists[j][i] = dist;
                
                if (dist < this.dists[i][this.mins[i]]) {
                    this.mins[i] = j;
                }
            }
        }
        
        var merged = this.mergeClosest();
        var i = 0;
        while (merged) {
            if (snapshotCb && (i++ % snapshotPeriod) == 0) {
                snapshotCb(this.clusters);
            }
            merged = this.mergeClosest();
        }
        
        this.clusters.forEach(function(cluster) {
            // clean up metadata used for clustering
            delete cluster.key;
            delete cluster.index;
        });
        
    return this.clusters;
    },
    
    mergeClosest: function() {
        // find two closest clusters from cached mins
        var minKey = 0, min = Infinity;
        for (var i = 0; i < this.clusters.length; i++) {
            var key = this.clusters[i].key, 
            dist = this.dists[key][this.mins[key]];
            if (dist < min) {
                minKey = key;
                min = dist;
            }
        }
        if (min >= this.threshold) {
            return false;
        }
        
        var c1 = this.index[minKey], 
        c2 = this.index[this.mins[minKey]];

        // merge two closest clusters
        var merged = {
            left: c1,
            right: c2,
                key: c1.key,
                size: c1.size + c2.size
            };
            
        this.clusters[c1.index] = merged;
        this.clusters.splice(c2.index, 1);
        this.index[c1.key] = merged;

         // update distances with new merged cluster
         for (var i = 0; i < this.clusters.length; i++) {
             var ci = this.clusters[i];
             var dist;
             if (c1.key == ci.key) {
                 dist = Infinity;
             } 
             else if (this.linkage == "single") {
                 dist = this.dists[c1.key][ci.key];
                 if (this.dists[c1.key][ci.key] > this.dists[c2.key][ci.key]) {
                     dist = this.dists[c2.key][ci.key];
                 }
             } 
             else if (this.linkage == "complete") {
                 dist = this.dists[c1.key][ci.key];
                 if (this.dists[c1.key][ci.key] < this.dists[c2.key][ci.key]) {
                     dist = this.dists[c2.key][ci.key];
                 }
             } 
             else if (this.linkage == "average") {
                 dist = (this.dists[c1.key][ci.key] * c1.size 
                 + this.dists[c2.key][ci.key] * c2.size) / (c1.size + c2.size);
             } 
             else {
                 dist = this.distance(ci.value, c1.value);
             }
             
             this.dists[c1.key][ci.key] = this.dists[ci.key][c1.key] = dist;
         }


         // update cached mins
         for (var i = 0; i < this.clusters.length; i++) {
             var key1 = this.clusters[i].key;
             if (this.mins[key1] == c1.key || this.mins[key1] == c2.key) {
                 var min = key1;
                 for (var j = 0; j < this.clusters.length; j++) {
                     var key2 = this.clusters[j].key;
                     if (this.dists[key1][key2] < this.dists[key1][min]) {
                         min = key2;
                     }
                 }
                 this.mins[key1] = min;
             }
             this.clusters[i].index = i;
         }

         // clean up metadata used for clustering
         delete c1.key;
         delete c2.key;
         delete c1.index;
         delete c2.index;
         
         return true;
     }

 

算法细节可以参考这里:http://harthur.github.com/clusterfck/demos/colors/

其实算法的难点不是计算过程,而是使用准确的数据结构,将分簇数据保存;然后再使用Dendrogram将结果绘制出来。

 

本文到此结束,大家可以自行揣摩进一步的学习。