PS:该系列数据都可以在图灵社区(点击此链接)中随书下载中下载(如下)
聚类是一种无监督的学习,它将相似的对象归到同一个簇中。它有点像全自动分类,聚类方法几乎可以应用于所有对象,簇内的对象越相似,聚类的效果越好。K-均值(K-means) 聚类算法,之所以称之为K-均值是因为它可以发现k个不同的簇,且每个簇的中心采用簇中所含值的均值计算而成。
簇识别(cluster identification):簇识别给出聚类结果的含义。假定有一些数据,现在将相似数据归到一起,簇识别会告诉我们这些簇到底都是些什么。聚类与分类的最大不同在于,分类的目标事先巳知,而聚类则不一样。因为其产生的结果与分类相同,而只是类别没有预先定义,聚类有时也被称为无监督分类(unsupervised classification )。
K-均值聚类
优点:容易实现。
缺点:可能收敛到局部最小值,在大规模数据集上收敛较慢。
使用数据类型:数值型数据。
K-均值是发现给定数据集的k个簇的算法。簇个数k是用户给定的,每一个簇通过其质心(centroid),即簇中所有点的中心来描述。
工作流程:首先,随机确定k个初始点作为质心。然后将数据集中的每个点分配到一个簇中,具体来讲,为每个点找距其最近的质心,并将其分配给该质心所对应的簇。这一步完成之后,每个簇的质心更新为该簇所有点的平均值。
伪代码:
创建k个点作为起始质心(经常是随机选择)
当任意一个点的簇分配结果发生改变时
对数据集中的每个数据点
对每个质心
计算质心与数据点之间的距离
对数据点分配到距其最近的簇
对每一个簇,计算簇中所有点的均值并将均值作为质心
创建kMeans.py,将数据集拷贝到该文件目录,编写如下代码并进行测试:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
def loadDataSet(fileName):
dataMat = []
with open(fileName, 'r') as fileObject:
for line in fileObject.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)))
def randCent(dataSet, k):
'''返回k个随机质心'''
n = np.shape(dataSet)[1]
centroids = np.mat(np.zeros((k, n)))
for j in range(n):
minJ = np.min(dataSet[:, j])
rangeJ = float(np.max(dataSet[:, j]) - minJ)
centroids[:, j] = minJ + rangeJ * np.random.rand(k, 1) #k行1列的[0,1)的随机数
return centroids
def kMeans(dataSet, k, distMeans=distEclud, createCent=randCent):
'''K-均值聚类算法'''
m = np.shape(dataSet)[0]
#簇分配结果矩阵,第一列存储簇索引值,第二列存储误差
clusterAssment = np.mat(np.zeros((m, 2)))
centroids = createCent(dataSet, k)
clusterChanged = True
while clusterChanged:
clusterChanged = False
for i in range(m):
minDist = float('inf')
minIndex = -1
#寻找最近的质心
for j in range(k):
distJI = distMeans(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):
ptsInClust = dataSet[np.nonzero(clusterAssment[:, 0].A == cent)[0]]
centroids[cent, :] = np.mean(ptsInClust, axis=0)
return centroids, clusterAssment
def showCluster(dataSet, k, centroids, clusterAssment):
m, n = np.shape(dataSet)
if n != 2:
print("Sorry! I cant not draw because the dimension of your data is not 2!")
return
mark = ['or', 'ob', 'og', 'ok', '^r', '+r', 'sr', 'dr', ', 'pr']
if k > len(mark):
print("Sorry! Your k is too large!")
return
for i in range(m):
markIndex = int(clusterAssment[i, 0])
plt.plot(dataSet[i, 0], dataSet[i, 1], mark[markIndex])
mark = ['Dr', 'Db', 'Dg', 'Dk', '^b', '+b', 'sb', 'db', ', 'pb']
for i in range(k):
plt.plot(centroids[i, 0], centroids[i, 1], mark[i], markersize=12)
plt.show()
上面的结果给出了4个质心,可以看到经过3次迭代之后K-均值算法收敛。
一种用于度量聚类效果的指标是SSE(Sum of Squared Error,误差平方和)。SSE值越小表示数据点越接近于它们的质心,聚类效果也越好。因为对误差取了平方,因此更加重视那些远离中心的点。一种肯定可以降低SSE值的方法是增加簇的个数,但这违背了聚类的目标。聚类的目标是在保持簇数目不变的情况下提高簇的质量。
可以对生成的簇进行后处理,一种方法是将具有最大SSE值的簇划分成两个簇。具体实现时可以将最大簇包含的点过滤出来并在这些点上运行K-均值。
为了保持簇总数不变,可以将某两个簇进行合并。有两种可以量化的办法:合并最近的质心,或者合并两个使得SSE增幅最小的质心。第一种思路通过计算所有质心之间的距离,然后合并距离最近的两个点来实现。第二种方法需要合并两个簇然后计算总SSE值。必须在所有可能的两个簇上重复上述处理过程,直到找到合并最佳的两个簇为止。
为克服K-均值算法收敛于局部最小值的问题,有人提出了另一个称为二分K-均值(bisecting K-means)的算法。该算法首先将所有点作为一个簇,然后将该簇一分为二。之后选择其中一个簇继续进行划分,选择哪一个簇进行划分取决于对其划分是否可以最大程度降低SSE的值。上述基于SSE的划分过程不断重复,直到得到用户指定的簇数目为止。
伪代码:
将所有点看成一个簇
当簇数目小于k时
对于每一个簇
计算总误差
在给定的簇上面进行K-均值聚类(k=2)
计算将该簇一分为二的总误差
选择使得误差最小的那个簇进行划分操作
def biKmeans(dataSet, k, distMeans=distEclud):
'''二分K-均值聚类算法'''
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] = distMeans(np.mat(centroid0), dataSet[j, :]) ** 2
while len(centList) < k:
lowestSSE = float('inf') #误差平方和
for i in range(len(centList)):
#尝试划分每一簇,对每个簇将该簇中的所有点看成一个小的数据集ptsInCurrCluster
pstInCurrCluster = dataSet[np.nonzero(clusterAssment[:, 0].A == i)[0], :]
#对该小数据集进行k=2的聚类
centroidMat, splitClustAss = kMeans(pstInCurrCluster, 2 , distMeans)
#划分误差与剩余数据集的误差之和作为本次划分的误差
sseSplit = np.sum(splitClustAss[:, 1])
sseNotSplit = np.sum(clusterAssment[np.nonzero(clusterAssment[:, 0].A != i)[0], 1])
print("sseSplit, and not Split: ", sseSplit, sseNotSplit)
if sseSplit + sseNotSplit < lowestSSE:
bestCentToSplit = i
bestNewCents = centroidMat
bestClustAss = splitClustAss.copy()
lowestSSE = sseSplit + sseNotSplit
#更新簇的分配结果,调用2-Means结果默认簇是0,1
bestClustAss[np.nonzero(bestClustAss[:, 0].A == 1)[0], 0] = len(centList)
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, :].tolist()[0]
centList.append(bestNewCents[1, :].tolist()[0])
clusterAssment[np.nonzero(clusterAssment[:, 0].A == bestCentToSplit)[0], :] = bestClustAss
return np.mat(centList), clusterAssment
命令行输入kMeans.showCluster(datMat2, 3, centList, myNewAssments)
查看聚类结果如下:
由于算法的随机性,运行的结果可能不一样。上述函数可以运行多次,聚类会收敛到全局最小值,而原始的kMeans()函数偶尔会陷入局部最小值。
有一个地址列表,里面有70个位置,保存在文件portland-Clubs.txt中,需要对这些地方进行聚类,这样就可以安排交通工具抵达这些簇的质心,然后步行到每个簇类的地址。
这里为了方便处理,就不用API进行数据收集,直接处理本地已经收集好的数据集文件。
def distSLC(vecA, vecB):
'''球面余弦定理计算两个经纬度之间的距离'''
a = np.sin(vecA[0, 1] * np.pi / 180) * np.sin(vecB[0, 1] * np.pi / 180)
b = np.cos(vecA[0, 1] * np.pi / 180) * np.cos(vecB[0, 1] * np.pi / 180) * \
np.cos(np.pi * (vecB[0, 0] - vecA[0, 0]) / 180)
return np.arccos(a + b) * 6371.0
def clusterClubs(numClust=5):
'''将文本文件中的俱乐部进行聚类并画出结果'''
datList = []
with open('places.txt', 'r') as fileObject:
for line in fileObject.readlines():
lineArr = line.split('\t')
datList.append([float(lineArr[4]), float(lineArr[3])])
datMat = np.mat(datList)
myCentroids, clustAssing = biKmeans(datMat, numClust, distMeans=distSLC)
fig = plt.figure()
rect = [0.1, 0.1, 0.8, 0.8]
scatterMarkers = ['s', 'o', '^', '8', 'p', 'd', 'v', 'h', '>', '<']
axprops = dict(xticks=[], yticks=[])
ax0 = fig.add_axes(rect, label='ax0', **axprops)
#基于图像创建矩阵
imgP = plt.imread('Portland.png')
ax0.imshow(imgP)
ax1 = fig.add_axes(rect, label='ax1', frameon=False)
for i in range(numClust):
ptsInCurrCluster = datMat[np.nonzero(clustAssing[:, 0].A == i)[0], :]
markerStyle = scatterMarkers[i % len(scatterMarkers)]
ax1.scatter(ptsInCurrCluster[:, 0].flatten().A[0], ptsInCurrCluster[:, 1].flatten().A[0],
marker=markerStyle, s=90)
ax1.scatter(myCentroids[:, 0].flatten().A[0], myCentroids[:, 1].flatten().A[0], marker='+', s=300)
plt.show()
python命令行输入如下命令查看聚类效果图:
importlib.reload(kMeans)
kMeans.clusterClubs(5)
聚类是一种无监督的学习爱方法。所谓无监督的学习是指事先并不知道要寻找的内容,即没有目标变量。聚类将数据点归到多个簇中,其中相似数据点处于同一簇,而不相似数据点处于不同簇中。
一种广泛使用的聚类算法是K-均值算法,其中k是用户指定的要创建的簇的数目。