numpy.einsum#

numpy.einsum(subscripts, *operands, out=None, dtype=None, order='K', casting='safe', optimize=False)[源代码]#

评估操作数的爱因斯坦求和约定。

使用爱因斯坦求和约定,许多常见的多维线性代数数组运算可以以简单的方式表示。在隐式模式下,einsum 计算这些值。

显式模式下,einsum 通过禁用或强制对指定的下标标签进行求和,为计算其他可能不被视为经典爱因斯坦求和运算的数组运算提供了更大的灵活性。

请参阅注释和示例以获得更清晰的说明。

参数:
subscriptsstr

指定求和的下标,形式为逗号分隔的下标标签列表。除非包含显式指示符‘->’以及输出精确形式的下标标签,否则将执行隐式(经典爱因斯坦求和)计算。

operandslist of array_like

这些是操作的数组。

outndarray,可选

如果提供,则计算将进行到此数组中。

dtype{data-type, None}, optional

如果提供,则强制计算使用指定的数据类型。请注意,您可能需要提供更宽松的casting参数以允许转换。默认为None。

order{‘C’, ‘F’, ‘A’, ‘K’}, optional

控制输出的内存布局。‘C’表示应为C连续。‘F’表示应为Fortran连续,‘A’表示应为‘F’(如果输入全部为‘F’),否则为‘C’。‘K’表示应尽可能接近输入的布局,包括任意排列的轴。默认为‘K’。

casting{‘no’, ‘equiv’, ‘safe’, ‘same_kind’, ‘unsafe’}, optional

控制可能发生的数据类型转换。不建议将此设置为‘unsafe’,因为它可能对累加产生不利影响。

  • ‘no’ 表示数据类型不得进行任何转换。

  • ‘equiv’ 表示只允许进行字节顺序更改。

  • ‘safe’ 表示只允许进行可以保留值的转换。

  • ‘same_kind’ 表示只允许安全的转换或同一类别的转换,例如从 float64 到 float32。

  • ‘unsafe’ 表示可以进行任何数据转换。

默认为‘safe’。

optimize{False, True, ‘greedy’, ‘optimal’}, optional

控制是否应发生中间优化。如果为False,则不发生优化,而True将默认为‘greedy’算法。还可以接受来自np.einsum_path函数的显式收缩列表。有关更多详细信息,请参阅np.einsum_path。默认为False。

返回:
outputndarray

基于爱因斯坦求和约定的计算。

另请参阅

einsum_path, dot, inner, outer, tensordot, linalg.multi_dot
einsum

einops包提供了类似的详细接口,用于覆盖其他操作:转置、重塑/展平、重复/平铺、压缩/解压和归约。opt_einsum以后端无关的方式优化einsum类表达式的收缩顺序。

备注

爱因斯坦求和约定可用于计算许多多维线性代数数组运算。einsum 提供了一种简洁的方式来表示它们。

下面列出了可以用einsum计算的、非详尽的这些操作列表,并附带示例。

下标字符串是逗号分隔的下标标签列表,其中每个标签对应于相应操作数的一个维度。每当重复一个标签时,都会对其进行求和,因此np.einsum('i,i', a, b)等同于np.inner(a,b)。如果一个标签只出现一次,则不会对其进行求和,因此np.einsum('i', a)会产生a的一个视图,没有任何变化。另一个例子np.einsum('ij,jk', a, b)描述了传统的矩阵乘法,等同于np.matmul(a,b)。一个操作数中重复的下标标签会取对角线。例如,np.einsum('ii', a)等同于np.trace(a)

隐式模式下,所选的下标很重要,因为输出的轴会按字母顺序重新排列。这意味着np.einsum('ij', a)不会影响2D数组,而np.einsum('ji', a)则对其进行转置。此外,np.einsum('ij,jk', a, b)返回矩阵乘法,而np.einsum('ij,jh', a, b)则返回乘法的转置,因为下标‘h’排在下标‘i’之前。

