kmeans优化算法:二分Kmeans聚类算法

彭玘
• 阅读 164

算法的理解

​ Bi这里是的意思就是Binary,二进制的意思,所以有时候叫这个算法为二进Kmeans算法。为什么我们需要用BiKmeans呢,就是为了解决初始化k个随机的质心点时其中一个或者多个点由于位置太极端而导致迭代的过程中消失的问题。BiKmeans只是Kmeans其中一个优化方案,其实还是有很多优化的方案,这里BiKmeans容易讲解和理解,并且容易用numpy, pandas实现。

​ 那为什么二进Kmeans算法可以有效的解决这个问题呢。我们需要从二进Kmeans的基础看是讲起。其实BiKmeans的迭代过程类似于一个决策树。首先我们看一下Kmeans算法的步骤。

​ 利用之前写好的Kmeans算法包,设置k为2。所以每次传入一个数据集的时候,都是进行2分类。

假设我们预先想分出K个簇。

  1. 使用Kmeans(k=2)将数据集分成2个簇,记录SSE
  2. 对于每一个簇来说,都有自己当前的SSE,取名为父节点SSE

    a. 对这些簇都进行Kmeans二分类,并且记录分出的2个簇的SSE只和,称之为子节点总SSE
    b. 记录这个簇被2分类之后SSE的差值,SSE差值 = 父节点SSE - 子节点SSE

  3. 选择SSE差值最大的那个簇进行划分,而其他的簇不进行划分。
  4. 重复第二的步骤,直到簇的总个数达到K

用决策树的方法理解BiKmeans

​ 那这个算法其实就是非常的类似决策树的算法。在决策树节点由父节点划分成子节点的过程中,用的是gini不纯度来判断是否需要划分,我们选择不纯度差值最大的那个特征来做划分。这里也类似,我们最后的目标是最小化SSE,所以对每一个簇来说,都可以得出该簇在划分出成2个簇之后总体上SSE降低了多少,我们需要做的就是保持其他的簇不变,选取的就是那个能够最大程度的降低SSE的那个簇进行Kmeans二分类。

​ 那个算法里面还是有个缺陷,就是在算法的过程中,会对每个簇重复计算划分后SSE的差值,所以这里我在对每个簇做划分后,记录下它的SSE差值,后期就可以直接使用SSE,不用重新再计算一遍了。

​ 我们首先用决策树的概念来看下BiKmeans,我们目标是分成4个簇,首先有我们有我们的根节点,也就是我们的整体的数据集,假设这个数据集的SSE为100.

  1. 使用Kmeans对根节点做2分类。得出2个簇,簇内SSE分别为40和30,也就是说如果我们对整体数据集做一次Kmeans二分类的话,我们的整体SSE会下降100-(30+40)=30

kmeans优化算法:二分Kmeans聚类算法

  1. 在此时可以对每个叶节点的数据集进行二分类,查看这个簇做二分Kmeans后,SSE下降多少。从这里可以看出,对簇1来说,SSE的变化为40-(20+15)=5,对簇2来说,SSE的变化为30-(10+10)=10。所以说对这2个簇来说的话,应该保留簇1,对簇2进行二分Kmeans。这样的话可以最大程度的减少总体SSE,因为簇2二分Kmeans后SSE下降的最快。结果就是以下的情况,当前叶节点有3个,也就是说当前我们有3个簇,还没有达到我们的目标,4个簇,继续对每个叶节点进行划分。注,这里原来的簇2就不复存在了,变成了小一点的簇2和簇3。

kmeans优化算法:二分Kmeans聚类算法

kmeans优化算法:二分Kmeans聚类算法

  1. 从上面的图看出,现在有3个簇,对每个簇都做二分Kmeans,之前簇1的SSE差值已经算过了,是40-(20+15)=5,对于簇2来说,SSE变化为10-(8+1)=1, 对于簇3来说,SSE的变化为10-(7+1)=2。这里簇1的SSE变化值最大,优先对簇1做二分Kmeans,得出以下结果。

kmeans优化算法:二分Kmeans聚类算法

  1. 当前决策树一共有4个叶节点,每个叶节点都代表一个簇,簇的个数已经达到我们的目标。算法结束。

代码实现

首先先读取数据,读取的是上次我们在Kmeans中间过程最后展示原始Kmeans理论缺陷的那组数据。

from Kmeans_pack import *
r = 4
k = 3
x , y = make_blobs(n_samples = 400,
                   cluster_std = [0.2, 0.4, 0.4, 0.4], 
                   centers = [[0.3,-1],[1,1],[-1,1], [-1,-1]],
                   random_state = r
                  )
np.random.seed(r)
sim_data = pd.DataFrame(x, columns = ['x', 'y'])
sim_data['label'] = y
dataset = sim_data.copy()

