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

本文介绍了K-均值聚类算法的基本原理和步骤,通过一个学生数据的例子展示了如何进行聚类。算法涉及随机选择初始质心,计算数据点与质心的距离并分配到最近的簇,然后更新质心。还提到了二分K-均值算法来解决局部最优问题。文章讨论了聚类效果的评估和后处理策略,并给出了实际的地理坐标聚类案例。
摘要由CSDN通过智能技术生成

1.算法简例

K-均值是一种聚类算法,通过将数据点分配到离其最近的中心点所代表的群体,又称为,将数据集分成K个不重叠的簇,以实现数据的聚类分析和归类,接下来用一个简单的例子来说明

假设我们有以下五个学生的数据,每个学生有两个特征:年龄和成绩。

学生编号年龄成绩
12080
22385
32575
43060
52870

现在的目标是使用K均值算法将这些学生分为两个簇,(K=2)

  1. 假设我们选择学生1和学生4作为初始群体中心(这俩年龄和分数差最大),也就是质点
  2. 然后我们分别计算每个学生与两个簇的距离,将每个学生分配到距离最近的簇所代表的群体中。对于学生1和学生4来说,它们是质心,所以它们自己就是属于自己的簇。对于其他学生,我们计算它们与两个簇的距离:
    • 对于学生2来说,距离质心1的距离是3,距离质心2的距离是10,所以学生2被分配到簇1
    • 对于学生3来说,距离质心1的距离是5,距离质心2的距离是5,所以学生3被分配到簇1
    • 对于学生5来说,距离质心1的距离是8,距离质心2的距离是2,所以学生5被分配到簇2

现在我们有以下两个簇:

群体学生质心
簇1{学生1, 学生2, 学生3}{学生1}
簇2{学生4, 学生5}{学生4}
  1. 更新质心:
    对于每个簇,我们计算簇中所有学生的年龄和成绩的平均值,然后将这些平均值作为新的群体中心。
    簇1的新质心:年龄平均值 = (20+23+25)/3=22.67,成绩平均值 = (80+85+75)/3=80
    簇2的新质心:年龄平均值 = (30+28)/2=29,成绩平均值 = (60+70)/2=65
  2. 重复步骤2和步骤3:
    现在,我们重新计算每个学生与新的质心的距离,并将学生重新分配到最近的质心所代表的簇中。然后更新质心。迭代此过程直到质心不再发生变化或达到预定的迭代次数。在这个例子中,我们只进行一次迭代后,质心不再发生变化,所以算法停止。

最终,我们得到了两个群体:

群体学生质心
簇1{学生1, 学生2, 学生3}{学生1}
簇2{学生4, 学生5}{学生4}

以上就是一个简单的K均值算法的例子,它将学生根据年龄和成绩特征分成了两个群体簇

2.算法原理

通过以上的例子,可以总结出K-均值的算法流程:先随机确定k个初始质心,再将数据集中所有点分配到一个簇中,根据每个点到最近质心的距离来进行簇的分类,之后重新计算每个簇的质心,重复以上过程直到质心不再变化

要注意的是,K-均值的分类性能与计算质心的距离有关,下面给出K-均值的各个支持函数

def loadDataSet(fileName):      # 通用函数用于解析制表符分隔的浮点数
    dataMat = []                # 假设最后一列是目标值
    fr = open(fileName)
    for line in fr.readlines():
        curLine = line.strip().split('\t')
        fltLine = list(map(float, curLine))  # 将所有元素映射为浮点数
        dataMat.append(fltLine)
    return dataMat

def distEclud(vecA, vecB):
    return sqrt(sum(power(vecA - vecB, 2)))  # 返回欧几里得距离 (la.norm(vecA-vecB))

def randCent(dataSet, k):
    n = shape(dataSet)[1]
    centroids = mat(zeros((k, n)))  # 创建centroid矩阵
    for j in range(n):  # 在每个维度的范围内创建随机的聚类中心
        minJ = min(dataSet[:, j])
        rangeJ = float(max(dataSet[:, j]) - minJ)
        centroids[:, j] = mat(minJ + rangeJ * random.rand(k, 1))
    return centroids

