很多复杂的矩阵计算可以使用 einsum 来表示,方便 PoC,性能也还过得去。
Einstein notation
你没有看错,Einstein notation 是那位著名的爱因斯坦发明的,用来对线性代数中求和的表示做的约定。我们还是看个例子[1]:
$$ y = \sum_{i=1}^{3}{c_i x_i} = c_1 x_1 + c_2 x_2 + c_3 x_3 $$
如果一个下标(如 $i$)在公式中出现两次,则隐式地认为需要遍历它的所有可能性。上面公式可以简化成:
$$ y = c_i x_i $$
于是矩阵乘法中,每个输出元素可以这样表示:
$$ c_{ij} = \sum_{k}{a_{ik} b_{kj}} \implies c_{ik} = a_{ik} b_{kj} $$
Einsum
在 Numpy
和 Pytorch 中都实现了类似的机制。einsum
函数的第一个参数就是把上节公式中的各个下标按
a,b->c
的格式写下来:
代码实例如下:
In [6]: a = np.asarray(range(1,9)).reshape(2,4) |
复杂应用
例如在 CNN 求卷积时,输入是 (n, c, ih, iw)
的矩阵,卷积权重是 (C, c, h, w)
(这里大写字母代表输出维度)。可以把原图像按卷积大小的各个子图求出,得到 (n, c, H, W, h, w)
的输入矩阵,于是可以使用 einsum 直接求结果:
def conv2d(x, weight, stride=(1,1), padding=(0,0)): |
看到 ncHWhw,Cchw->nCHW
的输入中,c, h, w
下标是重复的,按约定要遍历所有三个下标的元素相乘,要是裸写代码的话,类似下面这样:
n,c,H,W,h,w = submatrix.shape |
性能
一般比裸写 for 循环是要快不少的(比如上面的卷积,比我自己裸写的快 3x~5x)。但比专门优化的肯定还是不能比的(pytorch 的 conv2d 是用 C++ 专门优化的,比相同的 einsum 快 10x)。
另外 这篇文章 建议无脑开 Numpy 中的
optimize
参数。
wiki 中 $x_i$ 表示成 $x^i$,我们这里还是以普通人视角来看 ↩