【问题标题】:N-D version of itertools.combinations in numpynumpy 中 itertools.combinations 的 N-D 版本
【发布时间】:2013-04-06 20:40:15
【问题描述】:

我想为 numpy 实现 itertools.combinations。基于this discussion,我有一个适用于一维输入的函数:

def combs(a, r):
    """
    Return successive r-length combinations of elements in the array a.
    Should produce the same output as array(list(combinations(a, r))), but 
    faster.
    """
    a = asarray(a)
    dt = dtype([('', a.dtype)]*r)
    b = fromiter(combinations(a, r), dt)
    return b.view(a.dtype).reshape(-1, r)

并且输出是有意义的:

In [1]: list(combinations([1,2,3], 2))
Out[1]: [(1, 2), (1, 3), (2, 3)]

In [2]: array(list(combinations([1,2,3], 2)))
Out[2]: 
array([[1, 2],
       [1, 3],
       [2, 3]])

In [3]: combs([1,2,3], 2)
Out[3]: 
array([[1, 2],
       [1, 3],
       [2, 3]])

但是,如果我可以将其扩展到 N 维输入,那将是最好的,其中额外的维度仅允许您一次快速地进行多个调用。所以,从概念上讲,如果combs([1, 2, 3], 2) 产生[1, 2], [1, 3], [2, 3],而combs([4, 5, 6], 2) 产生[4, 5], [4, 6], [5, 6],那么combs((1,2,3) and (4,5,6), 2) 应该产生[1, 2], [1, 3], [2, 3] and [4, 5], [4, 6], [5, 6],其中“and”只表示平行的行或列(无论哪个有意义)。 (对于其他维度也是如此)

