《机器学习实战》学习笔记(十)

第10章 利用K-均值聚类算法对未标注数据分组

引言

聚类

聚类是一种无监督的学习,它将相似的对象归到同一个簇中。簇内的对象越相似,聚类的效果越好。

K-均值(K-means)

K-均值(K-means)聚类的算法,之所以称之为K-均值是因为它可以发现K个不同的簇,且每个簇的中心采用簇中所含值的均值计算而成。

簇识别(cluster identification)

簇识别给出聚类结果的含义。假定有一些数据,现在将相似数据归到一起,簇识别会告诉我们这些簇到底都是些什么。

聚类与分类的最大不同在于,分类的目标事先巳知,而聚类则不一样。因为其产生的结果与分类相同,而只是类别没有预先定义,聚类有时也被称为无监督分类(unsupervised classification )。

聚类分析试图将相似对象归入同一簇,将不相似对象归到不同簇。相似这一概念取决于所选择的相似度计算方法。

K-均值聚类优缺点

  • 优点:容易实现。
  • 缺点:可能收敛到局部最小值,在大规模数据集上收敛较慢。
  • 适用数据类型:数值型数据

K-均值聚类的一般流程

  1. 收集数据:使用任意方法。
  2. 准备数据:需要数值型数据来计算距离,也可以将标称型数据映射为二值型数据再用 于距离计算。
  3. 分析数据:使用任意方法。
  4. 训练算法:不适用于无监督学习,即无监督学习没有训练过程。
  5. 测试算法:应用聚类算法、观察结果。可以使用量化的误差指标如误差平方和来评价算法的结果。
  6. 使用算法:可以用于所希望的任何应用。通常情况下,簇质心可以代表整个簇的数据 来做出决策。

10.1 K-均值聚类算法

K-均值算法的性能会受到所选距离计算方法的影响。

K-均值算法的工作流程如下:

  • 首先,随机确定K个初始点作为质心。
  • 然后将数据集中的每个点分配到一个簇中,具体来讲,为每个点找距其最近的质心,并将其分配给该质心所对应的簇。
  • 最后,每个簇的质心更新为该簇所有点的平均值。

伪代码表示如下:

创建K个点作为起始质心(经常是随机选择)
当任意一个点的簇分配结果发生改变时
	对养据集中的每个数据点
 		对每个质心
 			计算质心与数据点之间的距离
		将数据点分配到距其最近的簇
	对每一个簇,计算簇中所有点的均值并将均值作为质心

K -均值聚类支持函数代码实现如下:

from numpy import *

#导入数据
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):
    #power是求得次方,两个点差值的平方。
    return sqrt(sum(power(vecA - vecB, 2))) 

#产生质心向量
def randCent(dataSet, k):
    #得到数据特征个数
    n = shape(dataSet)[1]
    #创建一个k x n 的矩阵
    centroids = mat(zeros((k,n)))
    #遍历数据集的每一特征
    for j in range(n):
        minJ = min(dataSet[:,j]) 
        #得到特征最大差值
        rangeJ = float(max(dataSet[:,j]) - minJ)
        #k个质心向量的第j维数据值随机为位于(最小值,最大值)内的某一值
        # rand(k,1)表示产生k行1列在0-1之间的随机数
        centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1))
    #返回初始化得到的k个质心向量
    return centroids

三个辅助函数,一个是加载数据函数,一个是计算距离函数,一个是产生质心函数。

K -均值聚类算法实现如下:


#实现k均值算法,当所有中心不再改变时退出
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
    #得到样本数
    m = shape(dataSet)[0]
    #创建一个m x 2 的矩阵
    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
            # 样本类型是否需要改变,需要改变就将标识符修改为true
            if clusterAssment[i,0] != minIndex: clusterChanged = True
            #更新这个点的聚簇索引及误差
            clusterAssment[i,:] = minIndex,minDist**2
       
        print (centroids)
        
        #遍历所有的簇,重新找质心
        for cent in range(k):
            # nonzero(a):返回数组a中非零元素的索引值数组
            # 矩阵.A是将矩阵转换为数组numpy
            #查找这个 cent 簇所有的点
            ptsInClust = dataSet[nonzero(clusterAssment[:,0].A==cent)[0]]
            #对于这个簇中每个点的列取均值,更新中心点centers[cent]
            centroids[cent,:] = mean(ptsInClust, axis=0) 
    return centroids, clusterAssment


dataMat=mat(loadDataSet(r'Ch10/testSet.txt'))
centers,clusterAssment=kMeans(dataMat,4)
print(centers)
#第一个列为簇的编号,第二列是当前点到这个簇的质心的距离
print(clusterAssment)

