numpy dot()和Python 3.5+矩阵乘法@之间的区别

188

我最近转向使用Python 3.5,并注意到新的矩阵乘法运算符(@)有时与numpy中的点积(dot)运算符表现不同。例如,对于3D数组:

import numpy as np

a = np.random.rand(8,13,13)
b = np.random.rand(8,13,13)
c = a @ b  # Python 3.5+
d = np.dot(a, b)

@ 操作符返回一个形状为:

c.shape
(8, 13, 13)

当使用np.dot()函数时,会返回:

d.shape
(8, 13, 8, 13)

我如何使用numpy dot方法获得相同的结果?还有其他重要的区别吗?


7
用dot运算无法得到那个结果。我认为人们普遍都认为,在处理高维度输入方面,dot的设计决策是错误的。 - user2357112
为什么他们多年前没有实现matmul函数?@作为中缀运算符是新的,但是该函数即使没有它也能正常工作。 - hpaulj
6个回答

222

@ 运算符调用数组的 __matmul__ 方法,而不是 dot。该方法也作为函数 np.matmul 存在于 API 中。

>>> a = np.random.rand(8,13,13)
>>> b = np.random.rand(8,13,13)
>>> np.matmul(a, b).shape
(8, 13, 13)

从文档中可以得知:

matmuldot 在两个重要的方面有所不同。

  • 不允许标量相乘。
  • 矩阵堆栈被视为元素一样进行广播。

最后一点表明当传递3D(或更高维)数组时,dotmatmul 方法的行为不同。继续引用文档:

对于 matmul

如果任一参数是 N-D,N > 2,则将其视为矩阵的堆栈,驻留在最后两个索引中并相应地进行广播。

对于np.dot

对于2D数组,它等价于矩阵乘法,对于1D数组,它等价于向量的内积(无复共轭)。对于N维数组,它是a的最后一个轴和b的倒数第二个轴上的乘积之和


21
混淆的原因可能是由于发布说明,其中将“@”符号直接等同于numpy中dot()函数的示例代码。 - Alex K

28

仅供参考,@ 和它的numpy等效函数 dotmatmul 都同样快。(图表由perfplot创建,这是我的一个项目。)

enter image description here

代码以重现图表:

import perfplot
import numpy


def setup(n):
    A = numpy.random.rand(n, n)
    x = numpy.random.rand(n)
    return A, x


def at(A, x):
    return A @ x


def numpy_dot(A, x):
    return numpy.dot(A, x)


def numpy_matmul(A, x):
    return numpy.matmul(A, x)


perfplot.show(
    setup=setup,
    kernels=[at, numpy_dot, numpy_matmul],
    n_range=[2 ** k for k in range(15)],
)

2
以上答案表明这些方法并不相同。 - Grzegorz Krug
这就是为什么答案以FYI开头的原因。 - PatrickT

22

@ajcr的回答解释了dotmatmul(由@符号调用)之间的区别。通过查看一个简单的例子,人们可以清楚地看到这两个在操作“矩阵堆栈”或张量时的不同行为。

为了澄清差异,拿一个4x4数组并返回它与一个3x4x2“矩阵堆栈”或张量的dot积和matmul积。

import numpy as np
fourbyfour = np.array([
                       [1,2,3,4],
                       [3,2,1,4],
                       [5,4,6,7],
                       [11,12,13,14]
                      ])


threebyfourbytwo = np.array([
                             [[2,3],[11,9],[32,21],[28,17]],
                             [[2,3],[1,9],[3,21],[28,7]],
                             [[2,3],[1,9],[3,21],[28,7]],
                            ])

print('4x4*3x4x2 dot:\n {}\n'.format(np.dot(fourbyfour,threebyfourbytwo)))
print('4x4*3x4x2 matmul:\n {}\n'.format(np.matmul(fourbyfour,threebyfourbytwo)))

每个操作的结果如下所示。请注意,点积是在a的最后一个轴和b的倒数第二个轴上进行的总和乘积,

...而矩阵积是通过将矩阵广播在一起形成的。

(注:该段文字描述了向量和矩阵运算中的点积和矩阵积的特点)