显式模式下,可以通过指定输出下标标签直接控制输出。这需要标识符‘->’以及输出下标标签列表。此功能增加了函数的灵活性,因为可以根据需要禁用或强制求和。如果a是1D数组,则调用np.einsum('i->', a)类似于np.sum(a);如果a是2D方阵,则np.einsum('ii->i', a)类似于np.diag(a)。区别在于einsum默认不允许广播。此外,np.einsum('ij,jh->ih', a, b)直接指定输出下标标签的顺序,因此返回矩阵乘法,这与隐式模式下的示例不同。

要启用和控制广播,请使用省略号。通过在每个项的左侧添加省略号,可以实现默认的NumPy式广播,例如np.einsum('...ii->...i', a)。对于任何形状的数组anp.einsum('...i->...', a)类似于np.sum(a, axis=-1)。要沿着第一个和最后一个轴取迹,可以使用np.einsum('i...i', a);或者,要执行一个矩阵-矩阵乘法,但使用最左边的索引而不是最右边的索引,可以使用np.einsum('ij...,jk...->ik...', a, b)

当只有一个操作数时,不进行任何轴求和,并且不提供输出参数,则会返回操作数的一个视图,而不是一个新数组。因此,取对角线操作np.einsum('ii->i', a)会生成一个视图(自1.10.0版本以来已更改)。

einsum还提供了一种替代方法来提供下标和操作数,即einsum(op0, sublist0, op1, sublist1, ..., [sublistout])。如果在此格式中未提供输出形状,则einsum将以隐式模式计算;否则,将以显式模式计算。下面的示例具有与两种参数方法相对应的einsum调用。

从einsum返回的视图现在是可写的,只要输入数组是可写的。例如,np.einsum('ijk...->kji...', a)现在将与np.swapaxes(a, 0, 2)具有相同的作用,而np.einsum('ii->i', a)将返回2D数组对角线的一个可写视图。

添加了optimize参数,它将优化einsum表达式的收缩顺序。对于具有三个或更多操作数的收缩,这可以大大提高计算效率,但会以计算过程中更大的内存占用为代价。

通常应用‘greedy’算法,经验测试表明在大多数情况下可以返回最佳路径。在某些情况下,‘optimal’会通过更昂贵、详尽的搜索返回最佳路径。对于迭代计算,建议一次计算最佳路径,并通过将其作为参数提供来重用该路径。下面给出了一个示例。

有关更多详细信息,请参阅numpy.einsum_path

示例

>>> a = np.arange(25).reshape(5,5)
>>> b = np.arange(5)
>>> c = np.arange(6).reshape(2,3)

矩阵的迹

>>> np.einsum('ii', a)
60
>>> np.einsum(a, [0,0])
60
>>> np.trace(a)
60

提取对角线(需要显式形式)

>>> np.einsum('ii->i', a)
array([ 0,  6, 12, 18, 24])
>>> np.einsum(a, [0,0], [0])
array([ 0,  6, 12, 18, 24])
>>> np.diag(a)
array([ 0,  6, 12, 18, 24])

沿一个轴求和(需要显式形式)

>>> np.einsum('ij->i', a)
array([ 10,  35,  60,  85, 110])
>>> np.einsum(a, [0,1], [0])
array([ 10,  35,  60,  85, 110])
>>> np.sum(a, axis=1)
array([ 10,  35,  60,  85, 110])

对于更高维度的数组,使用省略号可以对单个轴进行求和

>>> np.einsum('...j->...', a)
array([ 10,  35,  60,  85, 110])
>>> np.einsum(a, [Ellipsis,1], [Ellipsis])
array([ 10,  35,  60,  85, 110])

计算矩阵转置,或重新排列任意数量的轴

>>> np.einsum('ji', c)
array([[0, 3],
       [1, 4],
       [2, 5]])
>>> np.einsum('ij->ji', c)
array([[0, 3],
       [1, 4],
       [2, 5]])
>>> np.einsum(c, [1,0])
array([[0, 3],
       [1, 4],
       [2, 5]])
>>> np.transpose(c)
array([[0, 3],
       [1, 4],
       [2, 5]])

向量内积

>>> np.einsum('i,i', b, b)
30
>>> np.einsum(b, [0], b, [0])
30
>>> np.inner(b,b)
30

矩阵向量乘法

