Featured image of post 聚类基本概念和算法

聚类基本概念和算法

主要讲述了Kmeans,biKmeans和DBSCAN三种聚类算法的实现
 

1.基本概念

简介
聚类是一种无监督学习的机器学习技术,它旨在将数据集中的对象分成几个相似的组,每个组通常被称为一个簇,以便簇内的对象之间具有高度相似性,而不同簇之间的对象具有较低的相似性。聚类的目标是根据数据的内在结构,将相似的数据点聚集在一起,以便识别模式、发现隐藏的结构以及实现数据的压缩和汇总。

聚类分析的目标是,组内的对象相互之间是相似的,而不同组中的对象是不相似的,组内的相似性越大,组间的差别越大,聚类就越好。

1.1一个简单的例子

当我们将鱼塘中的鱼按其相似性进行分组,这个过程就是聚类。种类的相似性或者颜色的相似性都可以作为相似性度量的方法。在一般的聚类算法中,我们常用欧氏距离、曼哈顿距离、余弦相似性等方法来度量。

1.2 不同的聚类类型

  1. 层次的与划分的

    • 划分聚类就是简单的将数据对象集划分为不重叠的簇,下图中b,c,d都是一个划分聚类
    • 层次聚类是嵌套簇的集族,组织成一棵树。层次聚类可以看做划分聚类的序列。下图中(a~d)显示的簇依次形成一个层次聚类,每层分别有1,2,4,6个簇。

相同点集的不同聚类方法

  1. 互斥的、重叠的、模糊的

    • 互斥的:每个对象都指派到单个簇
    • 重叠的:一个对象可指派到多个簇,例如,在大学里,一个人可能既是学生又是雇员。
    • 模糊的:每个对象以一个0到1之间的隶属权值属于每个簇。
  2. 完全的与部分的

    • 完全聚类中每个对象都会被指派到一个簇中
    • 部分聚类存在未指派簇的对象

1.3 不同的簇类型

用二维点集图示的不同簇类型

  • 1)明显分离的:图a中显示的就是明显分离的簇的例子,不同组中的任意两点之间的距离都大于组内任意两点的距离。明显分离的簇不必是球形的,可以具有任意形状。
  • 2)基于原型的:簇内对象到定义该簇的原型的距离比到其他簇的原型的距离更近。通常把基于原型的簇看作基于中心的簇,一般这种簇趋近球形。
  • 3)基于图的: 如果数据点作为图的节点,边代表数据对象之间的联系来构成一个图,那么簇就可以定义为连通分支,连通的一组数据点可以指派到一个簇。
  • 4)基于密度的:簇是对象的稠密区域,被低密度的区域环绕。
  • 5)概念簇:概念簇的基本思想是将相关的概念或主题归为一组,从而形成一个聚类,以帮助用户更容易找到相关信息或了解数据的结构。

2.几种经典的聚类算法的实现

2.1.基本的K均值算法(KMeans)

K均值是基于原型的、划分的聚类技术。

基本算法: 随机选择K个初始质心,其中K是用户指定的参数,即所期望簇的个数,然后将所有的数据点指派到距离最近的质心的簇。指派完后根据簇中的点更新每个簇的质心。重复指派和更新步骤,直到簇不发生变化。

伪代码:

基本K均值算法

Python代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
    """
    k-Means聚类算法,返回最终的k各质心和点的分配结果
    """
    m = dataSet.shape[0]  # 获取样本数量
    # 构建一个簇分配结果矩阵,共两列,第一列为样本所属的簇类值,第二列为样本到簇质心的误差
    clusterAssment = np.mat(np.zeros((m, 2)))
    # 1. 初始化k个质心
    centroids = createCent(dataSet, k)
    clusterChanged = True
    while clusterChanged:
        clusterChanged = False
        for i in range(m):
            minDist = np.inf
            minIndex = -1
            # 2. 找出最近的质心
            for j in range(k):
                distJI = distMeas(centroids[j, :], dataSet[i, :])
                if distJI < minDist:
                    minDist = distJI
                    minIndex = j
            # 3. 更新每一行样本所属的簇
            if clusterAssment[i, 0] != minIndex:
                clusterChanged = True
            clusterAssment[i, :] = minIndex, minDist ** 2
        print(centroids)  # 打印质心
        # 4. 更新质心
        for cent in range(k):
            ptsClust = dataSet[np.nonzero(clusterAssment[:, 0].A == cent)[0]]  # 获取给定簇的所有点
            centroids[cent, :] = np.mean(ptsClust, axis=0)  # 沿矩阵列的方向求均值
    return centroids, clusterAssment

运行示例

K指定为2

K指定为10

2.2.二分K均值算法(biKMeans)

