Comment fonctionne la méthode de «visualisation» dans PyTorch?

207

Je suis confus au sujet de la méthode view()dans l'extrait de code suivant.

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

Ma confusion concerne la ligne suivante.

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

Que fait la tensor.view()fonction? J'ai vu son utilisation à de nombreux endroits, mais je ne comprends pas comment il interprète ses paramètres.

Que se passe-t-il si je donne des valeurs négatives comme paramètres à la view()fonction? Par exemple, que se passe-t-il si j'appelle tensor_variable.view(1, 1, -1),?

Quelqu'un peut-il expliquer le principe principal de la view()fonction avec quelques exemples?

Wasi Ahmad
la source

Réponses:

285

La fonction de visualisation est destinée à remodeler le tenseur.

Dis que tu as un tenseur

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

aest un tenseur qui a 16 éléments de 1 à 16 (inclus). Si vous souhaitez remodeler ce tenseur pour en faire un 4 x 4tenseur, vous pouvez utiliser

a = a.view(4, 4)

Maintenant, asera un 4 x 4tenseur. Notez qu'après le remodelage, le nombre total d'éléments doit rester le même. Le remodelage du tenseur aen 3 x 5tenseur ne serait pas approprié.

Quelle est la signification du paramètre -1?

S'il y a une situation où vous ne savez pas combien de lignes vous voulez mais êtes sûr du nombre de colonnes, vous pouvez le spécifier avec un -1. ( Notez que vous pouvez l'étendre aux tenseurs avec plus de dimensions. Une seule valeur d'axe peut être -1 ). C'est une façon de dire à la bibliothèque: "donnez-moi un tenseur qui a ces nombreuses colonnes et vous calculez le nombre approprié de lignes qui est nécessaire pour y arriver".

Cela peut être vu dans le code de réseau neuronal que vous avez donné ci-dessus. Après la ligne x = self.pool(F.relu(self.conv2(x)))de la fonction avant, vous obtiendrez une carte d'entités à 16 profondeurs. Vous devez l'aplatir pour le donner à la couche entièrement connectée. Vous dites donc à pytorch de remodeler le tenseur que vous avez obtenu pour avoir un nombre spécifique de colonnes et lui dites de décider du nombre de lignes par lui-même.

Dessiner une similitude entre numpy et pytorch, viewest semblable à de numpy Reshape fonction.

Kashyap
la source
93
"La vue est similaire à la refonte de Numpy" - pourquoi ne l'ont-ils pas simplement appelé reshapedans PyTorch?!
MaxB
54
@MaxB Contrairement à remodeler, le nouveau tenseur retourné par "view" partage les données sous-jacentes avec le tenseur d'origine, c'est donc vraiment une vue sur l'ancien tenseur au lieu d'en créer un tout nouveau.
qihqi
37
@blckbird "remodeler copie toujours la mémoire. afficher ne copie jamais la mémoire." github.com/torch/cutorch/issues/98
devinbost
3
@devinbost Le remodelage de la torche copie toujours la mémoire. La modification de NumPy ne le fait pas.
Tavian Barnes
32

Faisons quelques exemples, du plus simple au plus difficile.

  1. La viewméthode renvoie un tenseur avec les mêmes données que le selftenseur (ce qui signifie que le tenseur renvoyé a le même nombre d'éléments), mais avec une forme différente. Par exemple:

    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. En supposant que ce -1n'est pas l'un des paramètres, lorsque vous les multipliez ensemble, le résultat doit être égal au nombre d'éléments dans le tenseur. Si vous faites:, a.view(3, 3)cela soulèvera un RuntimeErrorcar la forme (3 x 3) n'est pas valide pour une entrée avec 16 éléments. En d'autres termes: 3 x 3 n'est pas égal à 16 mais 9.

  3. Vous pouvez utiliser l' -1un des paramètres que vous transmettez à la fonction, mais une seule fois. Tout ce qui se passe, c'est que la méthode fera le calcul pour vous sur la façon de remplir cette dimension. Par exemple, a.view(2, -1, 4)est équivalent à a.view(2, 2, 4). [16 / (2 x 4) = 2]

  4. Notez que le tenseur retourné partage les mêmes données . Si vous modifiez la "vue", vous modifiez les données du tenseur d'origine:

    b = a.view(4, 4)
    b[0, 2] = 2
    a[2] == 3.0
    False
  5. Maintenant, pour un cas d'utilisation plus complexe. La documentation indique que chaque nouvelle dimension de vue doit être soit un sous-espace d'une dimension d'origine, soit uniquement s'étendre sur d, d + 1, ..., d + k qui satisfont à la condition de contiguïté suivante qui, pour tout i = 0,. .., k - 1, foulée [i] = foulée [i + 1] x taille [i + 1] . Sinon, contiguous()doit être appelé avant que le tenseur ne puisse être visualisé. Par exemple:

    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)

    Notez que pour a_t, foulée [0]! = Foulée [1] x taille [1] depuis 24! = 2 x 3

Jadiel de Armas
la source
7

torch.Tensor.view()

En termes simples, torch.Tensor.view()qui est inspiré par numpy.ndarray.reshape()ou numpy.reshape(), crée une nouvelle vue du tenseur, tant que la nouvelle forme est compatible avec la forme du tenseur d'origine.

Comprenons cela en détail à l'aide d'un exemple concret.

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

Avec ce tenseur tde forme (18,), de nouvelles vues ne peuvent être créées que pour les formes suivantes:

