机器学习实战第十章 k均值聚类

本文详细介绍了k均值聚类算法的原理和实现过程,包括如何初始化质心、如何迭代更新簇分配,以及Python代码示例。同时,文章探讨了k均值算法的局限性,并提出了二分k均值聚类作为改进方案,通过不断划分簇以优化结果。实验结果显示,二分k均值聚类在某些情况下能提供更好的聚类效果。
摘要由CSDN通过智能技术生成

k均值聚类

什么是k均值聚类

试想一下,如果给一张图如下,要求对这张图中的点分类,你会怎么进行呢?请添加图片描述
我们当然可以认为所有的点都只有一个种类,毕竟他们本身只有坐标不同,也可以左右分成两个大类,也可以四个角落划分成四类,这一切都取决于最初定的分类个数,而这就是k均值聚类。

所谓k,就是我们的目标要把数据划分为k个类。
所谓聚类,就是向上面的例子一样,实现不给任何标签,让我们自己区随意分类
均值则是代表了一种方法,本文后面会介绍到

具体实现

前面我们已经了解了我们的目的是什么。下面我们来讲解如何实现。

我们要分类的话,肯定是在同一类中相似度越高越好。也就是说,在特征空间中,他们的距离(欧氏距离或者随便什么距离)整体而言是最近的。k均值聚类正是采取了这种思想,其执行步骤如下:

  1. 由于要划分k个类,因此首先随机选取k个点(算法结束后这k个点会成为各自类的质点)
  2. 遍历所有的点,把这些点分类给距离其最近的质点(就是1中的点)
  3. 更新质点的位置,使其成为所在类的质点(也就是同类所有点的坐标取平均值)
  4. 重复2和3直至点的分类不变

这里的均值便是k均值聚类中的来源了。现在步骤清楚了,下面可以开始用代码去实现了,同样的,所有需要注意的点已均在代码中注释:

from numpy import *

# 加载数据集
def loadDataSet(fileName):
    dataMat = []
    fr = open(fileName)
    for line in fr.readlines():
        curLine = line.strip().split('\t')
        # 这里要注意一下,map函数返回的是一个迭代器,需要用list函数转换成列表,原本的书上没有这个list函数,但是在python3中需要加上这个函数
        fltLine = list(map(float, curLine))
        dataMat.append(fltLine)
    return dataMat

# 计算欧式距离
def distEclud(vecA, vecB):
    return sqrt(sum(power(vecA - vecB, 2)))

# 为给定数据集构建一个包含k个随机质心的集合
def randCent(dataSet, k):
    n = shape(dataSet)[1]
    centroids = mat(zeros((k, n)))
    for j in range(n):
        # 计算最小值,最大值,保证随机点在数据集的边界之内
        minJ = min(dataSet[:, j])
        rangeJ = float(max(dataSet[:, j]) - minJ)
        centroids[:, j] = minJ + rangeJ * random.rand(k, 1)
    # 返回质心
    return centroids

# k均值聚类算法本体
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
    # 数据集的行数
    m = shape(dataSet)[0]
    # 每个点的簇分配结果矩阵,第一列记录簇索引值,第二列存储误差
    clusterAssment = mat(zeros((m, 2)))
    # 创建质心
    centroids = createCent(dataSet, k)
    clusterChanged = True
    # 只要簇分配结果改变就一直迭代
    while clusterChanged:
        clusterChanged = False
        # 计算每个点到质心的距离
        for i in range(m):
            minDist = inf
            minIndex = -1
            for j in range(k):
                distJI = distMeas(centroids[j, :], dataSet[i, :])
                # 寻找最近的质心
                if distJI < minDist:
                    minDist = distJI
                    minIndex = j
            # 若任一点的簇分配结果发生改变,则更新clusterChanged标志
            if clusterAssment[i, 0] != minIndex:
                clusterChanged = True
            # 更新簇分配结果
            clusterAssment[i, :] = minIndex, minDist ** 2
        # 更新质心的位置
        for cent in range(k):
            ptsInClust = dataSet[nonzero(clusterAssment[:, 0].A == cent)[0]]
            centroids[cent, :] = mean(ptsInClust, axis=0)
    return centroids, clusterAssment


