Advertisement

K-means算法优化(二分K-means算法)

阅读量:

关注微信公众号【Microstrong】,我写过四年Android代码,了解前端、熟悉后台,现在研究方向是机器学习、深度学习!一起来学习,一起来进步,一起来交流吧!

本文同步更新在我的微信公众号里,地址:https://mp.weixin.qq.com/s?__biz=MzI5NDMzMjY1MA==&mid=2247484014&idx=1&sn=516cdf9ec77302c2c11d398469ddf3f1&chksm=ec6533ebdb12bafd09112e86d655435456bd3b91bc9e2edc062494e76d25938e98e75ea052d4#rd

目录:

(1)回顾K-means算法缺点

(2)二分K-means算法介绍

(3)用Python实现二分k-means算法

(1)k-means算法缺点

上一篇文章《聚类算法之K-means算法》中介绍了K-means算法所有知识点并指出K-means算法的缺点。今天我们来回顾一下K-means的这些缺点:

  1. 对噪声和孤立点数据比较敏感。表现为:K-means将簇中所有点的均值作为新质心,若簇中含有异常点,将导致均值偏离严重,容易陷入局部最小值。

  2. 必须事先给出K值。

这些缺点我们在上一篇文中都有介绍和说明。如果不清楚,可以看《聚类算法之K-means算法》

在K-均值聚类中簇的数目k是一个用户预先定义的参数,那么用户如何才能知道k的选择是否正确呢?如何才能知道生成的簇比较好呢?在包含簇分配结果的矩阵中保存着每个样本的误差,即该样本到簇质心的距离平方值。这一点在《聚类算法之K-means算法》中有用Python实现。下面会讨论利用该误差来评价聚类质量的方法。

图1:K-means算法实现西瓜数据集4.0聚成3簇

考虑图1的聚类结果,这是在一个包含三个簇的数据集上k-means算法之后的结果,但是点的簇分结果值没有那么准确。K-means算法收敛但聚类效果较差的原因是,k-means算法收敛到了局部最小值,而非全局最小值(局部最小值指结果还可以但并非最好结果,全局最小值是可能的最好结果)。

一种用于度量聚类效果的指标是SSE(Sum of Squared Error,误差平方和),对应《聚类算法之K-means算法》的Python程序代码,如图2所示。SSE值越小表示数据点越接近于它们的质心,聚类效果也越好。因为对误差取了平方,因此更加重视那些远离中心的点。一种肯定可以降低SSE值的方法是增加簇的个数,但这违背了聚类的目标。聚类的目标是在保持簇数目不变的情况下提高簇的质量。

图2:Python实现SSE

(2)二分K-means算法介绍

那么如何对图1的结果进行改进呢?你可以对生成的簇进行后处理,一种方法是将具有最大SSE值的簇划分成两个簇。具体实现时可以将最大簇包含的点过滤出来并在这些点上K-means算法,其中的K设置为2。

图3:由于质心随机初始化导致K-means算法效果不好的一个例子,这需要额外的后处理操作来清理聚类结果

为了保持簇总数不变,可以将某两个簇进行合并。从图3中很明显就可以看出,应该将图下部两个出错的簇质心进行合并。可以很容易对二维数据上的聚类进行可视化,知道哪几个簇可以合并,但是如果遇到40维的数据应该如何去做?

有两种可以量化的办法:合并最近的质心,或者合并两个使得SSE增幅最小的质心。第一种思路通过计算所有质心之间的距离,然后合并距离最近的两个点来实现。第二种方法需要合并两个簇然后计算总SSE值。必须在所有可能的两个簇上重复上述处理过程,直到找到合并最佳的两个簇为止。接下来将讨论利用上述簇划分技术得到更好的聚类结果的方法。

为克服K-均值算法收敛于局部最小值的问题,有人提出了另一个称为二分K-均值(bisecting K-means)的算法。该算法首先将所有点作为一个簇,然后将该簇一分为二。之后选择其中一个簇继续进行划分,选择哪一个簇进行划分取决于对其划分是否可以最大程度降低SSE的值。上述基于SSE的划分过程不断重复,直到得到用户指定的簇数目为止。

二分K-means算法的伪代码形式如下:

(3)用Python实现二分k-means算法