现在我们尝试的将数据只做一次二分Kmeans的迭代,查看结果,这个时候会有两种结果

kmeans优化算法:二分Kmeans聚类算法

kmeans优化算法:二分Kmeans聚类算法

​ 在这两个情况下,我们看到2分Kmeans可以将当前数据集分成2个簇,紧接着我们就需要尝试分别对蓝色和黄色的簇进行2分Kmeans查看每个簇划分后SSE下降了少。我们会首先写一个Split函数,对每个传进去的数据集进行2分Kmeans。但是这里需要注意是否是第一次做划分,就比如上面的情况。

​ 这里我们首先有个split函数,专门用来对传入的数据做2分Kmeans,算出聚类前和聚类后的SSE,比如说假如这个时候我们有x和y,$\bar{x}$和$\bar{y}$为x和y的平均值

$$ \left[ \begin{matrix} (x_{1}-\bar{x})^2 + (y_{1}-\bar{y})^2 \\ (x_{2}-\bar{x})^2 + (y_{2}-\bar{y})^2 \\ (x_{3}-\bar{x})^2 + (y_{3}-\bar{y})^2 \end{matrix} \right] $$

  1. 用的方法就是使用np.power(data - mean, 2).sum(axis = 1)得出的就是这个簇一开始的SSE。
  2. 将数据集带入之前写的Kmeans_regular后设置k=2,会给出SSE_list, SSE_list[-1]会给出Kmeans聚类好数据集之后2个簇加起来SSE的综合,并且也会给出curr_group, 用来划分我们的簇,方便选取其中的簇带入下一次迭代
def Split(dataset):
    #假设当前数据不是第一次二分Kmeans,就是说传进来的是整体的数据集,当前的质心点就是每个特征的平均值
    temp_data = dataset.loc[:, dataset.columns != 'label'].copy()

    #计算当前数据集的SSE
    current_error = np.power(temp_data - temp_data.mean(), 2).sum().sum()

    #对数据集做二分Kmeans
    curr_group, SSE_list, centers = Kmeans_regular(temp_data, k = 2)
    
    #记录二分后的SSE
    after_split_error = SSE_list[-1]
    
    #已经有了curr_group将二分类后的数据集先拿出来
    clusters = list(dataset.groupby(curr_group))
    
    #这里有非常非常少的情况会出现二分Kmeans中初始的质心点其中一个由于离所有的都太远,导致丢失的情况
    #所以这里多加了一个判断,假如其中一个质心掉了,那上面给的clusters只有一个而不是两个
    #在这个情况下,dataset没有被成功二分类,我们需要将dataset自身给return,进行下一次迭代,肯定有一次迭代能成功分出2个簇
    #所以在这个情况下entropy就是current_error, cluster1就是dataset自己,cluster2为空
    if len(clusters) == 1:
        entropy = current_error
        return [entropy, dataset, None, current_error]
    
    #分别取出2个簇
    cluster1, cluster2 = [i[1] for i in clusters]
    
    #计算这个簇做了二分Kmeans后SSE下降的多少
    entropy = current_error - after_split_error
    
    #最后返回结果,返回当前这个簇二分后的SSE差值,分出的簇1和簇2,还有当前这个簇的SSE
    return [entropy, cluster1, cluster2, current_error]

这个函数写好之后我们来测试一下,当前我们将所有的数据全部传进去后,给出的结果

entropy, cluster1, cluster2, current_error = Split(dataset)
entropy, cluster1.shape[0], cluster2.shape[0], current_error
(432.9176440191153, 200, 200, 813.3842612925762)

当前数据集做完二分后整体SSE由原来的813,下降了432.92。

接下来就是需要完成2分Kmeans的迭代过程