def main():
    # test kMeans
    dataMat = mat(loadDataSet('testSet.txt'))
    myCentroids, clustAssing = kMeans(dataMat, 4)
    # 画图,画出聚类结果
    fig = plt.figure()
    # 将dataMat中的点画出来
    ax = fig.add_subplot(111)
    
    ax.scatter(dataMat[:, 0].flatten().A[0], dataMat[:, 1].flatten().A[0], s=50, c='blue')
    # 将聚类中心画出来
    ax.scatter(myCentroids[:, 0].flatten().A[0], myCentroids[:, 1].flatten().A[0], s=300, c='red', marker='+')
    plt.show()
    
if __name__ == '__main__':
    main()

最终得到的结果如下图:请添加图片描述

可以看到分类的结果很棒,基本上每个质心都处于其类别的中心,每个的分类也较为合理,但是这个代码其实是有问题的,那就是它极其依赖于初始的质心分布,比方说有质心有可能会陷入到局部最优解的困境之中,如下图:
在这里插入图片描述
下面我们将会学习一种解决方案

二分k均值聚类

二分k均值聚类的主要思想:二分K均值聚类首先将所有数据划分为一个初始簇,然后逐步对簇进行二分,直到达到指定的簇的数量。

具体实现步骤:

  1. 初始化:将所有数据样本划分为一个初始簇。
  2. 循环:
    a. 对当前的簇进行K均值聚类操作,将其分成两个子簇。这可以通过应用K均值聚类算法一次来实现。
    b. 对刚刚分割得到的两个子簇计算聚类质量度量,例如误差平方和(SSE)或轮廓系数等。
    c. 选择具有较低质量度量的子簇进行进一步的分割操作,这意味着选择该子簇进行新一轮的K均值聚类分割。
    d. 如果达到所需的簇的数量,或者无法选择更多的子簇进行分割,则结束循环。
  3. 输出结果:最终得到所需数量的簇,每个簇包含一组数据样本。

值得一提的是,二分K均值聚类相对于传统的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]
    # 计算每个点到质心的距离
    for j in range(m):
        clusterAssment[j, 1] = distMeas(mat(centroid0), dataSet[j, :]) ** 2
    # 对簇进行划分,直到簇的数目达到k
    while (len(centList) < k):
        lowestSSE = inf
        # 对每一个簇进行划分
        for i in range(len(centList)):
            # 获取当前簇的数据集
            ptsInCurrCluster = dataSet[nonzero(clusterAssment[:, 0].A == i)[0], :]
            # 对当前簇进行k均值聚类
            centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas)
            # 计算划分后的SSE
            sseSplit = sum(splitClustAss[:, 1])
            # 计算其他簇的SSE
            sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:, 0].A != i)[0], 1])
            # 比较划分前后的SSE,选择最佳划分
            print ("sseSplit, and notSplit: ", sseSplit, sseNotSplit)
            if (sseSplit + sseNotSplit) < lowestSSE:
                # 将最佳划分的簇的索引值改为当前簇的索引值
                bestCentToSplit = i
                # 保存最佳划分的质心
                bestNewCents = centroidMat
                # 保存最佳划分的簇分配结果
                bestClustAss = splitClustAss.copy()
                # 保存最佳划分的SSE
                lowestSSE = sseSplit + sseNotSplit
        # 更新簇的分配结果
        bestClustAss[nonzero(bestClustAss[:, 0].A == 1)[0], 0] = len(centList)
        bestClustAss[nonzero(bestClustAss[:, 0].A == 0)[0], 0] = bestCentToSplit
        centList[bestCentToSplit] = bestNewCents[0, :].tolist()[0]
        centList.append(bestNewCents[1, :].tolist()[0])
        clusterAssment[nonzero(clusterAssment[:, 0].A == bestCentToSplit)[0], :] = bestClustAss
    return mat(centList), clusterAssment

def main():
    # test bikMeans
    dataMat = mat(loadDataSet('testSet.txt'))
    centList, myNewAssments = biKmeans(dataMat, 4)
    
    # 画图,画出聚类结果
    fig = plt.figure()
    # 将dataMat中的点画出来
    ax = fig.add_subplot(111)
    
    # 画出聚类结果,每一类用一种颜色
    for i in range(shape(dataMat)[0]):
        if int(myNewAssments[i, 0]) == 0:
            ax.scatter(dataMat[i, 0], dataMat[i, 1], c='red')
        elif int(myNewAssments[i, 0]) == 1:
            ax.scatter(dataMat[i, 0], dataMat[i, 1], c='blue')
        elif int(myNewAssments[i, 0]) == 2:
            ax.scatter(dataMat[i, 0], dataMat[i, 1], c='green')
        elif int(myNewAssments[i, 0]) == 3:
            ax.scatter(dataMat[i, 0], dataMat[i, 1], c='yellow')
        elif int(myNewAssments[i, 0]) == 4:
            ax.scatter(dataMat[i, 0], dataMat[i, 1], c='black')
            
    # 画出质心
    ax.scatter(centList[:, 0].tolist(), centList[:, 1].tolist(), c='orange', marker='+', s=300)
    plt.show()
    

