了解NumPy的einsum

时间:2014-09-28 21:33:52

标签: python arrays numpy multidimensional-array numpy-einsum

我很难准确理解einsum的确切运作方式。我查看了文档和一些示例,但它似乎并不坚持。

这是我们在课堂上看到的一个例子:

C = np.einsum("ij,jk->ki", A, B)

表示两个数组AB

我认为这需要A^T * B,但我不确定(它正在对其中一个进行转置吗?)。任何人都可以告诉我这里发生了什么(通常在使用einsum时)?

8 个答案:

答案 0 :(得分:256)

(注意:此答案基于我前一段时间写过的关于einsum的简短blog post。)

einsum做什么?

想象一下,我们有两个多维数组,AB。现在我们假设我们想......

  • 以特定方式将{/ 1> AB相乘,以创建新的产品数组;然后也许
  • 求和沿特定轴的这个新数组;然后也许
  • 以特定顺序转置新阵列的轴。

einsum很有可能帮助我们更快,更高效地执行此操作,以及multiplysumtranspose等NumPy函数的组合将允许

einsum如何运作?

这是一个简单(但不是完全无关紧要)的例子。采用以下两个数组:

A = np.array([0, 1, 2])

B = np.array([[ 0,  1,  2,  3],
              [ 4,  5,  6,  7],
              [ 8,  9, 10, 11]])

我们将以元素方式乘以AB,然后沿新数组的行求和。在“正常”的NumPy中,我们写道:

>>> (A[:, np.newaxis] * B).sum(axis=1)
array([ 0, 22, 76])

所以在这里,A上的索引操作将两个数组的第一个轴对齐,以便可以广播乘法。然后将产品数组的行相加以返回答案。

现在,如果我们想要使用einsum,我们可以写:

>>> np.einsum('i,ij->i', A, B)
array([ 0, 22, 76])

签名字符串'i,ij->i'是此处的关键,需要一些解释。你可以把它分成两半。在左侧(->的左侧),我们标记了两个输入数组。在->的右侧,我们标记了我们想要结束的数组。

接下来会发生什么:

  • A有一个轴;我们已将其标记为iB有两个轴;我们将轴0标记为i,将轴1标记为j

  • 通过重复两个输入数组中的标签i,我们告诉einsum这两个轴应该相乘 。换句话说,我们将数组A与数组B的每列相乘,就像A[:, np.newaxis] * B一样。

  • 请注意j在我们所需的输出中不会显示为标签;我们刚刚使用了i(我们希望最终得到一维数组)。通过省略标签,我们会沿此轴告诉einsum 。换句话说,我们正在总结产品的行,就像.sum(axis=1)一样。

使用einsum基本上只需要知道。它有助于玩一点;如果我们将两个标签留在输出'i,ij->ij'中,我们就会得到一个2D数组的产品(与A[:, np.newaxis] * B相同)。如果我们说没有输出标签'i,ij->,我们会返回一个数字(与执行(A[:, np.newaxis] * B).sum()相同)。

然而,关于einsum的伟大之处在于,它不是首先构建临时产品阵列;它只是对产品进行总结。这可以大大节省内存使用量。

稍微大一点的例子

为了解释点积,这里有两个新数组:

A = array([[1, 1, 1],
           [2, 2, 2],
           [5, 5, 5]])

B = array([[0, 1, 0],
           [1, 1, 0],
           [1, 1, 1]])

我们将使用np.einsum('ij,jk->ik', A, B)计算点积。这是一张图片,显示AB的标签以及我们从函数中获得的输出数组:

enter image description here

您可以看到标签j重复出现 - 这意味着我们将A的行与B的列相乘。此外,标签j未包含在输出中 - 我们正在对这些产品进行求和。标签ik保留用于输出,因此我们返回2D数组。

将此结果与标签j 求和的数组进行比较可能更清楚。在左下方,您可以看到撰写np.einsum('ij,jk->ijk', A, B)后得到的3D数组(即我们保留了标签j):

enter image description here

求和轴j给出了预期的点积,如右图所示。

一些练习

为了更好地感受einsum,使用下标符号实现熟悉的NumPy数组操作会很有用。任何涉及乘法和求和轴组合的东西都可以使用einsum来编写。

设A和B为两个长度相同的1D阵列。例如,A = np.arange(10)B = np.arange(5, 15)

  • 可以写出A的总和:

    np.einsum('i->', A)
    
  • 可以编写元素乘法A * B

    np.einsum('i,i->i', A, B)
    
  • 可以写出内积或点积np.inner(A, B)np.dot(A, B)

    np.einsum('i,i->', A, B) # or just use 'i,i'
    
  • 可以写出外部产品np.outer(A, B)

    np.einsum('i,j->ij', A, B)
    

对于2D数组CD,只要轴是兼容的长度(长度相同或长度为1),这里有几个例子:

  • 可以写出C(主对角线的总和)np.trace(C)的痕迹:

    np.einsum('ii', C)
    
  • C的元素乘法和DC * D.T的转置可以写成:

    np.einsum('ij,ji->ij', C, D)
    
  • C的每个元素乘以数组D(以制作4D数组)C[:, :, None, None] * D,可以写成:

    np.einsum('ij,kl->ijkl', C, D)  
    

答案 1 :(得分:31)

如果您直观地理解numpy.einsum(),那么掌握numpy.einsum()的想法非常容易。作为一个例子,让我们从一个涉及矩阵乘法的简单描述开始。

要使用np.einsum(),您所要做的就是将所谓的下标字符串作为参数传递,然后传递输入数组。< / p>

假设你有两个2D数组, A B ,你想要进行矩阵乘法。所以,你这样做:

np.einsum("ij, jk -> ik", A, B)

此处下标字符串 ij 对应数组 A ,而下标字符串 jk 对应数组 B 。此外,最重要的是要注意每个下标字符串 中的字符数必须匹配数组的维度。 (即2D阵列的两个字符,3D数组的三个字符,等等。)如果在我们的下标字符串 j 之间重复字符case),那意味着你希望ein 在这些维度上发生。因此,他们将减少总和。 (即该维度将消失

-> 之后的下标字符串将是我们的结果数组。 如果将其保留为空,则将对所有内容求和,并返回标量值作为结果。否则,结果数组将具有根据下标字符串的维度。在我们的示例中,它将是 ik 。这很直观,因为我们知道对于矩阵乘法,数组 A 中的列数必须与数组 B 中的行数相匹配就是这里发生的事情(即我们通过在下标字符串中重复字符 j 来编码这些知识)

以下是一些示例,说明了Double Dot Products在实现一些常见的 tensor nd-array 操作时的使用/功能,简洁明了。

<强>输入

# a vector
In [197]: vec
Out[197]: array([0, 1, 2, 3])

# an array
In [198]: A
Out[198]: 
array([[11, 12, 13, 14],
       [21, 22, 23, 24],
       [31, 32, 33, 34],
       [41, 42, 43, 44]])

# another array
In [199]: B
Out[199]: 
array([[1, 1, 1, 1],
       [2, 2, 2, 2],
       [3, 3, 3, 3],
       [4, 4, 4, 4]])

1)矩阵乘法(类似于 np.matmul(arr1, arr2)

In [200]: np.einsum("ij, jk -> ik", A, B)
Out[200]: 
array([[130, 130, 130, 130],
       [230, 230, 230, 230],
       [330, 330, 330, 330],
       [430, 430, 430, 430]])

2)沿主对角线提取元素(类似于 np.diag(arr)

In [202]: np.einsum("ii -> i", A)
Out[202]: array([11, 22, 33, 44])

3)Hadamard产品(即两个数组的元素产品)(类似于 arr1 * arr2

In [203]: np.einsum("ij, ij -> ij", A, B)
Out[203]: 
array([[ 11,  12,  13,  14],
       [ 42,  44,  46,  48],
       [ 93,  96,  99, 102],
       [164, 168, 172, 176]])

4)元素平方(类似于 np.square(arr) arr ** 2

In [210]: np.einsum("ij, ij -> ij", B, B)
Out[210]: 
array([[ 1,  1,  1,  1],
       [ 4,  4,  4,  4],
       [ 9,  9,  9,  9],
       [16, 16, 16, 16]])

5)跟踪(即主要对角线元素的总和)(类似于 np.trace(arr)

In [217]: np.einsum("ii -> ", A)
Out[217]: 110

6)矩阵转置(类似于 np.transpose(arr)

In [221]: np.einsum("ij -> ji", A)
Out[221]: 
array([[11, 21, 31, 41],
       [12, 22, 32, 42],
       [13, 23, 33, 43],
       [14, 24, 34, 44]])

7)外部产品(向量)(类似于 np.outer(vec1, vec2)

In [255]: np.einsum("i, j -> ij", vec, vec)
Out[255]: 
array([[0, 0, 0, 0],
       [0, 1, 2, 3],
       [0, 2, 4, 6],
       [0, 3, 6, 9]])

8)内部产品(向量)(类似于 np.inner(vec1, vec2)

In [256]: np.einsum("i, i -> ", vec, vec)
Out[256]: 14

9)沿0轴求和(类似于 np.sum(arr, axis=0)

In [260]: np.einsum("ij -> j", B)
Out[260]: array([10, 10, 10, 10])

10)沿轴1求和(类似于 np.sum(arr, axis=1)

In [261]: np.einsum("ij -> i", B)
Out[261]: array([ 4,  8, 12, 16])

11)批量矩阵乘法

