机器学习实战(九)K-means(K-均值)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhq9695/article/details/83501816

目录

0. 前言

1. K-means

2. K-means的后处理

3. 二分K-means

4. K 的选择

5. 实战案例

5.1. 原始K-means

5.2. 二分K-means


学习完机器学习实战的K-means,简单的做个笔记。文中部分描述属于个人消化后的理解,仅供参考。

本篇综合了先前的文章,如有不理解,可参考:

吴恩达机器学习(十一)K-means

所有代码和数据可以访问 我的 github

如果这篇文章对你有一点小小的帮助,请给个关注喔~我会非常开心的~

0. 前言

算法有监督学习和无监督学习之分:

  • 监督学习(supervised learning):训练集合已全部标注所属类别或者目标结果
  • 无监督学习(unsupervised learning):训练集合未进行标注,无法预先知道每个数据的所属类别或者目标结果

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

聚类可以采用多种不同的方法计算一个簇中的相似程度。K-means(K-均值)采用的就是数据与簇中心的欧式距离度量相似程度,簇中心由一个簇中数据的均值构成。

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

1. K-means

K-means,K 为用户定义的簇的个数,每一个簇通过其中心(质心)表示。

K-means的算法可表示为:

  1. 随机创建 K 个簇的质心
  2. 簇分配:将数据集中每一个点分配到距离最近的簇中
  3. 移动聚类中心:重新计算每个簇的中心,更新为该簇所有点的平均值
  4. 继续进行簇分配和移动聚类中心,直到没有数据点的分配结果发生改变为止

2. K-means的后处理

在K-means中,采用误差平方和(SSE,sum of squared error)度量聚类效果:

\sum_{i=1}^{m}\left\|x^{(i)}-\mu_{c^{(i)}}\right\|^2

其中,\mu_{c^{(i)}} 表示当前样本所属的簇的质心的向量。

K-means聚类算法有时候会陷入局部最优值,如下图所示(图源:吴恩达机器学习):

为避免局部最优,有几种方法:

  1. 可在上述算法外再嵌套一层循环,每次确定簇中心之后计算SSE,多次迭代之后,选择SSE最小的一组结果
  2. 将具有最大SSE的簇运用K-means划分为两个簇,然后再合并最近的质心,或者合并两个使SSE增幅最小的质心

注:聚类的目标是在保持簇数目不变的情况下提高簇的质量。

3. 二分K-means

为解决K-means算法的局部最优问题,可采用二分K-means。

二分K-means的思想是,首先只有一个簇,所有的数据属于当前簇,然后运用K-means将簇分为两个簇,再选择其中一个簇,对其进行K-means将其分为两个,直到达到用户指定的簇数目。

在选择簇的思想中,主要有两种:

  1. 选择划分后可以最大程度降低SSE的簇
  2. 选择当前SSE最大的簇进行划分

4. K 的选择

簇的数量的选择,通常有两种方法,均要求 K< m :

  • 人工选择:根据需求或者已知的知识,进行人工选择簇的数量
  • 肘部法则:如下图所示(图源:吴恩达机器学习),尝试不同的 K ,选择变化率明显变缓的“肘部点”

5. 实战案例

以下将展示书中案例的代码段,所有代码和数据可以在github中下载:

5.1. 原始K-means

# coding:utf-8
from numpy import *

"""
原始k-means
"""


# 加载数据集
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)))


# 随机生成k个质心
def randCent(dataSet, k):
    n = shape(dataSet)[1]
    # K个质心的向量
    centroids = mat(zeros((k, n)))
    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-means算法
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
    m = shape(dataSet)[0]
    # 每个数据对应的簇和距离
    clusterAssment = mat(zeros((m, 2)))
    # 随机生成k个质心
    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
            # 当前数据的质心改变
            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]]
            # 按列求平均,重新计算质心位置
            centroids[cent, :] = mean(ptsInClust, axis=0)
    return centroids, clusterAssment


if __name__ == '__main__':
    datMat = mat(loadDataSet('testSet.txt'))
    myCentroids, clusterAssing = kMeans(datMat, 4)
    print(myCentroids)

5.2. 二分K-means

# coding:utf-8
from numpy import *
import matplotlib
import matplotlib.pyplot as plt

"""
二分k-means
"""


# 向量的欧式距离
def distEclud(vecA, vecB):
    return sqrt(sum(power(vecA - vecB, 2)))


# 随机生成k个质心
def randCent(dataSet, k):
    n = shape(dataSet)[1]
    # K个质心的向量
    centroids = mat(zeros((k, n)))
    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-means算法
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
    m = shape(dataSet)[0]
    # 每个数据对应的簇和距离
    clusterAssment = mat(zeros((m, 2)))
    # 随机生成k个质心
    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
            # 当前数据的质心改变
            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]]
            # 按列求平均,重新计算质心位置
            centroids[cent, :] = mean(ptsInClust, axis=0)
    return centroids, clusterAssment


# 二分k-means
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
    # 循环执行二分,直到簇的数量达到k
    while (len(centList) < k):
        lowestSSE = inf
        # 遍历每一个簇
        for i in range(len(centList)):
            # 获取属于当前簇的数据
            ptsInCurrCluster = dataSet[nonzero(clusterAssment[:, 0].A == i)[0], :]
            # 进行2-means
            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
        # 将划分后,数据属于新簇的簇索引
        # 由0, 1改为bestCentToSplit和len(centList)
        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  # reassign new clusters, and SSE
    return mat(centList), clusterAssment


# 球面余弦定理
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


# 聚类,画图
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)
    # 二分k-means
    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=[])
    # 创建底图
    # 每一个axes都是一个独立的图层
    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()


if __name__ == '__main__':
    clusterClubs(5)

如果这篇文章对你有一点小小的帮助,请给个关注喔~我会非常开心的~

猜你喜欢

转载自blog.csdn.net/zhq9695/article/details/83501816