einsum的原理与使用

简介

einsum(爱因斯坦求和)是pytorch、numpy中一个十分优雅的方法,如果利用得当,可完全代替所有其他的矩阵计算方法,不过这需要一定的学习成本。本文旨在详细解读einsum方法的原理,并给出一些基本示例。

问题引入

在线性代数中,我们最多涉及的是二阶及以下的张量.在这种情况下,纸面上可以很方便地写出低阶张量的矩阵形式,高阶的张量,它们的坐标就没法用矩阵表示.我们当然可以把矩阵拓展为立体阵等概念,但随着阶数上升,这种表示法的复杂程度几何级增加;我们也可以使用张量词条中所提过的向量矩阵的方法,比起立体阵要清楚一些,但套娃式的表达方式也对理解一个张量的性质造成了障碍.

爱因斯坦求和约定正是为了简洁地表达高阶张量的坐标运算而存在的.

一、矩阵乘法

假设 A , B A,B A,B 矩阵大小分别是 2 ∗ 3 2*3 23 3 ∗ 2 3*2 32 ,矩阵乘法的定义如下:
[ a 11 a 12 a 12 a 21 a 22 a 23 ] ∗ [ b 11 b 12 b 21 b 22 b 31 b 32 ] = [ c 11 c 12 c 21 c 22 ] [ \begin{array} { c c } { a _ { 1 1 } } & { a _ { 1 2 } } & { a _ { 1 2 } } \\ { a _ { 2 1 } } & { a _ { 2 2 } } & { a _ { 2 3 } } \end{array} ]*[ \begin{array} { c c } { b _ { 1 1 } } & { b _ { 1 2 } } \\ { b _ { 2 1 } } & { b _ { 2 2 } } \\ { b _ { 3 1 } } & { b _ { 3 2 } } \\ \end{array} ] = [ \begin{array} { c c } { c _ { 1 1 } } & { c _ { 1 2 } } \\ { c _ { 2 1 } } & { c _ { 2 2 } } \\ \end{array} ] [a11a21a12a22a12a23][b11b21b31b12b22b32]=[c11c21c12c22]
其中, C i j = ∑ k A i k B k j C _ { i j } = \sum _ { k } A _ { i k } B _ { k j } Cij=kAikBkj

python 循环实现:

import numpy as np
np.random.seed(42)

A = np.random.rand(2, 3)
B = np.random.rand(3, 2)
M = np.zeros((2, 2))

for i in range(2):
    for j in range(2):
        for k in range(3):
            M[i, j] += A[i, k] * B[k, j]
print("Matrix A is: \n",A)
print("Matrix B is: \n",B)
print("M = A*B = \n",M)

结果为:

Matrix A is: 
 [[0.37454012 0.95071431 0.73199394]
 [0.59865848 0.15601864 0.15599452]]
Matrix B is: 
 [[0.05808361 0.86617615]
 [0.60111501 0.70807258]
 [0.02058449 0.96990985]]
M = A*B = 
 [[0.60831101 1.70756058]
 [0.13176846 0.78031684]]

二、爱因斯坦求和法

爱因斯坦求和是一种对求和公式简洁高效的记法,其原则是当变量下标重复出现时,即可省略繁琐的求和符号。

比如求和公式:

∑ i = 1 n a i b i = a 1 b 1 + a 2 b 2 + . . . + a n b n \sum_{i=1}^n a_{i} b_{i} = a_{1} b_{1} + a_{2} b_{2} + ... + a_{n} b_{n} i=1naibi=a1b1+a2b2+...+anbn

其中变量 a 和变量 b 的下标重复出现,则可将其表示为:

a i b i = ∑ i = 1 n a i a_{i} b_{i} = \sum_{i=1}^n a_{i} aibi=i=1nai

由此我们可以将上述矩阵运算化简为:

C i j = ∑ k A i k B k j = A i k B k j C_ { i j } = \sum _ { k } A _ { i k } B _ { k j } = A _ { i k } B _ { k j } Cij=kAikBkj=AikBkj

