【问题标题】:K-means algorithm variation with equal cluster size具有相等簇大小的 K-means 算法变化
【发布时间】:2011-07-24 01:55:45
【问题描述】:

我正在寻找最快的算法,用于按距离将地图上的点分组为大小相等的组。 k-means clustering algorithm 看起来简单而有前途,但不会产生同样大小的组。

此算法是否有变体或允许所有集群的成员数量相等的不同算法?

另见:Group n points in k clusters of equal size

【问题讨论】:

  • k-means 聚类本身就是 NP-hard。也许您可以开始更改距离函数,直到所有点都落入相同大小的组中,但是恐怕这不是凸优化问题,因此您需要在这里进行一些认真的计算。
  • 感谢大家的好回答。同时,我对最初的问题采取了完全不同的方法,不再涉及聚类。因此,我无法判断应该接受哪个答案,我将保持开放状态,希望您不要介意。
  • @pixelistik 嗨,您能否介绍一下您为解决此问题所采取的方法。我也试图解决同样的问题。任何提示/建议都会起作用。提前致谢。
  • 对不起,我帮不上忙,恐怕。我的方法根本不再涉及聚类。
  • @Atendra 在下面的 many 答案中有链接的实现。有些似乎已经过时(python),有些仍然可以工作(ELKI),有些你需要自己编写代码(我的回答)。您尝试过其中任何一个吗?

标签: algorithm map cluster-analysis k-means


【解决方案1】:

这可能会奏效:应用 Lloyd's algorithm 来获得 k 个质心。按数组中相关簇的降序大小对质心进行排序。对于 i = 1 到 k-1,将簇 i 中的数据点以最小距离推送到任何其他质心 j em> (i jk) 到 j 并重新计算质心 i em>(但不要重新计算集群),直到集群大小为 n / k

这个后处理步骤的复杂度是 O(k² n lg n)。

【讨论】:

  • 谢谢,这听起来是个不错的主意,可以在第二步中实现同等规模的小组。
  • 我试过这个解决方案没有成功,请看我的相关问题stackoverflow.com/questions/8796682/…
  • 离散集上的劳埃德算法不是和k-means一样吗?
【解决方案2】:

ELKI 数据挖掘框架有一个tutorial on equal-size k-means

这不是一个特别好的算法,但它是一个足够简单的 k-means 变体,可以编写教程并教人们如何实现自己的聚类算法变体;显然有些人确实需要他们的集群具有相同的大小,尽管 SSQ 质量会比使用常规 k-means 更差。

在 ELKI 0.7.5 中,您可以选择此算法为tutorial.clustering.SameSizeKMeansAlgorithm

