【问题标题】:(memory-)efficient operations between arbitrary columns of numpy arraynumpy 数组的任意列之间的(内存)高效操作
【发布时间】:2019-07-25 09:57:24
【问题描述】:

我有一个大型 2D numpy 数组。我希望能够在不复制数据的情况下对列的子集高效地运行逐行操作。

接下来, a = np.arange(1000000).reshape(1000, 10000)columns = np.arange(1, 1000, 2)。供参考,

In [4]: %timeit a.sum(axis=1)
7.26 ms ± 431 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

我知道的方法是:

  1. 带有列列表的精美索引
In [5]: %timeit a[:, columns].sum(axis=1)
42.5 ms ± 197 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
  1. 带有列掩码的精美索引
In [6]: cols_mask = np.zeros(10000, dtype=bool)
   ...: cols_mask[columns] = True                                                                                                                                                                                                                                                                                             

In [7]: %timeit a[:, cols_mask].sum(axis=1)
42.1 ms ± 302 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
  1. 屏蔽数组
In [8]: cells_mask = np.ones((1000, 10000), dtype=bool)

In [9]: cells_mask[:, columns] = False

In [10]: am = np.ma.masked_array(a, mask=cells_mask)

In [11]: %timeit am.sum(axis=1)
80 ms ± 2.71 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
  1. python 循环
In [12]: %timeit sum([a[:, i] for i in columns])
31.2 ms ± 531 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

让我有些惊讶的是,最后一种方法是最有效的:此外,它避免了复制完整数据,这对我来说是一个先决条件。但是,它仍然比简单求和慢得多(数据大小翻倍),最重要的是,它可以推广到其他操作(例如,cumsum)。

有什么我缺少的方法吗?我可以编写一些 cython 代码,但我希望该方法适用于任何 numpy 函数,而不仅仅是 sum

