【问题标题】:How to optimize a for loop that uses consecutive values with Numpy?如何优化使用 Numpy 连续值的 for 循环?
【发布时间】:2019-02-25 10:23:51
【问题描述】:

我正在尝试创建一个返回numpy.arrayn 0 到1 之间均匀分布的伪随机数的函数。所用方法的详细信息可以在这里找到:https://en.wikipedia.org/wiki/Linear_congruential_generator

到目前为止,效果很好。唯一的问题是每个新值都是使用前一个值计算的,所以到目前为止我发现的唯一解决方案是使用循环,为了提高效率,我试图摆脱那个循环,可能通过矢量化操作 - 但是,我不知道该怎么做。

您对如何优化此功能有什么建议吗?

import numpy as np
import time

def unif(n):
    m = 2**32
    a = 1664525
    c = 1013904223

    result = np.empty(n)
    result[0] = int((time.time() * 1e7) % m)

    for i in range(1,n):
        result[i] = (a*result[i-1]+c) % m

    return result / m

【问题讨论】:

  • 我认为这不可能。看看这里的答案:stackoverflow.com/questions/44455481/…
  • 仅供参考:如果您坚持使用 m=2**32,实际上有一个非常 (90x) 快速、完全矢量化的 numpy 解决方案。我已经更新了我的答案。

标签: python numpy vectorization


【解决方案1】:

更新:

利用 2^32 的模数,我们可以消除所有 Python 循环并获得 ~91.1 的加速。

允许任何模数仍然可以将线性长度循环减少为对数长度循环。对于500,000 样本,这给出了~17.1 的加速。如果我们预先计算多步因子和偏移量(它们对于任何种子都是相同的),则它会上升到 ~44.8

代码:

import numpy as np
import time

def unif(n, seed):
    m = 2**32
    a = 1664525
    c = 1013904223

    result = np.empty(n)
    result[0] = seed

    for i in range(1,n):
        result[i] = (a*result[i-1]+c) % m

    return result / m

def precomp(n):
    l = n.bit_length()
    a, c = np.empty((2, 1+(1<<l)), np.uint64)
    m = 2**32
    a[:2] = 1, 1664525
    c[:2] = 0, 1013904223

    p = 1
    for j in range(l):
        a[1+p:1+(p<<1)] = a[p] * a[1:1+p] % m
        c[1+p:1+(p<<1)] = (a[p] * c[1:1+p] + c[p]) % m
        p <<= 1

    return a, c

def unif_opt(n, seed, a=None, c=None):
    if a is None:
        a, c = precomp(n)
    return (seed * a[:n] + c[:n]) % m / m

def unif_32(n, seed):
    out = np.empty((n,), np.uint32)
    out[0] = 1
    np.broadcast_to(np.uint32(1664525), (n-1,)).cumprod(out=out[1:])
    c = out[:-1].cumsum(dtype=np.uint32)
    c *= 1013904223
    out *= seed
    out[1:] += c
    return out / m

m = 2**32
seed = int((time.time() * 1e7) % m)
n = 500000
a, c = precomp(n)

print('results equal:', np.allclose(unif(n, seed), unif_opt(n, seed)) and 
      np.allclose(unif_opt(n, seed), unif_opt(n, seed, a, c)) and
      np.allclose(unif_32(n, seed), unif_opt(n, seed, a, c)))

from timeit import timeit

t = timeit('unif(n, seed)', globals=globals(), number=10)
t_opt = timeit('unif_opt(n, seed)', globals=globals(), number=10)
t_prc = timeit('unif_opt(n, seed, a, c)', globals=globals(), number=10)
t_32 = timeit('unif_32(n, seed)', globals=globals(), number=10)
print(f'speedup without precomp: {t/t_opt:.1f}')
print(f'speedup with precomp:    {t/t_prc:.1f}')
print(f'speedup special case:    {t/t_32:.1f}')

示例运行:

results equal: True
speedup without precomp: 17.1
speedup with precomp:    44.8
speedup special case:    91.1

【讨论】:

    【解决方案2】:

    虽然没有矢量化,但我相信下面的解决方案大约快 2 倍(使用 numba 解决方案快 60 倍)。它将每个 result 保存为局部变量,而不是按位置访问 numpy 数组。

    def unif_improved(n):
        m = 2**32
        a = 1664525
        c = 1013904223
    
        results = np.empty(n)
        results[0] = result = int((time.time() * 1e7) % m)
    
        for i in range(1, n):
            result = results[i] = (a * result + c) % m
    
        return results / m
    

    您也可以考虑使用 Numba 来进一步提高速度。 https://numba.pydata.org/

    只需添加装饰器 @jit 就可以摆脱其他解决方案。

    from numba import jit
    
    @jit
    def unif_jit(n):
        # Same code as `unif_improved`
    

    时间

    >>> %timeit -n 10 unif_original(500000)
    715 ms ± 21.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
    
    >>> %timeit -n 10 unif_improved(500000)
    323 ms ± 8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
    
    >>> %timeit -n 10 unif_jit(500000)
    12 ms ± 2.68 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
    

    【讨论】:

    • 除非其他人来这里进行另一项改进,否则我想我会坚持使用您的代码。感谢您的提示!
    【解决方案3】:

    这是不可能完全做到的,因为答案是按顺序相互依赖的。模块化算术的魔力确实意味着您可以通过以下更改获得小幅改进(根据@Alexander 的建议修改为使用局部变量而不是数组查找)。

    def unif_2(n):
        m = 2**32
        a = 1664525
        c = 1013904223
    
        results = np.empty(n)
        results[0] = result = int((time.time() * 1e7) % m)
    
        for i in range(1, n):
            result = results[i] = (a * result + c)
    
        return results % m / m
    

    【讨论】:

    • 在针对 Alexander 的版本测试了这个版本之后,由于某种原因,这个算法的效率降低了。当使用 n = 1e8 调用时,这个版本的效率比我 PC 上的另一个版本低大约 0.5 秒。不过,谢谢你的提示!
    猜你喜欢
    • 2021-11-22
    • 2011-06-15
    • 1970-01-01
    • 2022-11-03
    • 2016-07-14
    • 1970-01-01
    • 2010-11-15
    • 2021-06-22
    相关资源
    最近更新 更多