输出结果如下:
在这里插入图片描述
上图第一个列为簇的编号,第二列是当前点到这个簇的质心的距离。
输出数据集散点图观察一下:

plt.figure()
#打印原始数据
plt.scatter(dataMat[:,0].tolist(),dataMat[:,1].tolist(),c="b",marker="o")
#打印聚类输出结果
plt.scatter(centers[:,0].tolist(),centers[:,1].tolist(),c='r',marker="+")
plt.show()

在这里插入图片描述
上图可知数据集散点分布大致可以化为四部分,所以程序中使k=4.输出聚类中心
实现函数如下:


def poltcluster(dataMat,clusterAssment,k,centers):

    fig = plt.figure()
    rect=[0.1,0.1,0.8,0.8]
    ax1=fig.add_axes(rect, label='ax1', frameon=False)
    MARK= ['8','s','p','h']
    for i in range(k):
        # nonzero(a):返回数组a中非零元素的索引值数组
        # 矩阵.A是将矩阵转换为数组numpy
        #查找这个 cent 簇所有的点
        markerStyle = MARK[i%len(MARK)]
        ptsInCurrCluster = dataMat[nonzero(clusterAssment[:,0].A==i)[0],:]    
        ax1.scatter(ptsInCurrCluster[:,0].flatten().A[0], ptsInCurrCluster[:,1].flatten().A[0], marker=markerStyle, s=90)
    # 画出质心
    ax1.scatter(centers[:,0].flatten().A[0], centers[:,1].flatten().A[0], marker='+', s=300)
    plt.show()  

    return

如下图:
在这里插入图片描述

图中红色十字即为聚类中心质心。大致也是位于四个部分中。区分是能区分,但总感觉效果不是太好,而且需要我们先去判断大致分为几类,这点感觉不是很好。当我将修改为5时输出如下:
在这里插入图片描述
在这里插入图片描述

可以看到聚类效果变得不再理想。同一簇出现了两个质心。而且由于质心最初是随机分布的,得到的最后的聚类效果也不相同。

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

如何才能知道生成的簇比较好呢?

在包含簇分配结果的矩阵中保存着每个点的误差,即该点到簇质心的距离平方值。利用该误差来评价聚类质量的方法。

K-均值算法收敛但聚类效果较差的原因是,K-均值算法收敛到了局部最小值,而非全局最小值(局部最小值指结果还可以但并非最好结果,全局最小值是可能的最好结果)。

一种用于度量聚类效果的指标是SSE(Sumof Squared Error,误差平方和)SSE值越小表示数据点越接近于它们的质心,聚类效果也越好。因为对误差取了平方,因此更加重视那些远离中心的点。

一种肯定可以降低SSE值的方法是增加簇的个数,但这违背了聚类的目标。聚类的目标是在保持族数目不变的情况下提高簇的质量。

合并最近的质心,或者合并两个使得SSE增幅最小的质心。第一种思路通过计算所有质心之间的距离,然后合并距离最近的两个点来实现。第二种方法需要合并两个簇然后计算总SSE值。必须在所有可能的两个簇上重复上述处理过程 ,直到找到合并最佳的两个簇为止。

10.3 二分K-均值算法

为克服K-均值算法收敛于局部最小值的问题,提出了另一个称为二分K-均 值 (bisecting K-means)的算法。

该算法首先将所有点作为一个簇,然后将该簇一分为二。之后选择其中一个
簇继续进行划分,选择哪一个簇进行划分取决于对"其划分是否可以最大程度降低38£的值。上述基于SSE的划分过程不断重复,直到得到用户指定的簇数目为止。

二分K-均值算法的伪代码如下:

将所有点看成一个襄
当簇数目小于灸时
对于每一个簇
	计算总误差
	在给定的簇上面进行艮-均 值 聚 类 (^^2)
	计算将该簇一分为二之后的总误差
选择使得误差最小的那个族进行划分操作

二分K-均值聚类算法实现如下:


