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

一、K-means聚类介绍

聚类是一种无监督的学习,它将相似的对象归到同一个簇中。它有点像全自动分类 。聚类方法几乎可以应用于所有对象,簇内的对象越相似,聚类的效果越好。之所以称之为K-均值是因为它可以发现k个不同的簇,且每个簇的中心采用簇中所含值的均值计算而成。

假定有一些数据,现在将相似数据归到一起,簇识别会告诉我们这些簇到底都是些什么。聚类与分类的最大不同在于,分类的目标事先已知,而聚类则不一样。因为其产生的结果与分类相同,而只是类别没有预先定义,聚类有时也被称为无监督分类(unsupervisedclassification)。

K-均值算法的工作流程是这样的。首先,随机确定k个初始点作为质心。然后将数据集中的每个点分配到一个簇中,具体来讲,为每个点找距其最近的质心,并将其分配给该质心所对应的簇。这一步完成之后,每个簇的质心更新为该簇所有点的平均值。算法伪代码如下:

上面提到“最近”质心的说法,意味着需要进行某种距离计算。读者可以使用所喜欢的任意距离度量方法。数据集上K-均值算法的性能会受到所选距离计算方法的影响。

二、算法实现

贴上全文全部代码:

from numpy import *
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans


def load_data_set(file_name):
    """
        Function:
            加载数据
        Parameters:
            file_name - 文件名
        Returns:
            data_mat - 数据矩阵
        Modify:
            2018-11-15
    """
    data_mat = []
    fr = open(file_name)
    for line in fr.readlines():
        cur_line = line.strip().split('\t')
        flt_line = list(map(float, cur_line))
        data_mat.append(flt_line)
    return data_mat


def dist_eclud(vec_a, vec_b):
    """
        Function:
            计算两个向量的欧式距离
        Parameters:
            vec_a - 向量a
            vec_b - 向量b
        Returns:
            欧式距离
        Modify:
            2018-11-15
    """
    return sqrt(sum(power(vec_a - vec_b, 2)))


def rand_cent(data_set, k):
    """
        Function:
            构建一个包含k个随机质心的集合
        Parameters:
            data_set - 数据集
            k - 质心个数
        Returns:
            centroids - 随机质心集合
        Modify:
            2018-11-15
    """
    n = shape(data_set)[1]
    # 初始化为一个(k,n)的矩阵
    centroids = mat(zeros((k, n)))
    # 遍历数据集的每一维度,每维先后选出k个值构成质心
    for j in range(n):
        # 得到该列数据的最小值
        min_j = min(data_set[:, j])
        # 得到该列数据的范围(最大值-最小值)
        range_j = float(max(data_set[:, j] - min_j))
        # rand函数根据给定维度生成[0, 1)之间的数据
        # k个质心向量的第j维数据值随机为位于(最小值,最大值)内的某一值
        centroids[:, j] = min_j + range_j * random.rand(k, 1)
    return centroids


def k_means(data_set, k, dist_meas=dist_eclud, create_cent=rand_cent):
    """
        Function:
            K-均值算法
        Parameters:
            data_set - 数据集
            k - 指定的k个类
            dist_meas - 距离计算方法,默认欧氏距离dist_eclud()
            create_cent - 获得k个质心的方法,默认随机获取rand_cent()
        Returns:
            centroids - k个聚类结果
            cluster_assment - 聚类误差
        Modify:
            2018-11-15
    """
    m = shape(data_set)[0]
    # 初始化一个(m,2)的矩阵
    cluster_assment = mat(zeros((m, 2)))
    # 创建初始的k个质心向量
    centroids = create_cent(data_set, k)
    # 聚类结果是否发生变化的布尔类型
    cluster_changed = True
    # 只要聚类结果一直发生变化,就一直执行聚类算法,直至所有数据点聚类结果不变化
    while cluster_changed:
        cluster_changed = False
        # 遍历数据集每一个样本向量
        for i in range(m):
            # 初始化最小距离最正无穷;最小距离对应索引为-1
            min_dist = inf
            min_index = -1
            # 循环k个类的质心
            for j in range(k):
                # 计算数据点到质心的欧氏距离
                dist_j_i = dist_meas(centroids[j, :], data_set[i, :])
                # 如果距离小于当前最小距离
                if dist_j_i < min_dist:
                    # 当前距离定为当前最小距离
                    min_dist = dist_j_i
                    # 最小距离对应索引对应为j(第j个类)
                    min_index = j
            # 当前聚类结果中第i个样本的聚类结果发生变化:布尔类型置为true,继续聚类算法
            if cluster_assment[i, 0] != min_index:
                cluster_changed = True
            # 更新当前变化样本的聚类结果和平方误差
            cluster_assment[i, :] = min_index, min_dist ** 2
        # print(centroids)
        # 遍历每一个质心
        for cent in range(k):
            # 将数据集中所有属于当前质心类的样本通过条件过滤筛选出来
            pts_in_clust = data_set[nonzero(cluster_assment[:, 0].A == cent)[0]]
            # 计算这些数据的均值(axis=0:求列的均值),作为该类质心向量
            centroids[cent, :] = mean(pts_in_clust, axis=0)
    return centroids, cluster_assment


