284

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()用一些例子来解释功能的主要原理?

4

9 回答 9

400

视图函数旨在重塑张量。

假设你有一个张量

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

a是一个张量,有 16 个元素,从 1 到 16(包括在内)。如果您想重塑此张量以使其成为4 x 4张量,则可以使用

a = a.view(4, 4)

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

参数-1是什么意思?

如果在任何情况下您不知道需要多少行但确定列数,则可以使用 -1 指定。(请注意,您可以将其扩展到具有更多维度的张量。只有一个轴值可以是 -1)。这是告诉库的一种方式:“给我一个包含这么多列的张量,然后计算实现这一点所需的适当行数”。

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

绘制一个numpy和pytorch的相似之处,view类似于numpy的reshape函数。

于 2017-02-27T10:04:34.830 回答
44

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

  1. view方法返回一个与张量具有相同数据的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),它将引发 aRuntimeError因为形状 (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. 请注意,返回的张量共享相同的数据。如果您在“视图”中进行更改,您将更改原始张量的数据:

    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_t, stride[0] != stride[1] x size[1] since 24 != 2 x 3

于 2018-02-06T19:15:51.373 回答
33

view()通过将张量的元素“拉伸”或“挤压”成您指定的形状来重塑张量:

在此处输入图像描述


如何view()工作?

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

在此处输入图像描述 在此处输入图像描述
张量及其底层storage 例如,右手张量(形状(3,2))可以从左手计算t2 = t1.view(3,2)

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

  • shape说明每个维度有多长
  • stride说明在到达每个维度中的下一个元素之前,您需要在内存中执行多少步

view(dim1,dim2,...)返回相同基础信息的视图,但重新整形为形状张量dim1 x dim2 x ...(通过修改shapestride属性)。

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


火炬-1

-1是 PyTorch 的别名,用于“在其他维度都已指定的情况下推断此维度”(即新产品与原始产品的商)。这是一个取自 的约定numpy.reshape()

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

于 2021-01-30T16:36:48.647 回答
14

torch.Tensor.view()

简单地说,torch.Tensor.view()它的灵感来自numpy.ndarray.reshape()or 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])

使用 shape 的张量t只能为以下形状创建(18,)视图:

(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*9必须3*6始终等于原始张量中元素的总数(18在我们的示例中)。

要观察的另一件事是,我们-1在每个形状元组的一个地方使用了 a。通过使用 a -1,我们在自己进行计算时变得懒惰,而是将任务委托给 PyTorch 在创建新视图时为形状计算该值。需要注意的重要一点是,我们只能在形状元组中使用单个-1。其余值应由我们明确提供。否则 PyTorch 会通过抛出一个来抱怨RuntimeError

RuntimeError:只能推断一维

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

下面是一些示例,说明了张量的步幅如何随着每个新视图的变化而变化。

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

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

# 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()函数的魔力。只要新视图的形状与原始形状兼容,它只会更改每个新视图的(原始)张量的步幅。

从strides元组中可能观察到的另一件有趣的事情是,第 0位置的元素的值等于形状元组的第 1 个位置的元素的值。

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)表示要沿着第 0 维从一个元素到下一个元素我们必须跳跃或走 6 步。(即从0到,必须采取 6 个步骤。)但是要从第一个维度6中的一个元素到下一个元素,我们只需要一个步骤(例如从到)。23

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


火炬.reshape()

只要新形状与原始张量的形状兼容,此函数将返回一个视图并且与使用完全相同。torch.Tensor.view()否则,它将返回一个副本。

但是,注释torch.reshape()警告说:

连续输入和具有兼容步幅的输入可以在不复制的情况下进行重塑,但不应依赖于复制与查看行为。

于 2019-11-17T03:34:34.690 回答
3

我发现它x.view(-1, 16 * 5 * 5)相当于x.flatten(1),其中参数 1 表示展平过程从第一个维度开始(不是展平“样本”维度)如您所见,后者的用法在语义上更清晰,更易于使用,所以我喜欢flatten()

于 2019-02-14T09:49:56.297 回答
3

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

    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 的值,反之亦然..

于 2020-06-10T13:44:10.570 回答
2

参数-1是什么意思?

您可以阅读-1为参数的动态数量或“任何东西”。因此,中只能有一个参数-1view()

如果你问x.view(-1,1)这将输出张量形状[anything, 1],具体取决于x. 例如:

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])
于 2019-04-27T13:22:18.467 回答
1

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

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

weights.view(a, b)将返回一个新张量,其数据与大小为 (a, b) 的权重相同

于 2019-03-14T05:37:30.493 回答
0

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

我想对 .view(...) 元素的排序方式添加一点见解

  • 对于形状为(a,b,c)的张量,其元素的顺序由编号系统确定:其中第一位数字为a 数字,第二位数字为b数字,第三位数字为c数字。
  • .view(...) 返回的新张量中元素的映射保留了原始张量的这个顺序
于 2020-01-29T06:35:10.480 回答