【讨论】:

    【解决方案3】:

    您可以将距离视为定义加权图。相当多的图划分算法明确地基于尝试将图顶点划分为两组相等大小。例如,这出现在 Kernighan-Lin algorithm 和使用拉普拉斯算子的 spectral graph partitioning 中。要获得多个集群,可以递归应用分区算法;在关于谱图分区的链接上有一个很好的讨论。

    【讨论】:

      【解决方案4】:

      试试这个 k-means 变体:

      初始化

      • 从数据集中随机选择k中心,或者使用kmeans++策略更好
      • 对于每个点,计算到其最近的聚类中心的距离,并为此构建一个堆
      • 从堆中提取点,并将它们分配给最近的集群,除非集群已经满了。如果是这样,计算下一个最近的集群中心并重新插入到堆中

      最后,您应该有一个满足您对每个集群 +-1 相同数量的对象的要求的分区(确保最后几个集群也有正确的数量。第一个 m 集群应该有 @987654323 @ 对象,其余正好是floor 对象。)

      迭代步骤

      必要条件:每个集群的列表,其中包含“交换建议”(希望位于不同集群中的对象)。

      E 步骤:按照常规 k-means 计算更新后的聚类中心

      M 步骤:遍历所有点(仅一个点,或一批中的所有点)

      计算离对象最近的集群中心/比当前集群更近的所有集群中心。如果是不同的集群:

      • 如果其他集群小于当前集群,只需将其移至新集群
      • 如果有来自另一个集群(或任何距离更短的集群)的交换提议,则交换两个元素集群分配(如果有多个提议,则选择改进最大的一个)
      • 否则,为其他集群指明交换建议

      集群大小保持不变(+- ceil/floor 差异),一个对象只从一个集群移动到另一个集群,只要它导致估计的改进。因此,它应该像 k-means 一样在某个点收敛。不过它可能会慢一些(即更多的迭代)。

      我不知道这是否已经发布或实施过。这正是我会尝试的(如果我会尝试 k-means。有更好的聚类算法。)

      【讨论】:

        【解决方案5】:

        以防万一有人想在这里复制和粘贴一个简短的函数 - 基本上是运行 KMeans,然后在分配给集群的最大点(集群大小)的约束下找到点与集群的最小匹配

        from sklearn.cluster import KMeans
        from scipy.spatial.distance import cdist
        from scipy.optimize import linear_sum_assignment
        import numpy as np
        
        
        def get_even_clusters(X, cluster_size):
            n_clusters = int(np.ceil(len(X)/cluster_size))
            kmeans = KMeans(n_clusters)
            kmeans.fit(X)
            centers = kmeans.cluster_centers_
            centers = centers.reshape(-1, 1, X.shape[-1]).repeat(cluster_size, 1).reshape(-1, X.shape[-1])
            distance_matrix = cdist(X, centers)
            clusters = linear_sum_assignment(distance_matrix)[1]//cluster_size
            return clusters
        

        【讨论】:

        • 我猜,这里的 X 是 (x,y) 值或坐标的列表。有没有办法输入距离/权重矩阵?
        • X 是一组表示为矩阵的向量。 sklearn 的 KMeans 有一个已弃用的 precompute_distances 参数,我认为这是您所追求的。您通常可以替换 KMeans 部分以以适合您的方式找到集群,并保留 assignment 部分以使它们大小相等
        【解决方案6】:

        在阅读了这个问题和几个类似的问题之后,我使用https://elki-project.github.io/tutorial/same-size_k_means 上的 Elki 教程创建了一个相同大小的 k-means 的 python 实现,它利用 scikit-learn 的 K-Means 实现来实现大多数常见的方法和熟悉API。

        我的实现在这里找到: https://github.com/ndanielsen/Same-Size-K-Means

        聚类逻辑在这个函数中找到:_labels_inertia_precompute_dense()

        【讨论】:

          【解决方案7】:

          一般来说,按距离将地图上的点分成大小相等的组在理论上是不可能完成的任务。因为将点分组到大小相等的组按距离对集群中的点进行分组是冲突的。

          看这个情节:

          有四点:

          A.[1,1]
          B.[1,2]
          C.[2,2]
          D.[5,5]
          

          如果我们将这些点聚类成两个聚类。显然,(A,B,C) 将是集群 1,D 将是集群 2。但是如果我们需要相同大小的组,则 (A,B) 将是一个集群,(C,D) 将是另一个。这违反了集群规则,因为 C 更靠近 (A,B) 的中心,但它属于集群 (C,D)。因此不能同时满足集群和同等大小的组的要求。

          【讨论】:

            【解决方案8】:

            在给定集群质心的情况下,后处理更清晰。设N 为项目数,K 为簇数,S = ceil(N/K) 为最大簇大小。

            • 创建元组列表(item_id, cluster_id, distance)
            • 根据距离对元组进行排序
            • 对于已排序的元组列表中的每个元素(item_id, cluster_id, distance)
              • 如果cluster_id 中的元素数量超过S,则什么也不做
              • 否则将item_id 添加到集群cluster_id

            这在 O(NK lg(N)) 中运行,应该给出与@larsmans 解决方案相当的结果,并且更容易实现。在伪python中

            dists = []
            clusts = [None] * N
            counts = [0] * K
            
            for i, v in enumerate(items):
                dist = map( lambda x: dist(x, v), centroids )
                dd = map( lambda (k, v): (i, k, v), enumerate(dist) )
                dists.extend(dd)
            
            dists = sorted(dists, key = lambda (x,y,z): z)
            
            for (item_id, cluster_id, d) in dists:
                if counts[cluster_id] >= S:
                    continue
                if clusts[item_id] == None:
                    clusts[item_id] = cluster_id
                    counts[cluster_id] = counts[cluster_id] + 1
            

            【讨论】:

              【解决方案9】:

              考虑某种形式的递归贪心合并——每个点都以单例集群开始,并反复合并最近的两个,这样合并不会超过最大值。尺寸。如果您别无选择,只能超过最大大小,则在本地重新集群。这是一种回溯层次聚类的形式:http://en.wikipedia.org/wiki/Hierarchical_clustering

              【讨论】:

              • 看起来是一个好的开始。但是,是的,你能定义“本地重新集群”吗?谢谢。
              【解决方案10】:

              最近我自己需要这个来处理一个不是很大的数据集。我的回答,虽然运行时间比较长,但保证收敛到局部最优。

              def eqsc(X, K=None, G=None):
                  "equal-size clustering based on data exchanges between pairs of clusters"
                  from scipy.spatial.distance import pdist, squareform
                  from matplotlib import pyplot as plt
                  from matplotlib import animation as ani    
                  from matplotlib.patches import Polygon   
                  from matplotlib.collections import PatchCollection
                  def error(K, m, D):
                      """return average distances between data in one cluster, averaged over all clusters"""
                      E = 0
                      for k in range(K):
                          i = numpy.where(m == k)[0] # indeces of datapoints belonging to class k
                          E += numpy.mean(D[numpy.meshgrid(i,i)])
                      return E / K
                  numpy.random.seed(0) # repeatability
                  N, n = X.shape
                  if G is None and K is not None:
                      G = N // K # group size
                  elif K is None and G is not None:
                      K = N // G # number of clusters
                  else:
                      raise Exception('must specify either K or G')
                  D = squareform(pdist(X)) # distance matrix
                  m = numpy.random.permutation(N) % K # initial membership
                  E = error(K, m, D)
                  # visualization
                  #FFMpegWriter = ani.writers['ffmpeg']
                  #writer = FFMpegWriter(fps=15)
                  #fig = plt.figure()
                  #with writer.saving(fig, "ec.mp4", 100):
                  t = 1
                  while True:
                      E_p = E
                      for a in range(N): # systematically
                          for b in range(a):
                              m[a], m[b] = m[b], m[a] # exchange membership
                              E_t = error(K, m, D)
                              if E_t < E:
                                  E = E_t
                                  print("{}: {}<->{} E={}".format(t, a, b, E))
                                  #plt.clf()
                                  #for i in range(N):
                                      #plt.text(X[i,0], X[i,1], m[i])
                                  #writer.grab_frame()
                              else:
                                  m[a], m[b] = m[b], m[a] # put them back
                      if E_p == E:
                          break
                      t += 1           
                  fig, ax = plt.subplots()
                  patches = []
                  for k in range(K):
                      i = numpy.where(m == k)[0] # indeces of datapoints belonging to class k
                      x = X[i]        
                      patches.append(Polygon(x[:,:2], True)) # how to draw this clock-wise?
                      u = numpy.mean(x, 0)
                      plt.text(u[0], u[1], k)
                  p = PatchCollection(patches, alpha=0.5)        
                  ax.add_collection(p)
                  plt.show()
              
              if __name__ == "__main__":
                  N, n = 100, 2    
                  X = numpy.random.rand(N, n)
                  eqsc(X, G=3)
              

              【讨论】:

                【解决方案11】:

                相等大小的 k-means 是受约束的 k-means 过程的一种特殊情况,其中每个集群必须具有最少的点数。这个问题可以表述为一个图问题,其中节点是要聚类的点,每个点到每个质心都有一条边,其中边权重是到质心的欧几里得距离的平方。在这里讨论:

                Bradley PS、Bennett KP、Demiriz A (2000)、Constrained K-Means Clustering。微软研究院。

                可以使用 Python 实现 here

                【讨论】:

                  【解决方案12】:

                  2012 年 1 月添加: 比后处理更好的是保持集群大小 与它们的生长大致相同。
                  例如,为每个 X 找到 3 个最近的中心, 然后将 X 添加到最好的那个 距离 - λ 簇大小。


                  如果您的 k-means 聚类大小大致相等,则 k-means 之后的简单贪心后处理可能就足够了。
                  (这近似于来自 k-means 的 Npt x C 距离矩阵的分配算法。)

                  可以迭代

                  diffsizecentres = kmeans( X, centres, ... )
                  X_centre_distances = scipy.spatial.distance.cdist( X, diffsizecentres )
                      # or just the nearest few centres
                  xtoc = samesizeclusters( X_centre_distances )
                  samesizecentres = [X[xtoc[c]].mean(axis=0) for c in range(k)]
                  ...
                  

                  如果迭代大大改变了中心,我会感到惊讶, 但这取决于™。

                  关于你的 Npoint Ncluster 和 Ndim 有多大?

                  #!/usr/bin/env python
                  from __future__ import division
                  from operator import itemgetter
                  import numpy as np
                  
                  __date__ = "2011-03-28 Mar denis"
                  
                  def samesizecluster( D ):
                      """ in: point-to-cluster-centre distances D, Npt x C
                              e.g. from scipy.spatial.distance.cdist
                          out: xtoc, X -> C, equal-size clusters
                          method: sort all D, greedy
                      """
                          # could take only the nearest few x-to-C distances
                          # add constraints to real assignment algorithm ?
                      Npt, C = D.shape
                      clustersize = (Npt + C - 1) // C
                      xcd = list( np.ndenumerate(D) )  # ((0,0), d00), ((0,1), d01) ...
                      xcd.sort( key=itemgetter(1) )
                      xtoc = np.ones( Npt, int ) * -1
                      nincluster = np.zeros( C, int )
                      nall = 0
                      for (x,c), d in xcd:
                          if xtoc[x] < 0  and  nincluster[c] < clustersize:
                              xtoc[x] = c
                              nincluster[c] += 1
                              nall += 1
                              if nall >= Npt:  break
                      return xtoc
                  
                  #...............................................................................
                  if __name__ == "__main__":
                      import random
                      import sys
                      from scipy.spatial import distance
                      # http://docs.scipy.org/doc/scipy/reference/spatial.distance.html
                  
                      Npt = 100
                      C = 3
                      dim = 3
                      seed = 1
                  
                      exec( "\n".join( sys.argv[1:] ))  # run this.py N= ...
                      np.set_printoptions( 2, threshold=200, edgeitems=5, suppress=True )  # .2f
                      random.seed(seed)
                      np.random.seed(seed)
                  
                      X = np.random.uniform( size=(Npt,dim) )
                      centres = random.sample( X, C )
                      D = distance.cdist( X, centres )
                      xtoc = samesizecluster( D )
                      print "samesizecluster sizes:", np.bincount(xtoc)
                          # Npt=100 C=3 -> 32 34 34
                  

                  【讨论】:

                  • 没有大数字:Npoint=180; NCluster=Npoint/9=20; Ndim=2(地理地图,2D)
                  【解决方案13】:

                  还要查看对数据进行分区的 K-d 树,直到每个分区的成员小于作为算法输入的 BUCKET_SIZE。

                  这不会强制存储桶/分区的大小完全相同,但它们都会小于 BUCKET_SIZE。

                  【讨论】:

                    【解决方案14】:

                    我可以谦虚地建议您尝试这个项目ekmeans

                    一个 Java K-means 聚类实现,带有一个可选的特殊相等选项,该选项在集群上应用相等基数约束,同时尽可能保持空间内聚。

                    它还处于试验阶段,所以请注意known bugs

                    【讨论】:

                      【解决方案15】:

                      我也一直在努力解决这个问题。但是,我意识到我一直使用错误的关键字。如果您希望点结果成员的数量相同,则您正在进行分组,而不是再进行聚类。我终于能够使用简单的 python 脚本和 postgis 查询来解决问题。

                      例如,我有一个名为 tb_points 的表,它有 4000 个坐标点,你想把它分成 10 个相同大小的组,每个组包含 400 个坐标点。下面是表结构的例子

                      CREATE TABLE tb_points (
                        id SERIAL PRIMARY KEY,
                        outlet_id INTEGER,
                        longitude FLOAT,
                        latitide FLOAT,
                        group_id INTEGER
                      );
                      

                      那么你需要做的是:

                      1. 找到作为起点的第一个坐标
                      2. 查找距您的起点最近的坐标,按距离升序排列,将结果限制为您首选成员的数量(在本例中为 400)
                      3. 通过更新 group_id 列来更新结果
                      4. 对其余数据执行 10 次以上 3 步,其中 group_id 列仍为 NULL

                      这是python中的实现:

                      import psycopg2
                      
                      dbhost = ''
                      dbuser = ''
                      dbpass = ''
                      dbname = ''
                      dbport = 5432
                      
                      conn = psycopg2.connect(host = dbhost,
                             user = dbuser,
                             password = dbpass,
                             database = dbname,
                             port = dbport)
                      
                      def fetch(sql):
                          cursor = conn.cursor()
                          rs = None
                          try:
                              cursor.execute(sql)
                              rs = cursor.fetchall()
                          except psycopg2.Error as e:
                              print(e.pgerror)
                              rs = 'error'
                          cursor.close()
                          return rs
                      
                      def execScalar(sql):
                          cursor = conn.cursor()
                          try:
                              cursor.execute(sql)
                              conn.commit()
                              rowsaffected = cursor.rowcount
                          except psycopg2.Error as e:
                              print(e.pgerror)
                              rowsaffected = -1
                              conn.rollback()
                          cursor.close()
                          return rowsaffected
                      
                      
                      def select_first_cluster_id():
                          sql = """ SELECT a.outlet_id as ori_id, a.longitude as ori_lon,
                          a.latitude as ori_lat, b.outlet_id as dest_id, b.longitude as
                          dest_lon, b.latitude as dest_lat,
                          ST_Distance(CAST(ST_SetSRID(ST_Point(a.longitude,a.latitude),4326)
                          AS geography), 
                          CAST(ST_SetSRID(ST_Point(b.longitude,b.latitude),4326) AS geography))
                          AS air_distance FROM  tb_points a CROSS JOIN tb_points b WHERE
                          a.outlet_id != b.outlet_id and a.group_id is NULL and b.group_id is
                          null order by air_distance desc limit 1 """
                          return sql
                      
                      def update_group_id(group_id, ori_id, limit_constraint):
                          sql = """ UPDATE tb_points
                          set group_id = %s
                          where outlet_id in
                          (select b.outlet_id
                          from tb_points a,
                          tb_points b
                          where a.outlet_id = '%s'
                          and a.group_id is null
                          and b.group_id is null
                          order by ST_Distance(CAST(ST_SetSRID(ST_Point(a.longitude,a.latitude),4326) AS geography),
                          CAST(ST_SetSRID(ST_Point(b.longitude,b.latitude),4326) AS geography)) asc
                          limit %s)
                          """ % (group_id, ori_id, limit_constraint)
                          return sql
                      
                      def clustering():
                          data_constraint = [100]
                          n = 1
                          while n <= 10:
                              sql = select_first_cluster_id()
                              res = fetch(sql)
                              ori_id = res[0][0]
                      
                              sql = update_group_id(n, ori_id, data_constraint[0])
                              print(sql)
                              execScalar(sql)
                      
                              n += 1
                      
                      clustering()
                      

                      希望对你有帮助

                      【讨论】:

                        【解决方案16】:

                        在聚类分配期间,还可以在距离上添加“频率惩罚”,这使得高频聚类更难获得额外的分数。这在“频率敏感竞争学习 高维超球面上的平衡聚类 - Arindam Banerjee 和 Joydeep Ghosh - IEEE Transactions on Neural Networks"

                        http://www.ideal.ece.utexas.edu/papers/arindam04tnn.pdf

                        他们也有在线/流媒体版本。

                        【讨论】:

                          【解决方案17】:

                          我将答案中提供的大部分算法添加到存储库https://github.com/brand17/clusters_equal_size

                          最有效率的就是投票最多的答案。

                          我开发了一些其他算法(投票最多的仍然是最好的):

                          1. 我修改了迭代交换提议 - 通过识别和消除直接循环而不是仅仅交换(它提高了一点效率)

                          2. 我还开发了以下算法:通过有效地移动 Voronoi 图边界来迭代最近的质心对并在它们之间交换点,使点的数量相差不超过一个。

                          【讨论】:

                            【解决方案18】:

                            您想查看空间填充曲线,例如 z 曲线或希尔伯特曲线。您可以考虑使用空间填充曲线将 2 维问题简化为 1 维问题。虽然 sfc 索引只是二维数据的重新排序,而不是数据的完美聚类,但当解决方案不能满足完美聚类并且必须相当快地计算时,它会很有用。

                            【讨论】:

                            • 您能解释一下如何使用空间填充曲线来解决问题吗?
                            猜你喜欢
                            • 2012-06-20
                            • 2017-10-13
                            • 2016-12-18
                            • 2018-09-08
                            • 2017-01-27
                            • 2016-04-14
                            • 2016-11-26
                            • 2013-07-03
                            • 2018-04-06
                            相关资源
                            最近更新 更多