本文讲一下mahout中kmeans算法和Canopy算法实现原理

 

一. Kmeans是一个很经典的聚类算法,我想大家都非常熟悉。虽然算法较为简单,在实际应用中却可以有不错的效果;其算法原理也决定了其比较容易实现并行化。

学习mahout就先从简单的kmeans算法开始学起,就当抛砖引玉了。

 

1. 首先来简单的回顾一下KMeans算法:

 

(1)   根据事先给定的k值建立初始划分,得到k个Cluster,比如,可以随机选择k个点作为k个Cluster的重心,又或者用其他算法得到的Cluster作为初始重心;

(2)、计算每个点到各个Cluster重心的距离,将它加入到最近的那个Cluster;

(3)、重新计算每个Cluster的重心;

(4)、重复过程2~3,直到各个Cluster重心在某个精度范围内不变化或者达到最大迭代次数;

 

其时间复杂度是:O(nkt),其中:n是聚类点个数,k是Cluster个数,t是迭代次数。容易并行化,

kmeans缺点是:对孤立点敏感,K值难以估计

 

2. kmeans的算法思想决定了其实现并行化还是比较容易的,主要是在迭代中计算某个点属于哪一个新的聚类中心的过程是可以并行化的。

 

主要分成以下几个步骤:

(1)datanode在map阶段读出位于本地的数据集,并且从HDFS中读到上一次聚类后的聚类中心(从cluster-N-1的目录中),计算并输出每个点及其对应的Cluster;

(2)combiner操作对位于本地包含在相同Cluster中的点进行reduce操作并输出,

(3)reduce操作得到全局Cluster集合并写入HDFS的指定目录(cluster-N)。

 

Kmeans每一轮迭代会生成新的全局聚类中心,这个聚类中心在下一次迭代的时候是会使用到的,所以每一轮迭代(实际上每一轮迭代是一个新mapreduce job)之后,reduce任务会负责将此次的聚类中心输出到集群中。命名是以cluster加上迭代次数命名的。为:Cluster-N

 

由于每次迭代是起了一个job,mahout中有一个KMeansDriver的类来对整个过程进行控制。抽象过程。最后,当获得了稳定的全局聚类中心后,简单的想想就能知道,只要用一个Map过程就可以实现对全体数据的分类。具体流程如下图:

alt

 

 

二. 在开始的时候说过,Kmeans算法收初始点的影响较大,如果起始点是一个孤立点是一个很麻烦的事情。解决的办法是先对数据集进行一个较为“粗”的聚类算法。目的是对全体数据集进行一个大致的分组过程,然后这些组为单位选取Kmeans的初始聚类中心点,这样初始点的选择就不至于因为随即而偏离的太远。Mahout里面是使用Canopy算法来实现这一初始聚类过程的,当然mahout中也会提供随机选取初始中心点的方法,用的是RandomSeedGenerator类。(运行mahout kMeans时会默认选择Canopy算法作为聚类中心选择的初始算法,但当你提供-k参数,即指明有几个聚类中心时,则会选择随机的方式生成初始中心点)

 

1. Canopy算法其实本身也可以用于聚类,但它的结果可以为之后代价较高聚类提供帮助,其用在数据预处理上要比单纯拿来聚类更有帮助;

Canopy算法的步骤如下:

(1) 将所有数据放进list中,选择两个距离,T1T2T1>T2

(2)While(list不为空)

 { 

随机选择一个节点做canopy的中心;并从list删除该点;

遍历list:

对于任何一条记录,计算其到各个canopy的距离;

如果距离<T2,则给此数据打上强标记,并从list删除这条记录;

如果距离<T1,则给此数据打上弱标记;

如果到任何canopy中心的聚类都>T1,那么将这条记录作为一个新的canopy的中心,并从list中删除这个元素;

}

 

同样,Canopy算法的实现也比较简单,其使用T1和T2两个距离来实现对数据集的一个粗略的划分,表现为最终会生成几个Canopy下面举一个例子。

8.1,8.1  

7.1,7.1  

6.2,6.2  

7.1,7.1  

2.1,2.1  

1.1,1.1  

0.1,0.1  

3.0,3.0  

一共有8个二维的点,为了计算简便,我们选择曼哈顿距离作为距离的量度,即|x1 – x2| + |y1 – y2|。选取T1=8,T2=4。mahout里面是可以为Kmeans算法设置不同的距离计算方法的,如欧式距离,向量夹角,曼哈顿距离,雅克比系数等。

