【问题标题】:Sympy: lambdify such that operations on arrays always result in arrays, also for constants?Sympy:lambdify 使得对数组的操作总是产生数组,也适用于常量?
【发布时间】:2020-01-15 18:29:10
【问题描述】:

我需要在很多方面评估用户给出的函数 (f') 的导数。这些点在一个列表中(或 numpy.array、pandas.Series ...)。当 f' 依赖于一个 sympy 变量时,我获得了预期值,但当 f' 是一个常量时,我​​没有:

import sympy as sp

f1 = sp.sympify('1')
f2 = sp.sympify('t')

lamb1 = sp.lambdify('t',f1)
lamb2 = sp.lambdify('t',f2)

print(lamb1([1,2,3]))
print(lamb2([1,2,3]))

我得到:

1
[1, 2, 3]

第二个没问题,但我希望第一个是一个列表。

这些函数在一个矩阵中,并且是 sympy 操作的最终结果,例如取导数。 f1 和 f2 的确切形式因问题而异。

【问题讨论】:

    标签: python numpy sympy


    【解决方案1】:

    lamb1 是一个返回常量 1 的函数:def lamb1(x): return 1

    lamb2 是一个返回其参数的函数:def lamb2(x): return x

    因此,输出非常符合预期。

    这是一种可行的方法。我将 f2 的测试函数更改为 t*t,因为这在我的测试中更烦人(处理 Pow(t,2))。

    import sympy as sp
    import numpy as np
    
    f1 = sp.sympify('1')
    f2 = sp.sympify('t*t')
    
    def np_lambdify(varname, func):
        lamb = sp.lambdify(varname, func, modules=['numpy'])
        if func.is_constant():
            return lambda t: np.full_like(t, lamb(t))
        else:
            return lambda t: lamb(np.array(t))
    
    lamb1 = np_lambdify('t', f1)
    lamb2 = np_lambdify('t', f2)
    
    print(lamb1(1))
    print(lamb1([1, 2, 3]))
    print(lamb2(2))
    print(lamb2([1, 2, 3]))
    

    输出:

    1
    [1 1 1]
    4
    [1 4 9]
    

    【讨论】:

    • f1 和 f2 是用户给出的函数的导数,就像我说的那样,使用 Sympy 的目的是避免用户必须派生和定义函数。此外,导数的数量取决于问题,您的解决方案不是通用解决方案。
    【解决方案2】:

    isympy/ipython自省:

    In [28]: lamb2??                                                                                 
    Signature: lamb2(t)
    Docstring:
    Created with lambdify. Signature:
    
    func(arg_0)
    
    Expression:
    
    t
    
    Source code:
    
    def _lambdifygenerated(t):
        return (t)
    

    第一个:

    In [29]: lamb1??                                                                                 
    Signature: lamb1(t)
    Docstring:
    Created with lambdify. Signature:
    
    func(arg_0)
    
    Expression:
    
    1
    
    Source code:
    
    def _lambdifygenerated(t):
        return (1)
    

    所以一个返回输入参数;另一个只返回常量,而不管输入。 lambdify 进行了从 sympynumpy Python 的相当简单的词法转换。

    编辑

    把你的函数放在sp.Matrix:

    In [55]: lamb3 = lambdify('t',Matrix([f1,f2]))                                                   
    
    In [56]: lamb3??                                                                                 
    ...
    def _lambdifygenerated(t):
        return (array([[1], [t]]))
    ...
    
    In [57]: lamb3(np.arange(3))                                                                     
    Out[57]: 
    array([[1],
           [array([0, 1, 2])]], dtype=object)
    

    所以这会返回一个 numpy 数组;但由于形状的混合,结果是对象 dtype,而不是 2d。

    我们可以通过直接的数组生成来看到这一点:

    In [53]: np.array([[1],[1,2,3]])                                                                 
    Out[53]: array([list([1]), list([1, 2, 3])], dtype=object)
    
    In [54]: np.array([np.ones(3,int),[1,2,3]])                                                      
    Out[54]: 
    array([[1, 1, 1],
           [1, 2, 3]])
    

    sympynp.array 都不会尝试“广播”该常量。有 numpy 结构可以做到这一点,例如乘法和加法,但这个简单的 sympy 函数和 lambdify 不能。

    编辑

    frompyfunc 是一种将一个(或多个)数组传递给仅适用于标量输入的函数的方法。虽然lamb2 与数组输入一起使用,但您对lamb1 的情况不满意,或者可能是lamb3

    In [60]: np.frompyfunc(lamb1,1,1)([1,2,3])                                                       
    Out[60]: array([1, 1, 1], dtype=object)
    
    In [61]: np.frompyfunc(lamb2,1,1)([1,2,3])                                                       
    Out[61]: array([1, 2, 3], dtype=object)
    

    这个 [61] 比简单的 lamb2([1,2,3]) 慢,因为它有效地迭代。

    In [62]: np.frompyfunc(lamb3,1,1)([1,2,3])                                                       
    Out[62]: 
    array([array([[1],
           [1]]), array([[1],
           [2]]),
           array([[1],
           [3]])], dtype=object)
    

    在这种 Matrix 案例中,结果是一个数组数组。但是由于形状匹配,它们可以组合成一个数组(以各种方式):

    In [66]: np.concatenate(_62, axis=1)                                                             
    Out[66]: 
    array([[1, 1, 1],
           [1, 2, 3]])
    

    【讨论】:

    • 我明白你在说什么,谢谢。然而,在我使用它的脚本中,正如我所提到的,这两个评估是在一个矩阵中进行的(函数是另一个函数的梯度)。所以当我评估时,我得到了类似 [1,[1,2,3]] 的东西,但我需要 [[1,1,1],[1,2,3]],有没有办法识别它们并使用在 lamb1 的情况下,类似于 [1]*len([1,2,3]) 以获得所需的 [1,1,1]?
    • frompyfunc 可能有用。它不会很快,但对于您的一般情况,它可能是最干净的解决方案。
    • 你的解决方案很有用,但我更喜欢@JohanC的解决方案(我不知道哪个最好或最快)
    【解决方案3】:

    通常lambdify 返回一个常量实际上不是问题,因为 NumPy 的广播语义会自动将常量视为具有适当形状的常量的数组。

    如果有问题,可以使用类似的包装器

    def broadcast(fun):
        return lambda *x: numpy.broadcast_arrays(fun(*x), *x)[0]
    

    (本文取自https://github.com/sympy/sympy/issues/5642,对此问题有更多讨论)。

    请注意,使用 broadcast 比使用 JohanC 的答案中的 full_like 更好,因为广播的常量数组实际上并不占用更多内存,而 full_like 将复制内存中的常量以构成数组。

    【讨论】:

      【解决方案4】:

      我经常使用技巧t * 0 + 1 创建一个与我的输入长度相同的零向量,然后将其每个元素加 1。它适用于 NumPy;检查它是否适用于 Sympy!

      【讨论】:

      • f1 和 f2 由 sympy.diff 计算(我在问题中省略了它),我无法修改它们。
      【解决方案5】:

      我从不使用lambdify,所以我不能对它的工作方式过于挑剔。但看起来你需要通过给它一个不会简化为标量的表达式来欺骗它,当用数字评估时 减少到所需的值:

      >>> import numpy as np
      >>> lambdify('t','(1+t)*t-t**2-t+42','numpy')(np.array([1,2,3]))
      array([42, 42, 42])
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-10-29
        • 1970-01-01
        • 1970-01-01
        • 2020-01-29
        • 1970-01-01
        • 2021-10-29
        • 2021-07-07
        • 1970-01-01
        相关资源
        最近更新 更多