进一步地,我们可以得到矩阵乘法的一个抽象

i k ∗ k j = i j ik * kj = ij ikkj=ij

einsum的原理

一、具体原理

einsum方法正是利用了爱因斯坦求和简洁高效的表示方法,从而可以驾驭任何复杂的矩阵计算操作。基本的框架如下:

C = einsum('ij,jk->ik', A, B)

上述操作表示矩阵A与矩阵B的点积。输入的参数分为两部分:

  • 前面表示计算操作的指令串,
  • 后面是以逗号隔开的操作对象(数量需与前面对应)。

其中在计算操作表示中,

  • “->” 左边是以逗号隔开的下标索引,重复出现的索引即是需要爱因斯坦求和的;
  • “->” 右边是最后输出的结果形式。

以上式为例,其计算公式为: C i k = ∑ j A i j B j k C_{ik} = \sum_{j} A_{ij} B_{jk} Cik=jAijBjk ,其等价于矩阵A与B的点积。

在矩阵之间的运算中,下标可以分为两类:

  • 自由标(Free index),也就是在输入和输出端都出现的下标
  • 哑标(Summation index),在输入端出现但输出端没有出现的下标

矩阵运算中所有参与运算的下标都被包含在次定义中。

以上述矩阵 A , B A,B A,B 的乘法过程为例:

C = np.einsum("ik,kj->ij", A, B)
print("einsum result is :\n", C)
print("M = A*B = \n",M)

可以看出,这与上述通过循环方式得出的结果一致。在 ij,jk -> ik 的例子中, i,j 是自由标,k 是哑标。

二、计算准则

  1. 两个不同矩阵相乘,哑标维度需要逐元素相乘并求和,自由标保留
  2. 自由标可在输出中以任意顺序出现,但只能出现一次

这是两条基本准则,具体的计算场景可以参考下文实例。

三、典型计算场景

利用einsum求解张量运算主要分为单操作数和多操作数的情况,我们分别讨论,并力图转化为循环形式便于明晰求解过程。

1. 单操作数

1.1 矩阵的迹:

迹(trace)指的是方针的对角线元素。
einsum表示为:

m = np.matrix([
    [1,2,3],
    [4,5,6],
    [7,8,9]
])
M=np.einsum("ii -> i", m)
print("Trace of m is :",M)

结果:

Trace of m is : [1 5 9]
1.2 矩阵转置

矩阵的转置(transpose)指矩阵行列互换。
einsum表示为:

x = np.random.rand(2, 3)
M=np.einsum("ij -> ji", x)
print("origin x is :\n",x)
print("transpose of x is :\n",M)

结果:

origin x is :
 [[0.43194502 0.29122914 0.61185289]
 [0.13949386 0.29214465 0.36636184]]
transpose of x is :
 [[0.43194502 0.13949386]
 [0.29122914 0.29214465]
 [0.61185289 0.36636184]]
1.3 矩阵求和

按行还是列求和,取决于最终保留的下标:

m = np.matrix([
    [1,2,3],
    [4,5,6],
    [7,8,9]
])
m_r = np.einsum("ij -> i", m) #按行求和
m_c = np.einsum("ij -> j", m) #按列求和
m_a = np.einsum("ij -> ", m) #全部求和
print("按行求和:\n",m_r)
print("按列求和:\n",m_c)
print("全部求和:\n",m_a)

结果:

按行求和:
 [ 6 15 24]
按列求和:
 [12 15 18]
全部求和:
 45

2. 多操作数

2.1 向量内/外积
a = np.array([1,2])
b = np.array([1,3,5])
c = np.array([3,4])

## 内积
inner = np.einsum("i, j ->", a, c)
## 外积
exter = np.einsum("i, j -> ij", a, b) 

print("{0} 与 {1} 内积: {2}".format(a,c,inner))
print("{0} 与 {1} 外积:\n{2}".format(a,b,exter))

