Come funziona il metodo "view" in PyTorch?


206

Sono confuso sul metodo view()nel seguente frammento di codice.

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

La mia confusione riguarda la seguente riga.

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

Cosa fa la tensor.view()funzione? Ho visto il suo utilizzo in molti luoghi, ma non riesco a capire come interpreta i suoi parametri.

Cosa succede se do valori negativi come parametri a view() funzione? Ad esempio, cosa succede se chiamo tensor_variable.view(1, 1, -1)?

Qualcuno può spiegare il principio principale della view()funzione con alcuni esempi?

Risposte:


284

La funzione di visualizzazione ha lo scopo di rimodellare il tensore.

Di 'che hai un tensore

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

aè un tensore che ha 16 elementi da 1 a 16 (inclusi). Se vuoi rimodellare questo tensore per renderlo un 4 x 4tensore, puoi usarlo

a = a.view(4, 4)

Ora asarà un 4 x 4tensore. Si noti che dopo la rimodulazione il numero totale di elementi deve rimanere lo stesso. Rimodellare il tensore ain un 3 x 5tensore non sarebbe appropriato.

Qual è il significato del parametro -1?

Se c'è qualche situazione in cui non sai quante righe vuoi ma sei sicuro del numero di colonne, puoi specificarlo con un -1. ( Si noti che è possibile estenderlo a tensori con più dimensioni. Solo uno del valore dell'asse può essere -1 ). Questo è un modo di dire alla biblioteca: "dammi un tensore che abbia queste molte colonne e calcoli il numero appropriato di righe necessarie per farlo accadere".

Questo può essere visto nel codice di rete neurale che hai dato sopra. Dopo la lineax = self.pool(F.relu(self.conv2(x))) nella funzione avanti, avrai una mappa caratteristica a 16 profondità. Devi appiattirlo per darlo allo strato completamente connesso. Quindi dici a pytorch di rimodellare il tensore che hai ottenuto per avere un numero specifico di colonne e dirgli di decidere da solo il numero di righe.

Disegnare una somiglianza tra numpy e pytorch, viewè simile alla funzione di rimodellamento di numpy .


93
"la vista è simile alla rimodulazione del numpy" - perché non l'hanno chiamata reshapein PyTorch ?!
MaxB

54
@MaxB A differenza di reshape, il nuovo tensore restituito da "view" condivide i dati sottostanti con il tensore originale, quindi è davvero una vista nel vecchio tensore invece di crearne uno nuovo.
qihqi,

37
@blckbird "reshape copia sempre la memoria. view non copia mai la memoria." github.com/torch/cutorch/issues/98
devinbost,

3
@devinbost Torch reshape copia sempre la memoria. La rimodulazione di NumPy no.
Tavian Barnes,

32

Facciamo alcuni esempi, dal più semplice al più difficile.

  1. Il viewmetodo restituisce un tensore con gli stessi dati del selftensore (il che significa che il tensore restituito ha lo stesso numero di elementi), ma con una forma diversa. Per esempio:

    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. Supponendo che -1non sia uno dei parametri, quando li moltiplichi insieme, il risultato deve essere uguale al numero di elementi nel tensore. Se lo fai: a.view(3, 3)aumenterà una RuntimeErrorforma perché (3 x 3) non è valida per l'input con 16 elementi. In altre parole: 3 x 3 non equivalgono a 16 ma 9.

  3. È possibile utilizzare -1come uno dei parametri passati alla funzione, ma solo una volta. Tutto ciò che accade è che il metodo farà la matematica per te su come riempire quella dimensione. Ad esempio a.view(2, -1, 4)è equivalente a a.view(2, 2, 4). [16 / (2 x 4) = 2]

  4. Si noti che il tensore restituito condivide gli stessi dati . Se si effettua una modifica nella "vista", si stanno modificando i dati del tensore originale:

    b = a.view(4, 4)
    b[0, 2] = 2
    a[2] == 3.0
    False
  5. Ora, per un caso d'uso più complesso. La documentazione afferma che ogni nuova dimensione della vista deve essere un sottospazio di una dimensione originale o solo span d, d + 1, ..., d + k che soddisfi la seguente condizione simile alla contiguità che per tutti i = 0,. .., k - 1, passo [i] = passo [i + 1] x dimensione [i + 1] . Altrimenti, contiguous()deve essere chiamato prima di poter visualizzare il tensore. Per esempio:

    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)

    Si noti che per a_t, passo [0]! = Passo [1] x dimensione [1] da 24! = 2 x 3


7

torch.Tensor.view()

In poche parole, torch.Tensor.view()che è ispirato da numpy.ndarray.reshape()o numpy.reshape(), crea una nuova vista del tensore, purché la nuova forma sia compatibile con la forma del tensore originale.

Comprendiamolo in dettaglio usando un esempio 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])

Con questo tensore tdi forma (18,), le nuove viste possono essere create solo per le seguenti forme:

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

