【问题标题】:Numpy - Clustering - Distance - VectorisationNumpy - 聚类 - 距离 - 向量化
【发布时间】:2016-02-08 00:36:02
【问题描述】:

我使用 sklearn Kmeans 对数据样本(400 k 样本,维度 = 205、200 个集群)进行了聚类。

我想知道,对于每个集群,集群中心与集群最远样本之间的最大距离,以便了解集群的“大小”。 这是我的代码:

import numpy as np
import scipy.spatial.distance as spd
diam = np.empty([200])
for i in range(200):
    diam[i] = spd.cdist(seed[np.newaxis, i, 1:], data[data[:, 0]==i][:,1:]).max()

“种子”是聚类中心(200x206)。 “种子”的第一列包含集群内的样本数(此处无关)。

“数据”是样本(400kx206)。数据的第一列包含集群编号。

问题:这是使用循环完成的(不是那么“numpy”)。是否可以“矢量化”它?

【问题讨论】:

  • 这实际上是相当合理的代码。与cdist 内部完成的计算量相比,您的 for 循环相对较小。由于cdist 是一个相当理想的速度增益,所以不太可能很大。
  • @Ophion - 可以避免重复的线性搜索data[:, 0]==i,将复杂度从 O(n**2) 降低到 O(n log(n)) 甚至 O(n) .
  • @moarningsun 是的,但什么是可能的和什么是可用的是两件不同的事情,特别是考虑到这是 O(n*m) 而不是 O(n^2) 和 n
  • @Ophion 在我的解决方案中对此进行了快速基准测试。
  • @Divakar 查看我在完整测试集上的更新答案。

标签: python arrays numpy vectorization


【解决方案1】:

我们可以更智能地编制索引并节省约 4 倍的成本。

首先让我们构建一些正确形状的数据:

seed = np.random.randint(0, 100, (200,206))
data = np.random.randint(0, 100, (4e5,206))
seed[:, 0] = np.arange(200)
data[:, 0] = np.random.randint(0, 200, 4e5)
diam = np.empty(200)

原答案时间:

%%timeit
for i in range(200):
    diam[i] = spd.cdist(seed[np.newaxis, i, 1:], data[data[:, 0]==i][:,1:]).max()

1 loops, best of 3: 1.35 s per loop

moarningsun 的回答:

%%timeit
seed_repeated = seed[data[:,0]]
dist_to_center = np.sqrt(np.sum((data[:,1:]-seed_repeated[:,1:])**2, axis=1))
diam = np.zeros(len(seed))
np.maximum.at(diam, data[:,0], dist_to_center)

1 loops, best of 3: 1.33 s per loop

迪瓦卡的回答:

%%timeit
data_sorted = data[data[:, 0].argsort()]
seed_ext = np.repeat(seed,np.bincount(data_sorted[:,0]),axis=0)
dists = np.sqrt(((data_sorted[:,1:] - seed_ext[:,1:])**2).sum(1))
shift_idx = np.append(0,np.nonzero(np.diff(data_sorted[:,0]))[0]+1)
diam_out = np.maximum.reduceat(dists,shift_idx)

1 loops, best of 3: 1.65 s per loop

正如我们所见,除了更大的内存占用之外,矢量化解决方案并没有真正获得任何好处。为了避免这种情况,我们需要回到原来的答案,这确实是做这些事情的正确方法,而是尝试减少索引量:

%%timeit
idx = data[:,0].argsort()
bins = np.bincount(data[:,0])
counter = 0
for i in range(200):
    data_slice = idx[counter: counter+bins[i]]
    diam[i] = spd.cdist(seed[None, i, 1:], data[data_slice, 1:]).max()
    counter += bins[i]

1 loops, best of 3: 281 ms per loop

仔细检查答案:

np.allclose(diam, dam_out)
True

这是假设 python 循环不好的问题。它们通常是,但并非在所有情况下都如此。

【讨论】:

    【解决方案2】:

    这是一种矢量化方法 -

    # Sort data w.r.t. col-0
    data_sorted = data[data[:, 0].argsort()]
    
    # Get counts of unique tags in col-0 of data and repeat seed accordingly. 
    # Thus, we would have an extended version of seed that matches data's shape.
    seed_ext = np.repeat(seed,np.bincount(data_sorted[:,0]),axis=0)
    
    # Get euclidean distances between extended seed version and sorted data
    dists = np.sqrt(((data_sorted[:,1:] - seed_ext[:,1:])**2).sum(1))
    
    # Get positions of shifts in col-0 of sorted data
    shift_idx = np.append(0,np.nonzero(np.diff(data_sorted[:,0]))[0]+1)
    
    # Final piece of puzzle is to get tag based maximum values from dists, 
    # where each tag is unique number in col-0 of data
    diam_out = np.maximum.reduceat(dists,shift_idx)
    

    运行时测试和验证输出 -

    定义函数:

    def loopy_cdist(seed,data):  # from OP's solution
        N = seed.shape[0]
        diam = np.empty(N)
        for i in range(N):
            diam[i]=spd.cdist(seed[np.newaxis,i,1:], data[data[:,0]==i][:,1:]).max()
        return diam
    
    def vectorized_repeat_reduceat(seed,data):  # from this solution
        data_sorted = data[data[:, 0].argsort()]
        seed_ext = np.repeat(seed,np.bincount(data_sorted[:,0]),axis=0)
        dists = np.sqrt(((data_sorted[:,1:] - seed_ext[:,1:])**2).sum(1))
        shift_idx = np.append(0,np.nonzero(np.diff(data_sorted[:,0]))[0]+1)
        return np.maximum.reduceat(dists,shift_idx)
    
    def vectorized_indexing_maxat(seed,data): # from @moarningsun's solution
        seed_repeated = seed[data[:,0]]
        dist_to_center = np.sqrt(np.sum((data[:,1:]-seed_repeated[:,1:])**2, axis=1))
        diam = np.zeros(len(seed))
        np.maximum.at(diam, data[:,0], dist_to_center)
        return diam
    

    验证输出:

    In [417]: # Inputs
         ...: seed = np.random.rand(20,20)
         ...: data = np.random.randint(0,20,(40000,20))
         ...: 
    
    In [418]: np.allclose(loopy_cdist(seed,data),vectorized_repeat_reduceat(seed,data))
    Out[418]: True
    
    In [419]: np.allclose(loopy_cdist(seed,data),vectorized_indexing_maxat(seed,data))
    Out[419]: True
    

    运行时:

    In [420]: %timeit loopy_cdist(seed,data)
    10 loops, best of 3: 35.9 ms per loop
    
    In [421]: %timeit vectorized_repeat_reduceat(seed,data)
    10 loops, best of 3: 28.9 ms per loop
    
    In [422]: %timeit vectorized_indexing_maxat(seed,data)
    10 loops, best of 3: 24.1 ms per loop
    

    【讨论】:

      【解决方案3】:

      与@Divakar 非常相似的想法,但无需排序:

      seed_repeated = seed[data[:,0]]
      dist_to_center = np.sqrt(np.sum((data[:,1:]-seed_repeated[:,1:])**2, axis=1))
      
      diam = np.zeros(len(seed))
      np.maximum.at(diam, data[:,0], dist_to_center)
      

      ufunc.at 虽然速度很慢,所以看看哪个更快会很有趣。

      【讨论】:

      • 智能移动,索引处理“重复”!
      猜你喜欢
      • 2017-05-03
      • 2012-01-28
      • 2016-01-10
      • 1970-01-01
      • 2022-06-16
      • 2015-03-12
      • 1970-01-01
      • 2021-12-18
      • 1970-01-01
      相关资源
      最近更新 更多