分类作为一种监督学习方法,要求必须事先明确知道各个类别的信息,并且断言所有待分类项都有一个类别与之对应。但是很多时候上述条件得不到满足,尤其是在处理海量数据的时候,如果通过预处理使得数据满足分类算法的要求,则代价非常大,这时候可以考虑使用聚类算法。聚类属于无监督学习,相比于分类,聚类不依赖预定义的类和类标号的训练实例。由于具有出色的速度和良好的可扩展性,Kmeans聚类算法算得上是最著名的聚类方法。
Kmeans算法是一个重复移动类中心点的过程,把簇的中心点,也称重心(centroids),移动到其包含成员的平均位置,然后重新划分其内部成员。k是算法计算出的超参数,表示簇的数量;Kmeans可以自动分配样本到不同的簇,但是不能决定究竟要分几个簇。k必须是一个比训练集样本数小的正整数。如果用数据表达式表示,假设簇划分之间的随机数为k,则我们的目标是最小化平方误差E:
μi是第k个簇的重心位置。E是各个类畸变程度(distortions)之和。每个簇的畸变程度等于该簇重心与其内部成员位置距离的平方和。若簇内部的成员彼此间越紧凑则簇的畸变程度越小,反之,若簇内部的成员彼此间越分散则类的畸变程度越大。求解成本函数最小化的参数就是一个重复配置每个簇包含的观测值,并不断移动簇重心的过程。首先,簇的重心是随机确定的位置。实际上,重心位置等于随机选择的观测值的位置。每次迭代的时候,Kmeans会把观测值分配到离它们最近的簇,然后把重心移动到该簇全部成员位置的平均值那里。
简而言之,k均值聚类算法由以下四步组成:
首先,随机选定k个初始类簇中心(不同的类簇中心会导致收敛速度和聚类结果有差别,有可能会陷入局部最优.)
其次,计算每个点到每个类簇中心的距离,并将其分配到最近的类簇中
第三,重新计算每个类簇的中心
第四,重复第二步和第三步直到类簇中心不再发生变化,聚类停止
from numpy import *
import numpy as np
#导入数据
def loadDataSet(fileName): #general function to parse tab -delimited floats
dataMat = [] #assume last column is target value
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = list(map(float,curLine)) #map all elements to float()
#书上程序因为py2和3的的差别在这里没有对map进行一个定义,使用py3是需在map前加list
dataMat.append(fltLine)
return dataMat
def distEclud(vecA, vecB):
return sqrt(sum(power(vecA - vecB, 2))) #la.norm(vecA-vecB)
#构建簇的质心
def randCent(dataSet, k):
n = shape(dataSet)[1]
centroids = mat(zeros((k,n)))#create centroid mat
for j in range(n):#create random cluster centers, within bounds of each dimension
minJ = min(dataSet[:,j])
rangeJ = float(max(dataSet[:,j]) - minJ)
centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1))
return centroids
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
m = shape(dataSet)[0]
clusterAssment = mat(zeros((m,2)))#create mat to assign data points
#to a centroid, also holds SE of each point
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 = distMeas(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):#recalculate centroids
ptsInClust = dataSet[nonzero(clusterAssment[:,0].A==cent)[0]]#get all the point in this cluster
centroids[cent,:] = mean(ptsInClust, axis=0) #assign centroid to mean
return centroids, clusterAssment
#绘制图形程序
import matplotlib.pyplot as plt
def draw(dataMat,centroids,clusterAssment):
k=len(centroids)
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(centroids[:,0].tolist(),centroids[:,1].tolist(),marker='+',c='r')
markers=['o','s','v','*'];colors=['blue','green','yellow','red']
for i in range(k):
data_class=dataMat[nonzero(clusterAssment[:,0].A == i)[0]]
ax.scatter(data_class[:,0].tolist(),data_class[:,1].tolist(),marker=markers[i],c=colors[i])
plt.show()
if __name__ =="__main__":
dataMat = mat(loadDataSet('testSet.txt'))
myCentroids, clustAssing = kMeans(dataMat,4)
draw(dataMat,myCentroids, clustAssing)
当选择簇的个数为4时绘制散点图图形如下:
当选择簇的个数为3时绘制散点图图形如下:
由此可以看出对此数据集而言,由四个质心所绘制的散点图更为合理,那么也就引出了接下来的问题,我们需要一种方法来确定我们的K值所选是正确的,才能生成最好的簇。
如果问题中没有指定k的值,可以通过肘部法则这一技术来估计聚类数量。肘部法则会把不同k值的成本函数值画出来。随着kk值的增大,平均畸变程度会减小;每个类包含的样本数会减少,于是样本离其重心会更近。但是,随着kk值继续增大,平均畸变程度的改善效果会不断减低。kk值增大过程中,畸变程度的改善效果下降幅度最大的位置对应的kk值就是肘部。为了让读者看的更加明白,下面让我们通过一张图用肘部法则来确定最佳的k值。下图数据明显可分成两类:
从图中可以看出,k值从1到2时,平均畸变程度变化最大。超过2以后,平均畸变程度变化显著降低。因此最佳的k是2。
稳定性方法对一个数据集进行2次重采样产生2个数据子集,再用相同的聚类算法对2个数据子集进行聚类,产生2个具有kk个聚类的聚类结果,计算2个聚类结果的相似度的分布情况。2个聚类结果具有高的相似度说明kk个聚类反映了稳定的聚类结构,其相似度可以用来估计聚类个数。采用次方法试探多个k,找到合适的k值。
为了克服k-均值算法的局部最小值问题,有人提出了二分k均值算法。该算法首先将所有点作为一个簇,然后讲该簇一分为二。之后选择其中一个簇继续划分,选择哪一个簇进行划分取决于对其划分是否可以最大程度降低SSE的值。上述基于SSE的划分过程不断重复,直到得到用户指定的簇数目为止。
def biKmeans(dataSet, k, distMeas=distEclud):
m = shape(dataSet)[0]
clusterAssment = mat(zeros((m,2)))
centroid0 = mean(dataSet, axis=0).tolist()[0]
centList =[centroid0] #create a list with one centroid #创建一个初始簇
for j in range(m):#calc initial Error
clusterAssment[j,1] = distMeas(mat(centroid0), dataSet[j,:])**2
while (len(centList) < k):
lowestSSE = inf
for i in range(len(centList)): #划分每一簇
ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:]#get the data points currently in cluster i
centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas)
sseSplit = sum(splitClustAss[:,1])#compare the SSE to the currrent minimum
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
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[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
return mat(centList), clusterAssment
仍然使用先前所使用的数据集,绘制散点图如下:
由此可见,相较于k均值算法而言,二份K均值算法在本数据集上的聚类更加成功。
kmean算法的特点是不能保证该算法收敛域全局最优解,并且它常常终止于一个局部最优解。结果可能依赖于初始簇中心的随机选择,所以为了尽可能的得到好的结果,我们通常会选择不同的初始簇中心,来多疑运行K-均值算法。
算法优点:
1)原理比较简单,实现也是很容易,收敛速度快。
2)聚类效果较优。
3)算法的可解释度比较强。
4)主要需要调参的参数仅仅是簇数k。
算法缺点:
1)K值的选取不好把握
2)对于不是凸的数据集比较难收敛
3)如果各隐含类别的数据不平衡,比如各隐含类别的数据量严重失衡,或者各隐含类别的方差不同,则聚类效果不佳。
4) 采用迭代方法,得到的结果只是局部最优。
参考资料:
《机器学习实战》 ——Peter Harrington著 人民邮电出版社
从零开始实现Kmeans聚类算法——CSDN博客 https://blog.csdn.net/u013719780/article/details/78413770
《机器学习》—— 周志华 著 清华大学出版社