机器学习实战—K-均值聚类算法

时间:2021-07-07 22:32:07

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

K-均值聚类算法,它可以发现K个不同的簇,且每个簇的中心采用簇中所含值的均值计算而成。簇识别概念:假定有一些数据,现在将相似数据归到一起,簇识别会告诉我们这些簇到底都是些什么,聚类与分类的最大不同在于,分类的目标事先已知,而聚类则不一样。因为其产生的结果与分类相同,而只是类别没有预先定义,聚类有时也被称为无监督分类。

一、K-均值聚类算法

K-均值是发现给定数据集的K个簇的算法。簇个数K是用户给定的,每一个簇通过其质心,即簇中所有点的中心来描述。算法流程:首先,随机确定k个初始点最为质心,然后将数据集中的每个点分配到一个簇中(为每个点找距其最近的质心,并将其分配到该质心所对应的簇),之后,每个簇的质心更新为该簇所有点的平均值。

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

K-均值聚类支持函数:

import numpy as np
import random
from numpy import *
#数据加载函数
def loadData(filename):
    #定义存储数据的列表
    dataList = []
    fr = open(filename)
    #读取文件,并对文件内容进行逐行遍历
    for line in fr.readlines():
        #去除行内容中的空格并且以tab键分割一行内容
        currLine = line.strip().split('\t')
        #将每个数据映射为float类型,这里要强制转换数据类型
        fltLine = list(map(float,currLine))
        dataList.append(fltLine)
    return dataList

#利用欧式距离计算两个向量(点)之间的距离
def distEclid(vecA, vecB):
    return np.sqrt(sum(np.power(vecA-vecB,2)))

#生成簇中心矩阵,每个簇中心向量值是样本每一维的平均值,初始情况下是随机值
def randCent(dataSet,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)
        #这里注意randint函数中的参数范围一定按大小顺序传入,此处使用random.rand(k,1)函数一定在文件头部写上如下语句
        #from numpy import *
        centroids[:,j] = np.mat(minJ + rangeJ * random.rand(k,1))
    return centroids

注:随即质心必须要在整个数据集的边界之内

K-均值聚类算法:

#k-均值聚类算法函数,后两个参数为计算两个向量之间的距离函数引用和计算簇中心的函数引用
def kMeans(dataSet,k,disMeas=distEclid,createCent=randCent):
    #获得样本数量
    m = shape(dataSet)[0]
    #定义矩阵,存储每个点的聚类标志和误差
    culsterAssment = mat(zeros((m,2)))
    #获取簇中心矩阵
    centroids = createCent(dataSet,k)
    #簇中心变量是否变化标志
    clusterChanged = True
    #迭代次数实现不确定,当簇中心矩阵不再变化时停止迭代
    while(clusterChanged):
        clusterChanged = False
        # 遍历每个样本
        for i in range(m):
            # 定义最小距离和最小距离所属的类索引
            minDist = np.inf
            minIndex = -1
            # 遍历所有的簇中心,计算该样本与所有簇中心的距离,比较获得距离最小的簇中心
            for j in range(k):
                distJI = disMeas(centroids[j, :], dataSet[i, :])
                #更新最小距离和最近中心索引
                if distJI < minDist:
                    minDist = distJI
                    minIndex = j
            # 如果样本聚类索引不等于计算出的最短距离中心索引,则继续迭代,并更新矩阵值
            if culsterAssment[i, 0] != minIndex:
                clusterChanged = True
            culsterAssment[i, :] = minIndex, minDist ** 2
        # print("centroids:", centroids)
            #在迭代中,聚类变化,则簇中心变化,需要在每次迭代中更新簇中心
        for cent in range(k):
            #过滤出已经聚类的样本
            ptsInClust = dataSet[nonzero(culsterAssment[:,0].A==cent)[0]]
            centroids[cent,:] = mean(ptsInClust,axis=0)
    return centroids,culsterAssment

注:这里的误差是指当前点到质心的距离

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

一种用于度量聚类效果的指标是SSE(误差平方和),SSE值越小表示数据点越接近于它们的质心,聚类效果越好,因为对误差取了平方,因此更加重视那些远离中心的点,聚类的目标是在保持簇数目不变的情况下提高簇的质量。

三、二分K-均值算法
为了克服K-均值算法收敛于局部最小值的问题而引申出二分K-均值算法。该算法首先将所有点作为一个簇,然后将该簇一分为二。之后选择一个簇继续进行划分,选择哪一个簇进行划分取决于对其进行划分是否可以最大程度降低SSE的值,上述基于SSE的划分不断重复,知道得到用户指定的簇数目为止。

二分K-均值算法的伪代码形式如下:
将所有点看成一个簇
当簇数目小于K时
对于每一个簇
计算总误差
在给定的簇上面进行尝试性的K-均值聚类(k=2)
计算将该簇一分为二之后的总误差
选择使得误差最小的那个簇进行实际划分操作

二分K-均值聚类算法:

