【机器学习实战-python3】K-均值聚类算法

时间:2022-04-14 22:09:59

本篇的数据和代码参见:https://github.com/stonycat/ML-in-Action

一、K-均值聚类算法
聚类是一种无监督的学习,它将相似的对象归到同一簇中,类似全自动分类。簇内的对象越相似,聚类的效果越好。K-均值聚类是每个类别簇都是采用簇中所含值的均值计算而成。聚类与分类的区别在于分类前目标已知,而聚类为无监督分类。
K-均值算法的伪代码如下:

创建k个点作为起始质心(通常随机选择)

   当任意一个点的簇分配结果发生改变时:
对数据集中的每个点:
对每个质心:
计算质心与数据点之间的距离
将数据点分配到距离其最近的簇
对每一个簇,计算簇中所有点的均值并将均值作为质心。

基本功能函数:加载数据、计算距离、初始化k个中心三个函数。

#coding=utf-8

from numpy import *

#load data
def loadDataSet(fileName):
dataMat = []
fr = open(fileName)
for line in fr.readlines(): #for each line
curLine = line.strip().split('\t')
fltLine = list(map(float,curLine)) #这里和书中不同 和上一章一样修改
dataMat.append(fltLine)
return dataMat

#distance func
def distEclud(vecA,vecB):
return sqrt(sum(power(vecA - vecB, 2))) # la.norm(vecA-vecB) 向量AB的欧式距离

#init K points randomly
def randCent(dataSet, k):
n = shape(dataSet)[1]
centroids = mat(zeros((k,n)))#create centroid mat
for j in range(n):#create random cluster centers, within bounds of each dimension
minJ = min(dataSet[:,j])
rangeJ = float(max(dataSet[:,j]) - minJ)
centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1))
return centroids

K-均值聚类算法接收4个参数,两个必要参数为数据集和k的值,另外两个为距离计算函数和初始化函数(可修改)。算法采用计算质心-分配-重新计算质心反复迭代的方式,直到所有点的分配结果不再改变。设置flag为clusterChange=True。

#K-均值算法:
def kMeans(dataSet,k,distMeas=distEclud,createCent=randCent):
#参数:dataset,num of cluster,distance func,initCen
m=shape(dataSet)[0]
clusterAssment=mat(zeros((m,2)))#store the result matrix,2 cols for index and error
centroids=createCent(dataSet,k)
clusterChanged=True
while clusterChanged:
clusterChanged=False
for i in range(m):#for every points
minDist = inf;minIndex = -1#init
for j in range(k):#for every k centers,find the nearest center
distJI=distMeas(centroids[j,:],dataSet[i,:])
if distJI<minDist:#if distance is shorter than minDist
minDist=distJI;minIndex=j# update distance and index(类别)
if clusterAssment[i,0] != minIndex:
clusterChanged = True
#此处判断数据点所属类别与之前是否相同(是否变化,只要有一个点变化就重设为True,再次迭代)
clusterAssment[i,:] = minIndex,minDist**2
print(centroids)
# update k center
for cent in range(k):
ptsInClust=dataSet[nonzero(clusterAssment[:,0].A==cent)[0]]
centroids[cent,:]=mean(ptsInClust,axis=0)
return centroids,clusterAssment

二、用后处理来提高聚类性能
聚类算法中,k的值是由用户初始定义的,如何才能判断k值定义是否合适,就需要用误差来评价聚类效果的好坏,误差是各个点与其所属类别质心的距离决定的。K-均值聚类的方法效果较差的原因是会收敛到局部最小值,而且全局最小。一种评价聚类效果的方法是SSE(Sum of Squared Error)误差平方和的方法,取平方的结果是使得远离中心的点变得更加突出。
一种降低SSE的方法是增加簇的个数,即提高k值,但是违背了聚类的目标,聚类的目标是在不改变簇数目的前提下提高簇的质量。可选的改进的方法是对生成的簇进行后处理,将最大SSE值的簇划分成两个(K=2的K-均值算法),然后再进行相邻的簇合并。具体方法有两种:1、合并最近的两个质心(合并使得SSE增幅最小的两个质心)2、遍历簇 合并两个然后计算SSE的值,找到使得SSE最小的情况。

下面将使用上述技术得到更好的聚类结果方法。