首先选取(8.1, 8.1)作为第一个Canopy的中心,并从list中删除这个点。

然后开始遍历整个list。

第二个点(7.1,7.1)

距离Canopy 1 (8.1,8.1)的距离是2,2<T2,所以第二个点属于Canopy 1,将其加入Canopy 1,并从list中删除该点;

第三个点(6.2,6.2)

距离Canopy 1 (8.1,8.1)的距离是3.8,3.8<T2,所以第三个点也是属于Canopy 1。同样将其加入Canopy 1,并从list删除该点;

第四个点(7.1,7.1)与第一个点是相同的。

第五个点(2.1,2.1)

其余Canopy 1的距离是12,大于T1,所以第五个点不属于任何Canopy,新生成一个Canopy,其中心是(2.1,2.1),并从list中删除该点,因为这个点不会属于其他Canopy了。

第六个点(1.1,1.1)

到Canopy 1 (8.1,8.1)的距离是14,14> T1,到Canopy 2(2.1,2.1)的距离是2,2<T2,所以第六个点属于Canopy 2(2.1,2.1),并从list中删除这个点。

第七个点(0.1,0.1 )

到Canopy 1 (8.1,8.1)的距离是16,16> T1,到Canopy 2(2.1,2.1)的距离是4,4=T2,所以将第七个点打上属于Canopy 2(2.1,2.1)的弱标记,但并不从集群中删除该点;

第八个点(3.0,3.0)

到Canopy 1 (8.1,8.1)的距离是10.2,10.2> T1,到Canopy 2(2.1,2.1)的距离是1.8,1.8<T2,所以将第八个点加入到Canopy 2(2.1,2.1)中。

此时所有Canopy的状态是:

Canopy 1 (8.1,8.1) :[ (8.1,8.1),  (7.1,7.1),  (6.2,6.2) ,(7.1,7.1) ]

Canopy 2 (2.1,2.1) :[ (2.1,2.1), (1.1,1.1) ,(0.1,0.1),  (3.0,3.0)  ]

并且算法的第一词While循环已经跑过,此时list中还剩下的元素是(0.1,0.1)

将他自己作为一个新的Canopy,Canopy 3(0.1,0.1),集群中Canopy的最后状态是

Canopy 1 (8.1,8.1) :[ (8.1,8.1),  (7.1,7.1),  (6.2,6.2) ,(7.1,7.1) ]

Canopy 2 (2.1,2.1) :[ (2.1,2.1), (1.1,1.1) ,(0.1,0.1),  (3.0,3.0)  ]

Canopy 3 (0.1,0.1) :[ (0.1,0.1)]

所以最终的Canopy聚类中心是(7.125,7.125),(1.575,1.575),(0.1,0.1)

 

2. Canopy算法的并行化:

Canopy算法的并行点也是比较明显的,即生成Canopy的过程可以并行,

(1)  各个mapper可以依据存储在本地的数据,各自在本地用上述算法生成若干Canopy(2)  reducer将这些Canopy用相同算法汇总后计算出最终的Canopy集合;

下图可以较好的反应并行化的过程:

alt

 

 

举个例子,我们只有两个map任务:

第一个map的数据是

8.1,8.1  

7.1,7.1  

6.2,6.2  

7.1,7.1  

2.1,2.1  

1.1,1.1  

0.1,0.1  

3.0,3.0  

根据上文的计算,我们得到其Canopy中心是(7.125,7.125),(1.575,1.575),(0.1,0.1)

那么第一个map的数据就是(7.125,7.125),(1.575,1.575),(0.1,0.1)

 

第二个map的数据是

8,8  

7,7  

6.1,6.1  

9,9  

2,2  

1,1  

0,0  

2.9,2.9

运用Canopy算法我们可以到,其Canopy中心是(7.525,7.525),(1.475,1.475),(1.45,1.45)

第二个map的输出数据就是(7.525,7.525),(1.475,1.475),(1.45,1.45)

 

那么Reduce中获得数据就是(7.125,7.125),(1.575,1.575),(0.1,0.1),(7.525,7.525),(1.475,1.475),(1.45,1.45)

Reduce任务会用Canopy算法对这个六个点进行Canopy划分。从而得到全局的Canopy中心。