在scipy.optimize.leastsq中使用预分配的缓冲存储器拟合函数

时间:2013-01-18 17:06:35

标签: python memory-management numpy scipy data-fitting

我正在尝试将scipy.optimize.leastsq与fit函数一起使用,该函数使用预分配的内存来存储残差。我有数百万的非线性拟合,时间至关重要。我在C编码了fit函数,我注意到当fit函数简单地返回对作为输入获得的缓冲区内存的引用时,scipy.optimize.leastsq无法正常工作。当我注意到问题可以在纯Py_INCREF中重现时,我以为我已经搞砸了Python s。这里有一些模拟代码来说明问题(实际问题有不同的拟合函数,而且要复杂得多):

import numpy as np
import scipy.optimize as opt

# the mock-up data to be fitted
x = np.arange(5.)
noise = np.array([0.0, -0.02, 0.01, -0.03, 0.01])
y = np.exp(-x) + noise

# preallocate the buffer memory
buf = np.empty_like(x)

# fit function writing residuals to preallocated memory and returning a reference 
def dy(p, x, buf, y):
    buf[:] = p[0]*np.exp(-p[1]*x) - y
    return buf

# starting values (true values are [1., 1.])
p0 = np.array([1.2, 0.8])

# leastsq with dy(): DOESN'T WORK CORRECTLY
opt.leastsq(dy, p0, args=(x, buf, y))
# -> (array([ 1.2,  0.8]), 4)

为了使其正常工作,我必须将fit函数包装成一个复制函数:

# wrapper that calls dy() and returns a copy of the array
def dy2(*args): 
    return dy(*args).copy()

# leastsq with dy2(): WORKS CORRECTLY
opt.leastsq(dy2, p0, args=(x, buf, y))
# -> (array([ 0.99917134,  1.03603201]), 1)

...但是制作副本显然首先使用缓冲内存!作为旁注,opt.fmin也适用于缓冲存储器(但实际上对我的应用来说太慢了):

def sum2(p, x, buf, y):
    dy(p, x, buf, y)
    return buf.dot(buf)

opt.fmin(sum2, p0, args=(x, buf, y))
# Optimization terminated successfully.
#     Current function value: 0.001200
#     Iterations: 32
#     Function evaluations: 63
# -> array([ 0.99915812,  1.03600273])

上述示例中scipy.optimize.leastsq使用dy2()而不是dy()的任何想法?

1 个答案:

答案 0 :(得分:2)

好的,我认为这就是这里发生的事情:基础FORTRAN例程LMDIF*fvec中显示用户定义的带内存的函数,其中存储结果。这个指针可能指向临时内存,因为LMDIF需要缓存几个函数求值的结果来估计雅可比行列式。

由于从Python调用了用户定义的函数,*fvec中的内存无法直接使用,因此包装器raw_multipack_lm_function()首先通过评估Python函数来工作然后然后将结果复制到*fvec。在输入LMDIF之前,调用用户定义的函数一次以找出输出数组的形状。

问题出现是因为第一次功能评估的内存随后作为原始LMDIF传递给*fvec,好像它是新的一次性内存。 LMDIF继续使用它来存储第一个函数求值,然后使用不同的*fvec再次调用用户函数。但是在dy()的示例中,用户函数会在将结果复制到LMDIF想要的内存之前覆盖前一个函数调用的内存。这使得LMDIF认为结果永远不会改变,因此适用参数对拟合质量没有影响的情况下退出。

我认为这是来自minpack_lmdif()的{​​{1}}中的错误,因为它假设用户定义的函数总是返回新的一次性内存,而scipy/optimize/__minpack.h则不是这样(似乎成为编码拟合函数的完全合法的方法)。以下dy()说明了一个简单的解决方法:

git diff