三、二分K-均值算法
二分K-均值类似后处理的切分思想,初始状态所有数据点属于一个大簇,之后每次选择一个簇切分成两个簇,这个切分满足使SSE值最大程度降低,直到簇数目达到k。另一种思路是每次选择SSE值最大的一个簇进行切分。
满足使SSE值最大程度降低伪代码如下:

将所有点看成一个簇

   当簇数目小于k时
对于每一个簇:
计算总误差
在给定的簇上面进行K-均值聚类(k=2)
计算将该簇一分为二后的总误差
选择使得误差最小的那个簇进行划分操作

函数biKmeans是上面二分K-均值聚类算法的实现,首先创建clusterAssment储存数据集中每个点的分类结果和平方误差,用centList保存所有已经划分的簇,初始状态为整个数据集。while循环不停对簇进行划分,寻找使得SSE值最大程度减小的簇并更新,添加新的簇到centList中。

#二分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] # create a list with one centroid
for j in range(m): # calc initial Error for each point
clusterAssment[j, 1] = distMeas(mat(centroid0), dataSet[j, :]) ** 2
while (len(centList) < k):
lowestSSE = inf #init SSE
for i in range(len(centList)):#for every centroid
ptsInCurrCluster = dataSet[nonzero(clusterAssment[:, 0].A == i)[0],:] # get the data points currently in cluster i
centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas)# k=2,kMeans
sseSplit = sum(splitClustAss[:, 1]) # compare the SSE to the currrent minimum
sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:, 0].A != i)[0], 1])
print("sseSplit, and notSplit: ", sseSplit, sseNotSplit)
if (sseSplit + sseNotSplit) < lowestSSE: #judge the error
bestCentToSplit = i
bestNewCents = centroidMat
bestClustAss = splitClustAss.copy()
lowestSSE = sseSplit + sseNotSplit
#new cluster and split cluster
bestClustAss[nonzero(bestClustAss[:, 0].A == 1)[0], 0] = len(centList) # change 1 to 3,4, or whatever
bestClustAss[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] # replace a centroid with two best centroids
centList.append(bestNewCents[1, :].tolist()[0])
clusterAssment[nonzero(clusterAssment[:, 0].A == bestCentToSplit)[0],:] = bestClustAss # reassign new clusters, and SSE
return mat(centList), clusterAssment

四、示例:对地图上的点聚类
书上给出的yahooAPI的baseurl已经改变,github上有oauth2供python使用,但是yahoo的BOOS GEO好像OAuth2验证出了问题,虽然写了新的placeFinder调用api的代码,仍然会有403错误。参考:http://blog.forec.cn/2016/02/21/machinelearning10/

好在随书代码中已经给出place.txt,所以直接调用,这里略过获取数据的步骤。

#draw function
import matplotlib
import matplotlib.pyplot as plt
def clusterClubs(numClust=5):#参数:希望得到的簇数目
datList = []
for line in open('places.txt').readlines():#获取地图数据
lineArr = line.split('\t')
datList.append([float(lineArr[4]), float(lineArr[3])])#逐个获取第四列和第五列的经纬度信息
datMat = mat(datList)
myCentroids, clustAssing = biKmeans(datMat, numClust, distMeas=distSLC)
#draw
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[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()

测试:k=3
【机器学习实战-python3】K-均值聚类算法
k=5
【机器学习实战-python3】K-均值聚类算法
k=7
【机器学习实战-python3】K-均值聚类算法

小结
聚类是一种无监督聚类算法,无监督指的是事先不知道所需要查找的内容(无目标变量)。聚类将数据点归入多个簇中,相似的数据点归入到同一个簇。有很多不同的方法来计算相似性。广泛使用的是K-均值算法:通过指定k值,随机分配k个质心,然后计算每个数据点到各个质心的距离,将点分配到距离最近的质心,重新计算每个簇的均值更新质心,反复迭代直到质心不在变化。(算法有效但初始k值不容易确定)
另一种是二分K-均值算法:首先将所有点作为一个簇,然后采用k=2的K-均值算法进行划分,下一次迭代时选择两个簇中SSE(平方误差)最大的簇进行再次划分,直到簇数目达到给定的k值。二分K-均值的算法要优于K-均值算法,不容易收敛到局部最小。