【问题标题】:Best practice for using common subexpression elimination with lambdify in SymPy在 SymPy 中使用带有 lambdify 的公共子表达式消除的最佳实践
【发布时间】:2015-08-24 16:08:12
【问题描述】:

我目前正在尝试使用 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。

【问题讨论】:

    标签: python performance numpy sympy


    【解决方案1】:

    可以提高计算速度:

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

    前言

    我假设这是“在 sympy 中计算一次函数,然后在不同的项目中多次使用它”类型的案例。因此,包含一些手动复制粘贴和创建文件。 但是可以使用函数自动创建新文件,以及编译步骤,但我现在将其排除在外。

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

    基准测试

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

    • 基线:使用 modules="numpy" 进行羔羊化处理: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(1090x提升)
    • (5) 使用 sympy 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) 使用 sympy autowrap

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

    【讨论】:

      【解决方案2】:

      所以这可能不是最理想的方法,但对于我的小例子来说它是可行的。

      以下代码的想法是对每个公共子表达式进行lambdify,并生成一个可能包含所有参数的新函数。我添加了一些额外的 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 包含所有子表达式,列表xs 包含评估的每个子表达式,这样的人最终可以正确地输入glamxs 与每个子表达式一起增长,最终可能变成瓶颈。

      你可以对sp.cse(sp.diff(g(sp.abc.x)))的表达做同样的处理。

      【讨论】:

      • 您可能也对 theano 感兴趣。它可以处理常见的子表达式并在lambdifying之前分析评估顺序。看看this blog post.
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-08-11
      • 1970-01-01
      • 2013-12-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多