def plot_cluster(data_set, k, centroids, cluster_assment):
    """
        Function:
            绘制聚类结果
        Parameters:
            data_set - 数据集
            k - 指定的k个类
            centroids - 质心集合
            cluster_assment - 聚类误差
        Returns:
            无
        Modify:
            2018-11-16
    """
    figure = plt.figure()
    ax = figure.add_subplot(111)
    ax.scatter(centroids[:, 0].tolist(), centroids[:, 1].tolist(), marker='+', s=60, c='black')
    markers = ['o', 's', 'v', '*'];
    colors = ['blue', 'green', 'yellow', 'red']
    for i in range(k):
        # data_class = data_set[nonzero(cluster_assment[:, 0].A == i)[0]]
        # KMeans().fit().labels_返回的是一维numpy.ndarray,绘制KMeans是用下面这行
        data_class = data_set[cluster_assment == i]
        ax.scatter(data_class[:, 0].tolist(), data_class[:, 1].tolist(), marker=markers[i], c=colors[i], s=20,
                   alpha=0.5)
    plt.show()


def bi_k_means(data_set, k, dist_meas=dist_eclud):
    """
        Function:
            K-均值算法
        Parameters:
            data_set - 数据集
            k - 指定的k个类
            dist_meas - 距离计算方法,默认欧氏距离dist_eclud()
        Returns:
            centroids - k个聚类结果
            cluster_assment - 聚类误差
        Modify:
            2018-11-24
    """
    m = shape(data_set)[0]
    # 将所有的点看成是一个簇
    # cluster_assment存储(所属的中心编号,距中心的距离)的列表
    cluster_assment = mat(zeros((m, 2)))
    # 获取数据集每一列数据的均值,组成一个长为列数的列表
    centroids_0 = mean(data_set, axis=0).tolist()[0]
    # cent_list存储聚类中心
    cent_list = [centroids_0]
    # 计算当前聚为一类时各个数据点距离质心的平方距离
    for j in range(m):
        cluster_assment[j, 1] = dist_meas(mat(centroids_0), data_set[j, :]) ** 2
    # 当簇小于数目k时
    while (len(cent_list) < k):
        lowest_ees = inf
        # 遍历当前每个聚类
        for i in range(len(cent_list)):
            # 通过数组过滤筛选出属于第i类的数据集合
            # nonzero函数是numpy中用于得到数组array中非零元素的位置(数组索引)的函数
            # matrix矩阵名.A矩阵转化为array数组类型
            pts_in_curr_cluster = data_set[nonzero(cluster_assment[:, 0].A == i)[0], :]
            # 对该类利用二分k-均值算法进行划分,返回划分后结果,及误差
            centroid_mat, split_cluster_ass = k_means(pts_in_curr_cluster, 2, dist_meas)
            # 计算该类划分后两个类的误差平方和
            sse_split = sum(split_cluster_ass[:, 1])
            # 计算数据集中不属于该类的数据的误差平方和
            sse_not_split = sum(cluster_assment[nonzero(cluster_assment[:, 0].A != i)[0], 1])
            # 打印这两项误差值
            print('sse_split, and sse_not_split:', sse_split, sse_not_split)
            # 选择使得误差最小的那个簇进行划分
            if (sse_split + sse_not_split) < lowest_ees:
                # 第i类作为本次划分类
                best_cent_to_split = i
                # 第i类划分后得到的两个质心向量
                best_new_cents = centroid_mat
                # 复制第i类中数据点的聚类结果即误差值
                best_clust_ass = split_cluster_ass.copy()
                # 将划分第i类后的总误差作为当前最小误差
                lowest_ees = sse_split + sse_not_split
        # 数组过滤筛选出本次2-均值聚类划分后类编号为1数据点,将这些数据点类编号变为当前类个数+1,作为新的一个聚类
        best_clust_ass[nonzero(best_clust_ass[:, 0].A == 1)[0], 0] = len(cent_list)
        # 将划分数据集中类编号为0的数据点的类编号仍置为被划分的类编号,使类编号连续不出现空缺
        best_clust_ass[nonzero(best_clust_ass[:, 0].A == 0)[0], 0] = best_cent_to_split
        print('the best_cent_to_split is:', best_cent_to_split)
        print('the len of best_clust_ass is:', len(best_clust_ass))
        # 更新质心列表中的变化后的质心向量
        cent_list[best_cent_to_split] = best_new_cents[0, :].tolist()[0]
        # 添加新的类的质心向量
        cent_list.append(best_new_cents[1, :].tolist()[0])
        # 更新cluster_assment列表中参与2-均值聚类数据点变化后的分类编号,及数据该类的误差平方
        cluster_assment[nonzero(cluster_assment[:, 0].A == best_cent_to_split)[0], :] = best_clust_ass
    return mat(cent_list), cluster_assment