因此,我们为diff --git a/scipy/optimize/__minpack.h b/scipy/optimize/__minpack.h index 2c0ea33..465724b 100644 --- a/scipy/optimize/__minpack.h +++ b/scipy/optimize/__minpack.h @@ -483,7 +483,7 @@ static PyObject *minpack_lmdif(PyObject *dummy, PyObject *args) { qtf = (double *) ap_qtf->data; fjac = (double *) ap_fjac->data; ldfjac = dims[1]; - wa = (double *)malloc((3*n + m)* sizeof(double)); + wa = (double *)malloc((3*n + 2*m)* sizeof(double)); if (wa == NULL) { PyErr_NoMemory(); goto fail; @@ -492,12 +492,15 @@ static PyObject *minpack_lmdif(PyObject *dummy, PyObject *args) { /* Call the underlying FORTRAN routines. */ n_int = n; /* to provide int*-pointed storage for int argument of LMDIF */ - LMDIF(raw_multipack_lm_function, &m, &n_int, x, fvec, &ftol, &xtol, &gtol, &maxfev, &epsfcn, diag, - + LMDIF(raw_multipack_lm_function, &m, &n_int, x, wa+3*n+m, &ftol, &xtol, &gtol, &maxfev, &epsfcn, d + RESTORE_FUNC(); if (info < 0) goto fail; /* Python error */ + /* Copy final residuals back to initial array */ + memcpy(fvec, wa+3*n+m, m*sizeof(double)); + free(wa); Py_DECREF(extra_args); Py_DECREF(ap_diag); 分配m个临时空间元素,并使用额外的内存作为初始LMDIF。这可以防止在调用用户函数时发生内存冲突。要返回正确的最终残差,需要额外的*fvec将最终结果存储在原始数组中。

与dy()的原始示例一样,这允许编写fit函数以使其不受内存分配的影响。由于memcpy()中的整个内部循环没有内存分配,因此可以预期性能得到改善。

<强>更新

以下是一些时间结果。显然,测试问题非常小且快速收敛,因此它可能无法代表真实世界的应用程序。这是修补版LMDIF

scipy.optimize.leastsq

因此,通过写入预分配的内存无法获得任何结果:In [1]: def dy0(p, x, y): return p[0]*np.exp(-p[1]*x) - y In [2]: %timeit p = opt.leastsq(dy2, p0, args=(x, buf, y)) 1000 loops, best of 3: 399 us per loop In [3]: %timeit p = opt.leastsq(dy, p0, args=(x, buf, y)) 1000 loops, best of 3: 363 us per loop In [4]: %timeit p = opt.leastsq(dy0, p0, args=(x, y)) 1000 loops, best of 3: 341 us per loop 的直接实现是最快的。但是如果我们为dy0()编写一个更有效的包装器来更好地利用预分配的内存呢?这是我写的一个:

LMDIF

那是件好事。 In [5]: %timeit p = mp.leastsq(dy, (p0.copy(), x, buf, y)) 1000 loops, best of 3: 279 us per loop 仍然评估一般的mp.leastsq函数,其限制是第一个参数将被结果覆盖,第三个参数是缓冲区内存。但是,让我们看看如果我们在Python中编码dy()会发生什么:

C

哎哟! In [6]: %timeit p = opt.leastsq(fitfun.e2_diff, p0, args=(x, buf, y)) 10000 loops, best of 3: 48.2 us per loop 完美矢量化代码(至少对于短数组)的性能如此之多。让我们使用改进的包装器:

numpy

In [7]: %timeit p = mp.leastsq(fitfun.e2_diff, (p0.copy(), x, buf, y)) 100000 loops, best of 3: 6.94 us per loop opt.leastsq之间的额外加速来自摆脱元组构建代码和mp.leastsq。最后,memcpy的原始效果不会回调到LMDIF

Python

没有太大的不同。所以回调Python并不会花费太多,但不要让In [8]: %timeit p = fitfun.e2_fit(p0.copy(), x, buf, y) 100000 loops, best of 3: 6.13 us per loop 为你做计算!许多后续拟合(我的应用程序)的进一步加速可以来自重用numpy的临时存储器wa

最重要的是,所有计算现在都会返回正确的结果!