在SymPy中使用lambdify进行公共子表达式消除的最佳实践

时间:2015-06-09 17:25:38

标签: python performance numpy sympy

我目前正在尝试使用SymPy来生成和数字评估函数及其渐变。为简单起见,我将使用以下函数作为示例(请记住,实际函数更长):

import sympy as sp
def g(x):
    return sp.cos(x) + sp.cos(x)**2 + sp.cos(x)**3

很容易用数字方式评估这个函数及其衍生物:

import numpy as np
g_expr = sp.lambdify(x,g(x),modules='numpy')
dg_expr = sp.lambdify(x,sp.diff(g(x)),modules='numpy')

print g_expr(np.linspace(0,1,50))
print dg_expr(np.linspace(0,1,50))

然而,对于我的真实函数,lambdify在生成数值函数和评估方面都很慢。由于我函数中的许多元素都是相似的,我想在lambdify中使用公共子表达式消除(cse)来加速这个过程。我知道SymPy有一个内置函数来执行cse,

>>> print sp.cse(g(x))
([(x0, cos(x))], [x0**3 + x0**2 + x0])

但是不知道使用什么语法来在lambdify函数中使用这个结果(我仍然希望使用x作为我的输入参数):

>>> g_expr_fast = sp.lambdify(x,sp.cse(g(x)),modules='numpy')
>>> print g_expr_fast(np.linspace(0,1,50))
Traceback (most recent call last):
  File "test3.py", line 34, in <module>
    print g_expr1(nx1)
  File "<string>", line 1, in <lambda>
NameError: global name 'x0' is not defined

任何有关如何在lambdify中使用cse的帮助将不胜感激。或者,如果有更好的方法来加速我的渐变计算,我也很感激听到这些。

如果相关,我使用的是Python 2.7.3和SymPy 0.7.6。

2 个答案:

答案 0 :(得分:1)

所以这可能不是最佳的方式,但对于我的小例子,它可以工作。

遵循代码的想法是对每个公共子表达式进行简化,并生成一个可能包含所有参数的新函数。我添加了一些额外的sin和cos术语来添加先前子表达式的可能依赖项。

import sympy as sp
import sympy.abc
import numpy as np
import matplotlib.pyplot as pl

def g(x):
    return sp.cos(x) + sp.cos(x)**2 + sp.cos(x)**3 + sp.sin(sp.cos(x)+sp.sin(x))**4 + sp.sin(x) - sp.cos(3*x) + sp.sin(x)**2

repl, redu=sp.cse(g(sp.abc.x))

funs = []
syms = [sp.abc.x]
for i, v in enumerate(repl):
    funs.append(sp.lambdify(syms,v[1],modules='numpy'))
    syms.append(v[0])

glam = sp.lambdify(syms,redu[0],modules='numpy')

x = np.linspace(-1,5,10)
xs=[x]

for f in funs:
    xs.append(f(*xs))

print glam(*xs)
glamlam = sp.lambdify(sp.abc.x,g(sp.abc.x),modules='numpy')
print glamlam(x)
print np.allclose(glamlam(x),glam(*xs))

repl包含:

[(x0, cos(x)), (x1, sin(x)), (x2, x0 + x1)]

和redu包含

[x0**3 + x0**2 + x1**2 + x2 + sin(x2)**4 - cos(3*x)]

因此funs包含lambdified的所有子表达式,列表xs包含评估的每个子表达式,这样可以最终正确地提供glamxs随着每个子表达式而增长,最终可能会变成瓶颈。

您可以对sp.cse(sp.diff(g(sp.abc.x)))的表达式采用相同的方法。

答案 1 :(得分:1)

可以提高计算速度:

  • 通过提取公因子而不重复计算来有点(在我的示例中为36倍的改进)。
  • 例如,通过使用numba创建扩展模块,可以获得更高的加速比(最高100x-1000x)。

前言

我假设这是“一次在sympy中计算函数,以后在不同的项目中多次使用”这种情况。因此,其中包含一些手动复制粘贴和创建的文件。 但是可以使用这些功能以及编译步骤来自动创建新文件,但是现在我不再赘述了。

