二分k均值算法
K均值聚类算法存在的问题
K均值聚类算法虽然便于理解和实现,但是有其自身的缺点,由于k均值算法初始的k个质心是随机生成的,然后对质心不断的优化,寻找最优的质心以进行聚类,但有时聚类的效果并不是很好,原因就是k均值算法是一种局部最优算法而非全局最优算法,如图对一个包含3个族类的数据进行聚类,由于初始质心的选择问题,当程序更新质心的过程中已经使每条数据的分类都不再变化,程序结束,但是明显数据并没有很好的归类,该质心只是保证每个属于该质心族类的数据到质心距离最小,但本身该质心是不是出现在最好的位置我们并不能保证。
二分k均值算法的提出:
为解决上述问题,我们可以引入SSE指标--误差平方和(见系列文章k均值聚类算法中的dataTag),使整个数据集的误差平方和最小这一全局考量可以做到全局的优化。上图中我们把具有最大误差平方的一个类分成两类,为保证总的分类数不变,再把某两个类聚合为一个,具体的合并方法一个是合并距离最近的两个质心,另一个是使合并后的误差平方和最小。
有了这个思路,我们可以逆向思维,因为直接拆分和合并太麻烦,我们可以按照使误差平方和最小的原则,对初始的数据集进行分类(这样没有合并的过程)。
算法思想:
二分k均值算法就此诞生了,顾名思义,二分k均值就是每次将数据集一分为二,即k均值算法中的k值为2,第一次是在整个数据集上划分,这里没什么异议,从第二次开始,每次划分的时候就要选取使整个数据集误差平方和最小的一个类进行一分为二了,以此进行下去直到分成我们想要的k类。
二分k均值的伪代码如下:
将所有点看成一个类别
当类别数小于k时
对每一个类
计算总的误差平方和
在当前类内进行k均值聚类,k的值为2
计算将该类一分为二后总的误差平方和
选择使得总的误差平方和最小的划分类进行划分
二分K均值算法实现
def biKmeans(dataSet, k, distMeas=disEclud):
m = shape(dataSet)[0]
clusterAssment = mat(zeros((m,2)))
centroid0 = mean(dataSet, axis=0).tolist()[0]
centList =[centroid0] #create a list with one centroid
for j in range(m):#calc initial Error
clusterAssment[j,1] = distMeas(mat(centroid0), dataSet[j,:])**2
while (len(centList) < k):
lowestSSE = inf
for i in range(len(centList)):
ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:]#get the data points currently in cluster i
centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas)
sseSplit = sum(splitClustAss[:,1])#compare the SSE to the currrent minimum
sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1])
print( "sseSplit, and notSplit: "+sseSplit+","+sseNotSplit)
if (sseSplit + sseNotSplit) < lowestSSE:
bestCentToSplit = i
bestNewCents = centroidMat
bestClustAss = splitClustAss.copy()
lowestSSE = sseSplit + sseNotSplit
bestClustAss[nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList) #change 1 to 3,4, or whatever
bestClustAss[nonzero(bestClustAss[:,0].A == 0)[0],0] = bestCentToSplit
print('the bestCentToSplit is: '+bestCentToSplit)
print('the len of bestClustAss is: '+ len(bestClustAss))
centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0]#replace a centroid with two best centroids
centList.append(bestNewCents[1,:].tolist()[0])
clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss#reassign new clusters, and SSE
return mat(centList), clusterAssment
对二分k均值算法的解析:(按程序从上到下)
首先shape()[0]函数得到总数据集的行数;
为记录数据集每条数据所属分类和距离质心的误差平方和生成一个m行2列的矩阵;
先用mean()函数得到整个数据集的质心(每个特征的平均值),centList记录质心;
计算数据到质心的距离;初始误差平方和定义为无穷大;从数据集中选出某一类(i类)的数据保存到ptsInCurrCluster ,对该类数据进行2分k均值算法将数据一分为二;
计算新划分出的两类数据的误差平方和;计算数据集中不属于i类的数据误差平方和,因为当前只是选出i类数据进行一分为二,对划分后的数据并没有改变其在原数据集中的分类或误差值,所以不等于i的就是剩余的所有数据(这一点我自己看的时候总是以为已经划分了,认为筛选不出剩余的数据。。。),计算总和,如果误差比原来小了就记录下本次分类是在哪个类(i)上进行的;
记录新的质心以及新的分类和误差等;for循环结束,遍历完所有的族类后,几个best..变量中存的就是使误差平方和最小的分类记录;然后开始进行划分(此处才真正执行划分!);
在执行2分k均值的i类上:a对于新划分出的1类数据给其分配的标签是族类的长度值,即扩展的类名,对于新划分出的0类数据仍然保持其原来分类标签,即还是i;b语句centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0]将原来被划分的i类处质心更新为新划分出的0类质心,语句centList.append(bestNewCents[1,:].tolist()[0]),将新划分出的1类质心增加到质心列表里,b部分更新了质心;c最后选出执行划分的数据分类及误差平方记录表,将其更新为新的类名和误差,a部分进行了命名。
此时都更新完,将数据多划分出了1类,继续执行,直到划分为k类。