下面看一下运行结果:
请添加图片描述
结果看起来很美好对不对?但是如果我们将分类个数改为3呢?
请添加图片描述
我们可以看到红色这个区域的划分明显是不够好的,但是由于我们划设定了只划分3个类别,因此算法也只能够这样子了,所以我认为k均值聚类算法其实是还有改进的空间的,不过如果真能该了的话,算法名字里的k也就应该不复存在了吧。

实验

最后我们来跑一下书上的小实验。包含爬虫的部分我们不做,直接做后面的部分。
就理论而言,并没有什么新的内容,多了一个计算距离的函数也并非是本书的重点,因此此处还是直接给出完整的代码,感兴趣的读者可以自己拿来跑一下:

from numpy import *

# 加载数据集
def loadDataSet(fileName):
    dataMat = []
    fr = open(fileName)
    for line in fr.readlines():
        curLine = line.strip().split('\t')
        # 这里要注意一下,map函数返回的是一个迭代器,需要用list函数转换成列表,原本的书上没有这个list函数,但是在python3中需要加上这个函数
        fltLine = list(map(float, curLine))
        dataMat.append(fltLine)
    return dataMat

# 计算欧式距离
def distEclud(vecA, vecB):
    return sqrt(sum(power(vecA - vecB, 2)))

# 为给定数据集构建一个包含k个随机质心的集合
def randCent(dataSet, k):
    n = shape(dataSet)[1]
    centroids = mat(zeros((k, n)))
    for j in range(n):
        # 计算最小值,最大值,保证随机点在数据集的边界之内
        minJ = min(dataSet[:, j])
        rangeJ = float(max(dataSet[:, j]) - minJ)
        centroids[:, j] = minJ + rangeJ * random.rand(k, 1)
    # 返回质心
    return centroids

# k均值聚类算法本体
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
    # 数据集的行数
    m = shape(dataSet)[0]
    # 每个点的簇分配结果矩阵,第一列记录簇索引值,第二列存储误差
    clusterAssment = mat(zeros((m, 2)))
    # 创建质心
    centroids = createCent(dataSet, k)
    clusterChanged = True
    # 只要簇分配结果改变就一直迭代
    while clusterChanged:
        clusterChanged = False
        # 计算每个点到质心的距离
        for i in range(m):
            minDist = inf
            minIndex = -1
            for j in range(k):
                distJI = distMeas(centroids[j, :], dataSet[i, :])
                # 寻找最近的质心
                if distJI < minDist:
                    minDist = distJI
                    minIndex = j
            # 若任一点的簇分配结果发生改变,则更新clusterChanged标志
            if clusterAssment[i, 0] != minIndex:
                clusterChanged = True
            # 更新簇分配结果
            clusterAssment[i, :] = minIndex, minDist ** 2
        # 更新质心的位置
        for cent in range(k):
            ptsInClust = dataSet[nonzero(clusterAssment[:, 0].A == cent)[0]]
            centroids[cent, :] = mean(ptsInClust, axis=0)
    return centroids, clusterAssment

