在 PyTorch 中. 毗连()是做什么的?

对于张量 xx.contiguous()做什么?

113098 次浏览

来自 火炬文件:

contiguous() → Tensor
返回包含与 self 相同数据的连续张量 如果自张量是连续的,这个函数返回自张量 张量。

这里的 contiguous不仅意味着内存中的连续性,而且意味着内存中的顺序与索引顺序相同: 例如,做一个转换不会改变内存中的数据,它只是改变从索引到内存指针的映射,如果你然后应用 contiguous(),它将改变内存中的数据,以便从索引到内存位置的映射是规范的。

正如在前面的答案连续()分配 连续的内存块,这将有助于当我们是 传递张量到 c 或 c + + 后端代码,其中张量是 作为指针传递

PyTorch 中有一些关于张量的操作不会改变张量的内容,但会改变数据的组织方式。这些行动包括:

narrow()view()expand()transpose()

例如: 当你调用 transpose()时,PyTorch 不会生成带有新布局的新张量,它只是修改 Tensor 对象中的元信息,以便偏移量和步长描述所需的新形状。在这个例子中,转置张量和原始张量共享相同的记忆:

x = torch.randn(3,2)
y = torch.transpose(x, 0, 1)
x[0, 0] = 42
print(y[0,0])
# prints 42

这就是 相邻的的概念出现的地方。在上面的例子中,x是连续的,但是 y不是连续的,因为它的内存布局不同于从零开始制作的相同形状张量的内存布局。请注意,“相连”这个词有点误导,因为它并不是张量的内容分布在不连接的内存块周围。这里的字节仍然分配在一块内存中,但元素的顺序不同!

当您调用 contiguous()时,它实际上是张量的一个副本,这样它的元素在内存中的顺序就像它是用相同的数据从头创建的一样。

通常你不用担心这个。通常可以安全地假设一切都会正常工作,然后等到得到一个 RuntimeError: input is not contiguous,PyTorch 希望在这个 RuntimeError: input is not contiguous中有一个连续张量来添加对 contiguous()的调用。

Continous ()将创建张量的一个副本,副本中的元素将以一种连续的方式存储在内存中。 当我们首先对张量进行置换,然后重新定义(视图)时,通常需要使用连续()函数。首先,让我们创建一个连续张量:

aaa = torch.Tensor( [[1,2,3],[4,5,6]] )
print(aaa.stride())
print(aaa.is_contiguous())
#(3,1)
#True

Stride () return (3,1)意味着: 当每一步(一行一行)沿着第一维移动时,我们需要在内存中移动3步。当沿着第二维度(一列一列)移动时,我们需要在内存中移动1步。这表明张量中的元素是连续存储的。

现在我们尝试把 come 函数应用到张量上:

bbb = aaa.transpose(0,1)
print(bbb.stride())
print(bbb.is_contiguous())


#(1, 3)
#False




ccc = aaa.narrow(1,1,2)   ## equivalent to matrix slicing aaa[:,1:3]
print(ccc.stride())
print(ccc.is_contiguous())


#(3, 1)
#False




ddd = aaa.repeat(2,1)   # The first dimension repeat once, the second dimension repeat twice
print(ddd.stride())
print(ddd.is_contiguous())


#(3, 1)
#True




## expand is different from repeat.
## if a tensor has a shape [d1,d2,1], it can only be expanded using "expand(d1,d2,d3)", which
## means the singleton dimension is repeated d3 times
eee = aaa.unsqueeze(2).expand(2,3,3)
print(eee.stride())
print(eee.is_contiguous())


#(3, 1, 0)
#False




fff = aaa.unsqueeze(2).repeat(1,1,8).view(2,-1,2)
print(fff.stride())
print(fff.is_contiguous())


#(24, 2, 1)
#True

好,我们可以发现 转位()、窄()、张量切片和展开()会使生成的张量不连续。有趣的是,repeat ()和 view ()并不使其不连续。现在的问题是: 如果我使用不连续张量会发生什么?

答案是 view ()函数不能应用于不连续张量。这可能是因为 view ()要求连续存储张量,以便在内存中快速重塑。例如:

bbb.view(-1,3)

我们会得到错误:

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-63-eec5319b0ac5> in <module>()
----> 1 bbb.view(-1,3)


RuntimeError: invalid argument 2: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Call .contiguous() before .view(). at /pytorch/aten/src/TH/generic/THTensor.cpp:203

要解决这个问题,只需向不连续张量添加毗连() ,以创建连续副本,然后应用 view ()

bbb.contiguous().view(-1,3)
#tensor([[1., 4., 2.],
[5., 3., 6.]])

根据我的理解,一个更加概括的答案是:

连续是用来表示张量的内存布局与其公开的元数据或形状信息不一致的术语。

在我看来,“连续”这个词是一个令人困惑的/误导性的术语,因为在正常情况下,它意味着当记忆没有在不连续的块中传播时(例如,它的“连续/连接/连续”)。

某些操作可能由于某些原因需要这个连续属性(最有可能是在 gpu 中的效率等)。

请注意,.view是另一个可能导致此问题的操作。看看下面的代码,我通过简单地调用毗连(而不是典型的转置问题造成它在这里是一个例子,这是原因,当一个 RNN 不满意它的输入)修复:

        # normal lstm([loss, grad_prep, train_err]) = lstm(xn)
