机器学习实战---读书笔记: 第10章 二分k均值聚类---2

#!/usr/bin/env python
# encoding: utf-8

'''

读书笔记之--<<机器学习实战>>--第10章 二分K-均值算法

关键:
1 k-均值聚类中的k是事先定义的,如何知道k是否正确。
包含簇分配结果的矩阵中保存着每个点的误差,即点到簇质心的距离平方值。
利用误差来评价聚类质量。

2 度量聚类结果指标:SSE(Sum of Squared Error,误差平方和)。误差平方和越小,
表示数据点越接近它们的质心。
降低SSE的方法:增大簇的个数
聚类目标:保持簇数目不变的情况下提高簇的质量
改进: 对生成的簇进行处理,
1: 将具有最大SSE值的簇划分为两个簇,可以将具有最大SSE值的簇划分成两个簇。
    将最大簇包含的点过滤出来并在这额点上运行K-均值算法,其中的k设为2
2: 为了保持簇总数不变,可以将某两个簇合并。
    具体:合并最近的质心,或者合并两个使得SSE增幅最小的质心。

3 二分K-均值算法
为了克服K-均值算法收敛于局部最小值问题,采用二分K-均值算法(bisecting K-means)。
思想:
将所有点作为1个簇,然后将该簇一分为二。之后选择其中一个簇继续进行划分,
选择哪一个簇进行划分取决于对其划分是否可以最大程度降低SSE的值。
上述基于SSE的划分过程不断重复,直到河道用户指定的簇数目为止。
算法:
将所有点看成一个簇
当簇数目小于k时
    对于每一个簇
        计算总误差
        在给定的簇平面上进行K-均值聚类(k=2)
        计算将该簇一分为二之后的总误差
    选择使得误差最小的那个簇进行划分操作

另一种做法是选择SSE最大的簇进行划分,知道簇数目达到用户指定的数目为止。
'''
import os

from matplotlib import pyplot as plt
from numpy import *

def shapeDemo():
    result = shape([ [1,1], [2,2], [3,3] ])
    print result
    return result


def process2():
    result = shapeDemo()
    answer = result[0]
    print answer


'''
作用: 加载数据集
'''
def loadDataSet(fileName):
    dataMat = []
    if not fileName or not os.path.exists(fileName):
        return dataMat
    with open(fileName, "r") as fr:
        for line in fr.readlines():
            if not line:
                continue
            tempData = line.strip().split('\t')
            data = map(float, tempData)
            dataMat.append(data)
    return dataMat


def distEclud(vecA, vecB):
    result = sqrt(sum(power(vecA - vecB, 2)))
    return result


def randCent(dataSet, k):
    columnNum = shape(dataSet)[1]
    cores = mat(zeros((k, columnNum)))
    for j in range(columnNum):
        minValue = min(dataSet[:, j])
        maxValue = max(dataSet[:, j])
        print "In column {column}, min value: {minValue}, max value: {maxValue} ".format(
            column=j, minValue=minValue, maxValue=maxValue
        )
        diff = float(maxValue - minValue)
        cores[:, j] = minValue + diff * random.rand(k, 1)
    return cores


def kMeans(dataSet, k, distMeasure=distEclud, createCent=randCent):
    rowNum, columnNum = shape(dataSet)
    clusterResult = mat(zeros((rowNum, 2)))
    cores = createCent(dataSet, k)
    isResultChanged = True
    while isResultChanged:
        isResultChanged = False
        # 计算每个点到每个质心的距离,更新簇分配结果
        for i in range(rowNum):
            minDist = inf
            minIndex = -1
            for j in range(k):
                dist = distMeasure(dataSet[i, :], cores[j, :])
                if dist < minDist:
                    minDist = dist
                    minIndex = j
            if clusterResult[i, 0] != minIndex:
                isResultChanged = True
            clusterResult[i, :] = minIndex, minDist ** 2
        # 更新质心
        for centIndex in range(k):
            filterResult = clusterResult[:, 0].A==centIndex
            indexes = nonzero(filterResult)
            nums = indexes[0]
            pointsInCluster = dataSet[ nums ]
            result = mean(pointsInCluster, axis=0)
            cores[centIndex, :] = result
    return cores, clusterResult

