聚类算法是机器学习中的一类无监督学习方法,用于将无标签的数据进行聚类划分。最简单的也最典型的一类算法就是KMeans算法。此处的K表示划分成K个聚类。利用各个点到质心之间的距离的平方和作为将节点划分到不同类的标准。当然也可以采用其他的距离计算方法,不一定是欧式距离方法。
一、KMeans
此方法一般是在数据分析前期使用,选取适当的K,将数据聚类后,研究不同聚类下数据的特点。
算法原理:
(1)随机选择K个中心点;
(2)在第j次迭代中,对于每个样本点,选取最近的中心点,归为该类;
(3)更新中心点为每类的均值;
(4)j < j-1,重复(2)(3)迭代更新,直到误差小到某个值或者达到一定的迭代步数,误差不变,也就是收敛为止;
空间复杂度为O(n),时间复杂度为O(l*K*N),其中N为样本个数,K为中心点个数,即聚类数,I为迭代次数
为什么迭代后误差逐渐减小:
SSE=
对于 而言,求导后,当 时,SSE最小,对应第(3)步;
对于 而言,求导后,当 时,SSE最小,对应第(2)步。
因此KMeans迭代能使误差逐渐减少直到不变
代码如下所示:
from numpy import * #general function to parse tab -delimited floats #assume last column is target value def loadDataSet(fileName): dataMat = [] fr = open(fileName) for line in fr.readlines(): curLine = line.strip().split('\t') #笔者使用的是python3,需要将map映射后的结果转化为list #map all elements to float() fltLine = list(map(float,curLine)) dataMat.append(fltLine) return dataMat #样本距离计算函数 def distEclud(vecA, vecB): return sqrt(sum(power(vecA - vecB, 2))) #la.norm(vecA-vecB) #创建簇中心矩阵,初始化为k个在数据集的边界内随机分布的簇中心 def randCent(dataSet, k): n = shape(dataSet)[1] #create centroid mat centroids = mat(zeros((k,n))) #create random cluster centers, within bounds of each dimension for j in range(n): #求出数据集中第j列的最小值(即第j个特征) minJ = min(dataSet[:,j]) #用第j个特征最大值减去最小值得出特征值范围 rangeJ = float(max(dataSet[:,j]) - minJ) #创建簇矩阵的第J列,random.rand(k,1)表示产生(10,1)维的矩阵,其中每行值都为0-1中的随机值 #可以这样理解,每个centroid矩阵每列的值都在数据集对应特征的范围内,那么k个簇中心自然也都在数据集范围内 centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1)) return centroids效果如下所示:
k聚类算法代码如下:
#distMeas为距离计算函数 #createCent为初始化随机簇心函数 def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent): m = shape(dataSet)[0] #create mat to assign data points to a centroid, also holds SE of each point #创建一个(m,2)维矩阵,第一列存储每个样本对应的簇心,第二列存储样本到簇心的距离 clusterAssment = mat(zeros((m,2))) #用createCent()函数初始化簇心矩阵 centroids = createCent(dataSet, k) #保存迭代中clusterAssment是否更新的状态,如果未更新,那么退出迭代,表示收敛 #如果更新,那么继续迭代,直到收敛 clusterChanged = True while clusterChanged: clusterChanged = False #for each data point assign it to the closest centroid #对每个样本找出离样本最近的簇心 for i in range(m): #minDist保存最小距离 #minIndex保存最小距离对应的簇心 minDist = inf; minIndex = -1 #遍历簇心,找出离i样本最近的簇心 for j in range(k): distJI = distMeas(centroids[j,:],dataSet[i,:]) if distJI < minDist: minDist = distJI; minIndex = j #如果clusterAssment更新,表示对应样本的簇心发生变化,那么继续迭代 if clusterAssment[i,0] != minIndex: clusterChanged = True #更新clusterAssment,样本到簇心的距离 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簇心 centroids[cent,:] = mean(ptsInClust, axis=0) #assign centroid to mean return centroids, clusterAssment其中centroids表示的是质心数组,clusterAssment表示的是对原始数据划分的结果,第一列表示的是每一个样本的分类,第二列表示的是该样本到相应类的质心的距离。
结果如图所示:
绘制测试如图所示:
paint函数为笔者写的绘图函数:
def paint(xArr,yArr,xArr1,yArr1): fig = plt.figure() ax = fig.add_subplot(111) ax.scatter(xArr,yArr,c='blue') ax.scatter(xArr1,yArr1,c='red') plt.show()绘制如图所示:
经过3次迭代后,趋于收敛,即群划分成功。
二、二分kmeans算法
上述kmeans算法还是存在一定的问题,即它对初始选取的k比较敏感,一旦k选取不合适就容易陷入局部最优解的怪圈中,而不是全局最优解,而接下来引入的二分kmeans算法就是用于解决此类问题的一个算法。一种用于度量聚类效果的指标是SSE(Sum of Squared Error,误差平方和),对应clusterAssment矩阵的第一列之和。SSE值越小表示数据点越接近于它们的质心,聚类效果也越好。因为对误差取了平方,因此更加重视那些远离中心的点。一种肯定可以降低SSE值的方法是增加簇的个数,但这违背了聚类的目标。聚类的目标是在保持簇数目不变的情况下提高簇的质量。
那么如何对结果进行改进?你可以对生成的簇进行后处理,一种方法是将具有最大SSE值的簇划分成两个簇。具体实现时可以将最大簇包含的点过滤出来并在这些点上运行K-均值聚类算法,其中的K为2。
有两种可以量化的办法:合并最近的质心,或者合并两个使得SSE增幅最小的质心。第一种思路通过计算所有质心之间的距离,然后合并距离最近的两个点来实现。第二种方法需要合并两个簇然后计算总SSE值。必须在所有可能的两个簇上重复上述处理过程,直到找到合并最佳的两个簇为止。接下来将讨论利用上述簇划分技术得到更好的聚类结果的方法。
二分kmeans算法伪代码如下:
为克服K-均值算法收敛于局部最小值的问题,有人提出了另一个称为二分K均值(bisectingK-means)的算法, 该算法首先将所有点作为一个簇,然后将该簇一分为二。之后选择其中一个簇继续进行划分,选择哪一个簇进行划分取决于对其划分是否可以最大程度降低SSE的值。上述基于SSE的划分过程不断重复,直到得到用户指定的簇数目为止。
二分K-均值算法的伪代码形式如下:
将所有点看成一个簇
当簇数目小于k时
对于每一个簇
计算总误差
在给定的簇上面进行K-均值聚类(k=2)
计算将该簇一分为二之后的总误差
选择使得误差最小的那个簇进行划分操作
另一种做法是选择SSE最大的簇进行划分,直到簇数目达到用户指定的数目为止。这个做法听起来并不难实现。下面就来看一下该算法的实际效果。
代码如下所示:
#distMeas为距离计算函数 def biKmeans(dataSet, k, distMeas=distEclud): m = shape(dataSet)[0] #(m,2)维矩阵,第一列保存样本所属簇,第二列保存样本到簇中心的距离 clusterAssment = mat(zeros((m,2))) #取数据集特征均值作为初始簇中心 centroid0 = mean(dataSet, axis=0).tolist()[0] #centList保存簇中心数组,初始化为一个簇中心 #create a list with one centroid centList =[centroid0] #calc initial Error for j in range(m): clusterAssment[j,1] = distMeas(mat(centroid0), dataSet[j,:])**2 #迭代,直到簇中心集合长度达到k while (len(centList) < k): #初始化最小误差 lowestSSE = inf #迭代簇中心集合,找出找出分簇后总误差最小的那个簇进行分解 for i in range(len(centList)): #get the data points currently in cluster i #获取属于i簇的数据集样本 ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:] #对该簇进行k均值聚类 centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas) #获取该簇分类后的误差和 sseSplit = sum(splitClustAss[:,1])#compare the SSE to the currrent minimum #获取不属于该簇的样本集合的误差和,注意矩阵过滤中用的是!=i sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1]) #打印该簇分类后的误差和和不属于该簇的样本集合的误差和 print("sseSplit, and notSplit: ",sseSplit,sseNotSplit) #两误差和相加即为分簇后整个样本集合的误差和,找出簇中心集合中能让分簇后误差和最小的簇中心,保存最佳簇中心(bestCentToSplit),最佳分簇中心集合(bestNewCents),以及分簇数据集中样本对应簇中心及距离集合(bestClustAss),最小误差(lowestSSE) if (sseSplit + sseNotSplit) < lowestSSE: bestCentToSplit = i bestNewCents = centroidMat bestClustAss = splitClustAss.copy() lowestSSE = sseSplit + sseNotSplit #更新用K-means获取的簇中心集合,将簇中心换为len(centList)和bestCentToSplit,以便之后调整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 print('the bestCentToSplit is: ',bestCentToSplit) print('the len of bestClustAss is: ', len(bestClustAss)) #更新簇中心集合,注意与bestClustAss矩阵是一一对应的 centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0]#replace a centroid with two best centroids centList.append(bestNewCents[1,:].tolist()[0]) #reassign new clusters, and SSE clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss return mat(centList), clusterAssment通俗地讲,初始时,将所有的点看成是一个簇,然后将此簇进行二分,接下来选取每个簇中的sse最小的簇进行二分,直到所有的簇的数量为k,则完成循环,也就是这是一个不断地循环的过程,通过循环得到k个簇,完成操作。
二分K值最重要的是记住要将最佳分簇集合与clusterAssment一一对应 ,测试代码如下:
datMat3 = mat(loadDataSet('testSet2.txt')) centList,myNewAssments = biKmeans(datMat3,3) print(centList) xArr = datMat3[:,0].flatten().A[0] yArr = datMat3[:,1].flatten().A[0] xArr1 = centList[:,0].flatten().A[0] yArr1 = centList[:,1].flatten().A[0] #paint为笔者自己写的绘图函数 paint(xArr,yArr,xArr1,yArr1) def paint(xArr,yArr,xArr1,yArr1): fig = plt.figure() ax = fig.add_subplot(111) ax.scatter(xArr,yArr,c='blue') ax.scatter(xArr1,yArr1,c='red') plt.show()生成的效果如下所示:
聚类是一种无监督的学习方法。所谓无监督学习是指事先并不知道要寻找的内容,即没有目标变量。聚类将数据点归到多个簇中,其中相似数据点处于同一簇,而不相似数据点处于不同簇中。聚类中可以使用多种不同的方法来计算相似度。
一种广泛使用的聚类算法是K-均值算法,其中K是用户指定的要创建的簇的数目。K-均值聚类算法以K个随机质心开始。算法会计算每个点到质心的距离。每个点会被分配到距其最近的簇质心,然后紧接着基于新分配到簇的点更新簇质心。以上过程重复数次,直到簇质心不再改变。这个简单的算法非常有效但是也容易受到初始簇质心的影响。为了获得更好的聚类效果,可以使用另一种称为二分K-均值的聚类算法。二分K-均值算法首先将所有点作为一个簇,然后使用K-均值算法(K = 2 ) 对其划分。下一次迭代时,选择有最大误差的簇进行划分。该过程重复直到K个簇创建成功为止。二分K-均值的聚类效果要好于K-均值算法。
K-均值算法以及变形的K-均值算法并非仅有的聚类算法, 另外称为层次聚类的方法也被广泛使用。