我遇到了类似的问题,并且对不同的方法进行了一些基准测试。我使用的函数很长(len(str(expr)) = 45857),cse(expr)将其分解为72个子表达式。在这里复制粘贴太长了,但是这里是将sympy创建的函数的速度提高100x-1000x的步骤。

基准

A)评估单个浮点数
是时候用每个参数一个浮点值评估函数了。使用timeit myfunc(*params)

  • 基线:以modules="numpy"进行lambdify:277µs
  • (1)复制粘贴str(expr)到函数定义:275µs(无差异)
  • (2)cse之后的表达式复制粘贴:8.2 µs(提高了33倍)
  • (3)cse(optimizations="basic")之后的表达式复制粘贴:7.6µs(提高了36倍)
  • (4)使用numba将代码编译为func_numba_f():0.25µs(改进了1090倍)
  • (5)使用符号autowrap:0.47 µs(改进589倍)

B)评估1000个浮点数的np.array

  • (1)将str(expr)复制粘贴到函数定义:15100 µs |每个值15.1µs
  • (2)cse之后的表达式复制粘贴:493 µs |每个值0.49µs(提高31倍)
  • (3)cse(optimizations="basic")之后的表达式的复制粘贴:413µs |每个值0.41µs(提高了37倍)
  • (4)使用numba将代码编译为func_numba_arr():114µs |每个值0.11µs(提高了132倍)
  • (5)使用np.vectorize的sympy自动包装:480µs |每个值0.48µs(提高31倍)

(1)复制粘贴str(expr)

  • 只需将表达式字符串粘贴到新函数中,然后返回值即可。
  • 将该功能保存在另一个文件中。

(2)cse之后的表达式的复制粘贴

  • 想法:通过识别公共部分使代码更短。
  • 首先,复制粘贴常用部分:
repl, redu = cse(K)
for variable, expr in repl:
    print(f"{variable} = {expr}")
  • 然后,复制粘贴返回值:print(redu[0])
  • 创建另一个文件,然后粘贴到函数定义中

(3)复制粘贴cse(optimizations="basic")之后的表达式

  • 与(2)相同,但带有optimizations="basic"
  • 这将创建比(2)稍短的代码

(4)使用numba编译代码

  • 使用numba.pycc.CC编译代码。因此,创建具有复制粘贴功能,如(3)
  • 然后,使用代码创建src_mymodule.py
from numba.pycc import CC

cc = CC("my_numba_module")


@cc.export("func_numba_f", "f8(f8, f8, f8, f8, f8)")
@cc.export("func_numba_arr", "f8[:](f8[:],f8[:],f8[:],f8[:],f8[:])")
def myfunc(x1, x2, x3, x4, x5):
    # your function definition here
    return value

if __name__ == "__main__":
    cc.compile()
  • 在函数func_numba_f()中,有五个浮点型输入变量和一个浮点型输出变量。 f8表示浮动。
  • func_numba_arr()是使用dtype="float64"dtype="float32"处理np.arrays的版本,具体取决于您用于编译的内容。
  • 然后通过运行python src_mymodule.py一次编译代码。这将创建my_numba_module.cp38-win_amd64.pyd或类似的内容。只能与文件名中的相同python版本和位数一起使用。
  • 然后,在另一个python文件中,您将导入函数并使用它们,例如:
from my_numba_module import func_numba_f, func_numba_arr

out = func_numba_f(4,3,2,1,100)

# or:
args = [np.array([x]*N, dtype='float64') for x in (4,3,2,1,100)]
out_arr = func_numba_arr(*args)

(5)使用符号autowrap

  • 这很容易。 installing并配置Cython之后,我需要两行代码才能使用autowrap创建一个函数。
from sympy.utilities.autowrap import autowrap
func = autowrap(expr, backend='cython')
  • 通过指定temp_dir参数,它将保存所有源文件(.c,.h,.pyx)和可用于导入文件的.pyd(win)/。so(unix)文件。稍后使用(假设temp_dirsys.path中)
from wrapper_module_1 import autofunc_c
  • 这是迄今为止最简单的方法,尽管生成的C代码并未得到高度优化。如果需要,可以在其他步骤中重命名autowrap的输出。
  • 该函数仅接受标量,但可以使用np.vectorize向量化
func = np.vectorize(func)