In [287]: BM = np.stack((A, B), axis=0)

In [288]: BM
Out[288]: 
array([[[11, 12, 13, 14],
        [21, 22, 23, 24],
        [31, 32, 33, 34],
        [41, 42, 43, 44]],

       [[ 1,  1,  1,  1],
        [ 2,  2,  2,  2],
        [ 3,  3,  3,  3],
        [ 4,  4,  4,  4]]])

In [289]: BM.shape
Out[289]: (2, 4, 4)

# batch matrix multiply using einsum
In [292]: BMM = np.einsum("bij, bjk -> bik", BM, BM)

In [293]: BMM
Out[293]: 
array([[[1350, 1400, 1450, 1500],
        [2390, 2480, 2570, 2660],
        [3430, 3560, 3690, 3820],
        [4470, 4640, 4810, 4980]],

       [[  10,   10,   10,   10],
        [  20,   20,   20,   20],
        [  30,   30,   30,   30],
        [  40,   40,   40,   40]]])

In [294]: BMM.shape
Out[294]: (2, 4, 4)

12)沿轴2求和(类似于 np.sum(arr, axis=2)

In [330]: np.einsum("ijk -> ij", BM)
Out[330]: 
array([[ 50,  90, 130, 170],
       [  4,   8,  12,  16]])

13)汇总数组中的所有元素(类似于 np.sum(arr)

In [335]: np.einsum("ijk -> ", BM)
Out[335]: 480

14)多个轴的总和(即边缘化)
(类似于 np.sum(arr, axis=(axis0, axis1, axis2, axis3, axis4, axis6, axis7))

# 8D array
In [354]: R = np.random.standard_normal((3,5,4,6,8,2,7,9))

# marginalize out axis 5 (i.e. "n" here)
In [363]: esum = np.einsum("ijklmnop -> n", R)

# marginalize out axis 5 (i.e. sum over rest of the axes)
In [364]: nsum = np.sum(R, axis=(0,1,2,3,4,6,7))

In [365]: np.allclose(esum, nsum)
Out[365]: True

15) np.matmul() (类似于 np.sum(hadamard-product) cf。 3

In [772]: A
Out[772]: 
array([[1, 2, 3],
       [4, 2, 2],
       [2, 3, 4]])

In [773]: B
Out[773]: 
array([[1, 4, 7],
       [2, 5, 8],
       [3, 6, 9]])

In [774]: np.einsum("ij, ij -> ", A, B)
Out[774]: 124

16) 2D和3D阵列乘法