复制代码
 from numpy import *

    
 import xlrd
    
 import matplotlib.pyplot as plt
    
  
    
 # 计算欧氏距离
    
 def distEclud(vector1, vector2):
    
     '''
    
     :param vector1: 第j个均值向量
    
     :param vector2: 第i个样本
    
     :return: 距离值
    
     '''
    
     return sqrt(sum(power(vector2 - vector1, 2)))
    
  
    
 #构建聚簇中心
    
 def randCent(dataSet, k):
    
     n = shape(dataSet)[1]
    
     centroids = mat(zeros((k,n)))
    
     for j in range(n):
    
     minJ = min(dataSet[:,j])
    
     maxJ = max(dataSet[:,j])
    
     rangeJ = float(maxJ - minJ)
    
     centroids[:,j] = minJ + rangeJ * random.rand(k, 1)
    
     return centroids
    
  
    
 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循环会不停的对簇进行划分,直到得到想要的簇数目为止
    
     while(len(centList) < k):
    
     lowestSSE = inf    #python中inf表示正无穷
    
     #遍历簇列表centList中的每一个簇
    
     for i in range(len(centList)):
    
         #-----尝试划分每一簇----
    
         #对每个簇,将该簇中的所有点看成一个小的数据集ptsInCurrCluster
    
         ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:]
    
         #将ptsInCurrCluster输入到函数KMeans()中进行处理(k=2)。K-均值算法会生成两个质心(簇),同时给出每个簇的误差值
    
         centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas)
    
  
    
         #划分后的样本误差之和
    
         sseSplit = sum(splitClustAss[:, 1])
    
         #剩余数据集的误差之和
    
         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
    
         #----更新簇的分配结果----
    
     #将划分簇中所有点的簇分配结果进行修改,当使用KMeans()函数并且指定簇数为2时,会得到两个编号分别为0和1的结果簇
    
     #需要将这些簇编号修改为划分簇及新加簇的编号,该过程可以通过两个数组过滤器来完成。
    
     bestClustAss[nonzero(bestClustAss[:, 0].A == 1)[0], 0] = len(centList)
    
     bestClustAss[nonzero(bestClustAss[:, 0].A == 0)[0], 0] = bestCentToSplit
    
     print('the bestCentToSplit is: ', bestCentToSplit)
    
     print('the len of bestClustAss is: ', len(bestClustAss))
    
     #新的质心会被添加到centList中
    
     centList[bestCentToSplit] = bestNewCents[0, :].tolist()[0]
    
     centList.append(bestNewCents[1, :].tolist()[0])
    
     # 更新SSE的值(sum of squared errors)
    
     clusterAssment[nonzero(clusterAssment[:, 0].A == bestCentToSplit)[0],:] = bestClustAss
    
     return mat(centList),clusterAssment
    
  
    
 #k-means 聚类算法
    
 def kMeans(dataSet, k, distMeans =distEclud, createCent = randCent):
    
     m = shape(dataSet)[0]
    
     clusterAssment = mat(zeros((m,2)))    #用于存放该样本属于哪类及质心距离
    
     centroids = createCent(dataSet, k)
    
     clusterChanged = True
    
     while clusterChanged:
    
     clusterChanged = False;
    
     for i in range(m):
    
         minDist = inf; minIndex = -1;
    
         for j in range(k):
    
             distJI = distMeans(centroids[j,:], dataSet[i,:])
    
             if distJI < minDist:
    
                 minDist = distJI; minIndex = j
    
         if clusterAssment[i,0] != minIndex: clusterChanged = True;
    
         clusterAssment[i,:] = minIndex,minDist**2
    
     print(centroids)
    
     for cent in range(k):
    
         ptsInClust = dataSet[nonzero(clusterAssment[:,0].A == cent)[0]]   # 去第一列等于cent的所有列
    
         centroids[cent,:] = mean(ptsInClust, axis = 0)
    
     return centroids, clusterAssment
    
  
    
 # show your cluster only available with 2-D data
    
 def showCluster(dataSet, k, centroids, clusterAssment):
    
     numSamples, dim = dataSet.shape
    
     if dim != 2:
    
     print ("Sorry! I can not draw because the dimension of your data is not 2!")
    
     return 1
    
  
    
     mark = ['or', 'ob', 'og', 'ok', '^r', '+r', 'sr', 'dr', '<r', 'pr']
    
     if k > len(mark):
    
     print ("Sorry! Your k is too large! please contact Zouxy")
    
     return 1
    
  
    
     # draw all samples
    
     for i in range(numSamples):
    
     markIndex = int(clusterAssment[i, 0])
    
     plt.plot(dataSet[i, 0], dataSet[i, 1], mark[markIndex])
    
  
    
     mark = ['Dr', 'Db', 'Dg', 'Dk', '^b', '+b', 'sb', 'db', '<b', 'pb']
    
     # draw the centroids
    
     for i in range(k):
    
     plt.plot(centroids[i, 0], centroids[i, 1], mark[i], markersize=12)
    
     plt.show()
    
  
    
 def main():
    
     ## step 1: load data
    
     print ("step 1: load data...")
    
     dataSet = []
    
     data = xlrd.open_workbook('C:/Users/Microstrong/Desktop/watermelon4.0.xlsx')
    
     table = data.sheets()[0]
    
     for line in range(0,table.nrows):
    
     lineArr = table.row_values(line)
    
     dataSet.append([float(lineArr[0]), float(lineArr[1])])
    
  
    
     ## step 2: clustering...
    
     print ("step 2: clustering...")
    
     dataSet = mat(dataSet)
    
     k = 3
    
     centroids, clusterAssment = biKmeans(dataSet, k)
    
  
    
     ## step 3: show the result
    
     print ("step 3: show the result...")
    
     showCluster(dataSet, k, centroids, clusterAssment)
    
  
    
 if __name__ == '__main__':
    
  main()
    
    
    
    
    AI生成项目python
    
    
![](https://ad.itadn.com/c/weblog/blog-img/images/2025-07-14/8hNJOky1xb7AiQdKqETojpC5vVZD.png)

代码后最终的效果,如图4所示。具体的代码和数据集在我的gitHub中,数据集是周志华《机器学习》西瓜数据集4.0,地址:https://github.com/Microstrong0305/machine_learning/tree/master/K-means

图4:二分K-means算法实现西瓜数据集4.0聚成3簇

下面我再把图1和图4放到一起来比较,如图5所示。很明显K-means算法和二分K-means算法最后聚类的结果差别很大,且二分K-means算法效果更好一些。

图5:左图是K-means算法聚类西瓜数据集4.0;右图是二分K-means算法聚类西瓜数据集4.0

全部评论 (0)

还没有任何评论哟~