这些代码是K-均值算法的一部分,包含了加载数据集、计算欧几里得距离以及生成随机聚类中心的功能。其中loadDataSet函数用于从文件中加载数据集,distEclud函数用于计算两个向量之间的欧几里得距离,randCent函数用于生成K个随机的聚类中心,作为算法的初始中心点。这些函数在K-均值算法的实现中都是非常重要的组成部分
请添加图片描述
切片查看矩阵的相关数值如上,通过randCent也能生成最小最大值之间的数值,distEclud也能计算距离,所有的函数都正常,下面就是K-均值的核心代码部分

def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
    m = shape(dataSet)[0]
    clusterAssment = mat(zeros((m, 2)))  # 创建矩阵以分配数据点到聚类中心,并保存每个点的误差平方和
    centroids = createCent(dataSet, k)  # 使用给定函数(默认为randCent)生成初始的聚类中心
    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
            if clusterAssment[i, 0] != minIndex:  # 如果数据点被重新分配到了不同的聚类中心,设置标志为True
                clusterChanged = True
            clusterAssment[i, :] = minIndex, minDist**2  # 记录数据点的分配结果和误差平方和
        print(centroids)
        for cent in range(k):  # 重新计算聚类中心
            ptsInClust = dataSet[nonzero(clusterAssment[:, 0].A == cent)[0]]  # 获取属于该聚类的所有数据点
            centroids[cent, :] = mean(ptsInClust, axis=0)  # 将聚类中心更新为该聚类的数据点的平均值
    return centroids, clusterAssment  # 返回最终的聚类中心和数据点的分配结果

这个函数是K-均值算法的核心实现。它接受一个数据集、聚类的数量K,以及可选的距离测量函数distMeas和生成初始质心的函数createCent。在每次迭代中,它将数据点分配到最近的质心,并更新质心为每个聚类中的数据点的平均值,直到质心不再发生变化或达到预定的迭代次数。最后,函数返回最终的质心和每个数据点的分配结果

请添加图片描述
以上是k-均值聚类的结果示意图,数据集在迭代三次后收敛,簇中心用十字箭头标记

3.后处理

我们知道簇的数量k是自定义的参数,如何选取合适的k值值得考虑。在上述代码结果中分配结果的矩阵所保存的点误差能用来评估聚类的质量,由此我们可以用来选择k值大小

有时使用的k值过小会造成k-均值算法收敛到了局部最小值,但非全局最小值前者指聚类结果在某个局部区域内已经达到了最小的目标函数值,但该结果并不一定是全局最优的;后者指的是整个数据集范围内的最小目标函数值,也就是最优的聚类结果

用来度量聚类效果的指标有SSE(Sum of Squared Error)误差平方和,SSE值越小表示数据点越接近于质心。为了降低SSE值,可以对生成的簇采用后处理,将最大SSE的簇分成两个簇,即用K-均值算法(K=2)作用在这一个簇上,并同时将某两个簇合并,可以是合并最近的质心或者合并两个使SSE增幅最小的质心。

4.二分K-均值算法

为了解决局部最小值问题,可以采用二分K-均值算法(Bisecting K-means)。二分K-均值算法的基本思想是:每次将簇划分为两个簇时,选择具有最大间距的两个簇,使得新的划分可以最大化不同簇之间的间隔。这样,每次划分都是在最大间距处进行的,从而提高了算法的收敛速度和聚类质量

  1. 初始化:随机选取一个数据点作为第一个聚类中心,将所有其他数据点分配到该簇
  2. 选择最大间距的簇:计算每个簇的质心,并计算所有簇质心之间的距离。选择距离最远的两个簇
  3. 划分簇:将所选的两个簇按照距离中点的位置进行划分,将距离中点较近的数据点分到一个簇,距离中点较远的数据点分到另一个簇
  4. 更新聚类中心:重新计算每个簇的质心
  5. 终止条件:重复步骤2-4,直到达到预定的簇数量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
    while (len(centList) < k):
        lowestSSE = inf # 最小误差平方和
        for i in range(len(centList)):
            ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:] # 获取当前聚类中的数据点
            centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas) # 使用K-均值算法划分当前聚类
            sseSplit = sum(splitClustAss[:,1]) # 计算分割后的误差平方和
            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) # 将分割后的第二个簇的索引更改为新聚类数量
        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] # 用两个最佳新聚类中心替换原始聚类中心
        centList.append(bestNewCents[1,:].tolist()[0])
        clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss # 更新簇分配矩阵
    return mat(centList), clusterAssment # 返回聚类中心列表和簇分配矩阵

