Como o método "view" funciona no PyTorch?

205

Estou confuso sobre o método view()no seguinte trecho de código.

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()

Minha confusão está em relação à seguinte linha.

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

O que a tensor.view()função faz? Vi seu uso em muitos lugares, mas não consigo entender como ele interpreta seus parâmetros.

O que acontece se eu der valores negativos como parâmetros para a view()função? Por exemplo, o que acontece se eu ligar tensor_variable.view(1, 1, -1)?

Alguém pode explicar o princípio principal da view()função com alguns exemplos?

Wasi Ahmad
fonte

Respostas:

283

A função view visa remodelar o tensor.

Digamos que você tenha um tensor

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

aé um tensor que possui 16 elementos de 1 a 16 (incluído). Se você deseja remodelar esse tensor para torná-lo um 4 x 4tensor, use

a = a.view(4, 4)

Agora aserá um 4 x 4tensor. Observe que, após a remodelagem, o número total de elementos precisa permanecer o mesmo. Remodelar o tensor apara um 3 x 5tensor não seria apropriado.

Qual é o significado do parâmetro -1?

Se houver alguma situação em que você não saiba quantas linhas deseja, mas tenha certeza do número de colunas, poderá especificar isso com -1. ( Observe que você pode estender isso para tensores com mais dimensões. Somente um dos valores do eixo pode ser -1 ). Esta é uma maneira de informar a biblioteca: "dê-me um tensor com muitas colunas e você calcula o número apropriado de linhas necessárias para que isso aconteça".

Isso pode ser visto no código de rede neural que você forneceu acima. Após a linha x = self.pool(F.relu(self.conv2(x)))na função de avanço, você terá um mapa de características de 16 profundidades. Você precisa achatar isso para dar à camada totalmente conectada. Então você diz ao pytorch para remodelar o tensor que obteve para ter um número específico de colunas e diz para ele decidir o número de linhas por si só.

Desenhando uma semelhança entre numpy e pytorch, viewé semelhante à função de remodelação do numpy .

Kashyap
fonte
93
"view é semelhante à remodelação do numpy" - por que eles não o chamaram reshapeno PyTorch ?!
MaxB
54
Ao contrário da remodelagem, o novo tensor retornado por "view" compartilha os dados subjacentes com o tensor original; portanto, é realmente uma visão do tensor antigo, em vez de criar um novo.
Qihqi
37
@blckbird "remodelar sempre copia memória. a visualização nunca copia memória." github.com/torch/cutorch/issues/98
devinbost
3
A remodelação da tocha @devinbost sempre copia a memória. A remodelação do NumPy não.
Tavian Barnes
32

Vamos fazer alguns exemplos, do mais simples ao mais difícil.

  1. O viewmétodo retorna um tensor com os mesmos dados que o selftensor (o que significa que o tensor retornado tem o mesmo número de elementos), mas com uma forma diferente. Por exemplo:

    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. Supondo que esse -1não seja um dos parâmetros, quando você os multiplica, o resultado deve ser igual ao número de elementos no tensor. Se você fizer:, a.view(3, 3)ele aumentará um RuntimeErrorformato porque (3 x 3) é inválido para entrada com 16 elementos. Em outras palavras: 3 x 3 não é igual a 16, mas 9.

  3. Você pode usar -1como um dos parâmetros que você passa para a função, mas apenas uma vez. Tudo o que acontece é que o método fará as contas para você sobre como preencher essa dimensão. Por exemplo, a.view(2, -1, 4)é equivalente a a.view(2, 2, 4). [16 / (2 x 4) = 2]

  4. Observe que o tensor retornado compartilha os mesmos dados . Se você fizer uma alteração na "visualização", está alterando os dados do tensor original:

    b = a.view(4, 4)
    b[0, 2] = 2
    a[2] == 3.0
    False
  5. Agora, para um caso de uso mais complexo. A documentação diz que cada nova dimensão de visualização deve ser um subespaço de uma dimensão original ou abranger apenas d, d + 1, ..., d + k que atendam à seguinte condição de contiguidade que, para todos os i = 0,. .., k - 1, passada [i] = passada [i + 1] x tamanho [i + 1] . Caso contrário, contiguous()precisa ser chamado antes que o tensor possa ser visualizado. Por exemplo:

    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)

    Observe que para a_t, passo [0]! = Passo [1] x tamanho [1] desde 24! = 2 x 3

Jadiel de Armas
fonte
7

torch.Tensor.view()

Simplificando, torch.Tensor.view()inspirado em numpy.ndarray.reshape()ou numpy.reshape(), cria uma nova visualização do tensor, desde que a nova forma seja compatível com a forma do tensor original.

Vamos entender isso em detalhes usando um exemplo concreto.

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])

Com esse tensor tde forma (18,), novas vistas podem ser criadas apenas para as seguintes formas:

