In [331]: A=np.random.rand(100,200,300)
In [332]: B=A
建议的einsum,直接从
C[i,j,k] = np.dot(A[i,k,:], B[j,k,:]
表达式:
In [333]: np.einsum( 'ikm, jkm-> ijk', A, B).shape
Out[333]: (100, 100, 200)
In [334]: timeit np.einsum( 'ikm, jkm-> ijk', A, B).shape
800 ms ± 25.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
matmul 在最后 2 个维度上执行 dot,并将领先的维度视为批处理。在您的情况下,“k”是批次维度,“m”是应该遵守last A and 2nd to the last of B 规则的维度。所以重写ikm,jkm... 以适应,并相应地转置A 和B:
In [335]: np.einsum('kim,kmj->kij', A.transpose(1,0,2), B.transpose(1,2,0)).shape
Out[335]: (200, 100, 100)
In [336]: timeit np.einsum('kim,kmj->kij',A.transpose(1,0,2), B.transpose(1,2,0)).shape
774 ms ± 22.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
性能差别不大。但是现在使用matmul:
In [337]: (A.transpose(1,0,2)@B.transpose(1,2,0)).transpose(1,2,0).shape
Out[337]: (100, 100, 200)
In [338]: timeit (A.transpose(1,0,2)@B.transpose(1,2,0)).transpose(1,2,0).shape
64.4 ms ± 1.17 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
并验证值是否匹配(但通常情况下,如果形状匹配,值也会匹配)。
In [339]: np.allclose((A.transpose(1,0,2)@B.transpose(1,2,0)).transpose(1,2,0),np.einsum( 'ikm, jkm->
...: ijk', A, B))
Out[339]: True
我不会尝试测量内存使用情况,但时间改进表明它也更好。
在某些情况下,einsum 被优化为使用matmul。这里似乎不是这样,尽管我们可以使用它的参数。我有点惊讶matmul 做得这么好。
===
我隐约记得另一个关于matmul 在两个数组相同时采取捷径的 SO,A@A。我在这些测试中使用了B=A。
In [350]: timeit (A.transpose(1,0,2)@B.transpose(1,2,0)).transpose(1,2,0).shape
60.6 ms ± 1.17 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [352]: B2=np.random.rand(100,200,300)
In [353]: timeit (A.transpose(1,0,2)@B2.transpose(1,2,0)).transpose(1,2,0).shape
97.4 ms ± 164 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
但这只会产生轻微的影响。
In [356]: np.__version__
Out[356]: '1.16.4'
我的 BLAS 等是标准的 Linux,没什么特别的。