def bi_iterate(dataset, k = 4):
    #首先准备一个空的cluster_info_list,这个list是用来存二分后的结果,里面每一个元素都是一个簇
    #对于每个元素来说,它代表的是个簇,里面记录的这个簇的[entropy, cluster1, cluster2, current_error]
    #也就是每个簇的[SSE差值,第一个二分后的簇,第二个二分后的簇,当前簇的SSE]
    cluster_info_list = []
    
    #使用while做循环直到cluster_info_list里面一共达到k个簇的时候停止
    while len(cluster_info_list) < k:
        #假如当前我们是第一次迭代的话也就是cluster_info_list是空list的话做以下操作
        if len(cluster_info_list) == 0:
            
            #直接用Split函数,将整体数据集放入cluster_info_list里,然后下面的操作都不用,continue进入下一个循环
            cluster_info_list.append(Split(dataset))
            continue
        
        #首先将cluster_info_list最后一个元素取出来,cluster_info_list里面是所有簇的信息
        #我们后面会对cluster_info_list做sort,由于cluster_info_list里面每个元素的第一位是SSE差值
        #所以我们做完sort后,最后一个元素肯定是SSE差值(entropy)最大的那一个,也就是我们需要下一步做二分的簇
        #将最后一个元素里的2个clusters取出来后,将这个当前在cluster_info_list里SSE最大的一个簇删除掉(pop方法)
        #取而代之的是Split(cluster1)和Split(cluster2),也是就尝试着对新的两个cluster尝试去算SSE差值
        
        cluster1, cluster2 = cluster_info_list[-1][1:-1]
        cluster_info_list.pop(-1)
        
        #将Split(cluster1)和Split(cluster2)的结果追加到cluster_info_list
        #注意假如只有cluster2给出的是None,则碰到二分类不成功的情况,cluster1还为原来上一次dataset,cluster2为空
        #不将cluster2追加到cluster_info_list当中
        cluster_info_list.append(Split(cluster1))
        if cluster2 is not None:
            cluster_info_list.append(Split(cluster2))
        
        #整体的cluster_info_list进行一次sort,找出当前所有的簇中,哪一个簇的SSE差值最大
        #这里我们是需要对整体cluster_info_list做sort,因为新追加进去的2个心cluster的SSE差值可能没有cluster_info_list里已经记录的簇的SSE大。
        cluster_info_list.sort()
        
        #进入下一个循环
    return cluster_info_list       

将总共的代码都放在一起,内容不多,和网上的代码相比的话,简单易懂量少,也避免了效率较低的for循环。

from Kmeans_pack import *

def Split(dataset):
    temp_data = dataset.loc[:, dataset.columns != 'label'].copy()
    current_error = np.power(temp_data - temp_data.mean(), 2).sum().sum()
    curr_group, SSE_list, centers = Kmeans_regular(temp_data, k = 2)
    after_split_error = SSE_list[-1]
    clusters = list(dataset.groupby(curr_group))
    if len(clusters) == 1:
        entropy = current_error
        return [entropy, dataset, None, None, current_error]
    cluster1, cluster2 = [i[1] for i in clusters]
    entropy = current_error - after_split_error
    return [entropy, cluster1, cluster2, centers, curr_group, current_error, dataset]

def bi_Kmeans(dataset, k = 4):
    cluster_info_list = []
    while len(cluster_info_list) < k:
        if len(cluster_info_list) == 0:
            cluster_info_list.append(Split(dataset))
            continue
        cluster1, cluster2 = cluster_info_list[-1][1:3]
        cluster_info_list.pop(-1)
        cluster_info_list.append(Split(cluster1))
        if cluster2 is not None:
            cluster_info_list.append(Split(cluster2))
        cluster_info_list.sort()
    return cluster_info_list     

我们测试一下代码,返回的cluster_info_list里面所有的元素都是簇的信息,每个元素的最后一位都是这个簇的簇内SSE,所以我们可以用列表解析的方法将每个元素的最后一位取出来,进行相加就能得到BiKmeans最后的结果给出的整体的SSE,我们可以看出在数据集要4个簇的前提下,我们SSE最后为95.64

np.random.seed(1)
cluster_info_list = bi_Kmeans(dataset, k = 4)

ReKmeans和BiKmeans的结果对比

我们也可以将这个结果和原始写好的Kmeans_regular做比较

regular_SSE = []
bi_SSE = []
for i in range(50):
    curr_group, SSE_list, centers = Kmeans_regular(dataset, k = 4)
    cluster_info_list = bi_Kmeans(dataset, k = 4)
    bi_sse = sum([i[-1] for i in cluster_info_list])
    regular_SSE.append(SSE_list[-1])
    bi_SSE.append(bi_sse)
data_compare = pd.DataFrame({'ReKmeans':regular_SSE,'BiKmeans':bi_SSE})
data = [go.Scatter(x = data_compare.index + 1,
                   y = data_compare[i],
                   mode = 'lines+markers',
                   name = i
                  ) for i in data_compare.columns]

layout = go.Layout(title = '原始Kmeans与二分Kmeans的SSE稳定性比较',
                   xaxis = {'title' : '次数'},
                   yaxis = {'title' : 'SSE值'},
                   template = 'plotly_white'
                  )

fig = go.Figure(data = data, layout = layout)
#fig.show()

kmeans优化算法:二分Kmeans聚类算法

我们这里随机的跑30次,来比较最后2个算法所得到的SSE,我们这里主要查看稳定性。可以从图中看出对于原始的(RegularKmeans)的SSE非常不稳定,而对于BiKmeans来说的话,SSE非常稳定,一直保持在大约95左右。这里就体现出的BiKmeans的优点,可以很大概率的(不是绝无可能)保证每次随机生成的2个质心点不会因为太极端而导致其中一个丢失的问题,从而导致最后SSE很高,结果陷入局部最优的这样一个问题。

