以《机器学习实战》为参考,实现K-means聚类算法
1.K-means聚类算法
数据示例: 每一个样本特征是二维向量
读取数据文件,加载数据集:
def loadDataSet(fileName): #general function to parse tab -delimited floats
dataMat = [] #assume last column is target value
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = list(map(float,curLine)) #map all elements to float()
dataMat.append(fltLine)
return dataMat
初始化K个初始质心:
def randCent(dataSet, k):
n = shape(dataSet)[1]
#k个均值向量 每个向量n个特征
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)
#返回一个kx1维数组 用0,1随机数填充
centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1))
return centroids
对于每一个特征来说,求出最小值和最大值,然后产生之间的一个随机数,其中random.rank(m,n)函数返回一个m*n的数组,数组中每一个元素均用(0,1)随机数来填充。
距离计算,采用欧式距离:
def distEclud(vecA, vecB):
return sqrt(sum(power(vecA - vecB, 2))) #la.norm(vecA-vecB)
K-means算法:
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
m = shape(dataSet)[0]
#存储每个样本的簇记号以及与均值向量的距离
clusterAssment = mat(zeros((m,2)))#create mat to assign data points
#to a centroid, also holds SE of each point
#随机产生k个质心
centroids = createCent(dataSet, k)
#用来标记在一次迭代中,均值向量是否改变 作为终止条件
clusterChanged = True
while clusterChanged:
clusterChanged = False
#计算每个样本与均值的距离,选择最近的均值向量索引以及最近距离存储
for i in range(m):#for each data point assign it to the closest centroid
minDist = inf; minIndex = -1
for j in range(k):
distJI = distMeas(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):#recalculate centroids
#返回属于第cent个均值的样本集合数据
ptsInClust = dataSet[nonzero(clusterAssment[:,0].A==cent)[0]]#get all the point in this cluster
#按列求均值
centroids[cent,:] = mean(ptsInClust, axis=0) #assign centroid to mean
return centroids, clusterAssment
随机产生K个质心 计算每个样本到每个均值向量的距离,将样本划分到最近的均值向量中,计算新的均值向量,如此迭代,终止条件是均值向量不再改变。
返回的4个质心为:
画出数据集以及质心分布:
def plotarr(arr, k):
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(arr[:,0].flatten().A[0], arr[:,1].flatten().A[0])
ax.scatter(k[:,0].flatten().A[0], k[:,1].flatten().A[0], c='r', marker='p')
#ax.scatter(arr[:,0], arr[:,1])
plt.show()
arr数据集矩阵 k质心矩阵
scatter函数 第一维数据 第二维数据 颜色c 形状marker
K-means均值算法与初始均值向量选择有关,容易陷入到局部最优值,可以采用二分K-均值算法来处理,避免陷入局部最优值。
2.二分K-均值算法
聚类效果的度量SSE(误差平方和):对应于clusterAssment矩阵的第一列之和
算法大致如下:
将所有点看成一个簇:
当簇数小于K时:
对于每一个簇
计算总误差
在给定簇上面进行2-means均值聚类
计算将簇一分为二的总误差
选择使得误差最小的簇进行划分操作
代码如下:
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
clusterAssment[j,1] = distMeas(mat(centroid0), dataSet[j,:])**2
while (len(centList) < k):
lowestSSE = inf
#遍历每一个簇
for i in range(len(centList)):
#找出属于该簇的所有样本数据矩阵
ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:]#get the data points currently in cluster i
#将该簇进行2-means聚类
centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas)
#聚类之后的sse
sseSplit = sum(splitClustAss[:,1])#compare the SSE to the currrent minimum
#没有聚类之前的sse
sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1])
print ("sseSplit, and notSplit: ",sseSplit,sseNotSplit)
#找出最好的簇聚类
if (sseSplit + sseNotSplit) < lowestSSE:
bestCentToSplit = i
bestNewCents = centroidMat
bestClustAss = splitClustAss.copy()
lowestSSE = sseSplit + sseNotSplit
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
centList存放 均值向量列表
clusterAssment 存放每个样本的均值向量索引 以及每个样本到均值向量的距离大小
centroidMat 将某一簇进行二划分的 均值向量矩阵 2Xm 共两个均值向量 索引依次为0,1
sliptClustAss 将某一簇进行二划分之后 该簇元素的均值向量centroidMat 索引(0或1)以及到均值向量的距离
bestClusterAss 最优划分之后的某一簇的 均值向量索引以及到均值向量的距离
进行簇划分时,将bestClusterAss的均值向量索引0改成被划分簇的索引bestCentToSplit以及均值向量索引1变为新增簇的索引len(centList)
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
centList中bestCentToSplit位置改为bestClusterAss对应0的均值向量,在len(centList)位置添加bestClusterAss对应1的均值向量,
centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0]#replace a centroid with two best centroids
centList.append(bestNewCents[1,:].tolist()[0])
最后改变clusterAssment中bestCentToSplit簇的相应距离向量为bestClusterAss
加载数据集测试:可以发现数据集约可以聚成3类
运行K-means聚类算法: 可知聚类效果不好 陷入了局部最优值
运行二分kMeans聚类算法: 质心位置很好的聚类了数据
3.对于地理坐标进行聚类
数据集文件: 第四列与第五列分别为经纬度
距离计算,球面上两点距离计算,可以参考
https://www.cnblogs.com/zhoug2020/p/7634151.html
球面两点间距离公式:
r*acos(cos(wa)*cos(wb)*cos(jb-ja) + sin(wa)*sin(wb))
def distSLC(vecA, vecB):#Spherical Law of Cosines
a = sin(vecA[0,1]*pi/180) * sin(vecB[0,1]*pi/180)
b = cos(vecA[0,1]*pi/180) * cos(vecB[0,1]*pi/180) * \
cos(pi * (vecB[0,0]-vecA[0,0]) /180)
return arccos(a + b)*6371.0 #pi is imported with numpy
进行二分均值聚类以及显示:
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)
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=5 k=10
如何选取K的值:
轮廓系数 用于评估聚类的效果,结合凝聚度与分离度。该值处于-1~1之间,值越大,表示聚类效果越好。
A(i)簇内凝聚度:样本点i与簇内其他样本距离的平均值
B(i)簇间分离度:计算样本i与其他各个簇的平均值的最小值
轮廓系数S(i)=(B(i)-A(i))/max{A(i),B(i)}
K值的选取:
1.多次测试取轮廓系数最大的K
2.(Calinski-Harabasz准则)
其中SSB是类间方差, ,m为所有点的中心点,mi为某类的中心点;
SSW是类内方差,;
(N-k)/(k-1)是复杂度;
比率越大,数据分离度越大.