【问题讨论】:

    标签: python arrays numpy cython


    【解决方案1】:

    至少在我的装备上,pythran 似乎比 numba 快一点:

    import numpy as np
    
    #pythran export col_sum(float[:,:], int[:])
    #pythran export col_sum(int[:,:], int[:])
    
    def col_sum(data, idx):
        return data.T[idx].sum(0)
    

    pythran <filename.py>编译

    时间安排:

    timeit(lambda:cs_pythran.col_sum(a, columns),number=1000)
    # 1.644187423051335
    timeit(lambda:cs_numba.col_sum(a, columns),number=1000)
    # 2.635075871949084
    

    【讨论】:

    • 不是 numba 专家,但是,是的,也许这可以调整
    【解决方案2】:

    如果您想击败 c 编译的块求和,最好使用 numba。任何保留在 python 中的索引(numba 使用jit 创建 c 编译函数)都会产生 python 开销。

    from numba import jit
    
    @jit
    def col_sum(block, idx):
        return block[:, idx].sum(1)
    
    %timeit a.sum(axis=1)
    100 loops, best of 3: 5.25 ms per loop
    
    %timeit a[:, columns].sum(axis=1)
    100 loops, best of 3: 7.24 ms per loop
    
    %timeit col_sum(a, columns)
    100 loops, best of 3: 2.46 ms per loop
    

    【讨论】:

    • 感谢您的回复...不幸的是,在我的设置中,这仍然比 sum([a[:, i] for i in columns]) 慢(33 毫秒 vs. 30) - 这很奇怪,因为在您的设置中它比简单的总和,在我的设置中比其他任何东西都快!也许您使用了不同的idx
    • 我使用了与您的问题规范完全相同的columns
    【解决方案3】:

    您可以使用 Numba。为了获得最佳性能,通常需要像在 C 中那样编写简单的循环。 (Numba 基本上是一个 Python 到 LLVM-IR 的代码翻译器,很像 C 的 Clang)

    代码

    import numpy as np
    import numba as nb
    @nb.njit(fastmath=True,parallel=True)
    def row_sum(arr,columns):
        res=np.empty(arr.shape[0],dtype=arr.dtype)
        for i in nb.prange(arr.shape[0]):
            sum=0.
            for j in range(columns.shape[0]):
                sum+=arr[i,columns[j]]
            res[i]=sum
        return res
    

    时间安排

    a = np.arange(1_000_000).reshape(1_000, 1_000)
    columns = np.arange(1, 1000, 2)
    
    %timeit res_1=a[:, columns].sum(axis=1)
    1.29 ms ± 8.05 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    
    %timeit res_2=row_sum(a,columns)
    59.3 µs ± 4.35 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
    
    np.allclose(res_1,res_2)
    True
    

    【讨论】:

    • 这令人印象深刻:它比其他解决方案少得多,甚至比简单(完整)总和还要少。然而,写下“简单循环”的需要使其难以概括(例如,a[:, columns].cumsum(axis=1),或numpy 已知的任何其他ufunc):有什么明显的解决方案吗?顺便说一句,添加不同的列在概念上与在内存中的随机位置添加向量没有区别,但是如果我重写row_sum 以直接接受(相同列的副本)数组的nb.typed.List,结果会差 40 倍。有更好的方法吗?
    • @Pietro Battiston 支持许多 ufunc,但不是全部。主要问题是所有 numpy ufunc(BLAS 调用除外)都被几个循环替换。编译器试图优化它,这有时有效,有时无效。使用列表numba.pydata.org/numba-doc/latest/reference/… 计划进行很多更改。通常我会尽可能避免使用列表,至少在性能很重要的代码中是这样。
    • 是的,我已经看到了弃用通知......事实上,在我的 numba (0.45.0) 版本中,typed.List 存在,我使用了它;但我无法声明其内容的类型 - 性能仍然很低。
    【解决方案4】:

    使用 Transonic (https://transonic.readthedocs.io),可以轻松编写可由不同 Python 加速器(实际上是 Cython、Pythran 和 Numba)加速的代码。

    例如,使用boost 装饰器,可以编写

    import numpy as np
    
    from transonic import boost
    
    T0 = "int[:, :]"
    T1 = "int[:]"
    
    
    @boost
    def row_sum_loops(arr: T0, columns: T1):
        # locals type annotations are used only by Cython
        i: int
        j: int
        sum_: int
        res: "int[]" = np.empty(arr.shape[0], dtype=arr.dtype)
        for i in range(arr.shape[0]):
            sum_ = 0
            for j in range(columns.shape[0]):
                sum_ += arr[i, columns[j]]
            res[i] = sum_
        return res
    
    
    @boost
    def row_sum_transpose(arr: T0, columns: T1):
        return arr.T[columns].sum(0)
    

    在我的电脑上,我获得:

    TRANSONIC_BACKEND="python" python row_sum_boost.py
    Checks passed: results are consistent
    Python
    row_sum_loops        108.57 s
    row_sum_transpose    1.38
    
    TRANSONIC_BACKEND="cython" python row_sum_boost.py
    Checks passed: results are consistent
    Cython
    row_sum_loops        0.45 s
    row_sum_transpose    1.32 s
    
    TRANSONIC_BACKEND="numba" python row_sum_boost.py
    Checks passed: results are consistent
    Numba
    row_sum_loops        0.27 s
    row_sum_transpose    1.16 s
    
    TRANSONIC_BACKEND="pythran" python row_sum_boost.py
    Checks passed: results are consistent
    Pythran
    row_sum_loops        0.27 s
    row_sum_transpose    0.76 s
    
    

    请参阅https://transonic.readthedocs.io/en/stable/examples/row_sum/txt.html 了解完整代码以及此问题示例的更完整比较。

    请注意,Pythran 使用 transonic.jit 装饰器也非常高效。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-07-06
      • 2015-09-30
      • 2015-11-08
      • 2023-03-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-12
      相关资源
      最近更新 更多