web-dev-qa-db-fra.com

pourquoi avons-nous "emballer" les séquences dans pytorch?

J'essayais de répliquer Comment utiliser l'emballage pour les entrées de séquence de longueur variable pour rnn mais je suppose que je dois d'abord comprendre pourquoi nous devons "emballer" la séquence. 

Je comprends pourquoi nous devons les "tamponner" mais pourquoi "emballer" (via pack_padded_sequence) est-il nécessaire? 

Toute explication de haut niveau serait appréciée!

16
Aerin

Je suis aussi tombé sur ce problème et voici ce que j’ai découvert. 

Lors de la formation RNN (LSTM ou GRU ou Vanilla-RNN), il est difficile de regrouper les séquences de longueur variable. Par exemple: si la longueur des séquences dans un lot de taille 8 est [4,6,8,5,4,3,7,8], vous compléterez toutes les séquences et cela donnera 8 séquences de longueur 8. Vous finissez par faire 64 calculs (8x8), mais vous n'aviez besoin que de 45 calculs. De plus, si vous vouliez faire quelque chose d'extraordinaire comme utiliser un RNN bidirectionnel, il serait plus difficile de faire des calculs par lots simplement par remplissage et vous risqueriez de faire plus de calculs que nécessaire. 

Au lieu de cela, pytorch nous permet d’emballer la séquence, séquence encapsulée en interne est un tuple de deux listes. On contient les éléments de séquences. Les éléments sont entrelacés par incréments de temps (voir exemple ci-dessous) et autres contient les taille de chaque séquence la taille du lot à chaque étape. Ceci est utile pour récupérer les séquences réelles et indiquer à RNN quelle est la taille du lot à chaque pas de temps. Cela a été souligné par @Aerin. Cela peut être passé à RNN et cela optimisera en interne les calculs. 

