机器学习——聚类算法

聚类算法

在机器学习中,若训练样本的标记信息未知,则称为无监督学习。无监督学习通过对无标记训练样本的学习来寻找这些数据的内在性质,其主要的工具就是聚类算法。

1 k-Means算法

k-Means算法也称为k-平均或k-均值算法,是一种聚类算法,它是一种基于相似性的无监督学习,通过比较样本之间的相似性,将较为相似的样本划分到同一个类别中。由于k-Means算法简单、易于实现的特点,故在图像分割中得到广泛的应用。

1.1 基本概念

簇:把数据分为几类,每一类就是一个簇,也是k的由来。

欧式距离(欧几里得距离):计算两个点之间的距离,即计算数据与指定点的距离的衡量。二维空间的两点欧式距离表示为:

在这里插入图片描述
两个n向量a和b之间的欧式距离:

在这里插入图片描述

该式也可以表示成向量运算的形式:

在这里插入图片描述

质心:每次分类后求得的均值。

1.2 k-Means算法原理

k-Means算法主要分为以下三个步骤:

  1. 初始化k个聚类中心,即将数据集分为k个簇,这个取值通常并不是一次性到位的,需要取值进行对比,最终得到一个最合适的取值,生成k个聚类中心。
  2. 计算出每个对象跟这k个中心的距离,假如x跟y这个中心的距离最小,那么x属于y这个中心。这一步就可以得到初步的k个聚类。

对于一个表示为m*n矩阵的样本,假设归为k个类,分别为{C1, C2, ···, Ck}。

  1. 在第二步得到的每个聚类分别计算出新的聚类中心,和旧的聚类中心进行对比,假如不相同则继续执行第二步,不断迭代,直到新旧两个中心相同,说明聚类不可变,已经成功。即求完质心后再求距离,不会有点进行浮动,这样相当于训练结束。

k-Means算法的目标是使得每一个样本X被划分到最相似的类别中,利用每个类别的 样本重新计算聚类中心Ck:

在这里插入图片描述

k-Means算法的停止条件是最终的聚类中心不再改变,此时所有的样本被划分到了最近的聚类中心所属的类别中:

在这里插入图片描述

其中样本X(i)数据集中的第i行,Cj表示的是第j个聚类中心。

1.3 k-Means算法的可视化演示

在演示模型中随机生成一些数据点,并以高斯分布的形式出现:

在这里插入图片描述
然后添加三个随机的质心作为初始化的点:

在这里插入图片描述
可以看到这三个点距离很近,就意味着他们可能并不能对这三堆数据进行正确的分组。换句话说就是初始点的选择会影响最终的分类结果。接下来就要用所有的数据对这个三个点求距离来看那些数据离哪个点最近。那么就认为他们是一类。

在这里插入图片描述
第一步之后,可以看到所有的数据点都被分类。接下来要对三堆数据重新求质心,即对他们每一类求均值。

在这里插入图片描述
更新完质心即新的质心到了图中的新位置。接下来做相同动作,针对新的三个质心求距离将数据重新分类。一直重复直到数据点不发生变化。

在这里插入图片描述
可以看出正是由于初始的质心选择不好,所以导致没有一个相对正确的结果。那么重新选择三个质心,完成上述的步骤。

在这里插入图片描述
在这里插入图片描述
可以看到重新选择了三个初始点,得到的结果相对正确了许多。

接下来选择不规则的图形进行演示。

在这里插入图片描述
在这里插入图片描述
上图对一个笑脸图进行分类发现得到的并不是我们想要的结果,那就说明k-Means算法对于任意形状的图形分类不一定得到正确的结果,因为它只是求一个距离的最小值并按这个距离来进行分类。

在这里插入图片描述

所以重新选择一个规则图形进行演示发现的确可以得到一个较好的分类结果。

在这里插入图片描述
但是观察上图,发现数据中有7个明显的圆,选择7个质心效果就比较好,但是如果只选择6个质心,如上图。可以发现他们对于多出来的一个圆的分类达不到想要的效果,分类效果确实不如7个质心。所以这就说明了在初始k值的选择对最后结果的影响还是比较大的。

1.4 实验

利用Python代码实现给定的随机数据点聚类分析。

import numpy as np
import random
import matplotlib.pyplot as plt

def k_means(data, k):
    sample_num = data.shape[0]
    center_index = random.sample(range(sample_num), k)
    cluster_cen = data[center_index, :]
    is_change = 1
    cat = np.zeros(sample_num)
    
    while is_change:
        is_change = 0
        
        for i in range(sample_num):
            min_distance = 100000
            min_index = 0
            
            for j in range(k):
                sub_data = data[i, :] - cluster_cen[j, :]
                distance = np.inner(sub_data, sub_data)
                
                if distance < min_distance:
                    min_distance = distance
                    min_index = j + 1
                    
            if cat[i] != min_index:
                is_change = 1
                cat[i] = min_index
                
        for j in range(k):
            cluster_cen[j] = np.mean(data[cat == j + 1], axis = 0)
            
    return cat, cluster_cen