当你想要验证结果的线性方程组( Ax = b )时,这样的乘法可能非常有用。

# inputs
In [115]: A = np.random.rand(3,3)
In [116]: b = np.random.rand(3, 4, 5)

# solve for x
In [117]: x = np.linalg.solve(A, b.reshape(b.shape[0], -1)).reshape(b.shape)

# 2D and 3D array multiplication :)
In [118]: Ax = np.einsum('ij, jkl', A, x)

# indeed the same!
In [119]: np.allclose(Ax, b)
Out[119]: True

相反,如果必须使用Einstein-Summation进行此验证,我们必须执行几项reshape操作才能获得相同的结果,如:

# reshape 3D array `x` to 2D, perform matmul
# then reshape the resultant array to 3D
In [123]: Ax_matmul = np.matmul(A, x.reshape(x.shape[0], -1)).reshape(x.shape)

# indeed correct!
In [124]: np.allclose(Ax, Ax_matmul)
Out[124]: True

奖金:在这里阅读更多数学:Tensor-Notation,绝对在这里:{{3}}

答案 2 :(得分:7)

让我们制作2个阵列,具有不同但兼容的尺寸,以突出它们的相互作用

In [43]: A=np.arange(6).reshape(2,3)
Out[43]: 
array([[0, 1, 2],
       [3, 4, 5]])


