pytorch中的 contiguous 是如何设计的?

tensor.contiguous()这个函数 是出于什么考虑 专门设计的?
关注者
129
被浏览
32,451

6 个回答

前面有人已经从理论上说了,我就不重复了,我写一个实际例子来验证这些说法,让你有个直观认识:

下面有一张狗的图片

让我们先把图像读入内存,可视化一下:


但是在实际任务中,我们可能不止需要一张图像,需要把图像划分为一些patch。比如把这张狗的图像,我们现在需要把它沿着高度分成4个patch(其实这种需求在医学图像任务中经常遇到)。大概像下面这种效果:


那么请问,如何实现?方法肯定是很多的,我下面就展示一种体现contiguous 用处的做法。

先查看原始图像的维度,如下:

维度为: [3,224,112],分别代表通道数,高度,宽度


那么我第一次怎么做的呢?我就想起了view函数,觉得这还不简单,直接下面一句代码,不就完事了吗?

img=img.view(4,3,56,112)

这样img[0],img[1],img[2],img[3]就正好对应四个patch。然后我全部画出来看了一下,是下面这个效果:


看起来,好像是被分成了四个patch。但是你仔细看,留意我展示的顺序(从show(img[0]) 到 show(img[3])),除去图上这花花绿绿的颜色先不说,这个狗如果沿着高度被划分为4个patch,这四个身体部位顺序明显不该是这样的。

如果这个你看得还不明显,我再画一张明显的,比如下面这张图:

我要把这张有数字1,2,3,4的图像,沿着高度分为4个patch,同样按照上面直接用view操作的话,效果是这样的:

这个是不是就很明显了,按道理,我们希望每个patch都只显示一个数字,但是这上面每个patch都同时有几个数字,而且还是五颜六色的。


原因很简单,因为直接通过view操作的办法是不科学的,把我们实际想要的patch的通道给打乱了。所以上面的数字才会是彩色,因为RGB三个通道被打乱了。为什么会把通道搞混淆了呢?这个就需要了解pytorch是以什么样的方式存储tensor数据了。这里不做赘述。


那么正确的方法应该怎么做,正确的代码应该是下面这样的:

img=img.view(3,4,56,112)
img=img.permute(1,0,2,3)

最后得到的img维度依然是[4,3,56,112],从数值上和上面直接用img.view(4,3,56,112)是一样的,但实际效果变了,每个patch画出来,如下:


现在可以看见,patch的顺序对了,颜色也正常了,每个patch上也没有了五颜六色。这些都跟tensor的存储方式有关,自己感受。


但是一般实际训练中,都是以batch为单位的,一个batch有很多张图片。维度如下:

imgs=[batch_size,c,h,w]

我们这个时候假设batch_size=2,一个batch里面就包含上面狗和数字的图片,具体就是:

imgs的维度为:[2, 3, 224, 112]

我现在想直接把这个img的batch,变成维度为[2*4,3,56,112]的batch(此时里面都是patch了)。

我就进行如下操作:

我们最后要的新的batch_size应该是,batch_size=2*4(patch的数量)。

很显然,应该下一步,直接:

imgs=imgs.view(2*4,3,56,112)

就可以变成一个维度为[8,3,56,112]的batch了,但是如果真的这样操作,会出现下面的错误:


虽然我们直观感觉一个[2, 4, 3, 56, 112]的tensor变成[8, 3, 56, 112]的tensor,通过view应该是可以的。但是这个tensor就特殊在它的size前面已经被处理过了。不行的具体原因已经有人回答了,这里不重复了。


那么,如何才能把这时的[2, 4, 3, 56, 112]的tensor变成[8, 3, 56, 112]的tensor呢?

如果我这时候刚初始化一个维度为[2, 4, 3, 56, 112]的tensor,变为[8, 3, 56, 112]维度的直接通过view就行了。

这个时候我们就需要contiguous了,它能帮我们把现在这个[2, 4, 3, 56, 112]的tensor回到和刚被初始化的状态一样。再通过view操作就行了。代码如下:



patch可视化验证一下:

一切正常。


到这里,应该能感受到contiguous实际在做啥了吧。

写完了《PyTorch中的contiguous》,可以来回答这个问题了。

1. torch.view等方法操作需要连续的Tensor。

transpose、permute 等操作虽然没有修改底层一维数组,但是新建了一份Tensor元信息,并在新的元信息中的 重新指定 stride。torch.view方法约定了不修改数组本身仅在底层数组上使用指定的形状进行变形,所以view无法在 transpose、permute 操作得到的Tensor上返回不同shape的Tensor(通过多次变换的方式可以,但违反view约定)。

2. 出于性能考虑。

连续的Tensor,语义上相邻的元素,在内存中也是连续的,访问相邻元素是矩阵运算中经常用到的操作,语义和内存顺序的一致性是缓存友好的(What is a “cache-friendly” code?[1]),在内存中连续的数据可以(但不一定)被高速缓存预取,以提升CPU获取操作数据的速度。transpose、permute 后使用 contiguous 方法则会重新开辟一块内存空间保证数据是在逻辑顺序和内存中是一致的,连续内存布局减少了CPU对对内存的请求次数(访问内存比访问寄存器慢100倍[2]),相当于空间换时间。

补充说明:

1.contiguous这个不是PyTorch首创的发明,numpy中有相同疗效的函数ascontiguousarray

2.如果不关心底层数据是否使用了新的内存,则使用reshape方法替代view更方便。 [3]

参考

  1. ^What is a “cache-friendly” code? https://stackoverflow.com/a/16699282
  2. ^计算机缓存Cache以及Cache Line详解 https://zhuanlan.zhihu.com/p/37749443
  3. ^view() after transpose() raises non contiguous error #764 https://github.com/pytorch/pytorch/issues/764#issuecomment-317845141