if __name__ == '__main__':
    cov = [[1,0], [0,1]]
    mean1 = [1,-1] 
    x1 = np.random.multivariate_normal(mean1, cov, 200)
    
    mean2 = [5.5,-4.5]
    x2 = np.random.multivariate_normal(mean2, cov, 200)
    
    mean3 = [1,4]
    x3 = np.random.multivariate_normal(mean3, cov, 200)
    
    mean4 = [6,4.5]
    x4 = np.random.multivariate_normal(mean4, cov, 200)
    
    mean5 = [9,0.0]
    x5 = np.random.multivariate_normal(mean5, cov, 200)
    
    X = np.vstack((x1,x2,x3,x4,x5))
    
    fig1 = plt.figure(1)
    p1 = plt.scatter(x1[:,0], x1[:,1],marker = 'o', color = 'r', label = 'x1')
    p2 = plt.scatter(x2[:,0], x2[:,1],marker = '+', color = 'm', label = 'x2')
    p3 = plt.scatter(x3[:,0], x3[:,1],marker = 'x', color = 'b', label = 'x3')
    p4 = plt.scatter(x4[:,0], x4[:,1],marker = '*', color = 'g', label = 'x4')
    p5 = plt.scatter(x5[:,0], x5[:,1],marker = '+', color = 'y', label = 'x5')
    plt.title('original data')
    plt.legend(loc = 'upper right')
    
    cat, cluster_cen = k_means(X,5)
    
    print('the number of cluster 1:', sum(cat ==1))
    print('the number of cluster 2:', sum(cat ==2))
    print('the number of cluster 3:', sum(cat ==3))
    print('the number of cluster 4:', sum(cat ==4))
    print('the number of cluster 5:', sum(cat ==5))
    
    fig2 = plt.figure(2)
    for i, m, lo, label in zip(range(5), ['o', '+', 'x', '*', '+'], ['r', 'm', 'b', 'g', 'y'], ['x1', 'x2', 'x3', 'x4', 'x5']):
        p = plt.scatter(X[cat == (i + 1), 0], X[cat == (i + 1), 1], marker = m, color = lo, label = label)
        
    plt.legend(loc = 'upper right')
    plt.title('the clusting result')
    plt.show()

在这里插入图片描述

这张图片显示的是打印出的每一类的数据点的个数。

在这里插入图片描述
这两张图分别表示原始的数据点的分布和聚类后的分布结果,可以看出通过k-Means聚类将图中的数据点较为均匀的分成5类。

可以改变参数观察实验结果:

在这里插入图片描述
在这里插入图片描述
上面两张图分别是分6类和分4类的实验结果,可以明显看出当分成6类时均匀想过明显不如5个中心的效果要好。而当只有4个聚类中心时发现左侧两簇距离明显很近而右边两簇距离较远,效果也不如5个聚类中心。

2 DBSCAN算法

DBSCN算法可以用力解决k-Means算法中的一些不好聚类的问题,例如前文实验中的笑脸图,k-Means无法对这种特殊形状来分组。

2.1 基本概念

DBSCAN算法是一种基于密度的聚类算法。这个密度就是指一个点周围包含的点的个数。
这类密度聚类算法一般假定类别可以由样本分布的紧密程度决定。同一类样本之间是紧密相连的,也就是说在该类别任意样本周围不远处一定有同类别的样本存在。通过将紧密相连的样本划为一类就得到了一个聚类类别。通过将所有各组紧密相连的样本划分为各个不同的类别,就得到了最终的所有聚类类别结果。

核心点:某个点的密度达到算法设定的阈值。例如在这个点邻域中的点的个数超过了设定值,那么这个点就是一个核心点。

距离阈值:点的半径r,即设定邻域的大小。

直接密度可达:某点a在点b的r距离阈值内,且b是核心点,则称a,b直接密度可达。

密度可达:如果点a和b直接密度可大,b和c直接密度可达,并且a和c不是直接密度可达,则称a和c密度可达。

2.2 DBSCAN算法原理

DBSCAN的聚类定义很简单,即由密度可达关系导出的最大密度相连的样本集合,即为最终聚类的一个类别或者说是一个簇。

它的方法很简单,它任意选择一个没有类别的核心对象作为种子,然后找到所有这个核心对象能够密度可达的样本集合,即为一个聚类簇。接着继续选择另一个没有类别的核心对象去寻找密度可达的样本集合,这就得到另一个聚类簇,一直运行到所有核心对象都有类别为止。