In [44]: B=np.arange(12).reshape(3,4)
Out[44]: 
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])

你的计算,得到a(2,3)与a(3,4)的'dot'(乘积之和)来产生(4,2)数组。 iA的第一个暗点,C的最后一个; k的最后一个BC的第一个。 j被总和“消耗”。

In [45]: C=np.einsum('ij,jk->ki',A,B)
Out[45]: 
array([[20, 56],
       [23, 68],
       [26, 80],
       [29, 92]])

这与np.dot(A,B).T相同 - 它是转置的最终输出。

要查看j发生的更多信息,请将C下标更改为ijk

In [46]: np.einsum('ij,jk->ijk',A,B)
Out[46]: 
array([[[ 0,  0,  0,  0],
        [ 4,  5,  6,  7],
        [16, 18, 20, 22]],

       [[ 0,  3,  6,  9],
        [16, 20, 24, 28],
        [40, 45, 50, 55]]])

这也可以通过以下方式生成:

A[:,:,None]*B[None,:,:]

也就是说,在k的末尾添加A维度,在i的前面添加B,结果为(2,3,4) )阵列。

0 + 4 + 16 = 209 + 28 + 55 = 92等;求和j并转置以获得更早的结果:

np.sum(A[:,:,None] * B[None,:,:], axis=1).T

# C[k,i] = sum(j) A[i,j (,k) ] * B[(i,)  j,k]

答案 3 :(得分:6)

在阅读einsum方程时,我发现最简单的方法就是 在头脑上将它们简化为命令式版本。

让我们从以下(强制)语句开始:

C = np.einsum('bhwi,bhwj->bij', A, B)

首先通过标点符号进行操作,我们看到在箭头之前,我们有两个4个逗号分隔的斑点-bhwibhwj, 以及其后的单个3字母blob bij。因此,该等式从两个4级张量输入产生3级张量结果。

现在,让每个斑点中的每个字母成为范围变量的名称。字母在斑点中出现的位置 是它在该张量范围内的轴的索引。 因此,产生C的每个元素的命令式求和必须从三个嵌套的for循环开始,每个C的索引一个。

for b in range(...):
    for i in range(...):
        for j in range(...):
            # the variables b, i and j index C in the order of their appearance in the equation
            C[b, i, j] = ...