我不确定:

  1. 如何使维度以与其他函数的工作方式一致的逻辑方式工作(例如一些 numpy 函数如何具有axis= 参数和默认轴 0。所以可能轴 0 应该是我正在合并,所有其他轴只代表并行计算?)
  2. 如何让上述代码与 ND 一起使用(现在我得到 ValueError: setting an array element with a sequence.
  3. 有没有更好的方法来做dt = dtype([('', a.dtype)]*r)

【问题讨论】:

    标签: python numpy combinatorics itertools


    【解决方案1】:

    案例 k = 2:np.triu_indices

    我已经使用perfplot 使用上述功能的许多变体测试了案例k = 2。毫无疑问,赢家是np.triu_indices,我现在看到,使用np.dtype([('', np.intp)] * 2) 数据结构即使对于像igraph.EdgeList 这样的奇特数据类型也能带来巨大的提升。

    from itertools import combinations, chain
    from scipy.special import comb
    import igraph as ig #graph library build on C
    import networkx as nx #graph library, pure Python
    
    def _combs(n):
        return np.array(list(combinations(range(n),2)))
    
    def _combs_fromiter(n): #@Jaime
        indices = np.arange(n)
        dt = np.dtype([('', np.intp)]*2)
        indices = np.fromiter(combinations(indices, 2), dt)
        indices = indices.view(np.intp).reshape(-1, 2)
        return indices
    
    def _combs_fromiterplus(n):
        dt = np.dtype([('', np.intp)]*2)
        indices = np.fromiter(combinations(range(n), 2), dt)
        indices = indices.view(np.intp).reshape(-1, 2)
        return indices
    
    def _numpy(n): #@endolith
        return np.transpose(np.triu_indices(n,1))
    
    def _igraph(n):
        return np.array(ig.Graph(n).complementer(False).get_edgelist())
    
    def _igraph_fromiter(n):
        dt = np.dtype([('', np.intp)]*2)
        indices = np.fromiter(ig.Graph(n).complementer(False).get_edgelist(), dt)
        indices = indices.view(np.intp).reshape(-1, 2)
        return indices
        
    def _nx(n):
        G = nx.Graph()
        G.add_nodes_from(range(n))
        return np.array(list(nx.complement(G).edges))
    
    def _nx_fromiter(n):
        G = nx.Graph()
        G.add_nodes_from(range(n))
        dt = np.dtype([('', np.intp)]*2)
        indices = np.fromiter(nx.complement(G).edges, dt)
        indices = indices.view(np.intp).reshape(-1, 2)
        return indices
    
    def _comb_index(n): #@HYRY
        count = comb(n, 2, exact=True)
        index = np.fromiter(chain.from_iterable(combinations(range(n), 2)), 
                            int, count=count*2)
        return index.reshape(-1, 2)
    
            
    fig = plt.figure(figsize=(15, 10))
    plt.grid(True, which="both")
    out = perfplot.bench(
            setup = lambda x: x,
            kernels = [_numpy, _combs, _combs_fromiter, _combs_fromiterplus, 
                       _comb_index, _igraph, _igraph_fromiter, _nx, _nx_fromiter],
            n_range = [2 ** k for k in range(12)],
            xlabel = 'combinations(n, 2)',
            title = 'testing combinations',
            show_progress = False,
            equality_check = False)
    out.show()
    

    想知道为什么np.triu_indices 不能扩展到更多维度吗?

    案例 2 ≤ k ≤ 4:triu_indices(在此处实现)= 最高 2 倍加速

    np.triu_indices 可能实际上是 k = 3 甚至 k = 4 的赢家,如果我们实施一个通用方法。此方法的当前版本相当于:

    def triu_indices(n, k):
        x = np.less.outer(np.arange(n), np.arange(-k+1, n-k+1))
        return np.nonzero(x)
    

    它为两个序列 0,1,...,n-1 构造关系 x 的矩阵表示,并找到它们不为零的单元格的位置。对于 3D 情况,我们需要添加额外的维度和相交关系 x 和 y 。对于下一维,过程是相同的,但这会产生巨大的内存过载,因为需要 n^k 个二进制单元,并且其中只有 C(n, k) 达到 True 值。内存使用量和性能以 O(n!) 增长,因此该算法仅在 k 值较小的情况下优于 itertools.combinations。这最好实际用于案例k=2k=3

    def C(n, k): #huge memory overload...
        if k==0:
            return np.array([])
        if k==1:
            return np.arange(1,n+1)
        elif k==2:
            return np.less.outer(np.arange(n), np.arange(n))
        else:
            x = C(n, k-1)
            X = np.repeat(x[None, :, :], len(x), axis=0)
            Y = np.repeat(x[:, :, None], len(x), axis=2)
            return X&Y
    
    def C_indices(n, k):
        return np.transpose(np.nonzero(C(n,k)))
    

    让我们结帐perfplot

    import matplotlib.pyplot as plt
    import numpy as np
    import perfplot
    from itertools import chain, combinations
    from scipy.special import comb
    
    def C(n, k):  # huge memory overload...
        if k == 0:
            return np.array([])
        if k == 1:
            return np.arange(1, n + 1)
        elif k == 2:
            return np.less.outer(np.arange(n), np.arange(n))
        else:
            x = C(n, k - 1)
            X = np.repeat(x[None, :, :], len(x), axis=0)
            Y = np.repeat(x[:, :, None], len(x), axis=2)
            return X & Y
    
    def C_indices(data):
        n, k = data
        return np.transpose(np.nonzero(C(n, k)))
    
    def comb_index(data):
        n, k = data
        count = comb(n, k, exact=True)
        index = np.fromiter(chain.from_iterable(combinations(range(n), k)),
                            int, count=count * k)
        return index.reshape(-1, k)
    
    def build_args(k):
        return {'setup': lambda x: (x, k),
                'kernels': [comb_index, C_indices],
                'n_range': [2 ** x for x in range(2, {2: 10, 3:10, 4:7, 5:6}[k])],
                'xlabel': f'N',
                'title': f'test of case C(N,{k})',
                'show_progress': True,
                'equality_check': lambda x, y: np.array_equal(x, y)}
    
    outs = [perfplot.bench(**build_args(n)) for n in (2, 3, 4, 5)]
    fig = plt.figure(figsize=(20, 20))
    for i in range(len(outs)):
        ax = fig.add_subplot(2, 2, i + 1)
        ax.grid(True, which="both")
        outs[i].plot()
    plt.show()
    

    因此,k=2 实现了最佳性能提升(相当于 np.triu_indices) and for k=3`,它几乎快两倍。

    案例 k > 3:numpy_combinations(在此处实现)= 最高 2.5 倍加速

    按照this question(感谢@Divakar)我设法找到了一种方法来根据前一列和帕斯卡三角形计算特定列的值。它还没有尽可能地优化,但结果确实很有希望。我们开始:

    from scipy.linalg import pascal
    
    def stretch(a, k):
        l = a.sum()+len(a)*(-k)
        out = np.full(l, -1, dtype=int)
        out[0] = a[0]-1
        idx = (a-k).cumsum()[:-1]
        out[idx] = a[1:]-1-k
        return out.cumsum()
    
    def numpy_combinations(n, k):
        #n, k = data #benchmark version
        n, k = data
        x = np.array([n])
        P = pascal(n).astype(int)
        C = []
        for b in range(k-1,-1,-1):
            x = stretch(x, b)
            r = P[b][x - b]
            C.append(np.repeat(x, r))
        return n - 1 - np.array(C).T
    

    基准测试结果是:

    # script is the same as in previous example except this part
    def build_args(k):
    return {'setup': lambda x: (k, x),
            'kernels': [comb_index, numpy_combinations],
            'n_range': [x for x in range(1, k)],
            'xlabel': f'N',
            'title': f'test of case C({k}, k)',
            'show_progress': True,
            'equality_check': False}
    outs = [perfplot.bench(**build_args(n)) for n in (12, 15, 17, 23, 25, 28)]
    fig = plt.figure(figsize=(20, 20))
    for i in range(len(outs)):
        ax = fig.add_subplot(2, 3, i + 1)
        ax.grid(True, which="both")
        outs[i].plot()
    plt.show()
    

    尽管它仍然无法与itertools.combinations 争夺n < 15,但在其他情况下它是一个新的赢家。最后但同样重要的是,numpy 在组合数量变得非常大时展示了它的强大功能。它能够在处理大约 40'000'000 个大小为 14 的 C(28, 14) 组合时存活

    【讨论】:

      【解决方案2】:

      r = k = 2 时,还可以使用 numpy.triu_indices(n, 1) 索引矩阵的上三角形。

      idx = comb_index(5, 2)
      

      来自HYRY's answer 等价于

      idx = np.transpose(np.triu_indices(5, 1))
      

      但是是内置的,并且 N 高于 ~20 时要快几倍:

      timeit comb_index(1000, 2)
      32.3 ms ± 443 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
      
      timeit np.transpose(np.triu_indices(1000, 1))
      10.2 ms ± 25.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
      

      【讨论】:

      • 应该是最佳答案
      • 哇,我曾经在 np.triu 中使用 np.where 找到这些索引,然后发现性能不足。这解决了我的问题。
      【解决方案3】:

      你可以使用itertools.combinations()创建索引数组,然后使用NumPy的花式索引:

      import numpy as np
      from itertools import combinations, chain
      from scipy.special import comb
      
      def comb_index(n, k):
          count = comb(n, k, exact=True)
          index = np.fromiter(chain.from_iterable(combinations(range(n), k)), 
                              int, count=count*k)
          return index.reshape(-1, k)
      
      data = np.array([[1,2,3,4,5],[10,11,12,13,14]])
      
      idx = comb_index(5, 3)
      print(data[:, idx])
      

      输出:

      [[[ 1  2  3]
        [ 1  2  4]
        [ 1  2  5]
        [ 1  3  4]
        [ 1  3  5]
        [ 1  4  5]
        [ 2  3  4]
        [ 2  3  5]
        [ 2  4  5]
        [ 3  4  5]]
      
       [[10 11 12]
        [10 11 13]
        [10 11 14]
        [10 12 13]
        [10 12 14]
        [10 13 14]
        [11 12 13]
        [11 12 14]
        [11 13 14]
        [12 13 14]]]
      

      【讨论】:

      • chain.from_iterable 是干什么用的?
      • @endolith:哦,我明白了。它消除了对dt = np.dtype... 的需求,而且似乎也使这个版本比 Jaime 的版本更快。
      • 刚刚花了很长时间试图找到解决方案,并意识到我在 2013 年已经做到了。我希望这被内置到 numpy 中!
      • 我想编译的 C 版本的 itertools 组合函数输出 numpy 索引会比这快得多。
      【解决方案4】:

      不确定它在性能方面的表现如何,但您可以对索引数组进行组合,然后使用 np.take 提取实际的数组切片:

      def combs_nd(a, r, axis=0):
          a = np.asarray(a)
          if axis < 0:
              axis += a.ndim
          indices = np.arange(a.shape[axis])
          dt = np.dtype([('', np.intp)]*r)
          indices = np.fromiter(combinations(indices, r), dt)
          indices = indices.view(np.intp).reshape(-1, r)
          return np.take(a, indices, axis=axis)
      
      >>> combs_nd([1,2,3], 2)
      array([[1, 2],
             [1, 3],
             [2, 3]])
      >>> combs_nd([[1,2,3],[4,5,6]], 2, axis=1)
      array([[[1, 2],
              [1, 3],
              [2, 3]],
      
             [[4, 5],
              [4, 6],
              [5, 6]]])
      

      【讨论】:

      • 那么np.dtype([('', np.intp)]*r) 是创建列表数据类型的“正确”方式吗?我只是有点刺痛它直到它起作用。
      • 非常酷!我发现这比@HYRY 的解决方案性能稍差(在速度和内存方面),但它仍然比直接使用 itertools.combinations 更好。
      猜你喜欢
      • 2019-03-29
      • 1970-01-01
      • 2021-01-14
      • 2019-12-13
      • 1970-01-01
      • 2018-01-04
      • 2021-04-16
      • 2021-01-13
      • 2017-06-27
      相关资源
      最近更新 更多