Windows
10,Python
3.10.0,NumPy
1.21.5
设置numpy
随机数种子:
import numpy as np
np.random.seed(42)
矩阵乘法是这样定义的:
M i j = ∑ k A i k B k j = A i k B k j M_{ij} = \sum_k A_{ik}B_{kj} = A_{ik}B_{kj} Mij=k∑AikBkj=AikBkj
假设 A , B A, B A,B都是 3 × 3 3\times 3 3×3矩阵,将上述矩阵乘法展开将是这个样子:
[ a 11 a 12 a 13 a 21 a 22 a 23 a 31 a 32 a 33 ] [ b 11 b 12 b 13 b 21 b 22 b 23 b 31 b 32 b 33 ] = [ c 11 c 12 c 13 c 21 c 22 c 23 c 31 c 32 c 33 ] \left[\begin{array}{lll} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33} \end{array}\right]\left[\begin{array}{lll} b_{11} & b_{12} & b_{13} \\ b_{21} & b_{22} & b_{23} \\ b_{31} & b_{32} & b_{33} \end{array}\right]=\left[\begin{array}{lll} c_{11} & c_{12} & c_{13} \\ c_{21} & c_{22} & c_{23} \\ c_{31} & c_{32} & c_{33} \end{array}\right] ⎣⎡a11a21a31a12a22a32a13a23a33⎦⎤⎣⎡b11b21b31b12b22b32b13b23b33⎦⎤=⎣⎡c11c21c31c12c22c32c13c23c33⎦⎤
用Python
循环实现如下:
A = np.random.rand(3, 5)
B = np.random.rand(5, 2)
M = np.empty((3, 2))
for i in range(3):
for j in range(2):
for k in range(5):
M[i, j] += A[i, k] * B[k, j]
我们将存储下标所用的变量名指代循环,比如上述代码中,最外层循环称为 i i i循环,最内层循环成为 k k k循环。
上式可分解为三个步骤:
A[i, :]
和B[:, j]
)进行逐元素相乘,得到一个新向量或者采用来自百度飞桨的表述1:
einsum
求和过程理论上等价于如下四步,但实现中实际执行的步骤会有差异。
- 第一步,维度对齐:将所有标记按字母序排序,按照标记顺序将输入张量逐一转置、补齐维度,使得处理后的所有张量其维度标记保持一致
- 第二步,广播乘积:以维度下标为索引进行广播点乘
- 第三步,维度规约:将哑标对应的维度分量求和消除
- 第四步,转置输出:若存在输出标记,则按标记进行转置,否则按广播维度+字母序自由标的顺序转置,返回转之后的张量作为输出
由此我们可以得到上述运算的一个抽象:
"ik, kj -> ij"
对照文章最开始的公式,是否已经理解?
这就是在众多Python
科学计算库中常用的einsum
函数,上式使用einsum
实现如下
M = np.einsum("ik, kj -> ij", A, B)
已知的前提条件:
for
循环实现在矩阵之间的运算中,下标可以分为两类:
这两种分类不重不漏地包含了所有参与运算的下标,但不管怎样,相同下标代指的一轴维度必须相同。
在上一节的例子中, i , j i,j i,j是自由标, k k k是哑标。
让我们来看一个简单的例子:
a = np.random.rand(5)
b = np.random.rand(3)
outer_prod = np.einsum("i, j -> ij", a, b)
参照之前,我们将einsum
展开为循环:
c = np.zeros((5, 3))
# 显而易见i循环和j循环的顺序对调,在数学上是等价的,也就是i循环和j循环无关
for i in range(5):
for j in range(3):
c[i, j] = a[i] * b[j]
在这个例子中, i , j i, j i,j都是自由标,所以它们都被保留了下来;没有哑标。
还是拿过来刚刚的例子
A = np.random.rand(3, 5)
B = np.random.rand(5, 2)
M = np.einsum("ik, kj -> ij", A, B)
在该代码中, k k k所在的维度被逐元素相乘。
依然参照以上代码,输出中没有 k k k,所以 k k k所在的维度被求和。
如果输出中存在 k k k,那么 k k k所在维度不会被求和。
M = np.einsum("ik, kj -> ijk", A, B)
展开为循环将是这样:
A = np.random.rand(3, 5)
B = np.random.rand(5, 2)
M = np.empty((3, 2, 5))
for i in range(3):
for j in range(2):
for k in range(5):
M[i, j, k] = A[i, k] * B[k, j]
输出顺序决定了轴的排列顺序,比如:
M = np.einsum("ik, kj -> kij", A, B)
聪明的你应该已经会写出数学上等价的循环形式了吧?
A = np.random.rand(3, 5)
B = np.random.rand(5, 2)
M = np.empty((5, 3, 2))
for i in range(3):
for j in range(2):
for k in range(5):
M[k, i, j] = A[i, k] * B[k, j]
如下的张量操作或运算均可视为 Einstein 求和的特例
trace
:einsum("ii -> i", x)
transpose
:einsum("ij -> ji", x)
sum
:einsum("ij -> i", x)
dot
outer
mul
matmul
bmm
mul
A.matmul(B).matmul(C)
# 单操作数
x = np.random.rand(2, 3)
m = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
## 迹
np.einsum("ii -> i", m)
# 如果看不懂就展开成循环
## 转置
np.einsum("ij -> ji", x)
## 求和
np.einsum("ij -> i", x) # 对行求和
np.einsum("ij -> j", x) #对列求和
np.einsum("ij -> ", x) # 全部求和
# 双操作数
a = np.random.rand(5)
b = np.random.rand(3)
c = np.random.rand(5)
## 内积
np.einsum("i, i ->", a, c)
## 外积
np.einsum("i, j -> ij", a, b) # (5, 3)
## 矩阵乘
y = np.random.rand(3, 5)
np.einsum("ik, kj -> ij", x, y)
# 多操作数
z = np.random.rand(5, 2)
np.einsum("ij, jk, kl -> il", x, y, z)
# 广播写法
A = numpy.random.rand(2, 3, 2)
B = numpy.random.rand(2, 2, 3)
np.einsum('...jk, ...kl->...jl', A,B) # (2, 3, 3)
可以前往https://ajcr.net/Basic-guide-to-einsum/查看更多高级用法。
numpy.einsum
隐式写法2上文中字符串中出现箭头符号->
为显式写法,没有->
为隐式写法,即隐式写法不指定输出。
没有指定输出但是会有输出,numpy
会按以下方法推断输出:
即,对于np.einsum("ik, kj ->", x, y)
,等价于np.einsum("ik, kj -> ij", x, y)
。
再来一个例子3:
M = np.random.rand(2, 3, 4)
np.einsum("kij", M) # (3, 4, 2)
不过还是希望大家记住The Zen of Python中那句话:
Explicit is better than implicit.
正如刚刚例子中最后一个:
A = numpy.random.rand(2, 3, 2)
B = numpy.random.rand(2, 2, 3)
np.einsum('...jk, ...kl->...jl', A,B) # (2, 3, 3)
...
指代任意多个轴,这在机器学习中处理batch
时尤为有效。
torch
、tensorflow
、paddle
等包含einsum
函数,并支持反向传播,MindSpore
的支持也在进行之中。
如果哪位大佬引用了我的文章,麻烦在文末加一个链接,谢谢!
https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/api/paddle/einsum_cn.html#einsum ↩︎
https://numpy.org/devdocs/reference/generated/numpy.einsum.html ↩︎
https://ajcr.net/Basic-guide-to-einsum/ ↩︎