def biKmeans(dataSet,k,distMea=distEclud):
    m=shape(dataSet)[0]
    #这个zeros是一个m行2列的数据(记录这个点所属的簇的索引,记录这个点到其质心的距离)
    clusterAssment=mat(zeros((m,2)))

    #全部数据集属于一个聚簇时,设置中心为均值即可
    center0=mean(dataSet,axis=0).tolist()[0]
    print('第一个中心点:',center0)
    centList=[center0]
    for j in range(m):
        clusterAssment[j,1]=distMea(mat(center0),dataSet[j,:])**2
    #循环来产生质心
    
    #当簇数目小于k时
    while(len(centList)<k):
        #初始化最小 误差平方和
        lowestSSE=inf 
        #循环每个簇
        for i in range (len(centList)):
            #到dataset中筛选出属于第i个簇的数据样本
            pointsInCluster=dataSet[nonzero(clusterAssment[:,0].A==i)[0],:]
            #对这个簇中的样本进行一次k=2的聚类
            centroidMat,splitClustAss=kMeans(pointsInCluster,2,distMea)
            sseSplit=sum(splitClustAss[:,1])
            #剩余的数据集的误差
            sseNotSplit=sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1])
            #总误差
            totalSplit=sseSplit+sseNotSplit
            #总误差小于最小误差
            if totalSplit<lowestSSE:
                #最好的质心索引
                bestCentToSplit=i
                bestNewCent=centroidMat
                bestClustAss=splitClustAss.copy()
                lowestSSE=totalSplit
        #分k聚类返回系数0或1,需要把1换成当前簇数目,以免造成重复
        bestClustAss[ nonzero(bestClustAss[:,0].A==1)[0],0 ]=len(centList)
        #返回的是第几行第几行
        #把0换成别切分的簇,或者与上面的交换赋值也可以
        bestClustAss[ nonzero(bestClustAss[:,0].A==0)[0],0]=bestCentToSplit
        #将centlist指定位置上的质心换成分割后的质心
        centList[bestCentToSplit]=bestNewCent[0,:].tolist()[0]
        #将另一个质心添加上去
        centList.append(bestNewCent[1,:].tolist()[0])
        #将划分后的新质点及点分布赋值给结果矩阵
        clusterAssment[nonzero(clusterAssment[:,0].A==bestCentToSplit)[0],:]=bestClustAss
    return mat(centList),clusterAssment

dataMat=mat(loadDataSet(r'Ch10/testSet2.txt'))
centList,clusterAssment=biKmeans(dataMat,3)
print(centList)

输出结果如下:
在这里插入图片描述

使用kMeans函数聚类函数结果如下:
在这里插入图片描述
对比起来,基本没有什么差别。没有出现书上展示的问题。是由于数据集局部最小值与全局最小值靠近吗?所以没有出现明显差异,导致二分均值算法没有明显差异??
在这里插入图片描述
上图为书中展示的数据集分类产生结果,但是实际这个问题并没有出现。
多试两次就出现了如下:
在这里插入图片描述
使用二分均值聚类,输出结果不会出现类似的情况。

10.4 示例:对地图上的点进行聚类

书中使用的位置经纬度在数据集places中已经有了,所以省去调用API获取经纬度。
实现代码如下:


#读取到数据的经纬度信息
def massPlaceFind():
    lis = []
    for line in open(r'./Ch10/places.txt').readlines():
        line = line.strip()
        lineArr = line.split('\t')
        lng,lat = lineArr[3],lineArr[4]#地理坐标的经度、纬度
        lis.append([lng,lat])
    return mat(lis).astype('float64')

#利用球面余弦定理计算(经度,纬度)两点的距离
def distSLC(vecA, vecB):
    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

import matplotlib

numClust=5
#获取经纬度信息
datMat = massPlaceFind()
#调用二分K聚类获取簇中心集合以及clustAssing矩阵
myCentroids, clustAssing = biKmeans(datMat, numClust, distMea=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(r'./Ch10/Portland.png')
ax0.imshow(imgP)
ax1=fig.add_axes(rect, label='ax1', frameon=False)
#迭代簇集合,根据不同的marker画出对应的簇
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()

输出结果如图:
在这里插入图片描述
程序新增根据经纬度计算距离的函数,之后便是调用二分聚类函数实现聚类。最后将聚类结果输出。
得到上图结果,感觉聚类不是很合理。要求是从质心到该簇各点的总距离要求最小。聚类结果图上橙色和红色划分不是很合理。调整簇数为三输出结果如下:
在这里插入图片描述
比较簇数为5,看起来密集的地方被分为了两部分,但远处孤立的点划分感觉不合理。理想中过远处点应该单独为一簇,但实际程序并不是这样处理的。没有单独点成簇。

10.5小结

聚类是一种无监督的学习方法,是第一个接触到的无监督方法。

无监督学习是指事先并不知道要寻找的内容,即没有目标变量。

K -均值聚类算法以K个随机质心开始。算法会计算每个点到质心的距离。每个点会被分配到距其最近的簇质心,然后紧接着基于新分配到簇的点更新簇质心。以上过程重复数次,直到簇质心不再改变。这个简单的算法非常有效但是也容易受到初始簇质心的影响。

二分K-均值算法首先将所有点作为一个簇,然后使用K-均值算法 (K = 2 ) 对其划分。下一次迭代时,选择有最大误差的簇进行划分。该过程重复直到K个簇创建成功为止。二分K-均值的聚类效果要好于K-均值算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值