【问题标题】:How does the "view" method work in PyTorch?PyTorch 中的“视图”方法是如何工作的?
【发布时间】:2017-07-17 17:18:28
【问题描述】:

我对以下代码 sn-p 中的方法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()函数的主要原理?

【问题讨论】:

    标签: python memory pytorch torch tensor


    【解决方案1】:

    视图函数旨在重塑张量。

    假设你有一个张量

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

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

    a = a.view(4, 4)
    

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

    参数-1是什么意思?

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

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

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

    【讨论】:

    • "view 类似于 numpy 的 reshape" -- 为什么他们在 PyTorch 中不直接称它为 reshape?!
    • @MaxB 与 reshape 不同,“view”返回的新张量与原始张量共享底层数据,因此它实际上是对旧张量的视图,而不是创建一个全新的张量。跨度>
    • @blckbird “重塑总是复制内存。视图从不复制内存。” github.com/torch/cutorch/issues/98
    • @devinbost Torch reshape 总是复制内存。 NumPy 不会重塑。
    • Torch reshape 并不总是返回副本。这是一个非常容易的检查。与 numpy 相同
    【解决方案2】:

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

    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),它将引发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. 注意返回的张量共享相同的数据。如果您在“视图”中进行更改,您将更改原始张量的数据:

      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

    【讨论】:

      【解决方案3】:

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


      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,...) 返回相同基础信息的视图,但重新整形为形状为dim1 x dim2 x ... 的张量(通过修改shapestride 属性)。

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


      PyTorch -1

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

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

      【讨论】:

        【解决方案4】:

        torch.Tensor.view()

        简单地说,torch.Tensor.view()numpy.ndarray.reshape()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,可以为以下形状创建新的视图

        (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) 或等效 @987654352 @(-1, 1)

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

        要注意的另一件事是,我们在每个形状元组的一个位置使用了-1。通过使用-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() 函数的魔力。只要新 view 的形状与原始形状兼容,它只会更改每个新 views 的(原始)张量的步幅。 p>

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

        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 维度从一个元素移动到下一个元素,我们必须跳转 或走 6 步。 (即从06,需要6 个步骤。)但是要从第一个st 维度中的一个元素到下一个元素,我们只需要一个步骤(例如从23)。

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


        torch.reshape()

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

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

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

        【讨论】:

          【解决方案5】:

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

          【讨论】:

            【解决方案6】:

            让我们试着通过下面的例子来理解视图:

                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 作为参数值是计算say x 值的简单方法,前提是我们知道y、z 的值,或者在3d 和2d 的情况下反过来又是计算say 值的简单方法x 只要我们知道 y 的值,反之亦然..

            【讨论】:

            • 插图不错!
            【解决方案7】:

            参数-1是什么意思?

            您可以将-1 解读为参数的动态数量或“任何东西”。因此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])
            

            【讨论】:

              【解决方案8】:

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

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

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

              【讨论】:

                【解决方案9】:

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

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

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

                【讨论】:

                  猜你喜欢
                  • 2017-10-27
                  • 2020-09-15
                  • 2018-12-24
                  • 2019-04-21
                  • 1970-01-01
                  • 1970-01-01
                  • 2013-03-11
                  • 2019-05-20
                  • 2012-04-21
                  相关资源
                  最近更新 更多