k-means算法:
聚类是一种无监督的学习,它将相似的对象归到同一个簇中,有点像全自动分类。
本章讲的K-means算法之所以称之为K-均值是因为它可以发现k个不同的簇,且每个簇的中心采用簇中所含值的均值计算。
算法原理: K-均值算法的工作流程是这样的。首先,随机确定k个初始点作为质心。然后将数据集中的每个点分配到一个簇中,具体来讲,为每个点找距其最近的质心,并将其分配给该质心所对应的簇。这一步完成之后,每个簇的质心更新为该簇所有点的平均值。
算法缺点:K-menas算法收敛但是聚类效果较差的原因是K-means算法收敛到了局部最小值,而非全局最小值。
下面介绍k-均值算法的一种改进
二分K-means算法:
伪代码形式如下:
将所有点看成一个簇
当簇数目小于k时
对每一个簇:
计算总误差
在给定的簇上面进行K-均值聚类(k=2)
计算将该簇一分为二之后的总误差
选择使得误差最小的那个簇进行划分操作
Python代码:
import numpy as np
# 读取数据
def loadDataSet(fileName):
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = [float(example) for example in line.strip().split('\t')]
dataMat.append(curLine)
return np.array(dataMat)
# 计算欧氏距离
def distEclud(vecA, vecB):
return np.sqrt(sum(np.power(vecA - vecB, 2)))
# 为数据集中每个特征随机创建k个聚簇中心
def randCent(dataSet, k):
n = dataSet.shape[1]
centroids = np.zeros((k, n)) # k个聚簇中心,每个簇中心特征数为n
for j in range(n):
minJ = min(dataSet[:, j])
rangeJ = float(max(dataSet[:, j]) - minJ)
# 为第j个特征创建k个聚簇中心
centroids[:, j] = minJ + rangeJ * np.random.rand(k)
return centroids
# kMeans算法
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
m = dataSet.shape[0]
clusterAssment = np.zeros((m, 2)) # 存储每个点的簇分配结果,第一列记录簇索引值,第二列存储误差(距离的平方)
centroids = createCent(dataSet, k) # k个聚簇中心矩阵(k,n)
clusterChanged = True
while clusterChanged:
clusterChanged = False
for i in range(m):
minDist = np.inf
minIndex = -1
for j in range(k):
distJI = distMeas(dataSet[i,:], centroids[j,:])
if distJI < minDist:
minDist = distJI
minIndex = j
if clusterAssment[i, 0] != minIndex: clusterChanged=True
clusterAssment[i, :] = [minIndex, minDist**2]
for cent in range(k):
ptsInClust = dataSet[np.nonzero(clusterAssment[:, 0] == cent)[0]] # 得到在这个簇中心的所有数据点
centroids[cent,:] = np.mean(ptsInClust, axis=0)
return centroids, clusterAssment
# 测试kMeans算法
# datMat = loadDataSet('testSet.txt')
# myCentroids, clustAssing = kMeans(datMat,4)
# print(myCentroids) # 经过数次迭代后会收敛
# print(clustAssing) # 第1列为簇索引,第2列为每个点到簇中心的误差
# 二分K均值算法:克服K-均值算法收敛于局部最小值的问题
def biKmeans(dataSet, k, distMeas=distEclud):
m = dataSet.shape[0]
clusterAssment = np.zeros((m, 2))
centroid0 = np.mean(dataSet, axis=0) # 创建一个初始簇,只有一个簇中心,并转化为列表
centList =[centroid0] # 加一层列表包含初始簇列表,以后会越来越多,直到等于k个
for j in range(m): # 计算初始误差
clusterAssment[j,1] = distMeas(centroid0, dataSet[j,:])**2
while (len(centList) < k): # 簇中心的数目小于k个
lowestSSE = np.inf
for i in range(len(centList)): # 对每一个簇划分
ptsInCurrCluster = dataSet[np.nonzero(clusterAssment[:,0] == i)[0],:] # 得到在簇i中的数据点
centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas) # 使用kMeans算法将簇一分为二
sseSplit = sum(splitClustAss[:,1]) # 计算新划分的簇中数据的SSE(误差平方和)
# 计算剩余簇中数据的SSE值,如果是第一次,该值为0,因为所有数据都用来划分新簇,故没有剩余簇的数据
sseNotSplit = sum(clusterAssment[np.nonzero(clusterAssment[:,0] != i)[0],1])
print("sseSplit, and notSplit: ",sseSplit,sseNotSplit)
if (sseSplit + sseNotSplit) < lowestSSE: # 两部分SSE作为总误差
bestCentToSplit = i # 最好的簇划分,即按照第i个簇划分
bestNewCents = centroidMat.copy() # 新簇,在原来基础上+1个簇中心,要用copy方法,防止矩阵传引用同时更改bestNewCents的值
bestClustAss = splitClustAss.copy() # 新的划分结果,要用copy方法,防止矩阵传引用同时更改bestClustAss的值
lowestSSE = sseSplit + sseNotSplit
bestClustAss[np.nonzero(bestClustAss[:,0] == 1)[0],0] = len(centList) # 为新划分出的簇赋一个新的编号,如1,2,3,...
bestClustAss[np.nonzero(bestClustAss[:,0] == 0)[0],0] = bestCentToSplit
print('the bestCentToSplit is: ',bestCentToSplit)
print('the len of bestClustAss is: ', len(bestClustAss))
centList[bestCentToSplit] = bestNewCents[0,:] # 更新原来的旧簇的值
centList.append(bestNewCents[1,:]) # 将新划分出的簇加入centList中
clusterAssment[np.nonzero(clusterAssment[:,0] == bestCentToSplit)[0],:] = bestClustAss # 更新clusterAssment
return centList, clusterAssment # 返回簇中心、簇分配结果矩阵
# 测试二分K均值算法
datMat3 = loadDataSet('testSet2.txt')
centList, myNewAssment= biKmeans(datMat3,3)
print(centList)
print(myNewAssment)