一、K-均值聚类
1.监督学习和无监督学习
学习K-均值聚类算法之前先了解下监督学习和无监督学习。
- 监督学习:生活中到处都是监督学习的例子,比如我们的高考,从小学至高考前的最后一次考试我们一直都在接受训练,高考就是测试我们训练成果的考试,平时考得好,高考考得不好,那可能是过拟合了,所以我们要学会题目后面真正考察的知识点而不是搞题海战术背住题目,貌似又跑题了……总的来说监督学习通常指分类算法,通过已有的训练样本(即已知数据以及其对应的输出)去训练得到一个最优模型(这个模型属于某个函数的集合,最优则表示在某个评价准则下是最佳的),再利用这个模型将所有的输入映射为相应的输出,对输出进行简单的判断从而实现分类的目的。
- 无监督学习:通过监督学习我们就能推断出无监督学习应该就是不经过训练,直接建立模型进行分类的算法,乍一听感觉好神奇,但是其实我们经常用到这种算法,比如说现在有一群人,白人、黑人、黄人混在一起,之前没见过这些人的人也可以大致推断出有可能是一个国家的人(这个例子不太严谨哈,假设肤色一样就是一个国家的),因为相同人种的相似度远远大于不同人种之间的相似度。无监督学习就是通过相似度来进行不同种类的区分的。
- 什么时候用监督学习什么时候用无监督学习?首先如果数据是独立同分布的,那么监督学习的效果会好于无监督学习,如果存在训练集或者训练集容易获得,则使用监督学习,反之则无监督学习,然而如果数据不是独立的,即数据之间存在联系则使用无监督学习。还是之前人种和国家的例子,但是其中一部分白人和一部分黄人存在亲子关系并且国籍相同,假如用监督学习,很有可能将他们分在不同的国家因为他们最大的特征肤色不同,但是如果用无监督学习就很有可能把他们分在同一个国家,这是因为既然存在亲子关系那他们有可能除了肤色不同其他地方的相似度还是很高的。
2.K-均值聚类
K-均值聚类就是一种无监督学习算法。它将相似的对象归到同一簇中,类似全自动分类。簇内的对象越相似,聚类的效果越好。其中簇的个数K是用户给定的,每一个簇通过其质心(centroid),即簇中所有点的中心来描述。
- 工作流程:首先,随机确定k个初始点作为质心。然后将数据集中的
每个点分配到一个簇中,具体来讲,为每个点找距其最近的质心,并将其分配给该质心所对应的簇。这一步完成之后,每个簇的质心更新为该簇所有点的平均值。
伪代码:
创建k个点作为起始质心(经常是随机选择)
当任意一个点的簇分配结果发生改变时:
对数据集中的每个点:
对每个质心:
计算质心与数据点之间的距离
将数据点分配到距离其最近的簇
对每一个簇,计算簇中所有点的均值并将均值作为质心。
代码实现:
import numpy as np
import matplotlib.pyplot as plt
#文本文件导入到列表中
def loadDataSet (fileName):
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = list(map(float, curLine))
dataMat.append(fltLine)
return dataMat
#两个向量的欧式距离
def distEclud(vecA,vecB):
return np.sqrt(np.sum(np.power(vecA - vecB, 2)))
#为给定数据集构建一个包含K个随机质心的集合
def randCent(dataSet, k):
n = np.shape(dataSet)[1]
centroids = np.mat(np.zeros((k, n)))
for j in range (n):
minJ = min(dataSet[:,j])
rangeJ = float(max(dataSet[:,j]) - minJ)
centroids[:,j] = minJ + rangeJ * np.random.rand(k,1)#确保随机点在数据的边界之内
return centroids
def KMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
m = np.shape(dataSet)[0]
clusterAssment = np.mat(np.zeros((m, 2)))#第一列存储质心索引,第二列存储数据与质心距离的平方
centroids = createCent(dataSet, k) #随机生成k个质心
clusterChanged = True #标志位(簇是否变化)
while clusterChanged:
clusterChanged = False
for i in range(m): #遍历数据集
minDist = np.inf #初始化距离(无穷大)
minIndex = -1 #初始化索引
for j in range(k): #遍历质心
distJI = distMeas(centroids[j, :],dataSet[i,:]) #数据和质心之间的距离
if distJI < minDist: #选择距离更近的质心
minIndex = j
minDist = distJI
if clusterAssment[i,0] != minIndex: #簇是否变化
clusterChanged = True
clusterAssment[i,:] = minIndex, minDist**2 #更新样本信息
#print(centroids)
for cent in range(k): #更新质心
ptsInClust = dataSet[np.nonzero(clusterAssment[:,0].A==cent)[0]] #选出质心下的所有样本
centroids[cent,:] = np.mean(ptsInClust, axis=0) #计算样本均值
return centroids, clusterAssment
#数据可视化
def showData():
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(centroids[:, 0].flatten().A[0], centroids[:, 1].flatten().A[0], #质心
marker='+', s=150, c='red')
ax.scatter(dataSet[:, 0].flatten().A[0], dataSet[:, 1].flatten().A[0], #数据集
marker='^', s=90)
plt.show()
if __name__ == "__main__":
dataSet = np.mat(loadDataSet('testSet.txt'))
# matrix = randCent(datMat, 2)
# print(matrix)
# distance = distEclud(dataSet[0], dataSet[1])
# print(distance)
centroids, clusterAssment = KMeans(dataSet, 4)
showData()
输出:
基本每一行代码都写了注释,应该很好理解,主要想说一下以前忽略的问题:
- clusterAssment = np.mat(np.zeros((m, 2))):np.zeros后面是双括号,要不然会报错
- np.sum()与sum()函数的区别:
例子:
a = [[1,2,3],[4,5,6]]
b = np.mat(a)
print('sum(b):',sum(b))
print('np.sum(b):',np.sum(b))
输出:
sum(b): [[5 7 9]]
np.sum(b): 21
可以看出二者的区别:sum()函数是按列相加,得到的还是一个矩阵。np.sum()函数是计算整个矩阵的和,得到的是一个数。
二、使用后处理来提高聚类性能
1.提出问题
在K-均值聚类中簇的数目k是一个用户预先定义的参数,那么用户如何才能知道k的选择是否正确?如何才能知道生成的簇比较好呢?
2.分析问题
K-均值聚类的方法效果较差的原因是会收敛到局部最小值,而非全局最小。一种用于度量聚类效果的指标是SSE(Sum of Squared Error,误差平方和),SE值越小表示数据点越接近于它们的质心,聚类效果也越好。因为对误差取了平方,因此更加重视那些远离中心的点。所以,应该想办法使得SSE尽可能的小。
3.解决问题
改进的方法是对生成的簇进行后处理,将最大SSE值的簇划分成两个(K=2的K-均值算法),然后再进行相邻的簇合并。具体方法有两种:1、合并最近的两个质心(合并使得SSE增幅最小的两个质心)2、遍历簇 合并两个然后计算SSE的值,找到使得SSE最小的情况。
三、二分K-均值聚类
为克服K-均值算法收敛于局部最小值的问题,有人提出了另一个称为二分K-均值(bisecting K-means)的算法。
1.主要思想
该算法首先将所有点作为一个簇,然后将该簇一分为二。之后选择其中一个
簇继续进行划分,选择哪一个簇进行划分取决于对其划分是否可以最大程度降低SSE的值。上述基于SSE的划分过程不断重复,直到得到用户指定的簇数目为止。
2.伪代码
将所有点看成一个簇
当簇数目小于k时
对于每一个簇
计算总误差
在给定的簇上面进行K-均值聚类(k=2)
计算将该簇一分为二之后的总误差
选择使得误差最小的那个簇进行划分操作
3.代码实现
def biKmeans(dataSet, k, distMeas=distEclud):
m = np.shape(dataSet)[0]
clusterAssment = np.mat(np.zeros((m,2)))
centroid0 = np.mean(dataSet, axis=0).tolist()[0] #计算整个数据集的质心
centList = [centroid0] #用列表来存储质心
for j in range(m): #遍历数据集
clusterAssment[j,1] = distMeas(np.mat(centroid0), dataSet[j,:])**2 #统计样本与质心之间的距离的平方
while (len(centList) < k):
lowestSSE = np.inf #初始化SSE(误差平方和)
for i in range(len(centList)): #遍历质心
ptsInCurrCluster = dataSet[np.nonzero(clusterAssment[:,0].A==i)[0],:] #找到质心所在的簇
centroidMat, splitClustAss = KMeans(ptsInCurrCluster, 2, distMeas) #尝试分成2个簇
sseSplit = np.sum(splitClustAss[:, 1]) #计算分成2个簇之后的SSE
sseNotSplit = np.sum(clusterAssment[np.nonzero(clusterAssment[:,0].A!=i)[0],1]) #计算不是该质心所在的簇的SSE
print("sseSplit, and notSplit:",sseSplit,sseNotSplit)
if (sseSplit + sseNotSplit) < lowestSSE:
bestCentToSplit = i
bestNewCents = centroidMat #最佳聚类的新质心
bestClustAss = splitClustAss.copy() #新聚类簇的属性信息(索引和误差平方)
lowestSSE = sseSplit + sseNotSplit
bestClustAss[np.nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList) #更换质心的索引,例如原来有0和1两个索引,现在把0分成了0和1,就变成了0,1,1三个索引,有两个1,所以得将后得到的那个1的索引更换,更换成2,也就是原质心的长度
bestClustAss[np.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, :] #更新质心列表
#centList.append(bestNewCents[1, :].tolist()[0]) #添加质心
centList.append(bestNewCents[1, :])
clusterAssment[np.nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:] = bestClustAss #更新整体的样本信息
#centListMat = np.mat(centList)
return centList, clusterAssment
在主程序中调用该函数便可以得到质心:
[matrix([[-0.45965615, -2.7782156 ]]), matrix([[2.93386365, 3.12782785]]), matrix([[-2.94737575, 3.3263781 ]])]
四、总结
K-均值聚类的优缺点:
优点:易于实现
缺点:可能收敛到局部最小值,在大规模数据集上收敛较慢。
K-均值聚类在数据运营中有广泛的应用,比如说想要对用户分层,确不知道该如何制定分层的规则,这时就可以请出K-均值聚类算法啦~