对数伽玛函数的快速算法

时间:2019-02-24 10:33:52

标签: python performance math scipy gamma-function

我正在尝试编写一种快速算法来计算log gamma function。目前,我的实现似乎还很幼稚,只是迭代了1000万次来计算gamma函数的对数(我也使用numba来优化代码)。

import numpy as np
from numba import njit
EULER_MAS = 0.577215664901532 # euler mascheroni constant
HARMONC_10MIL = 16.695311365860007 # sum of 1/k from 1 to 10,000,000

@njit(fastmath=True)
def gammaln(z):
"""Compute log of gamma function for some real positive float z"""
    out = -EULER_MAS*z - np.log(z) + z*HARMONC_10MIL
    n = 10000000 # number of iters
    for k in range(1,n+1,4):
        # loop unrolling
        v1 = np.log(1 + z/k)
        v2 = np.log(1 + z/(k+1))
        v3 = np.log(1 + z/(k+2))
        v4 = np.log(1 + z/(k+3))
        out -= v1 + v2 + v3 + v4

    return out

我将代码计时为scipy.special.gammaln实现,而我的速度实际上慢了100,000倍。因此,我正在做一些非常错误或非常幼稚的操作(可能两者兼而有之)。尽管与scipy相比,我的回答至少在最差的小数点后4位内是正确的。

我试图阅读实现scipy的gammaln函数的_ufunc代码,但是我不理解_gammaln函数所写入的cython代码。

有没有更快,更优化的方法来计算对数伽马函数?我怎么能理解scipy的实现,以便将其与我的结合起来?

3 个答案:

答案 0 :(得分:2)

您的函数的运行时将随着迭代次数线性扩展(最多保持不变的开销)。因此,减少迭代次数是加快算法的关键。尽管事先计算HARMONIC_10MIL是一个聪明的主意,但实际上在截断序列时会导致准确性降低;仅计算该系列的一部分即可提供更高的准确性。

下面的代码是上面发布的代码的修改版本(尽管使用cython而不是numba)。

from libc.math cimport log, log1p
cimport cython
cdef:
    float EULER_MAS = 0.577215664901532 # euler mascheroni constant

@cython.cdivision(True)
def gammaln(float z, int n=1000):
    """Compute log of gamma function for some real positive float z"""
    cdef:
        float out = -EULER_MAS*z - log(z)
        int k
        float t
    for k in range(1, n):
        t = z / k
        out += t - log1p(t)

    return out

如下图所示,即使经过100次逼近也可以获得近似值。

this question

在100次迭代中,其运行时间与scipy.special.gammaln处于相同的数量级:

%timeit special.gammaln(5)
# 932 ns ± 19 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit gammaln(5, 100)
# 1.25 µs ± 20.3 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

剩下的问题当然是要使用多少次迭代。函数log1p(t)可以扩展为小t的泰勒级数(与大k的限制有关)。特别是

log1p(t) = t - t ** 2 / 2 + ...

使得对于大k,总和的自变量变为

t - log1p(t) = t ** 2 / 2 + ...

因此,在t中,总和的自变量为零直至第二阶,如果t足够小,则可以忽略不计。换句话说,迭代次数应至少与z一样大,最好至少大一个数量级。

但是,如果可能的话,我会坚持使用scipy经过良好测试的实现。

答案 1 :(得分:0)

通过尝试使用numba的并行模式并使用大多数矢量化函数,我设法将性能提高了大约3倍(可悲的是,numba无法理解numpy.substract.reduce

from functools import reduce
import numpy as np
from numba import njit

@njit(fastmath=True, parallel=True)
def gammaln_vec(z):
    out = -EULER_MAS*z - np.log(z) + z*HARMONC_10MIL
    n = 10000000

    v = np.log(1 + z/np.arange(1, n+1))

    return out-reduce(lambda x1, x2: x1-x2, v, 0)

时间:

#Your function:
%timeit gammaln(1.5)
48.6 ms ± 1.23 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

#My function:
%timeit gammaln_vec(1.5)
15 ms ± 340 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

#scpiy's function
%timeit gammaln_sp(1.5)
1.07 µs ± 18.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

因此,通过使用scipy的功能,您的状况会更好。没有C代码,我不知道如何进一步分解

答案 2 :(得分:0)

关于您之前的问题,我猜想将scipy.special函数包装到Numba的示例也很有用。

示例

包装Cython cdef函数非常容易且可移植,只要只涉及简单的数据类型(int,double,double *,...)。有关如何调用scipy.special函数have a look at this的文档。实际包装功能所需的功能名称位于scipy.special.cython_special.__pyx_capi__中。可以使用不同的数据类型调用的函数名称被弄乱了,但是确定正确的名称很容易(只需查看数据类型)

#slightly modified version of https://github.com/numba/numba/issues/3086
from numba.extending import get_cython_function_address
from numba import vectorize, njit
import ctypes
import numpy as np

_PTR = ctypes.POINTER
_dble = ctypes.c_double
_ptr_dble = _PTR(_dble)

addr = get_cython_function_address("scipy.special.cython_special", "gammaln")
functype = ctypes.CFUNCTYPE(_dble, _dble)
gammaln_float64 = functype(addr)

@njit
def numba_gammaln(x):
  return gammaln_float64(x)

在Numba中使用

#Numba example with loops
import numba as nb
import numpy as np
@nb.njit()
def Test_func(A):
  out=np.empty(A.shape[0])
  for i in range(A.shape[0]):
    out[i]=numba_gammaln(A[i])
  return out

时间

data=np.random.rand(1_000_000)
Test_func(A): 39.1ms
gammaln(A):   39.1ms

当然,您可以轻松地并行化此函数,并且在scipy中优于单线程gammaln实现,并且可以在任何Numba编译函数中有效地调用此函数。

相关问题