Python + alglib + NumPy:如何避免将数组转换为列表?

9
背景: 我最近发现了alglib(用于数值计算),它似乎是我正在寻找的东西(强大的插值,数据分析...),在numpy或scipy中无法找到。

然而,我担心的是(例如对于插值),它不接受numpy数组作为有效的输入格式,而仅仅接受常规的python列表对象。

问题: 我深入研究了代码和文档,并发现(如预期的那样)这个列表格式只是过渡,因为该库将无论如何将其转换为ctypes(cpython库只是底层C / C ++库的接口)。

这就是我的担忧所在:在我的代码中,我使用numpy数组工作,因为它对我进行的科学计算是一个巨大的性能提升。因此,我担心必须将传递给alglib例程的任何数据转换为列表(将转换为ctypes)将对性能产生巨大影响(我正在处理可能包含数百万个浮点数的数组,并且有数千个数组)。

问题: 您认为我确实会有性能损失,还是您认为我应该开始修改alglib代码(仅修改python接口),以便它可以接受numpy数组,并进行一次转换(从numpy数组到ctypes)?我甚至不知道这是否可行,因为它是一个相当大的库... 也许您们有更好的想法或建议(即使是关于类似但不同的库)...


编辑

似乎我的问题没有引起太多关注,或者我的问题不清楚/不相关。或者可能没有人有解决方案或建议,但我怀疑有这么多专家在周围 :) 无论如何,我编写了一个小的、快速而肮脏的测试代码来说明问题...

#!/usr/bin/env python

import xalglib as al
import timeit
import numpy as np

def func(x):
    return (3.14 *x**2.3 + x**3 -x**2.34 +x)/(1.+x)**2

def fa(x, y, val=3.14):
    s = al.spline1dbuildakima(x, y)
    return (al.spline1dcalc(s, val), func(val))

def fb(x, y, val=3.14):
    _x = list(x)
    _y = list(y)
    s = al.spline1dbuildakima(_x, _y)
    return (al.spline1dcalc(s, val), func(val))

ntot = 10000
maxi = 100
x = np.random.uniform(high=maxi, size=ntot)
y = func(x)
xl = list(x)
yl = list(y)

print "Test for len(x)=%d, and x between [0 and %.2f):" % (ntot, maxi)
print "Function: (3.14 *x**2.3 + x**3 -x**2.34 +x)/(1.+x)**2"
a, b = fa(xl, yl)
err = np.abs(a-b)/b * 100
print "(x=3.14) interpolated, exact =", (a, b)
print "(x=3.14) relative error should be <= 1e-2: %s (=%.2e)" % ((err <= 1e-2), err)

if __name__ == "__main__":
    t = timeit.Timer(stmt="fa(xl, yl)", setup="from __main__ import fa, xl, yl, func")
    tt = timeit.Timer(stmt="fb(x, y)", setup="from __main__ import fb, x, y, func")
    v = 1000 * t.timeit(number=100)/100
    vv = 1000 * tt.timeit(number=100)/100
    print "%.2f usec/pass" % v
    print "%.2f usec/pass" % vv
    print "%.2f %% less performant using numpy arrays" % ((vv-v)/v*100.)

运行它,我得到了以下结果:
"""
Test for len(x)=10000, and x between [0 and 100.00):
Function: (3.14 *x**2.3 + x**3 -x**2.34 +x)/(1.+x)**2
(x=3.14) interpolated, exact = (3.686727834705164, 3.6867278531266905)
(x=3.14) relative error should be <= 1e-2: True (=5.00e-07)
25.85 usec/pass
28.46 usec/pass
10.09 % less performant using numpy arrays
"""

性能损失在8%至14%之间波动,对我来说非常巨大...

3个回答

5
您可以创建自己的wrap函数,将numpy数组的数据缓冲区直接传递给向量的数据指针,这样不会复制数据,并且会大大加快您的wrap函数。以下代码将x.ctypes.data传递给x_vector.ptr.p_ptr,其中x是一个numpy数组。
当您传递numpy数组时,必须确保数组的元素在连续的内存中。以下代码不检查这一点。
import xalglib as al
import numpy as np
import ctypes