二分K均值算法是K均值算法的直接扩充,是基于一种简单的想法:为了得到K个簇,将所有的点的集合作为初始簇,然后分裂成两个簇,从这些簇中选取一个簇进行分裂,重复选取和分裂,直到产生K个簇。

伪代码

二分K均值算法伪代码

待分裂的簇有许多种不同的选择方法,可以选择最大的簇,或者最大SSE的簇,或者使用一个基于大小和SSE的标准进行选择。不同的选择将导致不同的簇。

SSE介绍

SSE介绍

符号说明

biKMean代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def biKMeans(dataSet, k, distMeas=distEclud):
    """
    二分k-Means聚类算法,返回最终各点的分配结果
    """
    m = dataSet.shape[0]  # 获取样本数量
    # 构建一个簇分配结果矩阵,共两列,第一列为样本所属的簇类值,第二列为样本到簇质心的误差
    clusterAssment = np.mat(np.zeros((m, 2)))
    n = dataSet.shape[1]  # 获取数据的维度
    centroids = np.mat(np.zeros((k, n)))
    centroids[0, :] = np.mean(dataSet[:, :], axis=0)  # 初始簇的质心
    for i in range(m):        # 计算初始簇样本到簇质心的误差
        clusterAssment[i, 1] = distMeas(centroids[0, :], dataSet[i, :]) ** 2

    new_cluster_label = 1  
    # 每次分裂生成一个新簇,分裂K-1次完成
    while new_cluster_label < k:
        largest_sse_cluster_label = -1
        largest_sse = -1
        # 找到现有簇中最大sse的簇
        for cluster in range(new_cluster_label):  
            cluster_sse = sum(clusterAssment[np.nonzero(clusterAssment[:, 0].A == cluster)[0], 1])
            if cluster_sse > largest_sse:
                largest_sse_cluster_label = cluster
                largest_sse = cluster_sse
        # 将最大sse的簇进行多次分裂(3次),选择最小总sse的两个簇
        cluster_data_indexs = np.nonzero(clusterAssment[:, 0].A == largest_sse_cluster_label)[0]
        largest_sse_cluster = dataSet[cluster_data_indexs]
        lowest_sse = np.inf
        for j in range(3):
            sub_centroids, sub_clusterAssment = kMeans(largest_sse_cluster, 2, distMeas=distMeas)
            tol_subClusters_sse = sum(sub_clusterAssment[:, 1])
            if tol_subClusters_sse < lowest_sse:
                best_newCentroids = sub_centroids
                best_assment = sub_clusterAssment
                lowest_sse = tol_subClusters_sse
        # 更新质心
        centroids[new_cluster_label - 1, :] = best_newCentroids[0, :]
        centroids[new_cluster_label, :] = best_newCentroids[1, :]
        # 更新簇分配矩阵,子簇类别为0,分为原簇标签,子簇类别为1,则新分配一个类别
        for i in range(len(cluster_data_indexs)):
            sub_cluster_label = best_assment[i, 0]
            clusterAssment[cluster_data_indexs[i], :] = largest_sse_cluster_label if sub_cluster_label == 0 else new_cluster_label, best_assment[i, 1]
        # 下一个新分配的簇标签
        new_cluster_label += 1

    return clusterAssment[:, 0].A[:, 0]

二分K均值(Bisecting K-Means)是对基本的K均值算法的一种改进,其主要提升在于以下几个方面:

  • 初始簇的选择更合理:在二分K均值中,一开始将整个数据集视为一个簇,然后通过反复的二分操作,将数据集一分为二,以生成两个初始簇。这有助于更合理地选择初始簇,减少了对初始质心的敏感性。
  • 减少局部最小值问题:由于二分K均值从整个数据集开始,而不是一个随机初始簇,它更有可能避免陷入局部最小值问题,即更有可能找到更优的簇划分。
  • 更均匀的簇大小:通过反复的二分,二分K均值可以生成具有相对均匀大小的簇,而不同于基本K均值,可能出现某些簇特别大或特别小的情况。

运行示例:

K=10

K=3

2.3.KMeans++算法的实现

KMeans是质心敏感的聚类算法,其初始质心的选择是随机的。 K-Means++ 采用了一种智能化的方法来选择初始的簇中心,而不是随机选择。它通过权衡距离数据点较远的中心,减少了初始簇中心的偶然性。这有助于减少 K-Means 算法陷入局部最小值的风险,从而提高了算法的收敛性和稳定性。

KMeans++对基本K均值算法的改进就是体现在初始K个质心的选择上。剩下的步骤和基本K均值算法一致。

伪代码:

首先随机选择一个初始质心,然后迭代地选择剩余的K-1个质心,确保这些质心相对分散,以减少陷入局部最小值的可能性。它遍历每个数据点,为每个数据点找到到当前所有质心中最近质心的距离,然后选择离最近质心距离最大的数据点作为新的质心,加入存放质心的数组。不断重复,最终函数最终返回K个初始化的质心。

