理解tensordot

62

我学会了如何使用einsum之后,现在正尝试理解np.tensordot的工作原理。

但是,关于参数axes的各种可能性,我有点迷失。

为了理解它,因为我从未练习张量微积分,所以我使用以下示例:

A = np.random.randint(2, size=(2, 3, 5))
B = np.random.randint(2, size=(3, 2, 4))
在这种情况下,有哪些不同可能的np.tensordot,您如何手动计算它?

在这种情况下,有哪些不同可能的np.tensordot,您如何手动计算它?

4个回答

72
tensordot的想法非常简单 - 我们输入数组和相应的轴,沿着这些轴进行求和缩减。参与求和缩减的轴在输出中被移除,在输出中保留输入数组的所有其余轴作为不同的轴,按照输入数组的顺序排列。
让我们看一些带有一个和两个轴的求和缩减的示例,并交换输入位置,看看输出中的顺序如何保持不变。
I. 一个轴的求和缩减
输入:
 In [7]: A = np.random.randint(2, size=(2, 6, 5))
   ...:  B = np.random.randint(2, size=(3, 2, 4))
   ...: 

案例#1:

In [9]: np.tensordot(A, B, axes=((0),(1))).shape
Out[9]: (6, 5, 3, 4)

A : (2, 6, 5) -> reduction of axis=0
B : (3, 2, 4) -> reduction of axis=1

Output : `(2, 6, 5)`, `(3, 2, 4)` ===(2 gone)==> `(6,5)` + `(3,4)` => `(6,5,3,4)`

案例 #2(与案例 #1 相同,但输入是互换的):

In [8]: np.tensordot(B, A, axes=((1),(0))).shape
Out[8]: (3, 4, 6, 5)

B : (3, 2, 4) -> reduction of axis=1
A : (2, 6, 5) -> reduction of axis=0

Output : `(3, 2, 4)`, `(2, 6, 5)` ===(2 gone)==> `(3,4)` + `(6,5)` => `(3,4,6,5)`.

II. 两个坐标轴的求和缩减

输入:

In [11]: A = np.random.randint(2, size=(2, 3, 5))
    ...: B = np.random.randint(2, size=(3, 2, 4))
    ...: 

案例#1:

In [12]: np.tensordot(A, B, axes=((0,1),(1,0))).shape
Out[12]: (5, 4)

A : (2, 3, 5) -> reduction of axis=(0,1)
B : (3, 2, 4) -> reduction of axis=(1,0)

Output : `(2, 3, 5)`, `(3, 2, 4)` ===(2,3 gone)==> `(5)` + `(4)` => `(5,4)`

案例 #2:

In [14]: np.tensordot(B, A, axes=((1,0),(0,1))).shape
Out[14]: (4, 5)

B : (3, 2, 4) -> reduction of axis=(1,0)
A : (2, 3, 5) -> reduction of axis=(0,1)

Output : `(3, 2, 4)`, `(2, 3, 5)` ===(2,3 gone)==> `(4)` + `(5)` => `(4,5)`

我们可以将此扩展到尽可能多的轴。


2
什么是“求和约简”? - floflo29
8
你可能知道矩阵乘法涉及在一个轴上保持元素逐个相乘,然后对齐的轴上的元素求和。通过这个求和运算,我们失去了该轴,这被称为约简,简言之,就是求和约简。 - Divakar
2
@BryanHead 使用np.tensordot重新排序输出轴的唯一方法是交换输入。如果这不能得到你想要的结果,那么使用transpose是一个可行的方法。 - Divakar
1
如果@Divakar加入了从1-D张量开始的示例以及如何计算每个条目,那就更好了。例如:t1=K.variable([[1,2],[2,3]] ) t2=K.variable([2,3]) print(K.eval(tf.tensordot(t1,t2,axes=0))) 输出:`[[[2. 3.] [4. 6.]] [[4. 6.] [6. 9.]]] 不确定输出形状为2x2x2`是如何得出的。 - CKM
1
@gboffi 嗯,tensordot 在这里行不通,因为我们需要通过输入保持一个轴对齐,并且在输出中也保持它 - i。因此,einsum 是正确的选择。 - Divakar
显示剩余16条评论

