k均值聚类
什么是k均值聚类
试想一下,如果给一张图如下,要求对这张图中的点分类,你会怎么进行呢?
我们当然可以认为所有的点都只有一个种类,毕竟他们本身只有坐标不同,也可以左右分成两个大类,也可以四个角落划分成四类,这一切都取决于最初定的分类个数,而这就是k均值聚类。
所谓k,就是我们的目标要把数据划分为k个类。
所谓聚类,就是向上面的例子一样,实现不给任何标签,让我们自己区随意分类
均值则是代表了一种方法,本文后面会介绍到
具体实现
前面我们已经了解了我们的目的是什么。下面我们来讲解如何实现。
我们要分类的话,肯定是在同一类中相似度越高越好。也就是说,在特征空间中,他们的距离(欧氏距离或者随便什么距离)整体而言是最近的。k均值聚类正是采取了这种思想,其执行步骤如下:
- 由于要划分k个类,因此首先随机选取k个点(算法结束后这k个点会成为各自类的质点)
- 遍历所有的点,把这些点分类给距离其最近的质点(就是1中的点)
- 更新质点的位置,使其成为所在类的质点(也就是同类所有点的坐标取平均值)
- 重复2和3直至点的分类不变
这里的均值便是k均值聚类中的来源了。现在步骤清楚了,下面可以开始用代码去实现了,同样的,所有需要注意的点已均在代码中注释:
from numpy import *
# 加载数据集
def loadDataSet(fileName):
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
# 这里要注意一下,map函数返回的是一个迭代器,需要用list函数转换成列表,原本的书上没有这个list函数,但是在python3中需要加上这个函数
fltLine = list(map(float, curLine))
dataMat.append(fltLine)
return dataMat
# 计算欧式距离
def distEclud(vecA, vecB):
return sqrt(sum(power(vecA - vecB, 2)))
# 为给定数据集构建一个包含k个随机质心的集合
def randCent(dataSet, k):
n = shape(dataSet)[1]
centroids = mat(zeros((k, n)))
for j in range(n):
# 计算最小值,最大值,保证随机点在数据集的边界之内
minJ = min(dataSet[:, j])
rangeJ = float(max(dataSet[:, j]) - minJ)
centroids[:, j] = minJ + rangeJ * random.rand(k, 1)
# 返回质心
return centroids
# k均值聚类算法本体
def kMeans(dataSet, k, distMeas=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 = distMeas(centroids[j, :], dataSet[i, :])
# 寻找最近的质心
if distJI < minDist:
minDist = distJI
minIndex = j
# 若任一点的簇分配结果发生改变,则更新clusterChanged标志
if clusterAssment[i, 0] != minIndex:
clusterChanged = True
# 更新簇分配结果
clusterAssment[i, :] = minIndex, minDist ** 2
# 更新质心的位置
for cent in range(k):
ptsInClust = dataSet[nonzero(clusterAssment[:, 0].A == cent)[0]]
centroids[cent, :] = mean(ptsInClust, axis=0)
return centroids, clusterAssment
def main():
# test kMeans
dataMat = mat(loadDataSet('testSet.txt'))
myCentroids, clustAssing = kMeans(dataMat, 4)
# 画图,画出聚类结果
fig = plt.figure()
# 将dataMat中的点画出来
ax = fig.add_subplot(111)
ax.scatter(dataMat[:, 0].flatten().A[0], dataMat[:, 1].flatten().A[0], s=50, c='blue')
# 将聚类中心画出来
ax.scatter(myCentroids[:, 0].flatten().A[0], myCentroids[:, 1].flatten().A[0], s=300, c='red', marker='+')
plt.show()
if __name__ == '__main__':
main()
最终得到的结果如下图:
可以看到分类的结果很棒,基本上每个质心都处于其类别的中心,每个的分类也较为合理,但是这个代码其实是有问题的,那就是它极其依赖于初始的质心分布,比方说有质心有可能会陷入到局部最优解的困境之中,如下图:
下面我们将会学习一种解决方案
二分k均值聚类
二分k均值聚类的主要思想:二分K均值聚类首先将所有数据划分为一个初始簇,然后逐步对簇进行二分,直到达到指定的簇的数量。
具体实现步骤:
- 初始化:将所有数据样本划分为一个初始簇。
- 循环:
a. 对当前的簇进行K均值聚类操作,将其分成两个子簇。这可以通过应用K均值聚类算法一次来实现。
b. 对刚刚分割得到的两个子簇计算聚类质量度量,例如误差平方和(SSE)或轮廓系数等。
c. 选择具有较低质量度量的子簇进行进一步的分割操作,这意味着选择该子簇进行新一轮的K均值聚类分割。
d. 如果达到所需的簇的数量,或者无法选择更多的子簇进行分割,则结束循环。 - 输出结果:最终得到所需数量的簇,每个簇包含一组数据样本。
值得一提的是,二分K均值聚类相对于传统的K均值聚类具有更好的聚类结果的机会,但无法完全避免局部最小值问题,不过不管怎么样,也算是一种解决方案。
具体实现代码如下,其中有一部分与前文提到的代码重复了,就略去不表:
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
# 对簇进行划分,直到簇的数目达到k
while (len(centList) < k):
lowestSSE = inf
# 对每一个簇进行划分
for i in range(len(centList)):
# 获取当前簇的数据集
ptsInCurrCluster = dataSet[nonzero(clusterAssment[:, 0].A == i)[0], :]
# 对当前簇进行k均值聚类
centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas)
# 计算划分后的SSE
sseSplit = sum(splitClustAss[:, 1])
# 计算其他簇的SSE
sseNotSplit = sum(clusterAssment[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()
# 保存最佳划分的SSE
lowestSSE = sseSplit + sseNotSplit
# 更新簇的分配结果
bestClustAss[nonzero(bestClustAss[:, 0].A == 1)[0], 0] = len(centList)
bestClustAss[nonzero(bestClustAss[:, 0].A == 0)[0], 0] = bestCentToSplit
centList[bestCentToSplit] = bestNewCents[0, :].tolist()[0]
centList.append(bestNewCents[1, :].tolist()[0])
clusterAssment[nonzero(clusterAssment[:, 0].A == bestCentToSplit)[0], :] = bestClustAss
return mat(centList), clusterAssment
def main():
# test bikMeans
dataMat = mat(loadDataSet('testSet.txt'))
centList, myNewAssments = biKmeans(dataMat, 4)
# 画图,画出聚类结果
fig = plt.figure()
# 将dataMat中的点画出来
ax = fig.add_subplot(111)
# 画出聚类结果,每一类用一种颜色
for i in range(shape(dataMat)[0]):
if int(myNewAssments[i, 0]) == 0:
ax.scatter(dataMat[i, 0], dataMat[i, 1], c='red')
elif int(myNewAssments[i, 0]) == 1:
ax.scatter(dataMat[i, 0], dataMat[i, 1], c='blue')
elif int(myNewAssments[i, 0]) == 2:
ax.scatter(dataMat[i, 0], dataMat[i, 1], c='green')
elif int(myNewAssments[i, 0]) == 3:
ax.scatter(dataMat[i, 0], dataMat[i, 1], c='yellow')
elif int(myNewAssments[i, 0]) == 4:
ax.scatter(dataMat[i, 0], dataMat[i, 1], c='black')
# 画出质心
ax.scatter(centList[:, 0].tolist(), centList[:, 1].tolist(), c='orange', marker='+', s=300)
plt.show()
下面看一下运行结果:
结果看起来很美好对不对?但是如果我们将分类个数改为3呢?
我们可以看到红色这个区域的划分明显是不够好的,但是由于我们划设定了只划分3个类别,因此算法也只能够这样子了,所以我认为k均值聚类算法其实是还有改进的空间的,不过如果真能该了的话,算法名字里的k也就应该不复存在了吧。
实验
最后我们来跑一下书上的小实验。包含爬虫的部分我们不做,直接做后面的部分。
就理论而言,并没有什么新的内容,多了一个计算距离的函数也并非是本书的重点,因此此处还是直接给出完整的代码,感兴趣的读者可以自己拿来跑一下:
from numpy import *
# 加载数据集
def loadDataSet(fileName):
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
# 这里要注意一下,map函数返回的是一个迭代器,需要用list函数转换成列表,原本的书上没有这个list函数,但是在python3中需要加上这个函数
fltLine = list(map(float, curLine))
dataMat.append(fltLine)
return dataMat
# 计算欧式距离
def distEclud(vecA, vecB):
return sqrt(sum(power(vecA - vecB, 2)))
# 为给定数据集构建一个包含k个随机质心的集合
def randCent(dataSet, k):
n = shape(dataSet)[1]
centroids = mat(zeros((k, n)))
for j in range(n):
# 计算最小值,最大值,保证随机点在数据集的边界之内
minJ = min(dataSet[:, j])
rangeJ = float(max(dataSet[:, j]) - minJ)
centroids[:, j] = minJ + rangeJ * random.rand(k, 1)
# 返回质心
return centroids
# k均值聚类算法本体
def kMeans(dataSet, k, distMeas=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 = distMeas(centroids[j, :], dataSet[i, :])
# 寻找最近的质心
if distJI < minDist:
minDist = distJI
minIndex = j
# 若任一点的簇分配结果发生改变,则更新clusterChanged标志
if clusterAssment[i, 0] != minIndex:
clusterChanged = True
# 更新簇分配结果
clusterAssment[i, :] = minIndex, minDist ** 2
# 更新质心的位置
for cent in range(k):
ptsInClust = dataSet[nonzero(clusterAssment[:, 0].A == cent)[0]]
centroids[cent, :] = mean(ptsInClust, axis=0)
return centroids, clusterAssment
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
# 对簇进行划分,直到簇的数目达到k
while (len(centList) < k):
lowestSSE = inf
# 对每一个簇进行划分
for i in range(len(centList)):
# 获取当前簇的数据集
ptsInCurrCluster = dataSet[nonzero(clusterAssment[:, 0].A == i)[0], :]
# 对当前簇进行k均值聚类
centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas)
# 计算划分后的SSE
sseSplit = sum(splitClustAss[:, 1])
# 计算其他簇的SSE
sseNotSplit = sum(clusterAssment[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()
# 保存最佳划分的SSE
lowestSSE = sseSplit + sseNotSplit
# 更新簇的分配结果
bestClustAss[nonzero(bestClustAss[:, 0].A == 1)[0], 0] = len(centList)
bestClustAss[nonzero(bestClustAss[:, 0].A == 0)[0], 0] = bestCentToSplit
centList[bestCentToSplit] = bestNewCents[0, :].tolist()[0]
centList.append(bestNewCents[1, :].tolist()[0])
clusterAssment[nonzero(clusterAssment[:, 0].A == bestCentToSplit)[0], :] = bestClustAss
return mat(centList), clusterAssment
def distSLC(vecA, vecB):
# calculate spherical distance
a = sin(vecA[0, 1] * pi / 180) * sin(vecB[0, 1] * pi / 180)
# calculate spherical distance
b = cos(vecA[0, 1] * pi / 180) * cos(vecB[0, 1] * pi / 180) * \
cos(pi * (vecB[0, 0] - vecA[0, 0]) / 180)
# calculate spherical distance
return arccos(a + b) * 6371.0
import matplotlib.pyplot as plt
def clusterClubs(numClust=5):
# create mat to assign data points
datList = []
# get data points
for line in open('places.txt').readlines():
lineArr = line.split('\t')
# get data points
datList.append([float(lineArr[4]), float(lineArr[3])])
# get data points
datMat = mat(datList)
# calculate cluster centers
myCentroids, clustAssing = biKmeans(datMat, numClust, distMeas=distSLC)
fig = plt.figure()
# create plot
rect = [0.1, 0.1, 0.8, 0.8]
# create plot
scatterMarkers = ['s', 'o', '^', '8', 'p', \
'd', 'v', 'h', '>', '<']
# create plot
axprops = dict(xticks=[], yticks=[])
# create plot
ax0 = fig.add_axes(rect, label='ax0', **axprops)
# create plot
imgP = plt.imread('Portland.png')
# create plot
ax0.imshow(imgP)
# create plot
ax1 = fig.add_axes(rect, label='ax1', frameon=False)
# create plot
for i in range(numClust):
# get all data points in this cluster
ptsInCurrCluster = datMat[nonzero(clustAssing[:, 0].A == i)[0], :]
# plot data points
markerStyle = scatterMarkers[i % len(scatterMarkers)]
# plot data points
ax1.scatter(ptsInCurrCluster[:, 0].flatten().A[0], \
ptsInCurrCluster[:, 1].flatten().A[0], \
marker=markerStyle, s=90)
# plot cluster centers
ax1.scatter(myCentroids[:, 0].flatten().A[0], \
myCentroids[:, 1].flatten().A[0], \
marker='+', s=300)
# plot cluster centers
plt.show()
def main():
clusterClubs(5)
if __name__ == '__main__':
main()
实验结果与书上基本一致,如下图所示:
小结
我感觉k均值聚类可能是聚类算法中最简单的一种了,由于要人工设定分类的个数,因此一些情况下效果应该会非常差,后续可能会有更高级的聚类算法,但是应该没有机会接触到了。不过了解了聚类算法的基本原理,后面上手应该也会很快。
最后总结一下k均值聚类算法的基本思想:
随机选择质心,通过不断迭代最后使得质心到各个点的距离最短。