(1, 18)ou de manière équivalente (1, -1)ou ou équivalente ou ou équivalente ou ou équivalente ou ou équivalente ou ou équivalente 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)

Comme nous pouvons déjà l'observer à partir des tuples de forme ci-dessus, la multiplication des éléments du tuple de forme (par exemple 2*9, 3*6etc.) doit toujours être égale au nombre total d'éléments dans le tenseur d'origine (18 dans notre exemple).

Une autre chose à observer est que nous avons utilisé un -1dans l'un des endroits de chacun des tuples de forme. En utilisant a -1, nous sommes paresseux dans le calcul nous-mêmes et déléguons plutôt la tâche à PyTorch pour faire le calcul de cette valeur pour la forme lors de la création de la nouvelle vue . Une chose importante à noter est que nous pouvons seulement utiliser un seul -1dans le tuple de forme. Les valeurs restantes doivent être fournies explicitement par nous. Sinon PyTorch se plaindra en lançant un RuntimeError:

RuntimeError: une seule dimension peut être déduite

Ainsi, avec toutes les formes mentionnées ci-dessus, PyTorch retournera toujours une nouvelle vue du tenseur d'originet . Cela signifie essentiellement qu'il modifie simplement les informations de foulée du tenseur pour chacune des nouvelles vues demandées.

Vous trouverez ci-dessous quelques exemples illustrant comment les foulées des tenseurs sont modifiées à chaque nouvelle vue .

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

Maintenant, nous allons voir les progrès pour les nouvelles vues :

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

Voilà donc la magie de la view()fonction. Il modifie simplement les enjambées du tenseur (d'origine) pour chacune des nouvelles vues , tant que la forme de la nouvelle vue est compatible avec la forme d'origine.

Une autre chose intéressante que l'on peut observer à partir des tuples de foulées est que la valeur de l'élément en 0 ème position est égale à la valeur de l'élément en 1 ère position du tuple de forme.

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

Ceci est dû au fait:

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

la foulée (6, 1)dit que pour passer d'un élément à l'élément suivant le long de la 0 ème dimension, il faut sauter ou faire 6 pas. (c.-à-d. pour aller de 0à 6, il faut faire 6 étapes.) Mais pour passer d'un élément à l'élément suivant dans la 1ère dimension, nous n'avons besoin que d'une seule étape (par exemple pour aller de 2à3 ).

Ainsi, les informations de pas sont au cœur de l'accès aux éléments depuis la mémoire pour effectuer le calcul.


torch.reshape ()

Cette fonction retournerait une vue et est exactement la même que celle utilisée torch.Tensor.view()tant que la nouvelle forme est compatible avec la forme du tenseur d'origine. Sinon, il en retournera une copie.

Cependant, les notes de torch.reshape()préviennent que:

Les entrées contiguës et les entrées avec des pas compatibles peuvent être remodelées sans copie, mais il ne faut pas dépendre du comportement de copie par rapport à l'affichage.

kmario23
la source
1

Je l'ai trouvé x.view(-1, 16 * 5 * 5)équivalent à x.flatten(1), où le paramètre 1 indique que le processus d'aplatissement commence à partir de la 1ère dimension (pas d'aplatissement de la dimension `` échantillon '') Comme vous pouvez le voir, cette dernière utilisation est sémantiquement plus claire et plus facile à utiliser, donc je préférez flatten().

FENGSHI ZHENG
la source
1

Quelle est la signification du paramètre -1?

Vous pouvez lire -1comme nombre dynamique de paramètres ou "n'importe quoi". Pour cette raison, il ne peut y avoir qu'un seul paramètre-1 dans view().

Si vous le demandez x.view(-1,1), la forme du tenseur sortira en [anything, 1]fonction du nombre d'éléments dans x. Par exemple:

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)

Sortira:

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
la source
1

weights.reshape(a, b) retournera un nouveau tenseur avec les mêmes données que les poids de taille (a, b) car il copie les données dans une autre partie de la mémoire.

weights.resize_(a, b)renvoie le même tenseur avec une forme différente. Cependant, si la nouvelle forme entraîne moins d'éléments que le tenseur d'origine, certains éléments seront supprimés du tenseur (mais pas de la mémoire). Si la nouvelle forme produit plus d'éléments que le tenseur d'origine, les nouveaux éléments ne seront pas initialisés en mémoire.

weights.view(a, b) renverra un nouveau tenseur avec les mêmes données que les poids de taille (a, b)

Jibin Mathew
la source
0

J'ai vraiment aimé les exemples de @Jadiel de Armas.

Je voudrais ajouter un petit aperçu de la façon dont les éléments sont ordonnés pour .view (...)

  • Pour un tenseur de forme (a, b, c) , l' ordre des éléments de celui - ci sont déterminés par un système de numérotation: où le premier chiffre a un nombre, le deuxième chiffre a b nombres et troisième chiffre a c nombres.
  • Le mappage des éléments dans le nouveau tenseur retourné par .view (...) préserve cet ordre du tenseur d'origine.
ychnh
la source
0

Essayons de comprendre la vue par les exemples suivants:

    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 comme valeur d'argument est un moyen facile de calculer la valeur de disons x à condition que nous connaissions les valeurs de y, z ou l'inverse en cas de 3d et pour 2d à nouveau un moyen facile de calculer la valeur de disons x à condition que nous connaître les valeurs de y ou vice versa ..

Lija Alex
la source