Come possiamo già osservare dalle tuple di forma sopra, la moltiplicazione degli elementi della tupla di forma (ad es . 2*9, 3*6Ecc.) Deve sempre essere uguale al numero totale di elementi nel tensore originale (18 nel nostro esempio).

Un'altra cosa da osservare è che abbiamo usato a -1in uno dei punti in ciascuna delle tuple di forma. Usando a -1, siamo pigri nel fare noi stessi il calcolo e piuttosto deleghiamo l'attività a PyTorch per fare il calcolo di quel valore per la forma quando crea la nuova vista . Una cosa importante da notare è che possiamo usare solo un singolo -1nella tupla di forma. I valori rimanenti devono essere esplicitamente forniti da noi. Else PyTorch si lamenterà lanciando un RuntimeError:

RuntimeError: è possibile dedurre solo una dimensione

Quindi, con tutte le forme sopra menzionate, PyTorch restituirà sempre una nuova vista del tensore originalet . Ciò significa sostanzialmente che cambia semplicemente le informazioni sul passo del tensore per ciascuna delle nuove viste richieste.

Di seguito sono riportati alcuni esempi che illustrano come i passi dei tensori vengono modificati con ogni nuova vista .

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

Ora vedremo i passi da compiere per le nuove viste :

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

Quindi questa è la magia della view()funzione. Cambia solo i passi del tensore (originale) per ciascuna delle nuove viste , purché la forma della nuova vista sia compatibile con la forma originale.

Un'altra cosa interessante potrebbe osservare dalle stride tuple è che il valore dell'elemento nella 0 ° posizione è uguale al valore dell'elemento nella 1 ° posizione della tupla forma.

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

Questo è perché:

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

il passo (6, 1)dice che per passare da un elemento all'elemento successivo lungo la 0a dimensione, dobbiamo saltare o fare 6 passi. (cioè per passare da 0a 6, si deve prendere 6 punti). Ma per passare da un elemento con l'elemento successivo nella 1 ° dimensione, abbiamo solo bisogno di un solo passo (per esempio per andare da 2a3 ).

Pertanto, le informazioni sui passi sono al centro di come gli elementi sono accessibili dalla memoria per eseguire il calcolo.


torch.reshape ()

Questa funzione restituirebbe una vista ed è esattamente uguale all'utilizzo torch.Tensor.view()purché la nuova forma sia compatibile con la forma del tensore originale. Altrimenti, restituirà una copia.

Tuttavia, le note di torch.reshape()avvertono che:

gli input contigui e gli input con passi compatibili possono essere rimodellati senza copiare, ma non si dovrebbe dipendere dal comportamento di copia vs. visualizzazione.


1

Ho capito che x.view(-1, 16 * 5 * 5)equivale a x.flatten(1), in cui il parametro 1 indica che il processo di appiattimento inizia dalla 1a dimensione (non appiattire la dimensione "campione") Come puoi vedere, quest'ultimo utilizzo è semanticamente più chiaro e più facile da usare, quindi io preferisco flatten().


1

Qual è il significato del parametro -1?

Puoi leggere -1come numero dinamico di parametri o "qualunque cosa". Per questo motivo può esserci solo un parametro -1in view().

Se lo chiedi, otterrai la x.view(-1,1)forma del tensore in [anything, 1]base al numero di elementi in x. Per esempio:

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)

Uscita:

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

1

weights.reshape(a, b) restituirà un nuovo tensore con gli stessi dati dei pesi con dimensioni (a, b) in quanto copia i dati in un'altra parte della memoria.

weights.resize_(a, b)restituisce lo stesso tensore con una forma diversa. Tuttavia, se la nuova forma risulta in un minor numero di elementi rispetto al tensore originale, alcuni elementi verranno rimossi dal tensore (ma non dalla memoria). Se la nuova forma risulta in più elementi rispetto al tensore originale, i nuovi elementi non saranno inizializzati in memoria.

weights.view(a, b) restituirà un nuovo tensore con gli stessi dati dei pesi con dimensioni (a, b)


0

Mi sono piaciuti molto gli esempi di @Jadiel de Armas.

Vorrei aggiungere una piccola idea di come vengono ordinati gli elementi per .view (...)

  • Per un tensore di forma (a, b, c) , l' ordine di esso di elementi sono determinate da un sistema di numerazione: dove la prima cifra è un numero, seconda cifra ha b numeri e terza cifra ha c numeri.
  • La mappatura degli elementi nel nuovo Tensore restituita da .view (...) conserva questo ordine del Tensore originale.

0

Proviamo a capire la vista dai seguenti esempi:

    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 come valore dell'argomento è un modo semplice per calcolare il valore di dire x a condizione che conosciamo i valori di y, z o viceversa nel caso di 3d e per 2d di nuovo un modo semplice per calcolare il valore di dire x a condizione che conoscere i valori di y o viceversa ..

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.