结果:

[1 2][3 4] 内积: 21
[1 2][1 3 5] 外积:
[[ 1  3  5]
 [ 2  6 10]]
2.2 矩阵乘法

矩阵乘法最典型的形式为:

A = np.random.rand(3, 5)
B = np.random.rand(5, 2)
M = np.einsum("ik, kj -> ij", A, B) # 3*2

它的循环形式可以展开为:

M = np.zeros((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]

当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]

此时,k不在作为哑标被求和,在输出中也会保留该维度,并且按照 ijk 的顺序排列输出维度。

多个矩阵的连乘可以按照同样的方式进行:

x = np.random.rand(2, 3)
y = np.random.rand(3, 5)
z = np.random.rand(5, 2)
m = np.einsum("ij, jk, kl -> il", x, y, z)
print(m.shape)

## (2,2)

3. 广播乘法

广播方式比较复杂,这里仅举一个常见例子:

在 Transformer 的 self-attention 机制中,对与子矩阵 Q K V QKV QKV 需要进行 Multi-Head 操作,
这里假设:batch=32, max_sequence=20, Heads=8, d_model=512

转化为多头后,维度变为:512 // 8 = 64,可以得到 Q , K Q,K Q,K 矩阵的张量表示:

Q = numpy.random.rand(32, 20, 8, 64)
K = numpy.random.rand(32, 20, 8, 64)

M=np.einsum("nqhd,nkhd->nhqk", Q, K) 
print(M.shape)

# (32, 8, 20 ,20)

通过这种方法,可以轻松完成多头下的自注意力乘积操作。

实际上,上述操作与下面的过程也是等价的:

Q=Q.transpose(0,2,1,3) # nqhd -> nhqd
K=K.transpose(0,2,1,3) # nkhd -> nhkd
M=np.einsum('nhqd, nhkd->nhqk', A,B) # (32, 8, 20 ,20)
print(M)

另外,广播乘法有一个更简洁的形式:

M = np.einsum('...qd, ...kd->...qk', A,B) 
# (32, 8, 20 ,20)

...指代任意多个维度,这在处理batch和图像中的多通道时尤为有效。

参考链接:

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PyTorch中的`torch.einsum`函数是一个用于执行张量运算的强大工具。它可以根据指定的公式对输入张量进行操作,并生成输出张量。 引用\[1\]中提供了一些常见的用法示例。例如,可以使用`torch.einsum`计算矩阵的行和、列和以及某个维度的和。例如,可以使用`torch.einsum('ij->i', A)`计算矩阵A的行和,使用`torch.einsum('ij->j', A)`计算矩阵A的列和,使用`torch.einsum('ijklmn->n', D)`计算张量D在某个维度上的和。 引用\[2\]中提供了一些更复杂的用法示例。例如,可以使用`torch.einsum('ij,jk->ik', A, B)`计算矩阵A和B的内积,使用`torch.einsum('ij,ik->jk', A, C)`计算矩阵A和C的外积,使用`torch.einsum('ij,jk,lj->jk', A, B, C)`进行多维张量相乘。 引用\[3\]中提供了一个高阶张量运算的示例。在这个示例中,使用`np.einsum('ijk,jil->kl', a, b)`计算了两个3阶张量a和b的乘积,并生成了一个2阶张量o。这个示例中的公式解析为对i和j进行求和,然后将结果存储在输出张量的k和l位置上。 总之,`torch.einsum`是一个非常灵活和强大的函数,可以用于执行各种张量运算。它可以根据指定的公式对输入张量进行操作,并生成输出张量。 #### 引用[.reference_title] - *1* *2* [【Pytorch写代码技巧--EinsumEinsum详解+常用写法](https://blog.csdn.net/ccaoshangfei/article/details/126995397)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Pytorch中, torch.einsum详解。](https://blog.csdn.net/a2806005024/article/details/96462827)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值