这段代码实现了二分K-均值算法。该算法在每次迭代中选取具有最大间距的两个簇进行划分,以最大化不同簇之间的间隔。二分K-均值算法相对于基本的K-均值算法具有更好的聚类效果,因为它始终在尝试最大化不同簇之间的间隔。然而,这个算法可能在某些情况下收敛到局部最优解,特别是在数据分布不均匀的情况下

请添加图片描述
请添加图片描述
以上就是分类的结果

5.示例

假设要在70个地方选择几个最佳的中转点,同时这些地方只有经纬度而无相距的距离

算法流程

  1. 收集数据:使用任意方法收集
  2. 准备数据:只保留经纬度信息
  3. 分析数据:使用Matplotlib构建二维数据图,包含簇与地图
  4. 训练算法:使用监督学习
  5. 测试算法:使用biKmeans函数
  6. 使用算法:输出包含簇及簇中心的地图

示例代码

首先使用相关的工具来收集数据信息

import urllib
import json
def geoGrab(stAddress, city):
    apiStem = 'http://where.yahooapis.com/geocode?' 
    params = {}
    params['flags'] = 'J'
    params['appid'] = 'aaa0VN6k'
    params['location'] = '%s %s' % (stAddress, city)
    url_params = urllib.parse.urlencode(params)
    yahooApi = apiStem + url_params    
    print(yahooApi)
    c=urllib.request.urlopen(yahooApi)
    return json.loads(c.read())

from time import sleep
def massPlaceFind(fileName):
    fw = open('places.txt', 'w')
    for line in open(fileName).readlines():
        line = line.strip()
        lineArr = line.split('\t')
        retDict = geoGrab(lineArr[1], lineArr[2])
        if retDict['ResultSet']['Error'] == 0:
            lat = float(retDict['ResultSet']['Results'][0]['latitude'])
            lng = float(retDict['ResultSet']['Results'][0]['longitude'])
            print("%s\t%f\t%f" % (lineArr[0], lat, lng))
            fw.write('%s\t%f\t%f\n' % (line, lat, lng))
        else: print("error fetching")
        sleep(1)
    fw.close()

上述代码是通过一个网站获得不同的地理编码信息并保存下来,由于API不可用结果就不放了

接下来就是对地理坐标进行聚类

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('./machinelearninginaction3x-master/Ch10/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('./machinelearninginaction3x-master/Ch10/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()

函数distSLC用于计算两个经纬度点之间的球面距离,使用了球面余弦定理。
函数clusterClubs用于对给定的地理位置数据进行聚类操作,读取文件places.txt中的地理位置数据,并将其存储在datList中。将地理位置数据转换为矩阵形式datMat。使用biKmeans算法对地理位置数据进行聚类操作,得到聚类中心myCentroids和每个样本所属聚类的标签clustAssing。之后创建一个图形窗口,并加载地图图像Portland.png。在图形窗口中绘制聚类结果,使用加号标记绘制聚类中心点,图像如下所示
请添加图片描述
从图像上可以看到分成了五个簇,其中红色和蓝色五角的最为密集,根据图像所示我们可以通过调整k值(比如增大k值)获得更好的聚类效果

6.总结

经过本章的学习,加深了对k-均值的了解,一个典型的聚类算法,主思想是将样本数据划分为K个簇,使得每个样本点都属于离其最近的簇中心,也就是质点,K-均值算法的优点包括简单、易于实现和计算效率高。然而,该算法的结果受到初始簇中心的选择和数据分布的影响,可能会陷入局部最优解。因此,在使用K-均值算法时,通常需要进行多次运行并选择最优的结果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值