但是需要考虑以下的三个问题:

  1. 一些异常样本点或者说少量游离于簇外的样本点,它们不在任何一个核心对象的周围,这些点被标记为噪声点。
  2. 如何度量某样本点和核心对象样本的距离,一般采用最近邻思想。
  3. 某些样本可能到两个核心对象的距离都小于距离阈值,但这两个核心对象由于不是密度可达又不属于同一聚类簇,那么一般来说采用先来后到的思想。这就说明DBSCAN并不是一个完全稳定的算法。

2.3 DBSCAN算法的可视化演示

选择高斯分布的数据

在这里插入图片描述
此时默认的距离阈值是1,密度阈值是4。开始运行该算法。

在这里插入图片描述
在这里插入图片描述
算法开始运行后会在数据点中随机寻找一个点作为核心点以1为半径开始画圈,当第一堆再也找不到核心点之后,就在这一堆以外的数据中随机选择核心点继续开始迭代分组。它相比与k-Means更智能的地方就在于不断自主的扩展、传播来进行分组。最后图中的白色圆点就是噪声点。

修改半径观察分组结果。

在这里插入图片描述

将半径扩大后发现分组效果更好,这是由于增大半径后囊括进行的数据就更多了,减少了噪声点。

接下来观察笑脸图的分组情况,选择距离阈值为1,密度阈值为4。

在这里插入图片描述
可以看到此时笑脸中的数据点被分为5组。这里发现,正式由于半径距离阈值的选择不够恰当,就导致笑脸脸部圆圈被分为了两组,而非我们想要获取的一组的样式,不是特别正确,但是眼睛和嘴部相对正确。

接下来调节距离阈值为1.22,密度阈值设为2,观察结果。

在这里插入图片描述

此时分类的效果就比较正确,可见DBSCAN算法对初始值设定的要求是很高的,初始值选择恰当得到的效果肯定更好。

对于初始值的选择问题,下图更能说明问题。

在这里插入图片描述
在这里插入图片描述

这里我们取密度阈值为4,发现只有右上部分一些点可以得到正确的划分,而其他点全部被归为噪声点,能分的组比不能分的组还要少。将距离阈值改为2,半径改为1.74,发现分组效果确实是好了一些。足以说明,对于距离阈值和密度阈值的初始值选择在这个算法中是非常重要的。

2.4 实验

用Python代码实现密度聚类。

输入一组数据集,每三个是一组,分别是西瓜的编号、密度和含糖量。

data = """1,0.697,0.46,2,0.774,0.376,3,0634,0.264,4,0.608,0.318,5,0.556,0.215,
            6,0.403,0.237,7,0.481,0.149,8,0.437,0.211,9,0.666,0.091,10,0.243,0.267,
            11,0.245,0.057,12,0.343,0.099,13,0.639,0.161,14,0.657,0.198,15,0.36,0.37
            16,0.593,0.042,17,0.719,0.103,18,0.359,0.188,19,0.339,0.241,20,0.282,0.257"""

接下来数据处理,dataest是30个样本的列表。

a = data.split(',')
dataset = [(float(a[i]), float(a[i + 1])) for i in range(1, len(a - 1), 3)]

计算欧几里得距离,a,b分别为两个元组。

def dist(a, b):
    return math.sqrt(math.pow(a[0] - b[0], 2) + math.pow(a[1] - b[1], 2))

算法模型。

def DBSCAN(D, e, Minpts):
    T = set()
    k = 0
    C = []
    P = set(D)
    
    for d in D:
        if len([i for i in D if dist(d, i) <= e]) >= Minpts:
            T.add(d)
            
    while len(T):
        P_old = P
        o = list(T)[np.random.randint(0, len(T))]
        P = P - set(o)
        Q = []
        Q.append(o)
        
        while len(Q):
            q = Q[0]
            Nq = [i for i in D if dist(q,i) <= e]
            
            if len(Nq) >= Minpts:
                S = P & set(Nq)
                Q += (list(S))
                P = P - S
                
            Q.remove(q)
            
        k += 1
        Ck = list(P_old - P)
        T = T - set(Ck)
        C.append(Ck)
        
    return C

画图,画出DBSCAN算法对随机数据进行聚类的效果。

def draw(C):
    colValue = ['r','y','g','b','c','k','m']
    
    for i in range(len(C)):
        coo_X = []
        coo_Y = []
        
        for j in range(len(C[i])):
            coo_X.append(C[i][j][0])
            coo_Y.append(C[i][j][1])
            
        pl.scatter(coo_X, coo_Y, marker = 'x', color = colValue[i % len(colValue)], label = i)
        
    pl.legend(loc = 'upper right')
    pl.show()
    
C = DBSCAN(dataset, 0.11, 5)
draw(C)

得到的结果如图所示:
在这里插入图片描述下面利用Python代码对随机数据进行DBSCAN聚类分析:

import numpy as np
import matplotlib.pyplot as plt
cs = ['black', 'blue', 'brown', 'red', 'yellow', 'green']
class NpCluster(object):
    def __init__(self):
        self.key = []
        self.value = []

    def append(self, data):
        if str(data) in self.key:
            return
        self.key.append(str(data))
        self.value.append(data)

    def exist(self, data):
        if str(data) in self.key:
            return True
        return False

    def __len__(self):
        return len(self.value)

    def __iter__(self):
        self.times = 0
        return self

    def __next__(self):
        try:
            ret = self.value[self.times]
            self.times += 1
            return ret
        except IndexError:
            raise StopIteration()
def create_sample():
    np.random.seed(10)  # 随机数种子,保证随机数生成的顺序一样
    n_dim = 2
    num = 100
    a = 3 + 5 * np.random.randn(num, n_dim)
    b = 30 + 5 * np.random.randn(num, n_dim)
    c = 60 + 10 * np.random.randn(1, n_dim)
    data_mat = np.concatenate((np.concatenate((a, b)), c))
    ay = np.zeros(num)
    by = np.ones(num)
    label = np.concatenate((ay, by))
    return {'data_mat': list(data_mat), 'label': label}
    
def region_query(dataset, center_point, eps):
    result = NpCluster()
    for point in dataset:
        if np.sqrt(sum(np.power(point - center_point, 2))) <= eps:
            result.append(point)
    return result

def dbscan(dataset, eps, min_pts):
    noise = NpCluster()
    visited = NpCluster()
    clusters = []
    for point in dataset:
        cluster = NpCluster()
        if not visited.exist(point):
            visited.append(point)
            neighbors = region_query(dataset, point, eps)
            if len(neighbors) < min_pts:
                noise.append(point)
            else:
                cluster.append(point)
                expand_cluster(visited, dataset, neighbors, cluster, eps, min_pts)
                clusters.append(cluster)
    for data in clusters:
        print(data.value)
        plot_data(np.mat(data.value), cs[clusters.index(data)])
    if noise.value:
        plot_data(np.mat(noise.value), 'green')
    plt.show()

def plot_data(samples, color, plot_type='o'):
    plt.plot(samples[:, 0], samples[:, 1], plot_type, markerfacecolor=color, markersize=14)

def expand_cluster(visited, dataset, neighbors, cluster, eps, min_pts):
    for point in neighbors:
        if not visited.exist(point):
            visited.append(point)
            point_neighbors = region_query(dataset, point, eps)
            if len(point_neighbors) >= min_pts:
                for expand_point in point_neighbors:
                    if not neighbors.exist(expand_point):
                        neighbors.append(expand_point)
                if not cluster.exist(point):
                    cluster.append(point)
                    
init_data = create_sample()
dbscan(init_data['data_mat'], 10, 3)

得到的效果如图所示:
在这里插入图片描述
由上图可以看出,对于随机生成的不规则数据点,DBSCAN算法可以很好的对数据点进行划分,对于单独的一个噪声点可以较好的将其区分出来。

在这里插入图片描述若是改变距离阈值和密度阈值,得到的效果并不是很满意,会有一些临近的数据点被错误的认为成噪声点。

总结

聚类算法的思想是将数据划分为若干个不相交的子集,称为簇,每个簇潜在对应某一个概念。但是聚类算法只生成簇结构,每个簇所代表的概念的语义由使用者自己解释。也就是说聚类算法并不会告诉你:它生成的这些簇分别代表什么意义,它只会告诉你算法已经将数据集划分为这些不相关的簇了。

这类算法作为一种探索性的分析方法,用来分析数据的内在特点,寻找数据之间的分布规律,同时在分类的处理过程中,首先对需要分类的数据进行聚类,然后对聚类出的结果的每一个簇进行分类,实现的是对数据的预处理。

k-Means算法就是通过某种相似性度量的方法,将较为相似的个体划分到同一个类别中。对于不同的应用场景,有着不同的相似性度量方法,为了度量样本X和样本Y之间的相似性一般会定义一个距离函数来分析相似性。虽然算法思想和计算都很简单,但是k的选取是需要不断实验的,同时如果在数据量特别大的时候,要计算每个点的欧式距离计算量过于巨大。对任意图像的处理结果也不好确定,毕竟只是以距离作为衡量标准,将相近的作为一组。

DBSCAN算法是一种很典型的密度聚类算法,相比于k-Means算法既适用于凸样本集也适用于非凸样本集。也就是样本集的密度不均匀、聚类间距相差很大时,用DBSCAN算法并不是很合适。与k-Means相比最大的不同就是在于不需要输入类别数k,其最大的优势更是在于可以发现任意形状的聚类簇,同时可以在聚类的同时发现异常点,聚类的结果偏差也不是很大,但是需要对距离阈值和密度阈值联合调参。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值