(1, 18)ou equivalentemente (1, -1)ou ou equivalentemente ou ou equivalentemente ou ou equivalentemente ou ou equivalentemente ou ou equivalentemente ou(-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)

Como já podemos observar pelas tuplas de forma acima, a multiplicação dos elementos da tupla de forma (por exemplo 2*9, 3*6etc.) deve sempre ser igual ao número total de elementos no tensor original ( 18no nosso exemplo).

Outra coisa a observar é que usamos um -1em um dos lugares em cada uma das tuplas de forma. Usando a -1, estamos sendo preguiçosos ao fazer o cálculo e delegar a tarefa ao PyTorch para fazer o cálculo desse valor para a forma quando ela cria a nova exibição . Uma coisa importante a ser observada é que podemos usar uma única -1na tupla de forma. Os valores restantes devem ser explicitamente fornecidos por nós. Outro PyTorch irá reclamar, lançando um RuntimeError:

RuntimeError: apenas uma dimensão pode ser deduzida

Portanto, com todas as formas mencionadas acima, o PyTorch sempre retornará uma nova visualização do tensor original t. Isso basicamente significa que apenas altera as informações de passada do tensor para cada uma das novas visualizações solicitadas.

Abaixo estão alguns exemplos que ilustram como as passadas dos tensores são alteradas a cada nova vista .

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

Agora, veremos os avanços para as novas visualizações :

# 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)

Então essa é a mágica da view()função. Ele apenas altera os passos do tensor (original) para cada uma das novas vistas , desde que a forma da nova vista seja compatível com a forma original.

Outra coisa interessante pode observar a partir dos tuplos Strides é que o valor do elemento no 0 ª posição é igual ao valor do elemento no 1 st posição da tupla forma.

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

Isto é porque:

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

o passo (6, 1)diz que para ir de um elemento para o próximo elemento ao longo da dimensão, temos que pular ou dar 6 passos. (ou seja, para ir de 0para 6, alguém tem que tomar 6 passos.) Mas, para ir de um elemento para o próximo elemento no 1 st dimensão, só precisamos de apenas um passo (por exemplo, para ir a partir 2de 3).

Assim, as informações de passada estão no centro de como os elementos são acessados ​​da memória para realizar o cálculo.


torch.reshape ()

Essa função retornaria uma vista e é exatamente a mesma que usar torch.Tensor.view(), desde que a nova forma seja compatível com a forma do tensor original. Caso contrário, ele retornará uma cópia.

No entanto, as notas de torch.reshape()adverte que:

entradas contíguas e entradas com passos compatíveis podem ser remodeladas sem copiar, mas não se deve depender do comportamento de cópia versus exibição.

kmario23
fonte
1

Eu descobri que x.view(-1, 16 * 5 * 5)é equivalente a x.flatten(1), onde o parâmetro 1 indica que o processo de nivelamento começa na 1ª dimensão (não nivelando a dimensão 'amostra') Como você pode ver, o último uso é semanticamente mais claro e fácil de usar, então eu preferir flatten().

FENGSHI ZHENG
fonte
1

Qual é o significado do parâmetro -1?

Você pode ler -1como número dinâmico de parâmetros ou "qualquer coisa". Por causa de que não pode haver apenas um parâmetro -1em view().

Se você perguntar, x.view(-1,1)isso produzirá a forma do tensor, [anything, 1]dependendo do número de elementos em x. Por exemplo:

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)

Saída:

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])
prosti
fonte
1

weights.reshape(a, b) retornará um novo tensor com os mesmos dados que pesos com tamanho (a, b), pois ele copia os dados para outra parte da memória.

weights.resize_(a, b)retorna o mesmo tensor com uma forma diferente. No entanto, se a nova forma resultar em menos elementos que o tensor original, alguns elementos serão removidos do tensor (mas não da memória). Se a nova forma resultar em mais elementos que o tensor original, novos elementos serão não inicializados na memória.

weights.view(a, b) retornará um novo tensor com os mesmos dados que pesos com tamanho (a, b)

Jibin Mathew
fonte
0

Gostei muito dos exemplos de @Jadiel de Armas.

Gostaria de adicionar um pequeno insight sobre como os elementos são ordenados para .view (...)

  • Para um tensor com forma (a, b, c) , a ordem de seus elementos é determinada por um sistema de numeração: onde o primeiro dígito tem um número, o segundo dígito tem números b e o terceiro dígito tem números c .
  • O mapeamento dos elementos no novo tensor retornado por .view (...) preserva essa ordem do tensor original.
ychnh
fonte
0

Vamos tentar entender a visualização pelos seguintes exemplos:

    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 como um valor de argumento é uma maneira fácil de calcular o valor de say x, desde que conheçamos os valores de y, z ou o inverso no caso de 3d e para 2d novamente uma maneira fácil de calcular o valor de say x, desde que conhecer valores de y ou vice-versa ..

Lija Alex
fonte