4x4*3x4x2 dot:
 [[[232 152]
  [125 112]
  [125 112]]

 [[172 116]
  [123  76]
  [123  76]]

 [[442 296]
  [228 226]
  [228 226]]

 [[962 652]
  [465 512]
  [465 512]]]

4x4*3x4x2 matmul:
 [[[232 152]
  [172 116]
  [442 296]
  [962 652]]

 [[125 112]
  [123  76]
  [228 226]
  [465 512]]

 [[125 112]
  [123  76]
  [228 226]
  [465 512]]]

3
dot(a, b) [i,j,k,m] 表示对于a的最后一个轴和b的倒数第二个轴进行求和乘积:sum(a[i,j,:] * b[k,:,m])。与文档介绍相同。请注意,我的回答可能略微超出字数限制。 - Ronak Agrawal
很不错,不过它是一个3x4x2的数组。另一种构建该矩阵的方式是 a = np.arange(24).reshape(3, 4, 2),它将创建一个尺寸为3x4x2的数组。 - Nathan

18
在数学中,我认为numpy中的dot更有意义。

dot(a,b)_{i,j,k,a,b,c} = formula

因为当a和b是向量时,它给出点积;当a和b是矩阵时,它给出矩阵乘法。
关于numpy中的matmul操作,它由dot结果的部分组成,并可以定义为
matmul(a,b)_{i,j,k,c} = ∑_m a_{i,j,k,m} b_{i,j,m,c}

因此,您可以看到matmul(a,b)返回一个形状较小的数组, 它具有更小的内存消耗,在应用程序中更有意义。 特别是,结合broadcasting,您可以获得

matmul(a,b)_{i,j,k,l} = formula

例如。


从上述两个定义可以看出使用这两种操作的要求。假设 a.shape=(s1,s2,s3,s4)b.shape=(t1,t2,t3,t4)
  • 要使用 dot(a,b),你需要
  1. t3=s4
  • 要使用 matmul(a,b),你需要
  1. t3=s4
  2. t2=s2,或者 t2 和 s2 中的一个为 1
  3. t1=s1,或者 t1 和 s1 中的一个为 1

使用下面的代码片段来验证自己。
import numpy as np
for it in range(10000):
    a = np.random.rand(5,6,2,4)
    b = np.random.rand(6,4,3)
    c = np.matmul(a,b)
    d = np.dot(a,b)
    #print ('c shape: ', c.shape,'d shape:', d.shape)
    
    for i in range(5):
        for j in range(6):
            for k in range(2):
                for l in range(3):
                    if c[i,j,k,l] != d[i,j,k,j,l]:
                        print (it,i,j,k,l,c[i,j,k,l]==d[i,j,k,j,l])  # you will not see them              

np.matmul 函数可以对向量进行点积运算,也可以对矩阵进行矩阵乘法运算。 - Subhaneil Lahiri

8
这里是与 np.einsum 的比较,以展示指数如何被投影。
np.allclose(np.einsum('ijk,ijk->ijk', a,b), a*b)        # True 
np.allclose(np.einsum('ijk,ikl->ijl', a,b), a@b)        # True
np.allclose(np.einsum('ijk,lkm->ijlm',a,b), a.dot(b))   # True

0

我与MATMUL和DOT的经验

在尝试使用MATMUL时,我经常遇到“ValueError:传递值的形状为(200,1),索引暗示为(200,3)”的错误。我想要一个快速的解决方法,并发现DOT可以提供相同的功能。使用DOT时我没有任何错误。我得到了正确的答案。

使用MATMUL

X.shape
>>>(200, 3)

type(X)

>>>pandas.core.frame.DataFrame

w

>>>array([0.37454012, 0.95071431, 0.73199394])

YY = np.matmul(X,w)

>>>  ValueError: Shape of passed values is (200, 1), indices imply (200, 3)"

使用 DOT

YY = np.dot(X,w)
# no error message
YY
>>>array([ 2.59206877,  1.06842193,  2.18533396,  2.11366346,  0.28505879, …

YY.shape

>>> (200, )

np.dot将数据框转换为数组,并返回一个(200,3)形状和一个(3,)=.(200,)形状的数组。matmul的错误是由pandas产生的,它试图从该数组中创建一个包含3个列的数据框。我怀疑完整的回溯将显示出这点。 - hpaulj

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