9
交换轴并重塑输入,以便可以将应用于2个2d数组。然后再次交换和重塑以返回目标。这可能比解释更容易实验。没有特殊的张量数学运算,只是将扩展到更高维度。只是指具有超过2d的数组。如果您已经熟悉,则将结果与其进行比较最简单。

一个样本测试,对1对轴求和

In [823]: np.tensordot(A,B,[0,1]).shape
Out[823]: (3, 5, 3, 4)
In [824]: np.einsum('ijk,lim',A,B).shape
Out[824]: (3, 5, 3, 4)
In [825]: np.allclose(np.einsum('ijk,lim',A,B),np.tensordot(A,B,[0,1]))
Out[825]: True

另一个,对两个数字求和。

In [826]: np.tensordot(A,B,[(0,1),(1,0)]).shape
Out[826]: (5, 4)
In [827]: np.einsum('ijk,jim',A,B).shape
Out[827]: (5, 4)
In [828]: np.allclose(np.einsum('ijk,jim',A,B),np.tensordot(A,B,[(0,1),(1,0)]))
Out[828]: True

我们可以使用(1,0)对来做同样的事情。鉴于维度的混合,我不认为还有其他组合。

我仍然没有完全掌握它 :(. 在文档的第一个示例中,他们正在对形状为(4,3)的2个数组进行逐元素相乘,然后在这两个轴上执行sum。如何使用“点”积获得相同的结果? - Brenlla
1
我能够重现文档中的第一个结果的方法是在扁平化的二维数组上使用np.dotfor aa in a.T: for bb in b.T: print(aa.ravel().dot(bb.T.ravel())) - Brenlla
2
tensordotaxes=([1,0],[0,1])einsum 等价,其表达式为 np.einsum('ijk,jil->kl',a,b)。此外,这个点积也可以实现:a.T.reshape(5,12).dot(b.reshape(12,2))。其中 dot 运算是在一个形状为 (5,12) 和 (12,2) 的数组 ab 之间执行的。a.T 将 5 放在了首位,并交换了(3,4)以匹配 b - hpaulj

6

以上的回答非常好,对我理解tensordot有很大帮助。但是它们没有展示操作背后的实际数学知识。因此,我在TF 2中进行了等效操作,并决定在这里分享:

a = tf.constant([1,2.])
b = tf.constant([2,3.])
print(f"{tf.tensordot(a, b, 0)}\t tf.einsum('i,j', a, b)\t\t- ((the last 0 axes of a), (the first 0 axes of b))")
print(f"{tf.tensordot(a, b, ((),()))}\t tf.einsum('i,j', a, b)\t\t- ((() axis of a), (() axis of b))")
print(f"{tf.tensordot(b, a, 0)}\t tf.einsum('i,j->ji', a, b)\t- ((the last 0 axes of b), (the first 0 axes of a))")
print(f"{tf.tensordot(a, b, 1)}\t\t tf.einsum('i,i', a, b)\t\t- ((the last 1 axes of a), (the first 1 axes of b))")
print(f"{tf.tensordot(a, b, ((0,), (0,)))}\t\t tf.einsum('i,i', a, b)\t\t- ((0th axis of a), (0th axis of b))")
print(f"{tf.tensordot(a, b, (0,0))}\t\t tf.einsum('i,i', a, b)\t\t- ((0th axis of a), (0th axis of b))")

[[2. 3.]
 [4. 6.]]    tf.einsum('i,j', a, b)     - ((the last 0 axes of a), (the first 0 axes of b))
[[2. 3.]
 [4. 6.]]    tf.einsum('i,j', a, b)     - ((() axis of a), (() axis of b))
[[2. 4.]
 [3. 6.]]    tf.einsum('i,j->ji', a, b) - ((the last 0 axes of b), (the first 0 axes of a))
8.0          tf.einsum('i,i', a, b)     - ((the last 1 axes of a), (the first 1 axes of b))
8.0          tf.einsum('i,i', a, b)     - ((0th axis of a), (0th axis of b))
8.0          tf.einsum('i,i', a, b)     - ((0th axis of a), (0th axis of b))

当形状为(2,2)时:

a = tf.constant([[1,2],
                 [-2,3.]])

b = tf.constant([[-2,3],
                 [0,4.]])
print(f"{tf.tensordot(a, b, 0)}\t tf.einsum('ij,kl', a, b)\t- ((the last 0 axes of a), (the first 0 axes of b))")
print(f"{tf.tensordot(a, b, (0,0))}\t tf.einsum('ij,ik', a, b)\t- ((0th axis of a), (0th axis of b))")
print(f"{tf.tensordot(a, b, (0,1))}\t tf.einsum('ij,ki', a, b)\t- ((0th axis of a), (1st axis of b))")
print(f"{tf.tensordot(a, b, 1)}\t tf.matmul(a, b)\t\t- ((the last 1 axes of a), (the first 1 axes of b))")
print(f"{tf.tensordot(a, b, ((1,), (0,)))}\t tf.einsum('ij,jk', a, b)\t- ((1st axis of a), (0th axis of b))")
print(f"{tf.tensordot(a, b, (1, 0))}\t tf.matmul(a, b)\t\t- ((1st axis of a), (0th axis of b))")
print(f"{tf.tensordot(a, b, 2)}\t tf.reduce_sum(tf.multiply(a, b))\t- ((the last 2 axes of a), (the first 2 axes of b))")
print(f"{tf.tensordot(a, b, ((0,1), (0,1)))}\t tf.einsum('ij,ij->', a, b)\t\t- ((0th axis of a, 1st axis of a), (0th axis of b, 1st axis of b))")
[[[[-2.  3.]
   [ 0.  4.]]
  [[-4.  6.]
   [ 0.  8.]]]

 [[[ 4. -6.]
   [-0. -8.]]
  [[-6.  9.]
   [ 0. 12.]]]]  tf.einsum('ij,kl', a, b)   - ((the last 0 axes of a), (the first 0 axes of b))
[[-2. -5.]
 [-4. 18.]]      tf.einsum('ij,ik', a, b)   - ((0th axis of a), (0th axis of b))
[[-8. -8.]
 [ 5. 12.]]      tf.einsum('ij,ki', a, b)   - ((0th axis of a), (1st axis of b))
[[-2. 11.]
 [ 4.  6.]]      tf.matmul(a, b)            - ((the last 1 axes of a), (the first 1 axes of b))
[[-2. 11.]
 [ 4.  6.]]      tf.einsum('ij,jk', a, b)   - ((1st axis of a), (0th axis of b))
[[-2. 11.]
 [ 4.  6.]]      tf.matmul(a, b)            - ((1st axis of a), (0th axis of b))
16.0    tf.reduce_sum(tf.multiply(a, b))    - ((the last 2 axes of a), (the first 2 axes of b))
16.0    tf.einsum('ij,ij->', a, b)          - ((0th axis of a, 1st axis of a), (0th axis of b, 1st axis of b))

3
除了上面的答案之外,如果将np.tensordot分解成嵌套循环,理解起来会更容易:
假设:
import numpy as np
a = np.arange(24).reshape(2,3,4)
b = np.arange(30).reshape(3,5,2)

然后 a
array([[[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11]],

       [[12, 13, 14, 15],
        [16, 17, 18, 19],
        [20, 21, 22, 23]]])

b

array([[[ 0,  1],
        [ 2,  3],
        [ 4,  5],
        [ 6,  7],
        [ 8,  9]],

       [[10, 11],
        [12, 13],
        [14, 15],
        [16, 17],
        [18, 19]],

       [[20, 21],
        [22, 23],
        [24, 25],
        [26, 27],
        [28, 29]]])

c = np.tensordot(a, b, axes=([0,1],[2,0]))

相当于

c = np.zeros((4,5))
for i in range(4):
    for j in range(5):
        for p in range(2):
            for q in range(3):
                c[i,j] += a[p,q,i] * b[q,j,p]

在两个张量中,具有相同维度的轴(这里是2和3)可以通过对它们求和来减少。而参数axes=([0,1],[2,0])与axes=([1,0],[0,2])是相同的。
最终的c为:
array([[ 808,  928, 1048, 1168, 1288],
       [ 871, 1003, 1135, 1267, 1399],
       [ 934, 1078, 1222, 1366, 1510],
       [ 997, 1153, 1309, 1465, 1621]])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接