Bisecting k-means聚类算法,即二分k均值算法,它是k-means聚类算法的一个变体,主要是为了改进k-means算法随机选择初始质心的随机性造成聚类结果不确定性的问题,而Bisecting k-means算法受随机选择初始质心的影响比较小。
首先,我们考虑在欧几里德空间中,衡量簇的质量通常使用如下度量:误差平方和(Sum of the Squared Error,简称SSE),也就是要计算执行聚类分析后,对每个点都要计算一个误差值,即非质心点到最近的质心的距离。那么,既然每个非质心点都已经属于某个簇,也就是要计算每个非质心点到其所在簇的质心的距离,最后将这些距离值相加求和,作为SSE去评估一个聚类的质量如何。我们的最终目标是,使得最终的SSE能够最小,也就是一个最小化目标SSE的问题。在n维欧几里德空间,SSE形式化地定义,计算公式如下:
Bisecting k-means聚类算法的基本思想是,通过引入局部二分试验,每次试验都通过二分具有最大SSE值的一个簇,二分这个簇以后得到的2个子簇,选择2个子簇的总SSE最小的划分方法,这样能够保证每次二分得到的2个簇是比较优的(也可能是最优的),也就是这2个簇的划分可能是局部最优的,取决于试验的次数。
Bisecting k-means聚类算法的具体执行过程,描述如下所示:
- 初始时,将待聚类数据集D作为一个簇C0,即C={C0},输入参数为:二分试验次数m、k-means聚类的基本参数;
- 取C中具有最大SSE的簇Cp,进行二分试验m次:调用k-means聚类算法,取k=2,将Cp分为2个簇:Ci1、Ci2,一共得到m个二分结果集合B={B1,B2,…,Bm},其中,Bi={Ci1,Ci2},这里Ci1和Ci2为每一次二分试验得到的2个簇;
- 计算上一步二分结果集合B中,每一个划分方法得到的2个簇的总SSE值,选择具有最小总SSE的二分方法得到的结果:Bj={Cj1,Cj2},并将簇Cj1、Cj2加入到集合C,并将Cp从C中移除;
- 重复步骤2和3,直到得到k个簇,即集合C中有k个簇。
聚类算法实现
基于上面描述的聚类执行过程,使用Java实现Bisecting k-means聚类,代码如下所示:
@Override public void clustering() { // parse sample files final List<Point2D> allPoints = Lists.newArrayList(); FileUtils.read2DPointsFromFiles(allPoints, "[\t,;\\s]+", inputFiles); // 从文件中读取二维坐标点,加入到集合allPoints中 final int bisectingK = 2; int bisectingIterations = 0; int maxInterations = 20; List<Point2D> points = allPoints; final Map<CenterPoint, Set<ClusterPoint<Point2D>>> clusteringPoints = Maps.newConcurrentMap(); // 最终的聚类结果集合 while(clusteringPoints.size() <= k) { // 当得到k个簇,则算法终止 LOG.info("Start bisecting iterations: #" + (++bisectingIterations) + ", bisectingK=" + bisectingK + ", maxMovingPointRate=" + maxMovingPointRate + ", maxInterations=" + maxInterations + ", parallism=" + parallism); // for k=bisectingK, execute k-means clustering // bisecting trials KMeansClustering bestBisectingKmeans = null; double minTotalSSE = Double.MAX_VALUE; for (int i = 0; i < m; i++) { // 执行二分试验:调用k-means聚类算法,将输入的点集进行二分,得到2个簇,试验执行m次 final KMeansClustering kmeans = new KMeansClustering(bisectingK, maxMovingPointRate, maxInterations, parallism); kmeans.initialize(points); // the clustering result should have 2 clusters kmeans.clustering(); double currentTotalSSE = computeTotalSSE(kmeans.getCenterPointSet(), kmeans.getClusteringResult()); // 计算一次二分试验中总的SSE的值 if(bestBisectingKmeans == null) { bestBisectingKmeans = kmeans; minTotalSSE = currentTotalSSE; } else { if(currentTotalSSE < minTotalSSE) { // 记录总SSE最小的二分聚类,通过kmeans保存二分结果 bestBisectingKmeans = kmeans; minTotalSSE = currentTotalSSE; } } LOG.info("Bisecting trial <<" + i + ">> : minTotalSSE=" + minTotalSSE + ", currentTotalSSE=" + currentTotalSSE); } LOG.info("Best biscting: minTotalSSE=" + minTotalSSE); // merge cluster points for choosing cluster bisected again int id = generateNewClusterId(clusteringPoints.keySet()); // 每次执行k-means聚类,都多了一个簇,为多出的这个簇分配一个编号 Set<CenterPoint> bisectedCentroids = bestBisectingKmeans.getCenterPointSet(); // 二分得到的2个簇的质心的集合 merge(clusteringPoints, id, bisectedCentroids, bestBisectingKmeans.getClusteringResult().getClusteredPoints()); // 将二分得到的2个簇的集合,合并加入到最终结果的集合中 if(clusteringPoints.size() == k) { // 已经得到k个簇,算法终止 break; } // compute cluster to be bisected ClusterInfo cluster = chooseClusterToBisect(clusteringPoints); // remove centroid from collected clusters map clusteringPoints.remove(cluster.centroidToBisect); LOG.info("Cluster to be bisected: " + cluster); points = Lists.newArrayList(); for(ClusterPoint<Point2D> cp : cluster.clusterPointsToBisect) { points.add(cp.getPoint()); } LOG.info("Finish bisecting iterations: #" + bisectingIterations + ", clusterSize=" + clusteringPoints.size()); } // finally transform to result format Iterator<Entry<CenterPoint, Set<ClusterPoint<Point2D>>>> iter = clusteringPoints.entrySet().iterator(); while(iter.hasNext()) { // 构造最终输出结果的数据结构 Entry<CenterPoint, Set<ClusterPoint<Point2D>>> entry = iter.next(); clusteredPoints.put(entry.getKey().getId(), entry.getValue()); centroidSet.add(entry.getKey()); } }
上面,我们调用chooseClusterToBisect方法区实现对具有最大的SSE的簇进行二分,具体选择的实现过程,代码如下所示:
private ClusterInfo chooseClusterToBisect(Map<CenterPoint, Set<ClusterPoint<Point2D>>> clusteringPoints) { double maxSSE = 0.0; int clusterIdWithMaxSSE = -1; CenterPoint centroidToBisect = null; Set<ClusterPoint<Point2D>> clusterToBisect = null; Iterator<Entry<CenterPoint, Set<ClusterPoint<Point2D>>>> iter = clusteringPoints.entrySet().iterator(); while(iter.hasNext()) { Entry<CenterPoint, Set<ClusterPoint<Point2D>>> entry = iter.next(); CenterPoint centroid = entry.getKey(); Set<ClusterPoint<Point2D>> cpSet = entry.getValue(); double sse = computeSSE(centroid, cpSet); // 计算一个簇的SSE值 if(sse > maxSSE) { maxSSE = sse; clusterIdWithMaxSSE = centroid.getId(); centroidToBisect = centroid; clusterToBisect = cpSet; } } return new ClusterInfo(clusterIdWithMaxSSE, centroidToBisect, clusterToBisect, maxSSE); // 将待分裂的簇的信息保存在ClusterInfo对象中 } private double computeSSE(CenterPoint centroid, Set<ClusterPoint<Point2D>> cpSet) { // 计算某个簇的SSE double sse = 0.0; for(ClusterPoint<Point2D> cp : cpSet) { // update cluster id for ClusterPoint object cp.setClusterId(centroid.getId()); double distance = MetricUtils.euclideanDistance(cp.getPoint(), centroid); sse += distance * distance; } return sse; }
在二分试验过程中,因为每次二分都生成2个新的簇,通过计算这2个簇的总SSE的值,通过迭代计算,找到一个局部最小的总SSE对应的2个簇的划分方法,然后将聚类生成的2簇加入到最终的簇集合中,总SSE的计算法方法在computeTotalSSE方法中,如下所示:
private double computeTotalSSE(Set<CenterPoint> centroids, ClusteringResult<Point2D> clusteringResult) { double sse = 0.0; for(CenterPoint center : centroids) { // 计算2个簇的总SSE值 int clusterId = center.getId(); for(ClusterPoint<Point2D> p : clusteringResult.getClusteredPoints().get(clusterId)) { double distance = MetricUtils.euclideanDistance(p.getPoint(), center); sse += distance * distance; } } return sse; }
聚类效果对比
下面,我们对比一下,k-means算法与Bisecting k-means多次运行的聚类结果对比,如下图所示:
上图中,第一排是执行k-means聚类得到的效果图,第二排是执行Bisecting k-means聚类得到的效果图,其中编号为9999的点为质心点。第二排效果图看似多次计算质心没有变化,但是实际是变化的,只是质心点的位置比较稳定,例如,下面是两组质心点:
462.9642857142857,303.5,7 172.0,279.625,8 236.54285714285714,136.25714285714287,10 105.125,65.9375,11 75.91304347826087,185.7826086956522,12 56.03333333333333,299.53333333333336,13 273.5,117.5,14 415.5952380952381,68.71428571428571,15 329.04,308.68,16 374.35,200.55,17 172.0,279.625,5 462.9642857142857,303.5,8 105.125,65.9375,9 236.54285714285714,136.25714285714287,10 273.6666666666667,124.44444444444444,11 415.5952380952381,68.71428571428571,12 75.91304347826087,185.7826086956522,13 56.03333333333333,299.53333333333336,14 379.57894736842104,201.6315789473684,15 329.04,308.68,16
同k-means算法一样,Bisecting k-means算法不适用于非球形簇的聚类,而且不同尺寸和密度的类型的簇,也不太适合。
本文基于署名-非商业性使用-相同方式共享 4.0许可协议发布,欢迎转载、使用、重新发布,但务必保留文章署名时延军(包含链接:http://shiyanjun.cn),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。
Bisecting k-means聚类算法,您好,看了这篇文章感觉写的很好,请问在哪里能得到这个算法的原码呢,如果可以的话?
可以到我的Github上找到:https://github.com/shirdrn/dm-clustering