因此,基本上,对于C的每个输出索引,您都有一个for循环。我们现在暂时不确定范围。

接下来,我们看一下左侧-是否有任何范围变量,出现在右侧一侧?在我们的情况下-是,hw。 为每个此类变量添加一个内部嵌套的for循环:

for b in range(...):
    for i in range(...):
        for j in range(...):
            C[b, i, j] = 0
            for h in range(...):
                for w in range(...):
                    ...

在最里面的循环中,我们现在定义了所有索引,因此我们可以编写实际的求和 翻译完成:

# three nested for-loops that index the elements of C
for b in range(...):
    for i in range(...):
        for j in range(...):

            # prepare to sum
            C[b, i, j] = 0

            # two nested for-loops for the two indexes that don't appear on the right-hand side
            for h in range(...):
                for w in range(...):
                    # Sum! Compare the statement below with the original einsum formula
                    # 'bhwi,bhwj->bij'

                    C[b, i, j] += A[b, h, w, i] * B[b, h, w, j]

如果您到目前为止已经能够遵循该代码,那么恭喜您!这就是您需要阅读einsum方程式所需的全部。请特别注意原始einsum公式如何映射到以上代码段中的最终sumsum语句。 for循环和范围边界只是起毛,而最后的声明就是您真正需要了解的一切。

为了完整起见,让我们看看如何确定每个范围变量的范围。好吧,每个变量的范围只是它索引的维度的长度。 显然,如果变量在一个或多个张量中索引一个以上的维度,则每个维度的长度必须相等。 这是上面带有完整范围的代码:

# C's shape is determined by the shapes of the inputs
# b indexes both A and B, so its range can come from either A.shape or B.shape
# i indexes only A, so its range can only come from A.shape, the same is true for j and B
assert A.shape[0] == B.shape[0]
assert A.shape[1] == B.shape[1]
assert A.shape[2] == B.shape[2]
C = np.zeros((A.shape[0], A.shape[3], B.shape[3]))
for b in range(A.shape[0]): # b indexes both A and B, or B.shape[0], which must be the same
    for i in range(A.shape[3]):
        for j in range(B.shape[3]):
            # h and w can come from either A or B
            for h in range(A.shape[1]):
                for w in range(A.shape[2]):
                    C[b, i, j] += A[b, h, w, i] * B[b, h, w, j]

答案 4 :(得分:5)

我找到了NumPy: The tricks of the trade (Part II)指导性的

  

我们使用 - &gt;指示输出数组的顺序。所以想想&#39; ij,i-&gt; j&#39;有左手边(LHS)和右手边(RHS)。 LHS上的任何重复标签都会计算产品元素,然后求和。通过改变RHS(输出)侧的标签,我们可以定义我们想要对输入数组进行处理的轴,即沿轴0,1等的总和。

import numpy as np

>>> a
array([[1, 1, 1],
       [2, 2, 2],
       [3, 3, 3]])
>>> b
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
>>> d = np.einsum('ij, jk->ki', a, b)

注意有三个轴,i,j,k,并且重复j(在左侧)。 i,j代表a的行和列。 j,k的{​​{1}}。

为了计算产品并对齐b轴,我们需要将轴添加到j。 (a将沿(?)第一轴播放)

b
右边没有

a[i, j, k] b[j, k] >>> c = a[:,:,np.newaxis] * b >>> c array([[[ 0, 1, 2], [ 3, 4, 5], [ 6, 7, 8]], [[ 0, 2, 4], [ 6, 8, 10], [12, 14, 16]], [[ 0, 3, 6], [ 9, 12, 15], [18, 21, 24]]]) 所以我们总结j这是3x3x3阵列的第二轴

j

最后,指数在右侧(按字母顺序)反转,因此我们进行转置。

>>> c = c.sum(1)
>>> c
array([[ 9, 12, 15],
       [18, 24, 30],
       [27, 36, 45]])