J'ai peut-être été incertain à certains moments, alors faites-le moi savoir et je peux ajouter d'autres explications. 

 a = [torch.tensor([1,2,3]), torch.tensor([3,4])]
 b = torch.nn.utils.rnn.pad_sequence(a, batch_first=True)
 >>>>
 tensor([[ 1,  2,  3],
    [ 3,  4,  0]])
 torch.nn.utils.rnn.pack_padded_sequence(b, batch_first=True, lengths=[3,2]
 >>>>PackedSequence(data=tensor([ 1,  3,  2,  4,  3]), batch_sizes=tensor([ 2,  2,  1]))
22
Umang Gupta

Les réponses ci-dessus répondaient à la question pourquoi très bien. Je veux juste ajouter un exemple pour mieux comprendre l'utilisation de pack_padded_sequence.

Prenons un exemple

Remarque: pack_padded_sequence nécessite des séquences triées dans le lot (dans l'ordre décroissant des longueurs de séquence). Dans l'exemple ci-dessous, les lots de séquence ont déjà été triés pour réduire l'encombrement. Visitez ce lien Gist pour la mise en œuvre complète.

Tout d'abord, nous créons un lot de 2 séquences de différentes longueurs, comme ci-dessous. Nous avons 7 éléments dans le lot totalement.

  • Chaque séquence a une taille d'intégration de 2.
  • La première séquence a la longueur: 5
  • La deuxième séquence a la longueur: 2
import torch 

seq_batch = [torch.tensor([[1, 1],
                           [2, 2],
                           [3, 3],
                           [4, 4],
                           [5, 5]]),
             torch.tensor([[10, 10],
                           [20, 20]])]

seq_lens = [5, 2]

Nous plaçons seq_batch pour obtenir le lot de séquences de longueur égale à 5 (La longueur maximale du lot). Maintenant, le nouveau lot a 10 éléments totalement.

# pad the seq_batch
padded_seq_batch = torch.nn.utils.rnn.pad_sequence(seq_batch, batch_first=True)
"""
>>>padded_seq_batch
tensor([[[ 1,  1],
         [ 2,  2],
         [ 3,  3],
         [ 4,  4],
         [ 5,  5]],

        [[10, 10],
         [20, 20],
         [ 0,  0],
         [ 0,  0],
         [ 0,  0]]])
"""

Ensuite, nous emballons le padded_seq_batch. Il retourne un tuple de deux tenseurs:

  • La première concerne les données comprenant tous les éléments du lot de séquence.
  • Le second est le batch_sizes qui dira comment les éléments sont liés les uns aux autres par les étapes.
# pack the padded_seq_batch
packed_seq_batch = torch.nn.utils.rnn.pack_padded_sequence(padded_seq_batch, lengths=seq_lens, batch_first=True)
"""
>>> packed_seq_batch
PackedSequence(
   data=tensor([[ 1,  1],
                [10, 10],
                [ 2,  2],
                [20, 20],
                [ 3,  3],
                [ 4,  4],
                [ 5,  5]]), 
   batch_sizes=tensor([2, 2, 1, 1, 1]))
"""

Maintenant, nous passons le tuple packed_seq_batch aux modules récurrents de Pytorch, tels que RNN, LSTM. Cela nécessite uniquement les calculs 5 + 2=7 dans le module récurrent.

lstm = nn.LSTM(input_size=2, hidden_size=3, batch_first=True)
output, (hn, cn) = lstm(packed_seq_batch.float()) # pass float tensor instead long tensor.
"""
>>> output # PackedSequence
PackedSequence(data=tensor(
        [[-3.6256e-02,  1.5403e-01,  1.6556e-02],
         [-6.3486e-05,  4.0227e-03,  1.2513e-01],
         [-5.3134e-02,  1.6058e-01,  2.0192e-01],
         [-4.3123e-05,  2.3017e-05,  1.4112e-01],
         [-5.9372e-02,  1.0934e-01,  4.1991e-01],
         [-6.0768e-02,  7.0689e-02,  5.9374e-01],
         [-6.0125e-02,  4.6476e-02,  7.1243e-01]], grad_fn=<CatBackward>), batch_sizes=tensor([2, 2, 1, 1, 1]))

>>>hn
tensor([[[-6.0125e-02,  4.6476e-02,  7.1243e-01],
         [-4.3123e-05,  2.3017e-05,  1.4112e-01]]], grad_fn=<StackBackward>),
>>>cn
tensor([[[-1.8826e-01,  5.8109e-02,  1.2209e+00],
         [-2.2475e-04,  2.3041e-05,  1.4254e-01]]], grad_fn=<StackBackward>)))
"""

Nous devons reconvertir output dans le lot de sortie rembourré:

padded_output, output_lens = torch.nn.utils.rnn.pad_packed_sequence(output, batch_first=True, total_length=5)
"""
>>> padded_output
tensor([[[-3.6256e-02,  1.5403e-01,  1.6556e-02],
         [-5.3134e-02,  1.6058e-01,  2.0192e-01],
         [-5.9372e-02,  1.0934e-01,  4.1991e-01],
         [-6.0768e-02,  7.0689e-02,  5.9374e-01],
         [-6.0125e-02,  4.6476e-02,  7.1243e-01]],

        [[-6.3486e-05,  4.0227e-03,  1.2513e-01],
         [-4.3123e-05,  2.3017e-05,  1.4112e-01],
         [ 0.0000e+00,  0.0000e+00,  0.0000e+00],
         [ 0.0000e+00,  0.0000e+00,  0.0000e+00],
         [ 0.0000e+00,  0.0000e+00,  0.0000e+00]]],
       grad_fn=<TransposeBackward0>)

>>> output_lens
tensor([5, 2])
"""

Comparez cet effort avec la méthode standard

  1. De manière standard, il suffit de passer du module padded_seq_batch à lstm. Cependant, il nécessite 10 calculs. Cela implique plusieurs calculs sur des éléments de remplissage qui seraient informatisés inefficaces.

  2. Notez que cela ne conduit pas à des représentations inexactes , mais nécessite beaucoup plus de logique pour extraire des représentations correctes.

    • Pour LSTM (ou tout module récurrent) avec uniquement le sens avant, si nous souhaitons extraire le vecteur caché de la dernière étape en tant que représentation d’une séquence, nous devrions choisir des vecteurs cachés dans T(th) pas, où T est la longueur de l'entrée. Ramasser la dernière représentation sera incorrecte. Notez que T sera différent pour différentes entrées en batch.
    • Pour le LSTM bidirectionnel (ou tous les modules récurrents), il est encore plus lourd, car il faudrait maintenir deux modules RNN, un qui fonctionne avec un remplissage au début de l’entrée et un avec un remplissage à la fin de l’entrée, et enfin, extraire et concaténer les vecteurs cachés comme expliqué ci-dessus.

Voyons la différence:

# The standard approach: using padding batch for recurrent modules
output, (hn, cn) = lstm(padded_seq_batch.float())
"""
>>> output
 tensor([[[-3.6256e-02, 1.5403e-01, 1.6556e-02],
          [-5.3134e-02, 1.6058e-01, 2.0192e-01],
          [-5.9372e-02, 1.0934e-01, 4.1991e-01],
          [-6.0768e-02, 7.0689e-02, 5.9374e-01],
          [-6.0125e-02, 4.6476e-02, 7.1243e-01]],

         [[-6.3486e-05, 4.0227e-03, 1.2513e-01],
          [-4.3123e-05, 2.3017e-05, 1.4112e-01],
          [-4.1217e-02, 1.0726e-01, -1.2697e-01],
          [-7.7770e-02, 1.5477e-01, -2.2911e-01],
          [-9.9957e-02, 1.7440e-01, -2.7972e-01]]],
        grad_fn= < TransposeBackward0 >)

>>> hn
tensor([[[-0.0601, 0.0465, 0.7124],
         [-0.1000, 0.1744, -0.2797]]], grad_fn= < StackBackward >),

>>> cn
tensor([[[-0.1883, 0.0581, 1.2209],
         [-0.2531, 0.3600, -0.4141]]], grad_fn= < StackBackward >))
"""

Les résultats ci-dessus montrent que hn, cn sont différentes de deux manières, tandis que output de deux manières conduisent à des valeurs différentes pour les éléments de remplissage.

7
David Ng

Ajoutant à la réponse d'Umang, j'ai trouvé cela important à noter.

Le premier élément du tuple renvoyé de pack_padded_sequence est une séquence compactée contenant des données (tenseur) - tenseur. Le deuxième élément est un tenseur d’entiers contenant des informations sur la taille du lot à chaque étape de la séquence.

Cependant, l’important ici est que le deuxième élément (tailles de lot) représente le nombre d’éléments à chaque étape de la séquence du lot, et non les longueurs de séquence variables passées à pack_padded_sequence

Par exemple, les données abc et x le: class: PackedSequence contiendrait des données axbc avec batch_sizes=[2,1,1].

7
Aerin

Voici quelques explications visuelles 1 cela pourrait aider à développer une meilleure intuition pour la fonctionnalité de pack_padded_sequence()

Supposons que nous ayons _6_ séquences (de longueurs variables) au total. Vous pouvez également considérer ce nombre _6_ comme hyperparamètre _batch_size_.

Nous souhaitons maintenant transmettre ces séquences à des architectures de réseaux de neurones récurrents. Pour ce faire, nous devons associer toutes les séquences (généralement avec _0_ s) de notre lot à la longueur de séquence maximale de notre lot (max(sequence_lengths)), qui dans la figure ci-dessous est _9_.

padded-seqs

Donc, le travail de préparation des données devrait être terminé maintenant, non? Pas vraiment .. Parce qu'il y a toujours un problème pressant, principalement en ce qui concerne le calcul que nous devons faire par rapport aux calculs réellement requis.

Par souci de compréhension, supposons également que nous multiplions par matrice le _padded_batch_of_sequences_ de forme _(6, 9)_ ci-dessus par une matrice de pondération W de forme _(9, 3)_.

Ainsi, nous devrons effectuer _6x9 = 54_ multiplication et _6x8 = 48_ addition (nrows x (n-1)_cols), uniquement pour jeter la plupart des résultats calculés car ils seraient _0_ s (où nous avons des pads). Le calcul réellement requis dans ce cas est:

_ 9-mult  8-add 
 8-mult  7-add 
 6-mult  5-add 
 4-mult  3-add 
 3-mult  2-add 
 2-mult  1-add
---------------
32-mult  26-add
_

C'est beaucoup plus d'économies même pour cet exemple de jouet. Vous pouvez maintenant imaginer combien de temps de calcul (coût, énergie, temps, émission de carbone, etc.) peut être économisé en utilisant pack_padded_sequence() pour les grands tenseurs avec des millions d'entrées.

La fonctionnalité de pack_padded_sequence() peut être comprise à partir de la figure ci-dessous, à l'aide du code de couleur utilisé:

pack-padded-seqs

En utilisant pack_padded_sequence(), nous obtiendrons un nuplet de tenseurs contenant (i) la valeur aplatie (le long de l'axe 1 dans la figure ci-dessus) sequences, (ii) les tailles de lot correspondantes, tensor([6,6,5,4,3,3,2,2,1]) pour l'exemple ci-dessus. .

Le tenseur de données (c'est-à-dire les séquences aplaties) peut ensuite être transmis à des fonctions objectives telles que CrossEntropy pour les calculs de perte.


1 crédit image à @ sgrvinod

4
kmario23

J'ai utilisé la séquence rembourrée pack comme suit. 

packed_embedded = nn.utils.rnn.pack_padded_sequence(seq, text_lengths)
packed_output, hidden = self.rnn(packed_embedded)

où text_lengths est la longueur de la séquence individuelle avant le remplissage et la séquence est triée par ordre décroissant de longueur dans un lot donné.

vous pouvez consulter un exemple ici .

Et nous faisons l'emballage pour que le RNN ne voie pas l'index complété non désiré lors du traitement de la séquence, ce qui affecterait les performances globales. 

1
Jibin Mathew

Ajoutant aux autres réponses: Voici un exemple de code minimal détaillé très utile pour comprendre le concept de séquencement de séquence: https://github.com/HarshTrivedi/packing-unpacking-pytorch-minimal -tutorial/arbre/maître

1
MicPie