n_learner_params = xn_lstm.size(1)
(lstmh, lstmc) = hs[0] # previous hx from first (standard) lstm i.e. lstm_hx = (lstmh, lstmc) = hs[0]
if lstmh.size(1) != xn_lstm.size(1): # only true when prev lstm_hx is equal to decoder/controllers hx
# make sure that h, c from decoder/controller has the right size to go into the meta-optimizer
expand_size = torch.Size([1,n_learner_params,self.lstm.hidden_size])
lstmh, lstmc = lstmh.squeeze(0).expand(expand_size).contiguous(), lstmc.squeeze(0).expand(expand_size).contiguous()
lstm_out, (lstmh, lstmc) = self.lstm(input=xn_lstm, hx=(lstmh, lstmc))

我曾经得到的错误:

RuntimeError: rnn: hx is not contiguous



资料来源:

接受的答案是如此伟大,我试图欺骗的 transpose()功能效应。我创建了两个函数,它们可以检查 samestorage()contiguous

def samestorage(x,y):
if x.storage().data_ptr()==y.storage().data_ptr():
print("same storage")
else:
print("different storage")
def contiguous(y):
if True==y.is_contiguous():
print("contiguous")
else:
print("non contiguous")

我以表格的形式检查并得到了这个结果:

functions

您可以查看下面的检查器代码,但让我们举一个例子,当张量是 不相连。我们不能简单地在那个张量上调用 view(),我们需要调用 reshape()或者我们也可以调用 .contiguous().view()

x = torch.randn(3,2)
y = x.transpose(0, 1)
y.view(6) # RuntimeError: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.
  

x = torch.randn(3,2)
y = x.transpose(0, 1)
y.reshape(6)


x = torch.randn(3,2)
y = x.transpose(0, 1)
y.contiguous().view(6)

进一步要注意的是,最后还有一些方法可以创建 相邻的不相连张量。有些方法可以在 同一个仓库上操作,有些方法如 flip()将在返回之前创建一个 新仓库(读: 克隆张量)。

检查代码:

import torch
x = torch.randn(3,2)
y = x.transpose(0, 1) # flips two axes
print("\ntranspose")
print(x)
print(y)
contiguous(y)
samestorage(x,y)


print("\nnarrow")
x = torch.randn(3,2)
y = x.narrow(0, 1, 2) #dim, start, len
print(x)
print(y)
contiguous(y)
samestorage(x,y)


print("\npermute")
x = torch.randn(3,2)
y = x.permute(1, 0) # sets the axis order
print(x)
print(y)
contiguous(y)
samestorage(x,y)


print("\nview")
x = torch.randn(3,2)
y=x.view(2,3)
print(x)
print(y)
contiguous(y)
samestorage(x,y)


print("\nreshape")
x = torch.randn(3,2)
y = x.reshape(6,1)
print(x)
print(y)
contiguous(y)
samestorage(x,y)


print("\nflip")
x = torch.randn(3,2)
y = x.flip(0)
print(x)
print(y)
contiguous(y)
samestorage(x,y)


print("\nexpand")
x = torch.randn(3,2)
y = x.expand(2,-1,-1)
print(x)
print(y)
contiguous(y)
samestorage(x,y)

一个张量的值从最右边的维度开始在存储器中排列(也就是说,沿着2D 张量的行移动)被定义为 contiguous。连续张量很方便,因为我们可以有效地访问它们,而不必在存储器中跳来跳去(改进数据本地性可以提高性能,因为内存访问在现代 CPU 上的工作方式)。这种优势当然取决于算法访问的方式。

PyTorch 中的一些张量操作只能在连续张量上工作,例如 view,[ ... ]。在这种情况下,PyTorch 将抛出一个信息性异常,并要求我们显式调用毗邻。值得注意的是,如果张量已经是连续的,那么调用 contiguous将不会做任何事情(并且不会影响性能)。

请注意,这比计算机科学中“连续”一词(即连续 并且命令)的一般用法更具体。

例如给定一个张量:

[[1, 2]
[3, 4]]
存储在内存中 PyTorch contiguous 在记忆空间中通常是“连续的”?
1 2 3 4 0 0 0 Something Something
1 3 2 4 0 0 0 Something Something
1 0 2 0 3 0 4 Something Something

一维数组[0,1,2,3,4]是相邻的,如果它的项在内存中挨着放置,如下所示:

contiguous memory allocation

如果存储它的内存区域如下所示,则它不是连续的:

non contiguous allocation

对于二维或更多数组,项也必须彼此相邻,但顺序遵循不同的约定。 让我们考虑下面的2D 数组:

>>> t = torch.tensor([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]])

two dimensional array

如果行之间像下面这样存储,那么内存分配就是 C 连续:

two dimensional memory

这就是派托雷斯认为的连续性。

>>> t.is_contiguous()
True

与数组关联的 stride 属性提供跳过的字节数,以获取每个维度中的下一个元素

>>> t.stride()
(4, 1)

我们需要跳过4个字节到下一行,但只有一个字节到同一行中的下一个元素。

正如在其他答案中所说的,一些 Pytch 操作不改变内存分配,只改变元数据。

例如,转置方法。 让我们换位张量:

two dimensional array

内存分配没有改变:

two dimensional memory non contiguous

但这种进步确实起到了作用:

>>> t.T.stride()
(1, 4)

我们需要跳过1个字节到下一行,跳过4个字节到同一行中的下一个元素。张量不再是 C 连续的了(事实上它是 Fortran 连续: 每个列都存储在一起)

>>> t.T.is_contiguous()
False

将重新安排内存分配,使张量为 C 连续:

two dimensional memory contiguous

>>> t.T.contiguous().stride()
(3, 1)