答案 5 :(得分:4)

关于np.einsum的另一种观点

这里的大多数答案都是通过例子来解释的,我想我会给出一个额外的观点。

np.einsum 是一个通用的矩阵运算工具。给定的字符串包含表示轴的标签的下标。我喜欢将其视为您的操作定义。下标提供了两个明显的约束:

  1. 每个输入数组的轴数,

  2. 输入之间的轴大小相等。

让我们以最初的例子为例:np.einsum('ij,jk->ki', A, B)。这里的约束1.转化为A.ndim == 2B.ndim == 2,以及2.A.shape[1] == B.shape[0]

正如您稍后将看到的,还有其他限制。例如:

  1. 输出下标中的标签不得出现多次。

  2. 输出下标中的标签必须出现在输入下标中。

查看 ij,jk->ki 时,您可以将其视为:

<块引用>

输入数组的哪些组件将有助于输出数组的组件 [k, i]

下标包含输出数组每个组件的操作的确切定义。

我们将坚持操作 ij,jk->ki,以及 AB 的以下定义:

>>> A = np.array([[1,4,1,7], [8,1,2,2], [7,4,3,4]])
>>> A.shape
(3, 4)

>>> B = np.array([[2,5], [0,1], [5,7], [9,2]])
>>> B.shape
(4, 2)

输出 Z 的形状为 (B.shape[1], A.shape[0]),可以通过以下方式天真地构造。从 Z 的空白数组开始:

Z = np.zeros((B.shape[1], A.shape[0]))
for i in range(A.shape[0]):
    for j in range(A.shape[1]):
        for k range(B.shape[0]):
           Z[k, i] += A[i, j]*B[j, k] # ki <- ij*jk

np.einsum 是关于在输出数组中累积贡献。每个 (A[i,j], B[j,k]) 对都有助于每个 Z[k, i] 组件。

您可能已经注意到,它看起来与您计算一般矩阵乘法的方式非常相似...


最小实现

这里是 Python 中 np.einsum 的最小实现。对于某些人来说,这可能有助于了解幕后真正发生的事情。我发现它与 np.einsum 本身的用法一样直观。

随着我们继续,我将继续参考前面的例子。并将 inputs 定义为 [A, B]

np.einsum 实际上可以接受两个以上的输入!在下文中,我们将关注一般情况:n 个输入和 n 个输入下标。主要目标是找到研究领域,所有可能范围的笛卡尔积。

我们不能依赖手动编写 for 循环,因为我们不知道需要多少个循环!主要思想是这样的:我们需要找到所有唯一的标签(我将使用 keykeys 来引用它们),找到相应的数组形状,然后为每个标签创建范围,并且 计算范围的乘积,使用 itertools.product 获得研究领域。

<头>
index keys 约束 sizes ranges
1 'i' A.shape[0] 3 range(0, 3)
2 'j' A.shape[1] == B.shape[0] 4 range(0, 4)
0 'k' B.shape[1] 2 range(0, 2)

