机器学习——KMeans之二分K均值算法(机器学习实战)

750 阅读5分钟

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

PS:本文只用一个例子实现二分K均值算法,无原理分析,不适合初学者观看。 KMeans聚类算法链接机器学习——KMeans聚类(机器学习实战) #掘金文章# juejin.cn/post/702894…

K均值算法

K均值算法将所有点作为一个簇,然后将簇一分为二。之后选择其中一个簇继续进行划分,选择哪一个簇进行划分取决于对其划分是否可以最大程度降低SSE的值。上述基于SSE的划分过程不断重复,直到得到用户指定的簇数目为止。PS:SSE(误差平方和)是度量聚类效果的指标,SSE值越小表示数据点越接近他们的质心,聚类效果越好。 伪代码形式如下:

将所有点看成一个簇

当簇数目小于k时

对于每一个簇
    计算总误差
    在给定的簇上面进行K均值聚类(k=2)
    计算将该簇一分为二之后的总误差
选择使得误差最小的那个簇进行划分操作

另一做法是选择SSE最大的簇进行划分,直到簇数目达到用户指定的数目为止。下面以这个方法来实现算法。

实战

数据集来源,机器学习西瓜书 watermelon4.0。

第一步:计算欧式距离

from numpy import *
import xlrd
import matplotlib.pyplot as plt

def distEclud(vector1, vector2):
    '''
    :param vector1: 第j个均值向量
    :param vector2: 第i个样本
    :return: 距离值
    '''
    return sqrt(sum(power(vector2 - vector1, 2)))

第二步:构建聚簇中心

def randCent(dataSet, k):
    n = shape(dataSet)[1]
    centroids = mat(zeros((k, n)))
    for j in range(n):
        minJ = min(dataSet[:, j])
        maxJ = max(dataSet[:, j])
        rangeJ = float(maxJ - minJ)
        centroids[:, j] = minJ + rangeJ * random.rand(k, 1)
    return centroids

第三步: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循环会不停的对簇进行划分,直到得到想要的簇数目为止
    while (len(centList) < k):
        lowestSSE = inf  # python中inf表示正无穷
        # 遍历簇列表centList中的每一个簇
        for i in range(len(centList)):
            # -----尝试划分每一簇----
            # 对每个簇,将该簇中的所有点看成一个小的数据集ptsInCurrCluster
            ptsInCurrCluster = dataSet[nonzero(clusterAssment[:, 0].A == i)[0], :]
            # 将ptsInCurrCluster输入到函数KMeans()中进行处理(k=2)。K-均值算法会生成两个质心(簇),同时给出每个簇的误差值
            centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas)

            # 划分后的样本误差之和
            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
            # ----更新簇的分配结果----
        # 将划分簇中所有点的簇分配结果进行修改,当使用KMeans()函数并且指定簇数为2时,会得到两个编号分别为0和1的结果簇
        # 需要将这些簇编号修改为划分簇及新加簇的编号,该过程可以通过两个数组过滤器来完成。
        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中
        centList[bestCentToSplit] = bestNewCents[0, :].tolist()[0]
        centList.append(bestNewCents[1, :].tolist()[0])
        # 更新SSE的值(sum of squared errors)
        clusterAssment[nonzero(clusterAssment[:, 0].A == bestCentToSplit)[0], :] = bestClustAss
    return mat(centList), clusterAssment