def biKmeans(dataSet, k, distMeas=distEclud):
    # 数据集的行数
    m = shape(dataSet)[0]
    # 每个点的簇分配结果矩阵,第一列记录簇索引值,第二列存储误差
    clusterAssment = mat(zeros((m, 2)))
    # 创建一个初始簇,值为数据集的均值,即整个数据集作为一个簇
    centroid0 = mean(dataSet, axis=0).tolist()[0]
    centList = [centroid0]
    # 计算每个点到质心的距离
    for j in range(m):
        clusterAssment[j, 1] = distMeas(mat(centroid0), dataSet[j, :]) ** 2
    # 对簇进行划分,直到簇的数目达到k
    while (len(centList) < k):
        lowestSSE = inf
        # 对每一个簇进行划分
        for i in range(len(centList)):
            # 获取当前簇的数据集
            ptsInCurrCluster = dataSet[nonzero(clusterAssment[:, 0].A == i)[0], :]
            # 对当前簇进行k均值聚类
            centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas)
            # 计算划分后的SSE
            sseSplit = sum(splitClustAss[:, 1])
            # 计算其他簇的SSE
            sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:, 0].A != i)[0], 1])
            # 比较划分前后的SSE,选择最佳划分
            print ("sseSplit, and notSplit: ", sseSplit, sseNotSplit)
            if (sseSplit + sseNotSplit) < lowestSSE:
                # 将最佳划分的簇的索引值改为当前簇的索引值
                bestCentToSplit = i
                # 保存最佳划分的质心
                bestNewCents = centroidMat
                # 保存最佳划分的簇分配结果
                bestClustAss = splitClustAss.copy()
                # 保存最佳划分的SSE
                lowestSSE = sseSplit + sseNotSplit
        # 更新簇的分配结果
        bestClustAss[nonzero(bestClustAss[:, 0].A == 1)[0], 0] = len(centList)
        bestClustAss[nonzero(bestClustAss[:, 0].A == 0)[0], 0] = bestCentToSplit
        centList[bestCentToSplit] = bestNewCents[0, :].tolist()[0]
        centList.append(bestNewCents[1, :].tolist()[0])
        clusterAssment[nonzero(clusterAssment[:, 0].A == bestCentToSplit)[0], :] = bestClustAss
    return mat(centList), clusterAssment

def distSLC(vecA, vecB):
    # calculate spherical distance
    a = sin(vecA[0, 1] * pi / 180) * sin(vecB[0, 1] * pi / 180)
    # calculate spherical distance
    b = cos(vecA[0, 1] * pi / 180) * cos(vecB[0, 1] * pi / 180) * \
        cos(pi * (vecB[0, 0] - vecA[0, 0]) / 180)
    # calculate spherical distance
    return arccos(a + b) * 6371.0

import matplotlib.pyplot as plt

def clusterClubs(numClust=5):
    # create mat to assign data points
    datList = []
    # get data points
    for line in open('places.txt').readlines():
        lineArr = line.split('\t')
        # get data points
        datList.append([float(lineArr[4]), float(lineArr[3])])
    # get data points
    datMat = mat(datList)
    # calculate cluster centers
    myCentroids, clustAssing = biKmeans(datMat, numClust, distMeas=distSLC)
    fig = plt.figure()
    # create plot
    rect = [0.1, 0.1, 0.8, 0.8]
    # create plot
    scatterMarkers = ['s', 'o', '^', '8', 'p', \
                      'd', 'v', 'h', '>', '<']
    # create plot
    axprops = dict(xticks=[], yticks=[])
    # create plot
    ax0 = fig.add_axes(rect, label='ax0', **axprops)
    # create plot
    imgP = plt.imread('Portland.png')
    # create plot
    ax0.imshow(imgP)
    # create plot
    ax1 = fig.add_axes(rect, label='ax1', frameon=False)
    # create plot
    for i in range(numClust):
        # get all data points in this cluster
        ptsInCurrCluster = datMat[nonzero(clustAssing[:, 0].A == i)[0], :]
        # plot data points
        markerStyle = scatterMarkers[i % len(scatterMarkers)]
        # plot data points
        ax1.scatter(ptsInCurrCluster[:, 0].flatten().A[0], \
                    ptsInCurrCluster[:, 1].flatten().A[0], \
                    marker=markerStyle, s=90)
    # plot cluster centers
    ax1.scatter(myCentroids[:, 0].flatten().A[0], \
                myCentroids[:, 1].flatten().A[0], \
                marker='+', s=300)
    # plot cluster centers
    plt.show()
    
def main():
    clusterClubs(5)
    
    
if __name__ == '__main__':
    main()

实验结果与书上基本一致,如下图所示:
请添加图片描述

小结

我感觉k均值聚类可能是聚类算法中最简单的一种了,由于要人工设定分类的个数,因此一些情况下效果应该会非常差,后续可能会有更高级的聚类算法,但是应该没有机会接触到了。不过了解了聚类算法的基本原理,后面上手应该也会很快。
最后总结一下k均值聚类算法的基本思想:
随机选择质心,通过不断迭代最后使得质心到各个点的距离最短。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值