我很难准确理解einsum
的确切运作方式。我查看了文档和一些示例,但它似乎并不坚持。
这是我们在课堂上看到的一个例子:
C = np.einsum("ij,jk->ki", A, B)
表示两个数组A
和B
我认为这需要A^T * B
,但我不确定(它正在对其中一个进行转置吗?)。任何人都可以告诉我这里发生了什么(通常在使用einsum
时)?
答案 0 :(得分:256)
(注意:此答案基于我前一段时间写过的关于einsum
的简短blog post。)
einsum
做什么?想象一下,我们有两个多维数组,A
和B
。现在我们假设我们想......
A
与B
相乘,以创建新的产品数组;然后也许 einsum
很有可能帮助我们更快,更高效地执行此操作,以及multiply
,sum
和transpose
等NumPy函数的组合将允许
einsum
如何运作?这是一个简单(但不是完全无关紧要)的例子。采用以下两个数组:
A = np.array([0, 1, 2])
B = np.array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
我们将以元素方式乘以A
和B
,然后沿新数组的行求和。在“正常”的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
有一个轴;我们已将其标记为i
。 B
有两个轴;我们将轴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)
计算点积。这是一张图片,显示A
和B
的标签以及我们从函数中获得的输出数组:
您可以看到标签j
重复出现 - 这意味着我们将A
的行与B
的列相乘。此外,标签j
未包含在输出中 - 我们正在对这些产品进行求和。标签i
和k
保留用于输出,因此我们返回2D数组。
将此结果与标签j
不求和的数组进行比较可能更清楚。在左下方,您可以看到撰写np.einsum('ij,jk->ijk', A, B)
后得到的3D数组(即我们保留了标签j
):
求和轴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数组C
和D
,只要轴是兼容的长度(长度相同或长度为1),这里有几个例子:
可以写出C
(主对角线的总和)np.trace(C)
的痕迹:
np.einsum('ii', C)
C
的元素乘法和D
,C * 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)数组。 i
是A
的第一个暗点,C
的最后一个; k
的最后一个B
,C
的第一个。 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 = 20
,9 + 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个逗号分隔的斑点-bhwi
和bhwj
,
以及其后的单个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
循环。我们现在暂时不确定范围。
接下来,我们看一下左侧-是否有任何范围变量,不出现在右侧一侧?在我们的情况下-是,h
和w
。
为每个此类变量添加一个内部嵌套的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
是一个通用的矩阵运算工具。给定的字符串包含表示轴的标签的下标。我喜欢将其视为您的操作定义。下标提供了两个明显的约束:
每个输入数组的轴数,
输入之间的轴大小相等。
让我们以最初的例子为例:np.einsum('ij,jk->ki', A, B)
。这里的约束1.转化为A.ndim == 2
和B.ndim == 2
,以及2.到A.shape[1] == B.shape[0]
。
正如您稍后将看到的,还有其他限制。例如:
输出下标中的标签不得出现多次。
输出下标中的标签必须出现在输入下标中。
查看 ij,jk->ki
时,您可以将其视为:
输入数组的哪些组件将有助于输出数组的组件 [k, i]
。
下标包含输出数组每个组件的操作的确切定义。
我们将坚持操作 ij,jk->ki
,以及 A
和 B
的以下定义:
>>> 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
循环,因为我们不知道需要多少个循环!主要思想是这样的:我们需要找到所有唯一的标签(我将使用 key
和 keys
来引用它们),找到相应的数组形状,然后为每个标签创建范围,并且 计算范围的乘积,使用 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)
。
下标处理:
>>> expr = 'ij,jk->ki'
>>> qry_expr, res_expr = expr.split('->')
>>> inputs_expr = qry_expr.split(',')
>>> inputs_expr, res_expr
(['ij', 'jk'], 'ki')
在输入下标中查找唯一键(标签):
>>> 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
是一个坏主意,但它适用于本示例的目的。
我们需要一个包含键(标签)的列表:
>>> to_key = [key for key, _ in keys]
['k', 'i', 'j']
获取相关的大小(用于初始化输出数组)并构造范围(用于创建我们的迭代域):
>>> 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)]
计算 range
s 的笛卡尔积
>>> domain = product(*ranges)
注意:itertools.product
返回一个迭代器,它会随着时间的推移消耗。
将输出张量初始化为:
>>> res = np.zeros([sizes[key] for key in res_expr])
我们将循环遍历 domain
:
>>> for indices in domain:
... pass
对于每次迭代,indices
将包含每个轴上的值。对于我们的示例,这会将 i
、j
和 k
值作为 元组:(k, i, j)
。就好像我们在三个 for
循环中一样。对于每个输入(A
和 B
),我们需要确定要获取的组件。那是 A[i, j]
和 B[j, k]
,是的!但这并不能解决问题,因为我们没有变量 i
、j
和 k
(字面意思)。
我们可以用 indices
压缩 to_key
以创建每个键(标签)与其当前值之间的映射:
>>> vals= {k: v for v, k in zip(indices, to_key)}
为了获得输出数组的坐标,我们使用 vals
并遍历键:[vals[key] for key in res_expr]
。但是,要使用这些来索引输出数组,我们需要用 tuple
和 zip
将其包裹起来,以沿每个轴分隔索引:
>>> 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]
我们将使用 itertools.reduce
来计算所有贡献组件的乘积:
>>> def reduce_mult(L):
... return reduce(lambda x, y: x*y, L)
域上的整体循环如下所示:
>>> 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]
ik = sum_j ij * jk
sum_j
项,因为它是隐式的。我们得到ik = ij * jk
*
替换为,
。我们得到ik = ij, jk
->
符号分隔。我们得到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) 中虚拟索引的求和,输出 ->
的整形就很容易了。因此重点关注:
j
中的公共索引np.einsum("ij,jk->ki", a, b)
j
对于 einsum("...", a, b)
,无论是否有公共索引,元素乘法总是发生在矩阵 a
和 b
之间。我们可以有 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] )
。
对于每个 np.inner(a[i], b)
,它是一个 点积 i
。这里具体使用 np.inner()
并避免使用 np.dot
,因为它不是严格的数学点积运算。
虚拟索引可以出现在任何地方,只要符合规则(详情请参见 youtube)。
对于 i
中的虚拟索引 np.einsum(“ik,il", a, b)
,它是矩阵 a
和 b
的行索引,因此是 a
中的列,并且从 b
中提取以生成点积。
因为求和沿虚拟索引发生,虚拟索引在结果矩阵中消失,因此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 中没有虚拟索引的示例。
np.einsum('ij,jh->ih', a, b)
)选择每个数组中的一个元素。"ij"
具有形状 (2,3),其每个元素都应用于形状 (2,2) 的 a
。因此,它创建了一个形状为 b
的矩阵,没有求和,因为 (2,3,2,2)
、(i,j)
都是自由索引。
(k.l)
# --------------------------------------------------------------------------------
# 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]]]])