# k-means 聚类算法
def kMeans(dataSet, k, distMeans=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 = distMeans(centroids[j, :], dataSet[i, :])
                if distJI < minDist:
                    minDist = distJI;
                    minIndex = j
            if clusterAssment[i, 0] != minIndex: clusterChanged = True;
            clusterAssment[i, :] = minIndex, minDist ** 2
        print(centroids)
        for cent in range(k):
            ptsInClust = dataSet[nonzero(clusterAssment[:, 0].A == cent)[0]]  # 去第一列等于cent的所有列
            centroids[cent, :] = mean(ptsInClust, axis=0)
    return centroids, clusterAssment

最后一步:整合所有函数,输入数据集,以Matplotlib显现结果

# show your cluster only available with 2-D data
def showCluster(dataSet, k, centroids, clusterAssment):
    numSamples, dim = dataSet.shape
    if dim != 2:
        print("Sorry! I can not draw because the dimension of your data is not 2!")
        return 1

    mark = ['or', 'ob', 'og', 'ok', '^r', '+r', 'sr', 'dr', '<r', 'pr']
    if k > len(mark):
        print("Sorry! Your k is too large! please contact Zouxy")
        return 1

        # draw all samples
    for i in range(numSamples):
        markIndex = int(clusterAssment[i, 0])
        plt.plot(dataSet[i, 0], dataSet[i, 1], mark[markIndex])

    mark = ['Dr', 'Db', 'Dg', 'Dk', '^b', '+b', 'sb', 'db', '<b', 'pb']
    # draw the centroids
    for i in range(k):
        plt.plot(centroids[i, 0], centroids[i, 1], mark[i], markersize=12)
    plt.show()


def main():
    dataSet = []
    data = xlrd.open_workbook('F:\watermelon4.0.xlsx')
    table = data.sheets()[0]
    for line in range(0, table.nrows):
        lineArr = table.row_values(line)
        dataSet.append([float(lineArr[0]), float(lineArr[1])])
    dataSet = mat(dataSet)
    k = 3
    centroids, clusterAssment = biKmeans(dataSet, k)
    showCluster(dataSet, k, centroids, clusterAssment)


if __name__ == '__main__':
    main()

解释

biKMeans函数中首先创建一个矩阵来存储数据及中每个点的簇分配结果及平方误差,然后计算整个数据集的质心,并使用一个列表来保留所有的质心。得到上述质心之后,可以遍历数据集中所有点来计算每个店到质心的误差值。

接下来程序进入循环,该循环对簇不停划分,直到得到想要的簇数目为止。可以通过考察列表中的值来获得当前簇的数目。然后遍历所有簇来决定最佳的簇进行画出。为此需要比较划分前后的SSE。一开始将最小SSE置设为无穷大,然后遍历簇列表centlist中的每一个簇。对每个簇,将该簇的所有点看成一个小的数据集ptsInCurrCluster。将ptsInCurrCluster输入到函数KMeans中进行处理(k=2)。k均值算法会生成两个质心,同时给出每个簇的误差值。这些误差与剩余数据集的误差之和作为本次划分的误差。如果该划分的SSE值最小,则本次划分被保存。一旦觉得了要划分的簇,接下来就要实际执行划分操作。只需要将要划分的簇中的所有点的簇分配结果进行修改即可。当使用KMeans函数并制定簇数为2是,会得到两个编号分别为0和1的结果簇。需要将这些簇编号修改为划分簇及新加粗的编号,该过程可以通过两个数组过滤器来完成。最后,新的出分配结果被更新,新的质心会被添加到centlist。

结果

image.png

QQ浏览器截图20211111224925.png 每次分割时,都会输出要分割的簇的误差。

总结

K均值聚类算法以k个随机质心开始。算法会计算每个点到质心得距离。每个点会被分配到距其最近的簇质心,然后紧接着基于新分配到簇的点更新簇质心。以上过程重复数次,直到簇质心不再改变。这个算法容易受到初始簇质心得影响。为了获得更好的聚类效果,二分K均值算法更优。二分K均值算法首先将所有点为一个簇,然后使用K均值算法(k=2)对其划分。下一次迭代时,选择有最大误差的簇进行划分。该过程重复直到K个簇创建为止。二分k均值的聚类效果要好于k均值算法。但无论是k均值算法还是二分k均值算法,都需要提前确定K的值,而这个值是不可能确定到最好的,所以k均值和二分k均值算法还是存在这个缺陷,现在常用的解决办法是用层次聚类(Hierarchical Clustering),或者借鉴下LDA中的话题聚类分析,感兴趣的用户可自行了解。