K-均值聚类算法
1.原理介绍
-
K-均值算法是一种聚类算法。聚类算法与分类算法的区别在于分类的目标事先已知,而聚类的类别没有事先定义。聚类算法将相似对象归为同一群落,将不同对象归到不同群落。相似的概念取决于选择的相似度的计算方法。
-
K-均值算法的原理是随机确定 k k k个初始点作为质心(centroid,群中所有点的中心),为每个点找距离最近的质心,将每个点中分到群中。更新质心为该群落所有点的平均值。(k是可选参数)
伪代码: 选择k个点作为起始质心 当任意一点的群落发生变化时: 对每个数据点: 对每个质心: 计算质心与数据点的距离 将数据点分配到距离最近的群 对每一个群落,计算群中所有点的均值并将均值作为质心
2.代码
(1)初始K-均值算法
import numpy as np
def loadData(filename):
dataMat = []
f = open(filename)
for line in f.readlines():
linec = line.strip().split('\t')
# map()将float()作用在linec每个元素上,得到一个新的list并返回。即将linec每个元素变为浮点数
floatLine = list(map(float, linec))
dataMat.append(floatLine)
return dataMat
# 计算群之间的欧式距离
def distance(vecA, vecB):
return np.sqrt(np.sum(np.power(vecA - vecB, 2)))
# 随机质心
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) # 随机生成 k×1 个质心
return centroids
# 求群分配结果和质心
def kMeans(dataset, k, distMeas=distance, 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, :]) # 第i个点与第j个质心的距离
if distJI < minDist: # 找到第i个点与所有质心的最短距离及其质心
minDist = distJI
minIndex = j
if clusterAssment[i, 0] != minIndex:# 若该点的群落改变,则需要再循环
clusterChanged = True
clusterAssment[i, :] = minIndex, minDist ** 2 # 更新该点的分配结果与距离
for cen in range(k):# 对于每个质心
ptsInClust = dataset[np.nonzero(clusterAssment[:, 0].A == cen)[0]]#划分为第cen类的所有点
centroids[cen, :] = np.mean(ptsInClust, axis=0) # 更新质心的值(axis=0表示按列计算均值)
return centroids, clusterAssment
if __name__ == "__main__":
datamat = np.mat(loadData('testSet.txt'))
centriods, clusterAssment = kMeans(datamat,4)
print(centriods,clusterAssment)
(2)二分K-均值算法
-
上面的算法由于 k k k值难以确定以及聚类效果的不确定,可能最后收敛到了局部最小值,而非全局最小值。需要改进
-
度量聚类效果的一种指标是误差平方和(SSE),误差越小越接近于质心,聚类效果越好。
-
二分K-均值的算法用于改进以上的算法,其原理是首先将所有点作为一个群,然后二分,再根据某个可以最大程度降低SSE的群落进行二分,直到达到指定 k k k个群为止。
伪代码: 将所有点视为一个群落 当群数目 < k 时 对于每一个群 计算总误差 在给定的群上面进行K-均值聚类(k=2) 计算将该群落二分后的总误差 选择使得误差最小的群进行二分
#二分K-均值
def bikMeans(dataset, k, distMeas=distance):
m = np.shape(dataset)[0]
clusterAssment = np.mat(np.zeros((m, 2))) # 储存群分配结果与平方误差
centriod0 = np.mean(dataset, axis=0).tolist()[0] # 初始质心:整个数据集的平均值
centList = [centriod0] # 用于保存质心的列表
for j in range(m): # 计算每个点到质心的距离/误差
clusterAssment[j, 1] = distMeas(np.mat(centriod0), dataset[j, :]) ** 2
while (len(centList) < k): # 直到分为k个群为止
lowestSSE = np.inf
for i in range(len(centList)): # 遍历所有质心
ptsCluster = dataset[np.nonzero(clusterAssment[:, 0].A == i)[0], :] # 第i个群的数据集
centriodMat, splitClustAss = kMeans(ptsCluster, 2, distMeas) # 二分第i个群
sseSplit = np.sum(splitClustAss[:, 1]) # 计算第i个群划分后的SSE
# 除第i个群之外的SSE
sseNotSplit = np.sum(clusterAssment[np.nonzero(clusterAssment[:, 0].A != i)[0], 1])
if (sseSplit + sseNotSplit) < lowestSSE: # 若划分后的SSE比当前最小SSE小,则进行二分
bestCenToSplit = i
bestNewCents = centriodMat
bestClustAss = splitClustAss.copy()
lowestSSE = sseSplit + sseNotSplit
# 更新群的分配结果(在二分中划分为0的数据索引值不变(原群的索引值),划分为1的数据索引值更新为新值)
bestClustAss[np.nonzero(bestClustAss[:, 0].A == 1)[0], 0] = len(centList)
bestClustAss[np.nonzero(bestClustAss[:, 0].A == 0)[0], 0] = bestCenToSplit
clusterAssment[np.nonzero(clusterAssment[:, 0].A == bestCenToSplit)[0], :] = bestClustAss
# 更新质心(更新旧群的质心,添加新群的质心)
centList[bestCenToSplit] = bestNewCents[0, :]
centList.append(bestNewCents[1, :])
cent = []
for i in range(len(centList)):
cen = centList[i].tolist()[0]
cent.append(cen)
centriodList = np.mat(cent)
return centriodList, clusterAssment
if __name__ == "__main__":
datamat = np.mat(loadData('testSet2.txt'))
centList, clusterAssment = bikMeans(datamat, 3)
print(centList)
3.实例
- 对地理坐标进行聚类
import matplotlib.pyplot as plt
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(cluster_num=5):
dataList = []
for line in open('places.txt').readlines():
lineArr = line.split('\t')
dataList.append([float(lineArr[4]), float(lineArr[3])])
datamat = np.mat(dataList)
centriod, clustAssing = bikMeans(datamat, cluster_num, distMeas=distSLC)
fig = plt.figure()
# 代表figure中左,右,宽,高的绘制比例
rect = [0.1, 0.1, 0.8, 0.8]
scatterMarkers = ['s', 'o', '^', '8', 'p', 'd', 'v', 'h', '>', '<']
axprops = dict(xticks=[], yticks=[])
# add_axes()可以叠加图形,第一层用于设置背景图,同时设置坐标体系为空值,方便后面散点图的坐标体系设置
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) # frameon用于叠加图层,为False不遮挡下一层
for i in range(cluster_num): # 用不同形状标记不同群并绘制出来
pstInCurrCluster = datamat[np.nonzero(clustAssing[:, 0].A == i)[0], :]
markerStyle = scatterMarkers[i % len(scatterMarkers)] # 第i个群选择标记列表第i个形状
ax1.scatter(pstInCurrCluster[:, 0].flatten().A[0], \
pstInCurrCluster[:, 1].flatten().A[0], marker=markerStyle, s=90)
# 用“+”代表质心,画质心的散点图
ax1.scatter(centriod[:, 0].flatten().A[0], centriod[:, 1].flatten().A[0], marker='+', s=300)
plt.show()
if __name__ == "__main__":
clusterClubs(5)