对于pytorch.einsum、einops的理解

1.矩阵乘法的位置可调换性

torch.einsum('bnc,bmc -> bnm',src_xyz,dst_xyz)
torch.einsum('bmc,bnc -> bnm',src_xyz,dst_xyz)
torch.einsum('cbm,bnc -> bnm',src_xyz,dst_xyz)

都一个样,本质都是关于c轴的乘法的累加,可以这么说,每一项的每一个字母都是可以调换顺序的,可以得到一样的结果(可以自己画画图试试,发现新的矩阵每一个元素不管a1,a2怎么排列维度都由相同的数进行加和)这个结论应该适用于所有类似的操作,不管维度变换到哪,都是对该维度进行操作,也包括einops中的各种操作,不包括(n 2)、(2 n)这种对类型限制的操作,例如

new_points=torch.randint(10,[1,2,5,3])
new_points = einops.rearrange(new_points,'B npoint nsample C->B C nsample npoint')
new_points=einops.reduce(new_points,'B C nsample npoint -> B npoint C ','max')
或者
new_points=einops.reduce(new_points,'B C nsample npoint -> B C  npoint ','max')
得到的结果一样,只不过转换了一下shape

矩阵的语义法强推,目前还没发现错误
且n和m的顺序是不会倒置的,比如我得到的bnm有没有可能m是正常乘的反序,比如m0,m1,m2变为m2,m1,m0这也是不会的,再怎么转置还是上面说的,第一个元素乘以的元素是不会变的,并且第一行要么变成第一列,要么还是第一行,元素下标从来都是递增的,没有反着来的情况

2.何时选择einsum和einops

einsum优先选择,在能写出表达式的情况下,可以用作内积,外积,乘法,提取对角线元素,转置,reduce只支持加和

einops支持对单个矩阵的重新排序,升维,降维
rearrange: transpose (axes permutation), reshape (view), squeeze, unsqueeze,stack, concatenate and other operations.

加法只能先repeat后再广播加法了

3. einops.reduce

命名是否可以有常数的轴

einops.reduce(dst_xyz**2,'b m c->b 1 m','sum')
einops.reduce(src_xyz**2,'b n c->b n 1','sum')

可以智能地辨别降维的维度是c,因为只有c维度消失了,并且中间加一维,目前还没有遇见不支持常数的情况,只要写的正确都是没有问题的,需要注意一点:一个项不能全是常数比如b=16 m=5 c=3,我知道他们的取值我直接写成

einops.reduce(dst_xyz**2,'16 5 3->16 1 5','sum')

是会报错的,因为出现了全是常数的项
在einops.rearrange中左边一个常数轴都不能出现

4. einops.repeat经常会出现的坑

前置知识,非常重要的干货:
PyTorch:view() 与 reshape() 区别详解
对比expand和repeat函数

进入正题,首先要知道einops.repeat()有两种情况:

1.要扩充的维度size如果是为1或者直接新建一个维度的话(其实就是对应满足pytorch.expend()的条件的),会返回view也叫做expanded tensor,这里就可以理解为调用了pytorch.expend(),例如

'新建一个维度'
einops.repeat(torch.arange(16).long(),f'N -> 3  N')
einops.repeat(torch.arange(16).reshape(4,4),f'N M-> 2 N M')
'扩充的维度size是为1'
einops.repeat(torch.arange(16).reshape(1,4,4),f'1 N M-> 4 N M')

扩充数组并非是重新创建新的内存的,而是在原来tensor的基础上通过修改步长来实现的。

2.对已有非size=1的维度进行扩充,则相当于调用pytorch.repeat(),返回的是内存中直接新建的tensor而不是视图

einops.repeat(torch.arange(16).reshape(4,4),f'N M-> N  (2 M)')

验证如下:

einops.repeat(torch.arange(16).long(),f'N -> 3  N')._is_view()
结果:
True
einops.repeat(torch.arange(16).reshape(4,4),f'N M-> 2 N M')._is_view()
结果:
True
einops.repeat(torch.arange(16).reshape(1,4,4),f'1 N M-> 4 N M')._is_view()
结果:
True
einops.repeat(torch.arange(16).reshape(4,4),f'N M-> N  (2 M)')._is_view()
结果:
False

使用视图正常情况下产生的expanded tensor都是和普通tensor没有什么区别的,但需要注意的是视图的一个索引的问题,在官方文档有写到:
对于pytorch.einsum、einops的理解_第1张图片
意思是在repeat产生view也就是expanded tensor而不是普通tensor的情况下,会出现共享内存的问题,不同索引可能指向了同一个存储单元,例子如下

a=einops.repeat(torch.arange(4),' N -> 2 N')
====结果====
tensor([[0, 1, 2, 3],
        [0, 1, 2, 3]])
        
einops.repeat(torch.arange(4),' N -> 2 N')._is_view()
====结果====
True

a[0][0]=7
print(a)
====结果====
tensor([[7, 1, 2, 3],
        [7, 1, 2, 3]])

可以看到改变了a[0][0],a[1][0]也跟着改变了!
有时会报出它的警告

masked_fill_ on expanded tensors is deprecated. 
Please clone() the tensor before performing this operation. 
This also applies to advanced indexing

意思是masked_fill是极其不推荐的,要先克隆一个新的在执行赋值之前。
如果要避免这个问题,在对repeat产生的tensor使用索引时要特别注意该tensor是否是expanded tensor,第二种方法是每次使用repeat创建tensor时都加上clone()来进行创建,更推荐第二种,因为第一种的出错概率会大大提高,不利于健壮性,虽然有时会造成空间上的浪费,但我觉得还是很有必要的,一个tensor占不了多少显存的,显存的大头还是在模型的参数上而不是这些小细节上。

你可能感兴趣的:(pytorch,python,深度学习)