上一篇文章《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
过程:
- 将 D D D中所有样本组成初始簇 C = { C 1 } C=\{C_1\} C={C1}
- repeat
- 初始化最小误差为正无穷: S S E m i n = i n f SSE_{min}=inf SSEmin=inf
- for i = 1 , 2 , . . . , ∣ C ∣ i = 1,2,...,|C| i=1,2,...,∣C∣ do
- 在给定的簇 C i C_i Ci上面进行k=2的K-均值切分,得到两个新簇: C i a C_{i_a} Cia与 C i b C_{i_b} Cib
- 计算将该簇
C
i
C_i
Ci一分为二之后的总误差:
S S E s u m = ∑ 1 ≤ j ≤ ∣ C ∣ , j ≠ i S S E C j + S S E C i a + S S E C i b SSE_{sum}=\sum_{1\leq j\leq {|C|},j \neq i} SSE_{C_j}+SSE_{C_{i_a}}+SSE_{C_{i_b}} SSEsum=∑1≤j≤∣C∣,j=iSSECj+SSECia+SSECib - if S S E s u m < S S E m i n SSE_{sum} < SSE_{min} SSEsum<SSEmin then
- 更新当前最小误差: S S E m i n = S S E s u m SSE_{min}=SSE_{sum} SSEmin=SSEsum
- 指定划分簇标记: λ s p l i t = i \lambda_{split}=i λsplit=i
- 划分后的两个新簇: C λ a = C i a C_{\lambda_a}=C_{i_a} Cλa=Cia, C λ b = C i b C_{\lambda_b}=C_{i_b} Cλb=Cib
- end for
- 更新被划分的簇为划分后的一个簇: C λ s p l i t = C λ a C_{\lambda_{split}}=C_{\lambda_a} Cλsplit=Cλa
- 将划分后的另一个簇并入簇集合: C = C ⋃ { C λ b } C=C\bigcup \{C_{\lambda_b}\} C=C⋃{Cλb}
- until 得到用户指定的簇数目 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的值的簇,这意味着该簇的聚类效果不好,把多个簇当成一个簇了。所以应该选择该簇进行划分,如此可以收敛到全局最小值。