在cython中生成高斯随机数的最有效和最便携的方法是什么?

时间:2017-03-13 15:45:12

标签: python performance numpy cython scientific-computing

我正在编写一个cython应用程序,我需要在紧密的嵌套循环中即时生成高斯随机变量。我想这样做而不引入任何额外的依赖,例如,在GSL上。

对于我目前能够使用统一随机数字实时执行此操作的最小版本:

from libc.stdlib cimport rand, RAND_MAX
import numpy as np

cdef double random_uniform():
    cdef double r = rand()
    return r/RAND_MAX

def my_function(int n):
    cdef int i
    cdef double[:] result = np.zeros(n, dtype='f8', order='C')
    for i in range(n):
        result[i] = random_uniform()
    return result

上面的代码在功能上等同于numpy.random.rand(n),并且可以使用以下最小安装文件进行编译:

from distutils.core import setup
from Cython.Build import cythonize
import numpy as np

setup(ext_modules=cythonize("example.pyx"), include_dirs=[np.get_include()])

# compile instructions:
# python setup.py build_ext --inplace

要回答这个问题,我正在寻找的是与np.random.randn(n)的功能等价物相同的最小解决方案,理想情况下,出于可移植性的原因,可以直接从libc.stdlib中导入任何依赖关系。

the Wikipedia entry for the Box-Muller algorithm上有一个示例实现,但由于定义了常量epsilon的方式,我无法实现它。

2 个答案:

答案 0 :(得分:1)

您说由于他们定义espilon的方式,您在实施Box-Muller转换时遇到问题:

const double epsilon = std::numeric_limits<double>::min();

According to here这是C等价物:

const double lowest_double = -DBL_MAX;

所以要在Cython中获得正确的导入:

from libc.float import DBL_MAX #it should still be portable btw.

现在应该解决epsilon的问题。

答案 1 :(得分:1)

我创建了一个函数,它根据Box-Muller变换的极坐标版本生成高斯分布的随机数,如伪代码here所述。 (维基百科页面也描述了这个版本,但没有提供任何方便的伪代码。)

此方法一次生成两个高斯分布的随机数。这意味着要获得全速cython速度,我们需要找到一种方法来传递两个数字而不将它们转换为Python对象。这样做最直接的方法(我能想到的)是将缓冲区传递给生成器进行直接操作。那是my_gaussian_fast的作用,它以适度的余量击败numpy

from libc.stdlib cimport rand, RAND_MAX
from libc.math cimport log, sqrt
import numpy as np
import cython

cdef double random_uniform():
    cdef double r = rand()
    return r / RAND_MAX

cdef double random_gaussian():
    cdef double x1, x2, w

    w = 2.0
    while (w >= 1.0):
        x1 = 2.0 * random_uniform() - 1.0
        x2 = 2.0 * random_uniform() - 1.0
        w = x1 * x1 + x2 * x2

    w = ((-2.0 * log(w)) / w) ** 0.5
    return x1 * w

@cython.boundscheck(False)
cdef void assign_random_gaussian_pair(double[:] out, int assign_ix):
    cdef double x1, x2, w

    w = 2.0
    while (w >= 1.0):
        x1 = 2.0 * random_uniform() - 1.0
        x2 = 2.0 * random_uniform() - 1.0
        w = x1 * x1 + x2 * x2

    w = sqrt((-2.0 * log(w)) / w)
    out[assign_ix] = x1 * w
    out[assign_ix + 1] = x2 * 2

@cython.boundscheck(False)
def my_uniform(int n):
    cdef int i
    cdef double[:] result = np.zeros(n, dtype='f8', order='C')
    for i in range(n):
        result[i] = random_uniform()
    return result

@cython.boundscheck(False)
def my_gaussian(int n):
    cdef int i
    cdef double[:] result = np.zeros(n, dtype='f8', order='C')
    for i in range(n):
        result[i] = random_gaussian()
    return result

@cython.boundscheck(False)
def my_gaussian_fast(int n):
    cdef int i
    cdef double[:] result = np.zeros(n, dtype='f8', order='C')
    for i in range(n // 2):  # Int division ensures trailing index if n is odd.
        assign_random_gaussian_pair(result, i * 2)
    if n % 2 == 1:
        result[n - 1] = random_gaussian()

    return result

测试。这是一个统一的基准:

In [3]: %timeit numpy.random.uniform(size=10000)
10000 loops, best of 3: 130 µs per loop

In [4]: %timeit numpy.array(example.my_uniform(10000))
10000 loops, best of 3: 85.4 µs per loop

对于普通随机数,这肯定比numpy快。如果我们对它很聪明,那么高斯随机数也会更快:

In [5]: %timeit numpy.random.normal(size=10000)
1000 loops, best of 3: 393 µs per loop

In [6]: %timeit numpy.array(example.my_gaussian(10000))
1000 loops, best of 3: 542 µs per loop

In [7]: %timeit numpy.array(example.my_gaussian_fast(10000))
1000 loops, best of 3: 266 µs per loop

Robert Kern确认后,numpy使用生成的两个值。 my_gaussian抛出一个; my_gaussian_fast使用两者并快速存储它们。 (请参阅此答案的历史,了解试图以缓慢方式返回该对的天真my_gaussian_pair。)