def dist_s_l_c(vec_a, vec_b):
    """
        Function:
            计算地球表面两点之间的距离
        Parameters:
            vec_a - 数据集
            vec_b - 指定的k个类
        Returns:
            地球表面两点之间的距离,单位英里
        Modify:
            2018-11-24
    """
    # sin()和cos()以弧度未输入,将float角度数值转为弧度,即*pi/180
    a = sin(vec_a[0, 1] * pi / 180) * sin(vec_b[0, 1] * pi / 180)
    b = cos(vec_a[0, 1] * pi / 180) * cos(vec_b[0, 1] * pi / 180) * cos(pi * (vec_b[0, 0] - vec_a[0, 0]) / 180)
    return arccos(a + b) * 6371.0


def cluster_clubs(num_clust=5):
    """
        Function:
            簇绘图函数
        Parameters:
            num_clust - 指定簇的个数,默认5个
        Returns:
            无
        Modify:
            2018-11-24
    """
    data_list = []
    for line in open('./machinelearninginaction/Ch10/places.txt').readlines():
        line_arr = line.split('\t')
        data_list.append([float(line_arr[4]), float(line_arr[3])])
    data_mat = mat(data_list)
    # 利用2-均值聚类算法进行聚类
    my_centroids, clust_assing = bi_k_means(data_mat, num_clust, dist_meas=dist_s_l_c)
    # 对聚类结果进行绘图
    fig = plt.figure()
    rect = [0.1, 0.1, 0.8, 0.8]
    scatter_markers = ['s', 'o', '^', '8', 'p', 'd', 'v', 'h', '>', '<']
    axprops = dict(xticks=[], yticks=[])
    # 创建一幅图
    ax0 = fig.add_axes(rect, label='ax0', **axprops)
    img_p = plt.imread('./machinelearninginaction/Ch10/Portland.png')
    ax0.imshow(img_p)
    # 创建矩形
    ax1 = fig.add_axes(rect, label='ax1', frameon=False)
    for i in range(num_clust):
        pts_in_cruu_cluster = data_mat[nonzero(clust_assing[:, 0].A == i)[0], :]
        marker_style = scatter_markers[i % len(scatter_markers)]
        ax1.scatter(pts_in_cruu_cluster[:, 0].flatten().A[0], pts_in_cruu_cluster[:, 1].flatten().A[0],
                    marker=marker_style, s=90)
    ax1.scatter(my_centroids[:, 0].flatten().A[0], my_centroids[:, 1].flatten().A[0], marker='+', s=300)
    plt.show()


if __name__ == '__main__':
    # data_mat = mat(load_data_set('./machinelearninginaction/Ch10/testSet.txt'))
    # # 测试k个随机质心集合
    # t = rand_cent(data_mat, 2)
    # # 测试距离计算方法
    # m = dist_eclud(data_mat[0], data_mat[1])
    # print('k个随机质心集合:', t)
    # print('距离计算:', m)

    # data_mat = mat(load_data_set('./machinelearninginaction/Ch10/testSet.txt'))
    # my_centroids, clust_assing = k_means(data_mat, 4)
    # print(my_centroids)
    #
    # plot_cluster(data_mat, 4, my_centroids, clust_assing)

    # data_mat = mat(load_data_set('./machinelearninginaction/Ch10/testSet2.txt'))
    # my_centroids, clust_assing = bi_k_means(data_mat, 3)
    # print(my_centroids)
    # print(clust_assing)
    #
    # plot_cluster(data_mat, 3, my_centroids, clust_assing)

    # cluster_clubs(5)

    data_mat = mat(load_data_set('./machinelearninginaction/Ch10/testSet2.txt'))
    k = 3
    y_pred = KMeans(n_clusters=k).fit(data_mat)
    centroids = y_pred.cluster_centers_
    clust_assing = y_pred.labels_
    print(clust_assing)
    print(centroids)
    plot_cluster(data_mat, k, centroids, clust_assing)