研究领域是笛卡尔积:range(0, 2) x range(0, 3) x range(0, 4)

  1. 下标处理:

    >>> expr = 'ij,jk->ki'
    >>> qry_expr, res_expr = expr.split('->')
    >>> inputs_expr = qry_expr.split(',')
    >>> inputs_expr, res_expr
    (['ij', 'jk'], 'ki')
    
  2. 在输入下标中查找唯一键(标签):

    >>> keys = set([(key, size) for keys, input in zip(inputs_expr, inputs) 
                   for key, size in list(zip(keys, input.shape))])
    {('i', 3), ('j', 4), ('k', 2)}
    

    我们应该检查约束(以及输出下标)!使用 set 是一个坏主意,但它适用于本示例的目的。

  3. 我们需要一个包含键(标签)的列表

    >>> to_key = [key for key, _ in keys]
    ['k', 'i', 'j']
    
  4. 获取相关的大小(用于初始化输出数组)并构造范围(用于创建我们的迭代域):

    >>> sizes = {key: size for key, size in keys}
    {'i': 3, 'j': 4, 'k': 2}
    
    >>> ranges = [range(size) for _, size in keys]
    [range(0, 2), range(0, 3), range(0, 4)]
    
  5. 计算 ranges 的笛卡尔积

    >>> domain = product(*ranges)
    

    注意itertools.product 返回一个迭代器,它会随着时间的推移消耗

  6. 将输出张量初始化为:

    >>> res = np.zeros([sizes[key] for key in res_expr])
    
  7. 我们将循环遍历 domain

    >>> for indices in domain:
    ...     pass
    

    对于每次迭代,indices 将包含每个轴上的值。对于我们的示例,这会将 ijk 值作为 元组(k, i, j)。就好像我们在三个 for 循环中一样。对于每个输入(AB),我们需要确定要获取的组件。那是 A[i, j]B[j, k],是的!但这并不能解决问题,因为我们没有变量 ijk(字面意思)。

    我们可以用 indices 压缩 to_key 以创建每个键(标签)与其当前值之间的映射:

    >>> vals= {k: v for v, k in zip(indices, to_key)}
    

    为了获得输出数组的坐标,我们使用 vals 并遍历键:[vals[key] for key in res_expr]。但是,要使用这些来索引输出数组,我们需要用 tuplezip 将其包裹起来,以沿每个轴分隔索引:

    >>> res_ind = tuple(zip([vals[key] for key in res_expr]))
    

    输入索引相同(虽然可以有多个):

    >>> inputs_ind = [tuple(zip([vals[key] for key in expr])) for expr in inputs_expr]
    
  8. 我们将使用 itertools.reduce 来计算所有贡献组件的乘积:

    >>> def reduce_mult(L):
    ...     return reduce(lambda x, y: x*y, L)
    
  9. 域上的整体循环如下所示:

    >>> for indices in domain:
    ...     vals = {k: v for v, k in zip(indices, to_key)}
    ...     res_ind = tuple(zip([vals[key] for key in res_expr]))
    ...     inputs_ind = [tuple(zip([vals[key] for key in expr])) 
    ...                     for expr in inputs_expr]
    ...
    ...     res[res_ind] += reduce_mult([M[i] for M, i in zip(inputs, inputs_ind)])
    

>>> res
array([[70., 44., 65.],
       [30., 59., 68.]]) 

*Phew*,非常接近np.einsum('ij,jk->ki', A, B)

答案 6 :(得分:0)

我认为最简单的示例在tensorflow docs

有四个步骤可将方程式转换为einsum表示法。让我们以这个方程为例C[i,k] = sum_j A[i,j] * B[j,k]

  1. 首先,我们删除变量名称。我们得到ik = sum_j ij * jk
  2. 我们删除sum_j项,因为它是隐式的。我们得到ik = ij * jk
  3. 我们将*替换为,。我们得到ik = ij, jk
  4. 输出在RHS上,并用->符号分隔。我们得到ij, jk -> ik

einsum解释器只是反向执行这四个步骤。将结果中所有缺失的索引相加。

这是文档中的更多示例

# Matrix multiplication
einsum('ij,jk->ik', m0, m1)  # output[i,k] = sum_j m0[i,j] * m1[j, k]

# Dot product
einsum('i,i->', u, v)  # output = sum_i u[i]*v[i]

# Outer product
einsum('i,j->ij', u, v)  # output[i,j] = u[i]*v[j]

# Transpose
einsum('ij->ji', m)  # output[j,i] = m[i,j]

# Trace
einsum('ii', m)  # output[j,i] = trace(m) = sum_i m[i, i]

# Batch matrix multiplication
einsum('aij,ajk->aik', s, t)  # out[a,i,k] = sum_j s[a,i,j] * t[a, j, k]

答案 7 :(得分:0)