代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def kpp_initialize(dataSet, k):
    """
    用于K-Means++的初始化质心
    """
    # 从数据点中随机选择第一个质心
    centroids = [dataSet[np.random.choice(dataSet.shape[0])]]

    # 选择剩下的k-1个质心
    for i in range(k - 1):
        dist = []
        for j in range(dataSet.shape[0]): # 遍历每个点找到离现有质心最近距离的最大的点
            min_dist = sys.maxsize
            for cent in range(len(centroids)):
                temp_dist = distEclud(centroids[cent], dataSet[j, :])
                min_dist = min(min_dist, temp_dist)
            dist.append(min_dist)
        dist = np.array(dist)
        # 将找到的点作为新的质心,加入质心数组中
        new_centroid = dataSet[np.argmax(dist), :]
        centroids.append(new_centroid)
  
    return np.array(centroids)

运行结果

K=3时KMeans++的聚类效果

K=10时KMeans++的聚类效果


2.4.DBSCAN算法的实现

DBSCAN是一种简单、有效的基于密度的聚类算法。

前置知识:核心点、边界点、噪声点的定义

核心点、边界点、噪声点的定义

DBSCAN算法伪代码

DBSCAN算法伪代码

代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def get_domain_point_mat(data_set, eps):
    # 获取距离矩阵
    distance_mat = get_distance_mat(data_set)
    # 所有点的方阵,在领域内为1,不在为0
    domain_point_mat = (eps > distance_mat).astype(int)
    return domain_point_mat


def get_nearest_core_point(data_set, core_points, border_points):
    # 获取距离矩阵
    distance_mat = get_distance_mat(data_set)
    # 选择子矩阵,边界点到核心点距离的
    temp_mat = distance_mat[border_points, :]
    result_mat = temp_mat[:, core_points]

    border_nearest_core_points = np.argmin(result_mat, axis=1)
    return border_nearest_core_points



# 请补全
def dbscan(data_set, eps, min_pts):
    """
    DBSCAN聚类算法,返回每个样本的cluster类别
    """
    m = data_set.shape[0]  # 获取样本数量
    # 簇类别数组,未分类的点标记为-1
    cluster = np.full(m, -1)
    # 区别核心点,边界点,噪声点,噪声点类别分为-1
    domain_point_mat = get_domain_point_mat(data_set, eps) # 获得的点的邻接矩阵,1表示在领域内,0反之
    core_points_idx = np.nonzero(np.sum(domain_point_mat, axis=1) >= min_pts)[0]   # 找到核心点索引
    not_noise_point = np.nonzero(np.sum(domain_point_mat[core_points_idx], axis=0))[0]           # 边界点和核心点的集合
    border_point = list(set(not_noise_point) - set(core_points_idx))

    # 将距离为eps之内的所有核心点赋予一条边,每组连同的核心点形成一个簇
    new_cluster_label = 0
    unallocated_core_point = set(core_points_idx.copy())
    while len(unallocated_core_point) > 0:
        start_point = unallocated_core_point.pop()      # 未分配核心点数组的第一个作为类似广度优先搜索的起始点
        new_cluster = set([start_point])
        allocated_core_point = []
        # 由start_point开始寻找新的一个连通分支
        while len(new_cluster) > 0:
            point = new_cluster.pop()
            cluster[point] = new_cluster_label
            allocated_core_point.append(point)
            adjc_points = np.nonzero(domain_point_mat[point, :])[0]
            adjc_core_points = set(adjc_points) & set(core_points_idx) - set(allocated_core_point)
            new_cluster = new_cluster | adjc_core_points
        new_cluster_label += 1
        unallocated_core_point = unallocated_core_point - set(allocated_core_point)   # 更新未分配核心点的集合
  
    # 分配边界点,将边界点分配到最邻近的核心点
    core_nearest = get_nearest_core_point(data_set, core_points_idx, border_point)  # 获取边界点最近核心点的索引,这个索引是对于核心点数组而言的
    for border_p in range(len(border_point)):
        cluster[border_point[border_p]] = cluster[core_points_idx[core_nearest[border_p]]]
    return cluster

运行示例

minpts=8时数据集的K-距离图

K-距离图:所有点到其第k个最邻近点的距离排序(从小到大),横轴为排序后点的索引,从0 ~ m-1(m为数据点的数量),纵轴为点到第k个最邻近点的距离。

eps=0.08 minpts=8时的聚类效果

eps=0.10 minpts=8时的聚类效果

参考资料

  1. 数据挖掘导论(完整版)Pang-Ning Tan等主编
  2. 《数据挖掘》课程实验一
Licensed under CC BY-NC-SA 4.0
总访问量  |  总访客数  
Built with Hugo   主题 StackJimmy 设计