为了克服K-均值算法收敛于局部最小值的问题,还有一种方法是二分K-均值的算法。该算法首先将所有点作为一个簇,然后将该簇一分为二。之后选择其中一个簇继续进行划分,选择哪一个簇进行划分取决于对其划分是否可以最大程度降低SSE的值。上述基于SSE的划分过程不断重复,直到得到用户指定的簇数目为止。
二分K-均值算法的伪代码形式如下:
将所有点看成一个簇
当簇数目小于k时:
对于每一个簇:
计算总误差
在给定的簇上面进行K-均值聚类(k=2)
计算将该簇一分为二之后的总误差
选择使得误差最小的那个簇进行划分操作
另一种做法是选择SSE最大的簇进行划分,直到簇数目达到用户指定的数目为止:
def biKmeans(dataSet,k,distMeas=distEclud):
m=shape(dataSet)[0]
clusterAssment=mat(zeros((m,2)))
#创建一个初始簇
centroid0=mean(dataSet,axis=0).tolist()[0]
centList=[centroid0]
for j in range(m):
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],:]
centroidMat,splitClustAss=kMeans(ptsInCurrCluster,2,distMeas)
sseSplit=sum(splitClustAss[:,1])
sseNotSplit=sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1])
print('sseAplit , and seeNotSplit',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)
bestClustAss[nonzero(bestClustAss[:,0].A==0)[0],0]=bestCentToSplit
print('bestCentToSplit: ',bestCentToSplit)
print('bestClustAss的长度:',len(bestClustAss))
centList[bestCentToSplit]=bestNewCents[0,:]
centList.append(bestNewCents[1,:])
clusterAssment[nonzero(clusterAssment[:,0].A==bestCentToSplit)[0],:]=bestClustAss
return mat(centList),clusterAssment
上述代码中biKmeans()的输入参数:数据集、簇的数目、计算距离和创建初始质心的函数;返回结果是聚类结果。
该函数首先创建一个矩阵来存储数据集中每个点的簇分配结果及平方误差,然后计算整个数据集的质心,并用一个列表来保留所有的质心。得到上述质心之后,可以遍历数据集中所有点来计算每个点到质心的误差值。
接下来程序进入到while循环,该循环会不停对簇进行划分,直到得到想要的簇数目为止。可以通过考察簇列表中的值来获得当前簇的数目。然后遍历所有的簇来决定最佳的簇进行划分。为此需要比较划分前后的SSE。一开始将最小SSE设置为无穷大,然后遍历簇列表centList中的每一个簇。对每个簇,将该簇中的所有点看成一个小的数据集ptsInCurrCluster。将ptsInCurrCluster输入到函数kMeans()中进行处理(k=2)。K-均值算法会生成两个质心(簇),同时给出每个簇的误差值。这些误差与生育数据集的误差之和作为本次划分的误差。如果该划分的SSE值最小,则本次划分被保存。一旦决定了要划分的簇,接下来就要实际划分操作。划分操作很容易,只需要将要划分的簇中所有点的簇分配结果进行修改即可。当使用kMeans()函数并指定簇数为2时,会得到两个编号分别为0和1的结果簇。需要将这些簇编号修改为划分簇及新加簇的编号,该过程可以通过两个数组过滤器来完成。最后,新的簇分配结果被更新,新的质心会被添加到centList中。
当while循环结束时,同kMeans()函数一样,函数返回质心列表与簇分配结果。
实际运行效果:
dataMat3=mat(loadDataSet('test/testSet2.txt'))
centList,myNewAssment=biKmeans(dataMat3,3)
print('质心:',centList)
print('分配结果:',myNewAssment)
上述函数可以运行多次,聚类会收敛到全局最小值,而原始的kMeans()函数偶尔会陷入局部最小值。