'''
二分K-均值算法:
步骤1:  将数据看成一个簇,求簇心,
       设置簇分配结果中每一行的(v1,v2)分别为对应数据集中这个点最近的(该点属于的簇编号,该点到簇心的距离),
       设置质心列表centList添加当前的簇心
步骤2: 不断对簇划分,直到簇的个数达到指定个数则表示簇划分完成;否则进入步骤3
步骤3: 设置minSSE值为无限大,遍历每个簇的标签,
        3.1 从簇分配结果的每一行的第1个元素,即该点属于的簇编号和当前待划分簇编号一致的点的的下标列表
        3.2 根据3.1得到的与待划分簇编号一致的下标列表到数据集中找到这些被划分为该簇编号的所有数据点
        3.3 对属于当前待划分簇的所有数据点调用K-Means算法划分为个数为2的簇,
            得到质心列表partionCents和划分后的簇分配结果partionClusterResult
        3.4 对3.3得到的簇分配结果partionClusterResult计算误差平方和,记为partionSSE
        3.5 对不属于当前待划分簇的所有点计算误差平方和,记为nonPartionSSE
        3.6 如果 partionSSE + nonPartionSSE 小于 minSSE
            3.6.1 设置最佳划分簇下标bestCentToSplit为当前待划分的簇下标
            3.6.2 设置最佳划分簇的质心列表为bestNewCents
            3.6.3 设置最佳划分簇的簇分配结果为bestClusterResult
            3.6.4 更新minSSE为 partionSSE 和 nonPartionSSE的和
        3.7 设置最佳划分簇的簇分配结果中:新划分的编号为0的簇对应所有点属于的簇编号为bestCentToSplit;
            设置最佳划分簇的簇分配结果中:新划分的编号为1的簇对应质心列表的长度len(centList)
        3.8 设置质心列表centList的bestCentToSplit这个下标对应的质心为最佳划分簇质心列表的第0个元素,即bestNewCents[0, :]
            设置质心列表centLis添加新的质心为最佳划分簇质心列表的第1个元素,即bestNewCents[1, :]
        3.9 设置原来簇分配结果中对应最佳划分簇下标的所有点的簇分配结果为 最佳划分簇的簇分配结果
步骤4: 返回最终的质心列表和簇分配结果

总结:
二分K-分类算法的主体就是:
1 遍历每个簇,对当前簇进行簇划分,
2 计算划分后的sse值和除当前簇其他点的sse值,从而确定出最佳划分簇的下标,以及得到最佳簇分配结果,
3 然后对最佳簇分配结果中的簇下标重新修改,并更新原来最佳划分簇下标对应的所有点对应的簇下标。

'''
def biKmeans(dataSet, k, distMeasure=distEclud):
    rowNum = shape(dataSet)[0]
    clusterResult = mat(zeros((rowNum , 2)))
    cent0 = mean(dataSet, axis=0).tolist()[0]
    print cent0
    # 设置质心列表添加簇个数为1的质心,并设置簇分配结果中每个点和簇心的距离
    centList = [cent0]
    for i in range(rowNum):
        # 注意需要将cent0转换为矩阵
        clusterResult[i, 1] = distMeasure(dataSet[i, :], mat(cent0) ) ** 2
    while len(centList) < k:
        minSSE = inf
        # 对每个簇进行簇划分,计算sse误差平方和
        for i in range(len(centList)):
            # 对当前簇进行kMeans划分
            # pointsInCluster = dataSet[nonzero(clusterResult[:, 0].A == i )]
            # 注意 nonzero(clusterResult[:, 0].A == i ) 后面需要加上[0] 表示求取的是x下标列表,否则不能找出对应的点
            pointsInCluster = dataSet[nonzero(clusterResult[:, 0].A == i )[0], :]
            partionCents, partionClusterResult = kMeans(pointsInCluster, 2)
            partionSSE = sum(partionClusterResult[:, 1])
            # nonPartionSSE = sum( dataSet[nonzero(clusterResult[:, 0].A != i)[0], 1] )
            # 注意这里是求取的是簇分配结果中的sse值,而不是dataSet数据集中的
            nonPartionSSE = sum( clusterResult[nonzero(clusterResult[:, 0].A != i)[0], 1] )
            print "partinSSE: {partionSSE}, nonPartionSSE: {nonPartionSSE}".format(
                partionSSE=partionSSE, nonPartionSSE=nonPartionSSE
            )
            # 计算误差平方和
            totalSSE = partionSSE + nonPartionSSE
            if totalSSE < minSSE:
                minSSE = totalSSE
                bestCentToSplit = i
                bestClusterResult = partionClusterResult.copy()
                bestCents = partionCents
        # 注意,这个更新簇分配结果的过程是在for循环外面的
        # 更新最佳簇分配结果中的簇下标
        # bestClusterResult[nonzero(bestClusterResult[:, 0].A == 0), 0] = bestCentToSplit
        # bestClusterResult[nonzero(bestClusterResult[:, 0].A == 1), 1] = len(centList)
        bestClusterResult[nonzero(bestClusterResult[:, 0].A == 0)[0], 0] = bestCentToSplit
        bestClusterResult[nonzero(bestClusterResult[:, 0].A == 1)[0], 0] = len(centList)
        # 更新质心列表,添加新的簇编号
        centList[bestCentToSplit] = bestCents[0, :]
        centList.append(bestCents[1, :])
        # 更新总的簇分配结果中粗下标为最佳划分簇下标对应的所有点的簇分配结果
        # clusterResult[nonzero(dataSet[:, 0].A == bestCentToSplit)] = bestClusterResult
        clusterResult[nonzero(clusterResult[:, 0].A == bestCentToSplit)[0], :] = bestClusterResult
    return centList, clusterResult


def showResult(clusterResult, dataSet):
    #画出密度聚类后的结果
    # mark = ['or', 'ob', 'og', 'ok', '^r', '+r', 'sr', 'dr', '<r', 'pr']
    mark = ['ob', 'og', '^r', 'ok', '+r', 'sr', 'dr', '<r', 'pr']
    num = len(mark)
    for i, points in enumerate(clusterResult):
        arr = points.A
        label = int(arr[0][0])
        dataPoint = dataSet[i].A
        x = dataPoint[0][0]
        y = dataPoint[0][1]
        # color = mark[label % num]
        color = mark[label]
        plt.plot(x, y, color)
    plt.show()


def process():
    dataMat = mat(loadDataSet('testSet2.txt'))
    centList, clusterResult = biKmeans(dataMat, 3)
    print "簇分配结果: {clusterResult}".format(clusterResult=clusterResult)
    print "质心结果: {cores}".format(cores=centList)
    showResult(clusterResult, dataMat)

if __name__ == "__main__":
    process()

猜你喜欢

转载自blog.csdn.net/qingyuanluofeng/article/details/86742545