>>> np.einsum('ij,j', a, b)
array([ 30,  80, 130, 180, 230])
>>> np.einsum(a, [0,1], b, [1])
array([ 30,  80, 130, 180, 230])
>>> np.dot(a, b)
array([ 30,  80, 130, 180, 230])
>>> np.einsum('...j,j', a, b)
array([ 30,  80, 130, 180, 230])

广播和标量乘法

>>> np.einsum('..., ...', 3, c)
array([[ 0,  3,  6],
       [ 9, 12, 15]])
>>> np.einsum(',ij', 3, c)
array([[ 0,  3,  6],
       [ 9, 12, 15]])
>>> np.einsum(3, [Ellipsis], c, [Ellipsis])
array([[ 0,  3,  6],
       [ 9, 12, 15]])
>>> np.multiply(3, c)
array([[ 0,  3,  6],
       [ 9, 12, 15]])

向量外积

>>> np.einsum('i,j', np.arange(2)+1, b)
array([[0, 1, 2, 3, 4],
       [0, 2, 4, 6, 8]])
>>> np.einsum(np.arange(2)+1, [0], b, [1])
array([[0, 1, 2, 3, 4],
       [0, 2, 4, 6, 8]])
>>> np.outer(np.arange(2)+1, b)
array([[0, 1, 2, 3, 4],
       [0, 2, 4, 6, 8]])

张量收缩

>>> a = np.arange(60.).reshape(3,4,5)
>>> b = np.arange(24.).reshape(4,3,2)
>>> np.einsum('ijk,jil->kl', a, b)
array([[4400., 4730.],
       [4532., 4874.],
       [4664., 5018.],
       [4796., 5162.],
       [4928., 5306.]])
>>> np.einsum(a, [0,1,2], b, [1,0,3], [2,3])
array([[4400., 4730.],
       [4532., 4874.],
       [4664., 5018.],
       [4796., 5162.],
       [4928., 5306.]])
>>> np.tensordot(a,b, axes=([1,0],[0,1]))
array([[4400., 4730.],
       [4532., 4874.],
       [4664., 5018.],
       [4796., 5162.],
       [4928., 5306.]])

可写返回数组(自1.10.0版本以来)

>>> a = np.zeros((3, 3))
>>> np.einsum('ii->i', a)[:] = 1
>>> a
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])

省略号使用示例

>>> a = np.arange(6).reshape((3,2))
>>> b = np.arange(12).reshape((4,3))
>>> np.einsum('ki,jk->ij', a, b)
array([[10, 28, 46, 64],
       [13, 40, 67, 94]])
>>> np.einsum('ki,...k->i...', a, b)
array([[10, 28, 46, 64],
       [13, 40, 67, 94]])
>>> np.einsum('k...,jk', a, b)
array([[10, 28, 46, 64],
       [13, 40, 67, 94]])

链式数组操作。对于更复杂的收缩,通过重复计算‘greedy’路径或预先计算‘optimal’路径并重复应用它(使用einsum_path插入)(自1.12.0版本以来)可能会获得速度提升。性能改进对于较大的数组尤其显著。

>>> a = np.ones(64).reshape(2,4,8)

基本einsum:约1520ms(在3.1GHz Intel i5上进行基准测试。)

>>> for iteration in range(500):
...     _ = np.einsum('ijk,ilm,njm,nlk,abc->',a,a,a,a,a)

次优einsum(由于重复计算路径时间):约330ms

>>> for iteration in range(500):
...     _ = np.einsum('ijk,ilm,njm,nlk,abc->',a,a,a,a,a,
...         optimize='optimal')

Greedy einsum(更快的最佳路径近似):约160ms

>>> for iteration in range(500):
...     _ = np.einsum('ijk,ilm,njm,nlk,abc->',a,a,a,a,a, optimize='greedy')

Optimal einsum(某些用例中的最佳使用模式):约110ms

>>> path = np.einsum_path('ijk,ilm,njm,nlk,abc->',a,a,a,a,a,
...     optimize='optimal')[0]
>>> for iteration in range(500):
...     _ = np.einsum('ijk,ilm,njm,nlk,abc->',a,a,a,a,a, optimize=path)