机器学习实战学习笔记(九)K-均值聚类算法

PS:该系列数据都可以在图灵社区(点击此链接)中随书下载中下载(如下)
在这里插入图片描述
  聚类是一种无监督的学习,它将相似的对象归到同一个簇中。它有点像全自动分类,聚类方法几乎可以应用于所有对象,簇内的对象越相似,聚类的效果越好。K-均值(K-means) 聚类算法,之所以称之为K-均值是因为它可以发现k个不同的簇,且每个簇的中心采用簇中所含值的均值计算而成。
  簇识别(cluster identification):簇识别给出聚类结果的含义。假定有一些数据,现在将相似数据归到一起,簇识别会告诉我们这些簇到底都是些什么。聚类与分类的最大不同在于,分类的目标事先巳知,而聚类则不一样。因为其产生的结果与分类相同,而只是类别没有预先定义,聚类有时也被称为无监督分类(unsupervised classification )

1 K-均值聚类算法

                                          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()

机器学习实战学习笔记(九)K-均值聚类算法_第1张图片
机器学习实战学习笔记(九)K-均值聚类算法_第2张图片
  上面的结果给出了4个质心,可以看到经过3次迭代之后K-均值算法收敛。

2 使用后处理来提高聚类性能

  一种用于度量聚类效果的指标是SSE(Sum of Squared Error,误差平方和)。SSE值越小表示数据点越接近于它们的质心,聚类效果也越好。因为对误差取了平方,因此更加重视那些远离中心的点。一种肯定可以降低SSE值的方法是增加簇的个数,但这违背了聚类的目标。聚类的目标是在保持簇数目不变的情况下提高簇的质量。
  可以对生成的簇进行后处理,一种方法是将具有最大SSE值的簇划分成两个簇。具体实现时可以将最大簇包含的点过滤出来并在这些点上运行K-均值。
  为了保持簇总数不变,可以将某两个簇进行合并。有两种可以量化的办法:合并最近的质心,或者合并两个使得SSE增幅最小的质心。第一种思路通过计算所有质心之间的距离,然后合并距离最近的两个点来实现。第二种方法需要合并两个簇然后计算总SSE值。必须在所有可能的两个簇上重复上述处理过程,直到找到合并最佳的两个簇为止。

3 二分K-均值算法

  为克服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

机器学习实战学习笔记(九)K-均值聚类算法_第3张图片
  命令行输入kMeans.showCluster(datMat2, 3, centList, myNewAssments)查看聚类结果如下:
机器学习实战学习笔记(九)K-均值聚类算法_第4张图片
  由于算法的随机性,运行的结果可能不一样。上述函数可以运行多次,聚类会收敛到全局最小值,而原始的kMeans()函数偶尔会陷入局部最小值。

4 对地图上的点进行聚类

  有一个地址列表,里面有70个位置,保存在文件portland-Clubs.txt中,需要对这些地方进行聚类,这样就可以安排交通工具抵达这些簇的质心,然后步行到每个簇类的地址。

4.1 对地理坐标进行聚类

  这里为了方便处理,就不用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-均值聚类算法_第5张图片
  可以尝试输入不同簇数目得到程序运行的效果。

5 小结

  聚类是一种无监督的学习爱方法。所谓无监督的学习是指事先并不知道要寻找的内容,即没有目标变量。聚类将数据点归到多个簇中,其中相似数据点处于同一簇,而不相似数据点处于不同簇中。
  一种广泛使用的聚类算法是K-均值算法,其中k是用户指定的要创建的簇的数目。

你可能感兴趣的:(机器学习,K-Means,聚类,机器学习)