一旦熟悉了虚拟索引(公共或重复索引)和 Einstein Summation (einsum) 中虚拟索引的求和,输出 -> 的整形就很容易了。因此重点关注:

  1. 虚拟索引,j中的公共索引np.einsum("ij,jk->ki", a, b)
  2. 沿虚拟索引求和 j

虚拟索引

对于 einsum("...", a, b),无论是否有公共索引,元素乘法总是发生在矩阵 ab 之间。我们可以有 einsum('xy,wz', a, b),它在下标 'xy,wz' 中没有公共索引。

如果有一个公共索引,如j中的"ij,jk->ki",那么它在爱因斯坦求和中被称为虚拟索引

<块引用>

求和的索引是求和索引,在本例中为“i”。它也被称为虚拟索引,因为任何符号都可以替换“i”而不改变表达式的含义,前提是它不与同一术语中的索引符号冲突。

沿虚拟索引求和

对于图中绿色矩形np.einsum("ij,j", a, b)j是虚拟索引。元素乘法 a[i][j] * b[j] 沿 j 轴汇总为 Σ ( a[i][j] * b[j] )

enter image description here

对于每个 np.inner(a[i], b),它是一个 点积 i。这里具体使用 np.inner() 并避免使用 np.dot,因为它不是严格的数学点积运算。

enter image description here

虚拟索引可以出现在任何地方,只要符合规则(详情请参见 youtube)。

对于 i 中的虚拟索引 np.einsum(“ik,il", a, b),它是矩阵 ab 的行索引,因此是 a 中的列,并且从 b 中提取以生成点积enter image description here

输出形式

因为求和沿虚拟索引发生,虚拟索引在结果矩阵中消失,因此i中的“ik,il"被删除并形成形状{{1} }.我们可以告诉(k,l)通过带有np.einsum("... -> <shape>")标识符的输出下标标签来指定输出形式。

有关详细信息,请参阅 numpy.einsum 中的显式模式

<块引用>

在显式模式下,可以通过指定直接控制输出 输出下标标签。这需要标识符 -> 以及 输出下标标签列表。此功能增加了 功能的灵活性,因为可以禁用或强制求和 在需要的时候。调用 ‘->’ 类似于 np.einsum('i->', a),而 np.sum(a, axis=-1) 类似于 np.einsum('ii->i', a)。区别 是 einsum 默认不允许广播。此外 np.diag(a) 直接指定顺序 输出下标标签,因此返回矩阵乘法, 与上面隐式模式中的示例不同。

没有虚拟索引

在 einsum 中没有虚拟索引的示例。

  1. 一个术语(下标索引,例如 np.einsum('ij,jh->ih', a, b))选择每个数组中的一个元素。
  2. 每个左侧元素都应用于右侧元素以进行元素乘法(因此总是会发生乘法)。

"ij" 具有形状 (2,3),其每个元素都应用于形状 (2,2) 的 a。因此,它创建了一个形状为 b 的矩阵,没有求和,因为 (2,3,2,2)(i,j) 都是自由索引。

(k.l)

示例

矩阵 A 行和矩阵 B 列的点积

enter image description here

# --------------------------------------------------------------------------------
# For np.einsum("ij,kl", a, b)
# 1-1: Term "ij" or (i,j), two free indices, selects selects an element a[i][j].
# 1-2: Term "kl" or (k,l), two free indices, selects selects an element b[k][l].
# 2:   Each a[i][j] is applied on b[k][l] for element-wise multiplication a[i][j] * b[k,l]
# --------------------------------------------------------------------------------
# for (i,j) in a:
#    for(k,l) in b:
#        a[i][j] * b[k][l]
np.einsum("ij,kl", a, b)

array([[[[ 0,  0],
         [ 0,  0]],

        [[10, 11],
         [12, 13]],

        [[20, 22],
         [24, 26]]],


       [[[30, 33],
         [36, 39]],

        [[40, 44],
         [48, 52]],

        [[50, 55],
         [60, 65]]]])