K-Means算法

介绍K均值算法:

优点:易于实现 
缺点:可能收敛于局部最小值,在大规模数据收敛慢

 

算法思想较为简单如下所示:

选择K个点作为初始质心

repeat

  • 将每个点指派到最近的质心形成K个簇
  • 重新计算每个簇的质心
  • until簇不发生变化或达到最大迭代次数

 

这里的重新计算每个簇的质心,如何计算的是根据目标函数得来的,因此在开始时我们要考虑距离度量和目标函数。

考虑欧几里得距离的数据,使用误差平方和(Sum of the Squared Error,SSE)作为聚类的目标函数,两次运行K均值产生的两个不同的簇集,我们更喜欢SSE最小的那个。

这里写图片描述

k表示k个聚类中心,ci表示第几个中心,dist表示的是欧几里得距离。 
这里有一个问题就是为什么,我们更新质心是让所有的点的平均值,这里就是SSE所决定的。

  

Python进行实现 :

 1 # dataSet样本点,k 簇的个数
 2 # disMeas距离量度,默认为欧几里得距离
 3 # createCent,初始点的选取
 4 def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
 5     m = shape(dataSet)[0] #样本数
 6     clusterAssment = mat(zeros((m,2))) #m*2的矩阵                   
 7     centroids = createCent(dataSet, k) #初始化k个中心
 8     clusterChanged = True             
 9     while clusterChanged:      #当聚类不再变化
10         clusterChanged = False
11         for i in range(m):
12             minDist = inf; minIndex = -1
13             for j in range(k): #找到最近的质心
14                 distJI = distMeas(centroids[j,:],dataSet[i,:])
15                 if distJI < minDist:
16                     minDist = distJI; minIndex = j
17             if clusterAssment[i,0] != minIndex: clusterChanged = True
18             # 第1列为所属质心,第2列为距离
19             clusterAssment[i,:] = minIndex,minDist**2
20         print centroids
21 
22         # 更改质心位置
# 循环每一个质心,找到属于当前质心的所有点,然后根据这些点去更新当前的质心。 
# nonzero()返回的是一个二维的数组,其表示非0的元素位置。
23 for cent in range(k): 24 ptsInClust = dataSet[nonzero(clusterAssment[:,0].A==cent)[0]] 25 centroids[cent,:] = mean(ptsInClust, axis=0) 26 return centroids, clusterAssment

 

ptsInClust = dataSet[nonzero(clusterAssment[:,0].A==cent)[0]] 
因此首先先比较clusterAssment[:,0].A==cent的真假,如果为真则记录了他所在的行,因此在用切片进行取值。

 

辅助的函数:

 1 def loadDataSet(fileName):      #general function to parse tab -delimited floats
 2     dataMat = []                #assume last column is target value
 3     fr = open(fileName)
 4     for line in fr.readlines():
 5         curLine = line.strip().split('\t')
 6         fltLine = map(float,curLine) #map all elements to float()
 7         dataMat.append(fltLine)
 8     return dataMat
 9 
10 def distEclud(vecA, vecB):
11     return sqrt(sum(power(vecA - vecB, 2))) #la.norm(vecA-vecB)
12 
13 def randCent(dataSet, k):
14     n = shape(dataSet)[1]
15     centroids = mat(zeros((k,n)))#create centroid mat
16     for j in range(n):#create random cluster centers, within bounds of each dimension
17         minJ = min(dataSet[:,j]) 
18         rangeJ = float(max(dataSet[:,j]) - minJ)
19         centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1))
20     return centroids 

 

绘图的程序如下:

 1 def draw(data,center):
 2     length=len(center)
 3     fig=plt.figure
 4     # 绘制原始数据的散点图
 5     plt.scatter(data[:,0],data[:,1],s=25,alpha=0.4)
 6     # 绘制簇的质心点
 7     for i in range(length):
 8         plt.annotate('center',xy=(center[i,0],center[i,1]),xytext=\
 9         (center[i,0]+1,center[i,1]+1),arrowprops=dict(facecolor='red'))
10     plt.show()

K-Means算法的缺陷

k均值算法非常简单且使用广泛,但是其有主要的两个缺陷: 
1. K值需要预先给定,属于预先知识,很多情况下K值的估计是非常困难的,对于像计算全部微信用户的交往圈这样的场景就完全的没办法用K-Means进行。

对于可以确定K值不会太大但不明确精确的K值的场景,可以进行迭代运算,然后找出Cost Function最小时所对应的K值,这个值往往能较好的描述有多少个簇类。 

2. K-Means算法对初始选取的聚类中心点是敏感的,不同的随机种子点得到的聚类结果完全不同 
3. K均值算法并不是很所有的数据类型。它不能处理非球形簇、不同尺寸和不同密度的簇,银冠指定足够大的簇的个数是他通常可以发现纯子簇。 
4. 对离群点的数据进行聚类时,K均值也有问题,这种情况下,离群点检测和删除有很大的帮助。

 

下面对初始质心的选择进行讨论:

拙劣的初始质心

当初始质心是随机的进行初始化的时候,K均值的每次运行将会产生不同的SSE,而且随机的选择初始质心结果可能很糟糕,

可能只能得到局部的最优解,而无法得到全局的最优解。如下图所示:

 

可以看到程序迭代了4次终止,其得到了局部的最优解,显然我们可以看到其不是全局最优的,我们仍然可以找到一个更小的SSE的聚类。

 

随机初始化的局限

你可能会想到:多次运行,每次使用一组不同的随机初始质心,然后选择一个具有最小的SSE的簇集。

该策略非常的简单,但是效果可能不是很好,这取决于数据集合寻找的簇的个数。

 

K-Means优化算法

为了克服K-Means算法收敛于局部最小值的问题,提出了一种二分K-均值(bisecting K-means)

bisecting K-means

算法的伪代码如下:

  • 将所有的点看成是一个簇
    • 当簇小于数目k时
      • 对于每一个簇
      • 计算总误差
      • 在给定的簇上进行K-均值聚类,k值为2
      • 计算将该簇划分成两个簇后总误差
    • 选择是的误差最小的那个簇进行划分

 

完整的Python代码:

 1 def biKmeans(dataSet, k, distMeas=distEclud):
 2     m = shape(dataSet)[0]
 3     # 这里第一列为类别,第二列为SSE
 4     clusterAssment = mat(zeros((m,2)))
 5     # 看成一个簇是的质心
 6     centroid0 = mean(dataSet, axis=0).tolist()[0]
 7     centList =[centroid0] #create a list with one centroid
 8     for j in range(m):    #计算只有一个簇是的误差
 9         clusterAssment[j,1] = distMeas(mat(centroid0), dataSet[j,:])**2
10 
11     # 核心代码
12     while (len(centList) < k):
13         lowestSSE = inf
14         # 对于每一个质心,尝试的进行划分
15         for i in range(len(centList)):
16             # 得到属于该质心的数据
17             ptsInCurrCluster =\ dataSet[nonzero(clusterAssment[:,0].A==i)[0],:]
18             # 对该质心划分成两类
19             centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas)
20             # 计算该簇划分后的SSE
21             sseSplit = sum(splitClustAss[:,1])
22             # 没有参与划分的簇的SSE
23             sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1])
24             print "sseSplit, and notSplit: ",sseSplit,sseNotSplit
25             # 寻找最小的SSE进行划分
26             # 即对哪一个簇进行划分后SSE最小
27             if (sseSplit + sseNotSplit) < lowestSSE:
28                 bestCentToSplit = i
29                 bestNewCents = centroidMat
30                 bestClustAss = splitClustAss.copy()
31                 lowestSSE = sseSplit + sseNotSplit
32 
33         # 较难理解的部分
34         bestClustAss[nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList) #change 1 to 3,4, or whatever
35         bestClustAss[nonzero(bestClustAss[:,0].A == 0)[0],0] = bestCentToSplit
36         print 'the bestCentToSplit is: ',bestCentToSplit
37         print 'the len of bestClustAss is: ', len(bestClustAss)
38         centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0]#replace a centroid with two best centroids 
39         centList.append(bestNewCents[1,:].tolist()[0])
40         clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss#reassign new clusters, and SSE
41     return mat(centList), clusterAssment

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

这里是更改其所属的类别,其中bestClustAss = splitClustAss.copy()是进行k-means后所返回的矩阵,
其中第一列为类别,第二列为SSE值,因为当k=2是k-means返回的是类别0,1两类,
因此这里讲类别为1的更改为其质心的长度,而类别为0的返回的是该簇原先的类别。
例如:目前划分成了0,1两个簇,而要求划分成3个簇,则在算法进行时,假设对1进行划分得到的SSE最小,
则将1划分成了2个簇,其返回值为0,1两个簇,将返回为1的簇改成2,返回为0的簇改成1,因此现在就有0,1,2三个簇了。


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 其中bestNewCents是k-means的返回簇中心的值,其有两个值,分别是第一个簇,和第二个簇的坐标(k=2),
这里将第一个坐标赋值给 centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0],
将另一个坐标添加到centList中 centList.append(bestNewCents[1,:].tolist()[0])

from numpy import *
>>> import kMeans
>>> dat = mat(kMeans.loadDataSet('testSet2.txt'))
>>> cent,assment=kMeans.biKmeans(dat,3)
sseSplit, and notSplit:  570.722757425 0.0
the bestCentToSplit is:  0
the len of bestClustAss is:  60
sseSplit, and notSplit:  68.6865481262 38.0629506357
sseSplit, and notSplit:  22.9717718963 532.659806789
the bestCentToSplit is:  0
the len of bestClustAss is:  40

可以看到进行了两次的划分,第一次最好的划分是在0簇,第二次划分是在1簇。 

 

 

本文来于:

深入理解K-Means聚类算法

 

posted @ 2018-04-02 18:05  寒杰士  阅读(369)  评论(0编辑  收藏  举报