"view" 方法在 PyTorch 中如何工作?

How does the "view" method work in PyTorch?

我对以下代码片段中的方法 view() 感到困惑。

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool  = nn.MaxPool2d(2,2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1   = nn.Linear(16*5*5, 120)
        self.fc2   = nn.Linear(120, 84)
        self.fc3   = nn.Linear(84, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16*5*5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

net = Net()

我对以下行感到困惑。

x = x.view(-1, 16*5*5)

tensor.view()函数有什么作用?我在很多地方看到它的用法,但是我无法理解它是如何解释它的参数的。

如果我将负值作为 view() 函数的参数,会发生什么情况?例如,如果我调用 tensor_variable.view(1, 1, -1)?

会发生什么

谁能举例说明view()函数的主要原理?

视图函数是为了重塑张量。

假设你有一个张量

import torch
a = torch.range(1, 16)

a是1到16(含)16个元素的张量。如果你想重塑这个张量使其成为 4 x 4 张量那么你可以使用

a = a.view(4, 4)

现在 a 将是一个 4 x 4 张量。 请注意,重塑后元素的总数需要保持不变。将张量 a 重塑为 3 x 5 张量是不合适的。

参数-1是什么意思?

如果有任何情况你不知道你想要多少行但确定列数,那么你可以用-1来指定。 (请注意,您可以将其扩展到具有更多维度的张量。只有一个轴值可以是 -1)。这是一种告诉图书馆的方式:"give me a tensor that has these many columns and you compute the appropriate number of rows that is necessary to make this happen".

这可以在您上面给出的神经网络代码中看到。在 forward 函数中的 x = self.pool(F.relu(self.conv2(x))) 行之后,你将得到一个 16 深度的特征图。您必须将其展平以将其提供给完全连接的层。因此,您告诉 pytorch 将获得的张量重塑为具有特定列数,并告诉它自己决定行数。

画出numpy和pytorch的相似性,view类似于numpy的reshape函数。

让我们做一些例子,从简单到困难。

  1. view方法returns一个与self张量有相同数据的张量(也就是说返回的张量有相同数量的元素) , 但形状不同。例如:

    a = torch.arange(1, 17)  # a's shape is (16,)
    
    a.view(4, 4) # output below
      1   2   3   4
      5   6   7   8
      9  10  11  12
     13  14  15  16
    [torch.FloatTensor of size 4x4]
    
    a.view(2, 2, 4) # output below
    (0 ,.,.) = 
    1   2   3   4
    5   6   7   8
    
    (1 ,.,.) = 
     9  10  11  12
    13  14  15  16
    [torch.FloatTensor of size 2x2x4]
    
  2. 假设-1不是参数之一,当你把它们相乘时,结果一定等于张量中的元素个数。如果您这样做:a.view(3, 3),它会引发 RuntimeError,因为形状 (3 x 3) 对于包含 16 个元素的输入无效。换句话说:3 x 3 不等于 16 而是 9。

  3. 您可以使用 -1 作为传递给函数的参数之一,但只能使用一次。所发生的只是该方法将为您计算如何填充该维​​度。例如 a.view(2, -1, 4) 等同于 a.view(2, 2, 4)。 [16 / (2 x 4) = 2]

  4. 注意返回的张量共享相同的数据。如果您在 "view" 中进行更改,您将更改原始张量的数据:

    b = a.view(4, 4)
    b[0, 2] = 2
    a[2] == 3.0
    False
    
  5. 现在,对于更复杂的用例。文档说每个新的视图维度必须是原始维度的子空间,或者仅跨度 d, d + 1, ..., d + k 满足以下类似连续性的条件,对于所有 i = 0, ..., k - 1, stride[i] = stride[i + 1] x size [i + 1]。否则,需要调用 contiguous() 才能查看张量。例如:

    a = torch.rand(5, 4, 3, 2) # size (5, 4, 3, 2)
    a_t = a.permute(0, 2, 3, 1) # size (5, 3, 2, 4)
    
    # The commented line below will raise a RuntimeError, because one dimension
    # spans across two contiguous subspaces
    # a_t.view(-1, 4)
    
    # instead do:
    a_t.contiguous().view(-1, 4)
    
    # To see why the first one does not work and the second does,
    # compare a.stride() and a_t.stride()
    a.stride() # (24, 6, 2, 1)
    a_t.stride() # (24, 2, 1, 6)
    

    请注意,对于 a_tstride[0] != stride[1] x size[1] 因为24 != 2 x 3

我发现x.view(-1, 16 * 5 * 5)等同于x.flatten(1),其中参数1表示展平过程从第一个维度开始(不展平'sample'维度) 可以看到,后一种用法在语义上更清晰,也更容易使用,所以我更喜欢flatten().

weights.reshape(a, b) 将 return 一个新的张量,其数据与大小为 (a, b) 的权重相同,因为它会将数据复制到内存的另一部分。

weights.resize_(a, b) returns 具有不同形状的相同张量。但是,如果新形状产生的元素比原始张量少,一些元素将从张量中移除(但不会从内存中移除)。如果新形状产生的元素多于原始张量,新元素将在内存中未初始化。

weights.view(a, b) 将 return 一个新张量,其数据与大小为 (a, b)

的权重相同

What is the meaning of parameter -1?

您可以将 -1 理解为参数的动态数量或 "anything"。因此 view().

中只能有一个参数 -1

如果你问 x.view(-1,1) 这将根据 x 中的元素数量输出张量形状 [anything, 1]。例如:

import torch
x = torch.tensor([1, 2, 3, 4])
print(x,x.shape)
print("...")
print(x.view(-1,1), x.view(-1,1).shape)
print(x.view(1,-1), x.view(1,-1).shape)

将输出:

tensor([1, 2, 3, 4]) torch.Size([4])
...
tensor([[1],
        [2],
        [3],
        [4]]) torch.Size([4, 1])
tensor([[1, 2, 3, 4]]) torch.Size([1, 4])

torch.Tensor.view()

简单地说,torch.Tensor.view()是受numpy.ndarray.reshape()[=启发110=]numpy.reshape(),创建张量的新视图,只要新形状与张量的形状兼容即可原始张量。

让我们用一个具体的例子来详细理解这一点。

In [43]: t = torch.arange(18) 

In [44]: t 
Out[44]: 
tensor([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17])

使用形状为 (18,) 的张量 t,新的 views 可以 only 为以下内容创建形状:

(1, 18) 或等效的 (1, -1)(-1, 18)
(2, 9) 或等同于 (2, -1)(-1, 9)
(3, 6) 或等同于 (3, -1)(-1, 6)
(6, 3) 或等同于 (6, -1)(-1, 3)
(9, 2) 或等同于 (9, -1)(-1, 2)
(18, 1) 或等同于 (18, -1)(-1, 1)

正如我们已经从上面的形状元组中观察到的,形状元组的元素相乘(例如2*93*6等)必须总是 等于原始张量中的元素总数(在我们的示例中为 18)。

另一件需要注意的事情是我们在每个形状元组的其中一个地方使用了 -1。通过使用 -1,我们懒得自己进行计算,而是将任务委托给 PyTorch,以便在创建新的 view 时计算形状的值.需要注意的一件重要事情是,我们可以 在形状元组中使用单个 -1。其余值应由我们明确提供。否则 PyTorch 将通过抛出 RuntimeError:

来抱怨

RuntimeError: only one dimension can be inferred

因此,对于上述所有形状,PyTorch 将始终 return 原始张量 t 新视图 。这基本上意味着它只是为请求的每个新视图更改张量的步幅信息。

下面是一些示例,说明张量的步幅如何随每个新 视图.

发生变化
# stride of our original tensor `t`
In [53]: t.stride() 
Out[53]: (1,)

现在,我们将看到新 views 的进步:

# shape (1, 18)
In [54]: t1 = t.view(1, -1)
# stride tensor `t1` with shape (1, 18)
In [55]: t1.stride() 
Out[55]: (18, 1)

# shape (2, 9)
In [56]: t2 = t.view(2, -1)
# stride of tensor `t2` with shape (2, 9)
In [57]: t2.stride()       
Out[57]: (9, 1)

# shape (3, 6)
In [59]: t3 = t.view(3, -1) 
# stride of tensor `t3` with shape (3, 6)
In [60]: t3.stride() 
Out[60]: (6, 1)

# shape (6, 3)
In [62]: t4 = t.view(6,-1)
# stride of tensor `t4` with shape (6, 3)
In [63]: t4.stride() 
Out[63]: (3, 1)

# shape (9, 2)
In [65]: t5 = t.view(9, -1) 
# stride of tensor `t5` with shape (9, 2)
In [66]: t5.stride()
Out[66]: (2, 1)

# shape (18, 1)
In [68]: t6 = t.view(18, -1)
# stride of tensor `t6` with shape (18, 1)
In [69]: t6.stride()
Out[69]: (1, 1)

这就是 view() 函数的神奇之处。只要新 view 的形状与原来的形状。

从步幅元组中可能会观察到的另一件有趣的事情是,第 0 位置的元素值等于第 1[=] 个元素的值139=]st 形状元组的位置。

In [74]: t3.shape 
Out[74]: torch.Size([3, 6])
                        |
In [75]: t3.stride()    |
Out[75]: (6, 1)         |
          |_____________|

这是因为:

In [76]: t3 
Out[76]: 
tensor([[ 0,  1,  2,  3,  4,  5],
        [ 6,  7,  8,  9, 10, 11],
        [12, 13, 14, 15, 16, 17]])

步长(6, 1)表示要沿着第 0th 维从一个元素移动到下一个元素,我们必须 jump 或走 6 步。 (即从 06,必须采取 6 个步骤。)但是要从第 1st 维中的一个元素转到下一个元素,我们只需要一步(例如,从 23)。

因此,步幅信息是如何从内存访问元素以执行计算的核心。


torch.reshape()

此函数将 return 一个 view 并且与使用 torch.Tensor.view() 完全相同,只要新形状与原始张量。否则,它将return复制一份。

然而,torch.reshape() 的注释警告说:

contiguous inputs and inputs with compatible strides can be reshaped without copying, but one should not depend on the copying vs. viewing behavior.

我真的很喜欢@Jadiel de Armas 的例子。

我想补充一点关于如何为 .view(...) 排序元素的见解

  • 对于形状为 (a,b,c) 的 Tensor,其元素的 order 是 由编号系统确定:第一个数字有 a 数字,第二个数字有 b 个数字,第三个数字有 c 个数字。
  • .view(...)返回的新Tensor中元素的映射 保留原始 Tensor 的这个 order

让我们尝试通过以下示例来理解视图:

    a=torch.range(1,16)

print(a)

    tensor([ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11., 12., 13., 14.,
            15., 16.])

print(a.view(-1,2))

    tensor([[ 1.,  2.],
            [ 3.,  4.],
            [ 5.,  6.],
            [ 7.,  8.],
            [ 9., 10.],
            [11., 12.],
            [13., 14.],
            [15., 16.]])

print(a.view(2,-1,4))   #3d tensor

    tensor([[[ 1.,  2.,  3.,  4.],
             [ 5.,  6.,  7.,  8.]],

            [[ 9., 10., 11., 12.],
             [13., 14., 15., 16.]]])
print(a.view(2,-1,2))

    tensor([[[ 1.,  2.],
             [ 3.,  4.],
             [ 5.,  6.],
             [ 7.,  8.]],

            [[ 9., 10.],
             [11., 12.],
             [13., 14.],
             [15., 16.]]])

print(a.view(4,-1,2))

    tensor([[[ 1.,  2.],
             [ 3.,  4.]],

            [[ 5.,  6.],
             [ 7.,  8.]],

            [[ 9., 10.],
             [11., 12.]],

            [[13., 14.],
             [15., 16.]]])

-1 作为参数值是计算 x 值的简单方法,前提是我们知道 y、z 的值,或者在 3d 和 2d 的情况下反过来也是计算值的简单方法假设 x 提供我们知道 y 的值,反之亦然..

view() 通过 'stretching' 或 'squeezing' 将张量的元素重塑为您指定的形状:


view() 是如何工作的?

首先让我们看看引擎盖下的张量是什么:

Tensor and its underlying storage e.g. the right-hand tensor (shape (3,2)) can be computed from the left-hand one with t2 = t1.view(3,2)

在这里你可以看到 PyTorch 通过添加 shapestride 属性将底层的连续内存块转换为类似矩阵的对象来生成张量:

  • shape 表示每个维度有多长
  • stride 表示在到达每个维度中的下一个元素之前需要记忆多少步

view(dim1,dim2,...) returns a view of the same underlying information, but reshaped to a tensor of shape dim1 x dim2 x ... (by modifying the shape and stride attributes).

请注意,这隐含地假设新旧维度具有相同的乘积(即新旧张量具有相同的体积)。


PyTorch

-1 是 PyTorch 的别名,表示“在其他所有维度都已指定的情况下推断此维度”(即原始乘积与新乘积的商)。这是取自 numpy.reshape().

的约定

因此在我们的示例中 t1.view(3,2) 等同于 t1.view(3,-1)t1.view(-1,2).