BiKmeans大致中间过程

这里我们不会像Kmeans中间过程那样给出详细的从随机选取的质心点到收敛的过程。我们这个给出一个大致的BiKmeans中间过程,有助于同学们理解。

kmeans优化算法:二分Kmeans聚类算法

点赞
收藏
评论区
推荐文章
Irene181 Irene181
4年前
盘点Python加密解密模块hashlib的7种加密算法
大家好,我是黄伟。今天给大家介绍hashlib模块!前言在程序中我们经常可以看到有很多的加密算法,比如说MD5sha1等,今天我们就来了解下这下加密算法的吧,在了解之前我们需要知道一个模块嘛就是hashlib,他就是目前Python一个提供字符加密的模块,它加密的字符类型为二进制编码,所以直接加密字符串会报错。importhashlibstring'任
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
拜占庭将军问题和 Raft 共识算法讲解
在分布式系统中,什么是拜占庭将军问题?产生的场景和解决方案是什么?什么是Raft共识算法?Raft算法是如何解决拜占庭将军问题的?其核心原理和算法逻辑是什么?除了Raft,还有哪些共识算法?共识问题作为分布式系统的一大难点和痛点,本文主要介绍了其产生的背景、原因,以及通用的Raft算法解决方案。
徐小夕 徐小夕
4年前
程序员必备的几种常见排序算法和搜索算法总结
前言最近为了巩固一下自己的算法基础,又把算法书里的基本算法刷了一遍,特地总结一下前端工程师需要了解的排序算法和搜索算法知识,虽然还有很多高深算法需要了解,但是基础还是要好好巩固一下的.本文将以图文的形式为大家介绍如下算法知识,希望在读完之后大家能有所收获:冒泡排序及其优化选择排序插入排序归并排序快速排序顺序搜索二分搜索
专注IP定位 专注IP定位
3年前
聚类算法有哪些?又是如何分类?
想要了解聚类算法并对其进行区别与比较的话,最好能把聚类的具体算法放到整个聚类分析的语境中理解。聚类分析是一个较为严密的数据分析过程。从聚类对象数据源开始到得到聚类结果的知识存档,共有四个主要研究内容聚类分析过程:1984年,Aldenderfer等人提出了聚类分析的四大功能:一是数据分类的进一步扩展;二是对实体归类的概念性探索;三是通过数据探索而生成假
Stella981 Stella981
3年前
Openssl生成RSA公私钥以及将公钥转换成C#支持的格式
Openssl生成RSA公私钥以及将公钥转换成C支持的格式1.RSA算法介绍RSA算法是一种非对称密码算法,所谓非对称,就是指该算法需要一对密钥,使用其中一个加密,则需要用另一个才能解密。RSA的算法涉及三个参数,n、e、d。其中,n是两个大质数p、q的积,n被称为模数,n的二进
Easter79 Easter79
3年前
TensorFlow Serving 模型更新毛刺的完全优化实践
_前言_在点击率CTR(ClickThroughRate)预估算法的推荐场景中使用TensorflowServing热更新较大模型时会出现短暂的延时毛刺,导致业务侧超时,降低算法效果,为了解决这个问题,爱奇艺深度学习平台团队经过多个阶段的优化实践,最后对TFServing和Tensorflow的源码进行深入优
数据堂 数据堂
2年前
点云标注的算法优化与性能提升
点云标注的算法优化和性能提升是提高自动驾驶技术的关键因素。通过优化算法和提升性能,可以获得更准确、更高效的结果。首先,算法优化可以通过使用先进的深度学习模型和算法来实现。例如,使用三维卷积神经网络(CNN)可以提取点云中的特征信息,提高障碍物检测和车道线标
分布式系统的主键生成方案对比 | 京东云技术团队
UUID​UUID(通用唯一识别码)是由32个十六进制数组成的无序字符串,通过一定的算法计算出来。为了保证其唯一性,UUID规范定义了包括网卡MAC地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素,以及从这些元素生成UUID的算法。
贾蔷 贾蔷
2个月前
洛谷P1168题终极解析:双堆法高效计算动态中位数 | 数据结构实战教程
一、问题理解与算法思路题目要求我们动态维护一个序列,并在每次读取奇数个数字时输出当前序列的中位数。这道题考察了两个核心算法点:堆数据结构的应用和中位数的高效计算。我们采用双堆法(一个大根堆和一个小根堆)来高效解决这个问题。‌解题关键步骤‌:使用大根堆存储较
彭玘
彭玘
Lv1
待到秋来九月八,我花开后百花杀。
文章
4
粉丝
0
获赞
0