美文网首页
机器学习实战-09-K均值(K-means)

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

作者: nobodyyang | 来源:发表于2018-12-19 17:22 被阅读0次

    一、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聚类算法。

    相关文章

      网友评论

          本文标题:机器学习实战-09-K均值(K-means)

          本文链接:https://www.haomeiwen.com/subject/rizohqtx.html