没有循环和内存错误的 Numpy 逐元素点积

Numpy element-wise dot product without loop and memory error

我正在用 numpy 处理一个简单的问题。我有两个矩阵列表 - 比如说 A,B - 编码为形状分别为 (n,p,q)(n,q,r) 的 3D 数组。

我想计算它们的元素点积,这是一个 3D 数组 C 使得 C[i,j,l] = sum A[i,j,:] B[i,:,l]。这在数学上非常简单,但我必须遵循以下规则:

1) 我必须只使用 numpy 函数(dottensordoteinsum 等):没有循环 & cie。这是因为我希望它能在我的 gpu 上工作(使用 cupy)并且循环很糟糕。我希望所有操作都在当前设备上进行。

2) 由于我的数据可能非常大,通常 AB 已经占用了几十 Mb 的内存,我不想构建任何形状大于 (n,p,q),(n,q,r),(n,p,r)(无需存储中间 4D 数组)。

例如,我找到的解决方案 there ,即使用:

C = np.sum(np.transpose(A,(0,2,1)).reshape(n,p,q,1)*B.reshape(n,q,1,r),-3)

从数学上讲是正确的,但意味着中间创建了一个 (n,p,q,r) 数组,这对我的目的来说太大了。

我遇到类似的问题

C = np.einsum('ipq,iqr->ipr',A,B)

不知道底层的操作构造是什么,但总是会导致内存错误

另一方面,有点天真,比如:

C = np.array([A[i].dot(B[i]) for i in range(n)])

在内存方面似乎还可以,但在我的 gpu 上效率不高:列表似乎是建立在 CPU 上的,并且将它重新分配给 gpu 很慢(如果有一个 cupy-friendly这样写,这将是一个很好的解决方案!)

感谢您的帮助!

你想要 numpy.matmul (cupy version here)。 matmul 是一个 "broadcasting" 矩阵乘法。

我认为人们已经知道 numpy.dot 语义不稳定,需要广播矩阵乘法,但是在 python 得到 @ 运算符。我没有看到 dot 有任何进展,但我怀疑更好的语义和更容易做的 A @ B 将意味着 dot 会随着人们发现新的函数和运算符而失宠.

您试图避免的迭代方法可能还不错。例如,考虑这些时间:

In [51]: A = np.ones((100,10,10))
In [52]: timeit np.array([A[i].dot(A[i]) for i in range(A.shape[0])])
439 µs ± 1.35 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [53]: timeit np.einsum('ipq,iqr->ipr',A,A)
428 µs ± 170 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [54]: timeit A@A
426 µs ± 54.6 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)

对于这种情况,所有三个时间都差不多。

但是我把后面的维度加倍了,迭代的方式其实更快:

In [55]: A = np.ones((100,20,20))
In [56]: timeit np.array([A[i].dot(A[i]) for i in range(A.shape[0])])
702 µs ± 1.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [57]: timeit np.einsum('ipq,iqr->ipr',A,A)
1.89 ms ± 1.63 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [58]: timeit A@A
1.89 ms ± 490 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)

当我将 20 更改为 30 和 40 时,同样的模式成立。我有点惊讶 matmul 时间与 einsum 如此接近。

我想我可以尝试将它们推到内存限制。我没有花哨的后端来测试这方面。

在考虑到内存管理问题后,对一个大问题进行适度的迭代并不可怕。在 numpy 中,您想要避免的事情是对一个简单任务进行多次迭代。