def biKmeans(dataSet,k,disMeas=distEclid):
    m = shape(dataSet)[0]
    #存储每个样本的聚类标号和距离中心的方差
    clusterAssment = mat(zeros((m,2)))
    #将整个数据集看作一个簇,计算其簇中心
    centroid0 = mean(dataSet,axis=0).tolist()[0]
    #簇中心列表
    cenList = [centroid0]
    #遍历每个样本,计算样本与中心的距离,初始化矩阵
    for j in range(m):
        clusterAssment[j,1] = disMeas(mat(centroid0),dataSet[j,:])**2
    #对每个簇进行划分,比较获得最好的划分结果,迭代直到划分到k个簇为止
    while(len(cenList)<k):
        #最小误差和
        lowestSSE = inf
        #对每个簇进行遍历
        for i in range(len(cenList)):
            #获取属于该类的样本集合
            ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:]
            #对这个簇划分为两个簇
            centroidMat,splitClustASS = kMeans(ptsInCurrCluster,2,disMeas)
            #计算划分的两个簇的所有样本误差和
            sseSplit = sum(splitClustASS[:,1])
            #计算除此次划分集合之外的簇的方差和
            ssNotSplit = sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1])
            # print("sseSplit:",sseSplit)
            # print("sseNotSplit:",ssNotSplit)
            #比较该尝试性划分是否使误差和达到最小,如果是,进行实际划分,更新相应数据
            if (sseSplit+ssNotSplit) < lowestSSE:
                bestCentToSplit = i
                bestNewCents = centroidMat
                bestClustAss = splitClustASS.copy()
                lowestSSE = sseSplit + ssNotSplit
        #对簇进行实际划分,更新划分的簇索引
        bestClustAss[nonzero(bestClustAss[:,0].A==1)[0],0] = len(cenList)
        bestClustAss[nonzero(bestClustAss[:,0].A==0)[0],0] = bestCentToSplit
        # print("the bestcentTOSplit is :",bestCentToSplit)
        # print("the len of bestClustAss:",len(bestClustAss))
        #更新簇中心向量,及更新每个样本到中的方差
        cenList[bestCentToSplit] = bestNewCents[0,:].tolist()[0]
        cenList.append(bestNewCents[1,:].tolist()[0])
        clusterAssment[nonzero(clusterAssment[:,0].A==bestCentToSplit)[0],:] = bestClustAss
    return mat(cenList),clusterAssment

注:一旦决定了要划分的簇,接下来就要实际执行划分操作。划分操作实际很容易,只需要将要划分的簇中的所有点的簇的分配结果进行修改即可。当使用kMeans()函数并且指定簇数为2时,会得到两个编号分别为0和1的结果簇。需要将这些簇编号修改为划分簇及新加簇的编号,该过程通过两个数组过滤器来完成。

示例:对地图上的点进行聚类:
对文本中的地点进行聚类,首先需要将这些地点转换为经纬度形式,这里需要使用雅虎服务将地址转化为经纬度。利用Yahoo!PlaceFinder API对地址进行转换:

#示例:对地图上的点进行聚类
#利用API将地址转化为维度和经度,这里没有进行注册,所以函数无法调用
def geoGrab(stAddress, city):
    import urllib
    import urllib.request
    import urllib.parse
    import json
    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
    c = urllib.request.urlopen(yahooApi)
    return json.load(c.read())

#对字典数据进行封装
def massPlaceFind(fileName):
    from time import sleep
    fw = open('place.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()

现在我们有了地址经纬度文件,就可以对这些地点进行聚类了,此处使用球面余玄定理来计算两个经纬度之间的距离。

球面距离计算及簇绘图函数:

#对地理坐标进行聚类
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
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, disMeas=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()

注:以上函数测试均在main函数中进行,main函数如下:

if __name__ == "__main__":
    # dataList = loadData('testSet.txt')
    # dataMat = np.mat(dataList)
    # # min1 = min(dataMat[:,0])
    # # min2 = min(dataMat[:,1])
    # # max1 = max(dataMat[:,0])
    # # max2 = max(dataMat[:,1])
    # # print("min1:", min1)
    # # print("max1:", max1)
    # # print("min2:", min2)
    # # print("max2:", max2)
    # # centords = randCent(dataMat,2)
    # # print("centorsd:",centords)
    # # dis = distEclid(dataMat[0],dataMat[1])
    # # print("dis:",dis)
    # myCentroids,clustAssing = kMeans(dataMat,4)
    # print("myCentroids:",myCentroids)
    # print("clustAssing:",clustAssing)
    # dataMat2 = mat(loadData('testSet2.txt'))
    # cenList,myNewAssments = biKmeans(dataMat2,3)
    # print("cenList:",cenList)
    # geoResult = geoGrab('1 VA Center','Augusta, ME')
    # print("geoResult:",geoResult)
    clusterClubs(5)

结果:
机器学习实战—K-均值聚类算法

总结:聚类算法时一种无监督的学习方法。所谓无监督学习是指实现不知道要寻找的内容,即没有目标变量。聚类将数据点归到多个簇中,其中相似的数据点处于同一簇,而不相似的数据点处于不同簇中。

k-均值算法以及变形的K-均值算法并非仅有的聚类算法,另外称为层次聚类的方法也被广泛使用。