下面用这个数据来测试实现

选取出来的质心和聚类结果:

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

有时候当我们观察聚类的结果图时,发现聚类的效果没有那么好,如上图所示,K-means算法在k值选取为3时的聚类结果,我们发现,算法能够收敛但效果较差。显然,这种情况的原因是,算法收敛到了局部最小值,而并不是全局最小值,局部最小值显然没有全局最小值的结果好。既然知道了算法已经陷入了局部最小值,如何才能够进一步提升K-means算法的效果呢?

一种用于度量聚类效果的指标是SSE,即误差平方和, 为所有簇中的全部数据点到簇中心的误差距离的平方累加和。SSE的值如果越小,表示数据点越接近于它们的簇中心,即质心,聚类效果也越好。因为,对误差取平方后,就会更加重视那些远离中心的数据点。

显然,一种改善聚类效果的做法就是降低SSE,那么如何在保持簇数目不变的情况下提高簇的质量呢?

一种方法是:我们可以将具有最大SSE值得簇划分为两个簇(因为,SSE最大的簇一般情况下,意味着簇内的数据点距离簇中心较远),具体地,可以将最大簇包含的点过滤出来并在这些点上运行K-means算法,其中k设为2。同时,当把最大的簇(上图中的下半部分)分为两个簇之后,为了保证簇的数目是不变的,我们可以再合并两个簇。

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

上面,是对已经聚类完成的结果进行改善的方法,在不改变k值的情况下,上述方法能够起到一定的作用,会使得聚类效果得到一定的改善。我们可以用二分k-均值克服算法收敛于局部最小值问题的K-means算法。

2.2 二分 K-均值算法

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

当然,也可以选择SSE最大的簇进行划分,知道簇数目达到用户指定的数目为止。下面就来看一下该算法的实际效果。

通过上述算法,之前陷入局部最小值的的这些数据,经过二分K-means算法多次划分后,逐渐收敛到全局最小值,从而达到了令人满意的聚类效果。

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

现在有一个存有70个地址、城市名、对应经纬度的文本数据,而没有这些地点的距离信息。因为在北极每走几米的经度变化可能达到数十度,而沿着赤道附近走相同的距离,带来的经度变化可能是零,所以我们使用球面余弦定理来计算两个经纬度之间的实际距离。想要对这些地点进行聚类,找到每个簇的质心地点,从而可以安排合理的行程,即质心之间选择交通工具抵达,而位于每个质心附近的地点就可以采取步行的方法抵达。显然,K-means算法可以为我们找到一种更加经济而且高效的出行方式。

四、应用scikit-learn构建KMeans聚类器

数据使用上面用过的testSet2.txt数据集。

从图中看出,scikit-learn的KMeans聚类效果和二分 K-均值聚类效果基本一样。

五,小结

聚类是一种无监督的学习方法。聚类区别于分类,即事先不知道要寻找的内容,没有预先设定好的目标变量。

聚类将数据点归到多个簇中,其中相似的数据点归为同一簇,而不相似的点归为不同的簇。相似度的计算方法有很多,具体的应用选择合适的相似度计算方法。

K-means聚类算法,是一种广泛使用的聚类算法,其中k是需要指定的参数,即需要创建的簇的数目,K-means算法中的k个簇的质心可以通过随机的方式获得,但是这些点需要位于数据范围内。在算法中,计算每个点到质心得距离,选择距离最小的质心对应的簇作为该数据点的划分,然后再基于该分配过程后更新簇的质心。重复上述过程,直至各个簇的质心不再变化为止。

K-means算法虽然有效,但是容易受到初始簇质心的情况而影响,有可能陷入局部最优解。为了解决这个问题,可以使用另外一种称为二分K-means的聚类算法。二分K-means算法首先将所有数据点分为一个簇;然后使用K-means(k=2)对其进行划分;下一次迭代时,选择使得SSE下降程度最大的簇进行划分;重复该过程,直至簇的个数达到指定的数目为止。实验表明,二分K-means算法的聚类效果要好于普通的K-means聚类算法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容