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

目录:
(1)回顾K-means算法缺点
(2)二分K-means算法介绍
(3)用Python实现二分k-means算法
(1)k-means算法缺点
上一篇文章《聚类算法之K-means算法》中介绍了K-means算法所有知识点并指出K-means算法的缺点。今天我们来回顾一下K-means的这些缺点:
-
对噪声和孤立点数据比较敏感。表现为:K-means将簇中所有点的均值作为新质心,若簇中含有异常点,将导致均值偏离严重,容易陷入局部最小值。
-
必须事先给出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

代码后最终的效果,如图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
