上一篇文章《K-均值算法的原理与实战》,讲到K均值算法只能收敛到局部最小值的问题,本文给大家带来了克服这个问题的二分K均值算法。
K-均值算法因为受到初始簇质心的影响,可能只收敛到局部最小值,而非全局最小值。比如下图,使用K-均值算法划分为3个簇,预期的聚类结果应该是上方2个簇,下方1个簇。但因为初始化簇质心是随机选择的点,其中的2个质心被分配到下方的1个簇中,导致聚类结果并非最好的结果。
二分K-均值(bisecting K-means)算法是对K-均值算法的改进,它可以很好的克服对初始簇质心比较敏感的问题。二分K-均值算法通过不断的使用2-means划分簇,保证每次分裂聚类效果最差的簇,克服初始化簇质心带来的问题,下面将详细讲解它的原理。
二分K-均值算法首先将所有点组成一个簇,然后使用k-means(k=2)算法将该簇分裂为两个簇。之后选择其中一个簇继续划分,选择哪一个簇划分取决于划分后是否可以最大程度的降低SSE(误差平方和)。上述基于SSE的划分过程不断重复,直到得到用户指定的簇数目 k k k为止。该过程的伪代码如下:
输入: 样本集 D D D = { x 1 , x 2 , . . . , x m x_1,x_2,...,x_m x1,x2,...,xm};
聚类簇数 k k k
过程:
输出: 簇划分 C = { C 1 , C 2 , . . . , C k } C = \{C_1,C_2,...,C_k\} C={C1,C2,...,Ck}
首先从百度网盘下载测试数据集,下载链接:https://pan.baidu.com/s/1pOQ9qvWrYE4J3Kf-498HIw ,提取码:x8wd
。然后复制上篇文章《K-均值算法的原理与实战》的代码和本文中代码,粘贴到Jupyter Notebook中运行。
#二分K-均值算法
def biKMeans(dataSet, k, distMeas=distED):
m = np.shape(dataSet)[0]
clusterAssment = np.mat(np.zeros((m,2))) # 记录点的簇分配结果
centroid0 = np.mean(dataSet, axis=0).tolist()[0] #创建一个初始簇
centList=[centroid0] #保存所有质心的列表
for j in range(m):
clusterAssment[j,1] = distMeas(np.mat(centroid0),dataSet[j,:])**2
# 重复分裂簇,直到得到想要的簇数目
while (len(centList) < k):
lowestSSE = np.inf #初始化误差平方和(Sum of Squared Error),置为无穷大
#尝试划分已有的每一个簇,寻找划分后使SSE最小的那一个簇,然后对其进行2-Means聚类划分。
for i in range(len(centList)):
ptsInCurrCluster = dataSet[np.nonzero(clusterAssment[:,0].A==i)[0],:] #当前簇i下所有点
centroidMat,splitClustAss=kMeans(ptsInCurrCluster, 2, distMeas) #对簇i进行2-kmeans聚类划分
sseSplit=np.sum(splitClustAss[:,1]) #当前簇i划分后的SSE值
sseNotSplit=np.sum(clusterAssment[np.nonzero(clusterAssment[:,0].A!=i)[0],1]) #其他簇的SSE值
# 选取簇划分后可以使总SSE值最小的簇
if(sseSplit + sseNotSplit) < lowestSSE:
bestCentToSplit = i
bestNewCents = centroidMat
bestClustAss = splitClustAss.copy()
lowestSSE = sseSplit + sseNotSplit
bestClustAss[np.nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList) #更新划分后的一个簇标记
bestClustAss[np.nonzero(bestClustAss[:,0].A == 0)[0],0] = bestCentToSplit #更新划分后的另一个簇标记
centList[bestCentToSplit] = bestNewCents[0,:].A[0] #更新被划分簇的质心
centList.append(bestNewCents[1,:].A[0]) #新增划分出的新簇质心
clusterAssment[np.nonzero(clusterAssment[:,0].A==bestCentToSplit)[0],:] = bestClustAss #更新簇的分配结果
return np.mat(centList), clusterAssment
#绘制数据点与质心
def drawPic(dataMat,centroids,clusterAssment):
type1_x=[];type1_y=[];type2_x=[];type2_y=[];type3_x=[];type3_y=[]
for i in range(datMat3.shape[0]):
if myClusterAssments[i,0]==0:
type1_x.append(datMat3[i,0])
type1_y.append(datMat3[i,1])
elif myClusterAssments[i,0]==1:
type2_x.append(datMat3[i,0])
type2_y.append(datMat3[i,1])
elif myClusterAssments[i,0]==2:
type3_x.append(datMat3[i,0])
type3_y.append(datMat3[i,1])
cent_x=myCentroids[:,0].T.tolist()[0]
cent_y=myCentroids[:,1].T.tolist()[0]
p1 = plt.scatter(type1_x,type1_y,s=30,marker='x')
p2 = plt.scatter(type2_x,type2_y,s=30,marker='x')
p3 = plt.scatter(type3_x,type3_y,s=30,marker='x')
cent = plt.scatter(cent_x,cent_y,s=200,marker='o')
plt.legend(["p1","p2","p3",'cent'],bbox_to_anchor=(1, 1))
plt.show()
#加载数据集
datMat3=np.mat(loadDataSet('biKMeansTestSet.txt'))
#生成簇质心,簇分配结果
myCentroids, myClusterAssments=biKMeans(datMat3,3)
#绘制聚类结果图
drawPic(datMat3,myCentroids,myClusterAssments)
最终的聚类结果如下图所示,其中p1-p3为划分为3种类别的簇,cent为簇质心。
二分K-均值算法是一种层次聚类方法,其实质就是在满足最小SSE(误差平方和)的情况下,不断的对选中的簇做k=2的k-means切分,直到聚类数等于用户指定的聚类数目k为止。
我们使用SSE(误差平方和)衡量聚类效果的好坏,SSE越小,则数据点越接近它们的簇质心,聚类效果就越好。二分K均值算法选择要划分的簇,是可以最大程度降低SSE的值的簇,这意味着该簇的聚类效果不好,把多个簇当成一个簇了。所以应该选择该簇进行划分,如此可以收敛到全局最小值。