def spline1dbuildakima(x, y):
    n = len(x)
    _error_msg = ctypes.c_char_p(0)
    __c = ctypes.c_void_p(0)
    __n = al.c_ptrint_t(n)
    __x = al.x_vector(cnt=n, datatype=al.DT_REAL, owner=al.OWN_CALLER, 
                      last_action=0,ptr=al.x_multiptr(p_ptr=x.ctypes.data))
    __y = al.x_vector(cnt=n, datatype=al.DT_REAL, owner=al.OWN_CALLER, 
                      last_action=0,ptr=al.x_multiptr(p_ptr=y.ctypes.data))

    al._lib_alglib.alglib_spline1dbuildakima(
        ctypes.byref(_error_msg), 
        ctypes.byref(__x), 
        ctypes.byref(__y), 
        ctypes.byref(__n), 
        ctypes.byref(__c))

    __r__c = al.spline1dinterpolant(__c)
    return __r__c    

def func(x):
    return (3.14 *x**2.3 + x**3 -x**2.34 +x)/(1.+x)**2

def fa(x, y, val=3.14):
    s = spline1dbuildakima(x, y)
    return al.spline1dcalc(s, val), func(val)

def fb(x, y, val=3.14):
    s = al.spline1dbuildakima(x, y)
    return al.spline1dcalc(s, val), func(val)

ntot = 10000
maxi = 100
x = np.random.uniform(high=maxi, size=ntot)
y = func(x)
xl = list(x)
yl = list(y)

import time
start = time.clock()
for i in xrange(100):
    a, b = fa(x, y)
print time.clock()-start
err = np.abs(a-b)/b * 100
print a, b, err

start = time.clock()
for i in xrange(100):
    a, b = fb(xl, yl)
print time.clock()-start
err = np.abs(a-b)/b * 100
print a, b, err

输出结果为:
0.722314760822 <- seconds of numpy array version
3.68672728107 3.68672785313 1.55166878281e-05
3.22011891502  <- seconds of list version
3.68672728107 3.68672785313 1.55166878281e-05

1
你可以调用 x = np.ascontiguousarray(x) 来确保它在内存中是连续的。 - jfs
哇,太棒了!非常感谢HYRY。我以前从未使用过包装方法,所以我正在阅读一些EOL指出的文档。你的例子对我来说来得正是时候,现在我有了具体的训练内容。 - mhavel
@Sebastian 谢谢您的评论。这也将对我有所帮助。 - mhavel

3
使C++ alglib接受NumPy数组是可行的:SciPy可以实现这一点。问题实际上在于它是否难以实现。你可以尝试使用其中一个半自动的C++→Python包装程序,比如(从我开始的那个开始 - 警告:我不是专家): 另外,我过去用SciPy成功地使用了插值样条。但我不确定这能否满足你的需求,因为你似乎没有在SciPy中找到想要的一切。

感谢您的回答EOL。SciPy包中的样条线按预期工作,但我缺少一些像Akima这样的样条线(我有一些需要超过线性插值的数据,但不足以使用立方或更高阶样条线而不会产生一些不必要的伪像,如结果中的振荡)。此外,alglib还具有一些SciPy没有的其他数据分析和统计工具,因此对我来说使用这样的外部库是有意义的。对于封装方面,看起来alglib开发人员已经在使用C->Python的一个封装器了... - mhavel
@mhavel:可以将C++代码包装起来,直接使用NumPy数组而不是Python列表;令人惊讶的是alglib的Python封装程序并未做到/允许这一点。自己包装所需函数可能比修改alglib Python封装程序更容易:如有需要,您将能够更轻松地更新您的封装程序以跟随alglib的更新。我列出的三个封装程序应该会对您有很大帮助。 - Eric O. Lebigot
哦,好的 :) 我不是 Python 专家,连包装都不是很懂。所以我可以试一试。非常感谢。 - mhavel
@mhavel:谢谢。我不是包装C++代码用于Python的专家,但调查一些列出的C++包装器可能对那些对快速科学计算感兴趣的人来说是一个不错的投资。 - Eric O. Lebigot

1

除了EOL的答案,您还可以尝试

以生成一个Python接口,该接口处理NumPy数组,但使用适当的参数调用底层的C/C++。

我发现文档足够清晰,可以为一个小型科学C库完成这个任务,而且我以前从未做过这个或者对C和Python的接口有很多经验。


谢谢你的回答,我会看一下这个。 - mhavel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接