pourquoi «emballons-nous» les séquences en pytorch?

93

J'essayais de reproduire Comment utiliser l'empaquetage 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 est-il nécessaire de les «emballer» (à travers pack_padded_sequence)?

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

Aerin
la source
toutes les questions sur l'emballage dans pytorch: discuss.pytorch.org/t/…
Charlie Parker

Réponses:

88

Je suis également tombé sur ce problème et voici ce que j'ai compris.

Lors de l'apprentissage de RNN (LSTM ou GRU ou vanilla-RNN), il est difficile de grouper 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 allez remplir toutes les séquences et cela donnera 8 séquences de longueur 8. Vous finirait par faire 64 calculs (8x8), mais vous n'aviez besoin que de 45 calculs. De plus, si vous vouliez faire quelque chose de sophistiqué comme utiliser un RNN bidirectionnel, il serait plus difficile de faire des calculs par lots juste par remplissage et vous pourriez finir par faire plus de calculs que nécessaire.

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

Je n'ai peut-être pas été clair à certains moments, alors faites-le moi savoir et je pourrai ajouter plus d'explications.

Voici un exemple de code:

 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]))
Umang Gupta
la source
4
Pouvez-vous expliquer pourquoi la sortie de l'exemple donné est PackedSequence (data = tensor ([1, 3, 2, 4, 3]), batch_sizes = tensor ([2, 2, 1]))?
ascetic652
3
La partie de données est juste tous les tenseurs concaténés le long de l'axe du temps. Batch_size est en fait le tableau des tailles de lots à chaque pas de temps.
Umang Gupta
2
Le batch_sizes = [2, 2, 1] représente le groupement [1, 3] [2, 4] et [3] respectivement.
Chaitanya Shivade
@ChaitanyaShivade pourquoi la taille du lot est-elle [2,2,1]? ça ne peut pas être [1,2,2]? quelle est la logique derrière cela?
Programmeur anonyme
1
Parce qu'à l'étape t, vous ne pouvez traiter que les vecteurs à l'étape t, si vous gardez les vecteurs ordonnés comme [1,2,2], vous mettez probablement chaque entrée comme un lot, mais cela ne peut pas être parallélisé et donc pas batchable
Umang Gupta
51

Voici quelques explications visuelles 1 qui pourraient aider à développer une meilleure intuition pour la fonctionnalité depack_padded_sequence()

Supposons que nous ayons des 6séquences (de longueurs variables) au total. Vous pouvez également considérer ce nombre 6comme l' batch_sizehyperparamètre.

Maintenant, nous voulons transmettre ces séquences à une ou plusieurs architectures de réseaux neuronaux récurrents. Pour ce faire, nous devons remplir toutes les séquences (généralement avec 0s) dans notre lot à la longueur de séquence maximale dans notre lot ( max(sequence_lengths)), ce qui est dans la figure ci-dessous 9.

rembourré-seqs

Donc, le travail de préparation des données devrait être terminé maintenant, non? Pas vraiment ... Parce qu'il y a encore un problème pressant, principalement en termes de combien de calculs devons-nous faire par rapport aux calculs réellement nécessaires.

Par souci de compréhension, supposons également que nous allons multiplier par matrice ce qui précède padded_batch_of_sequencesde forme (6, 9)avec une matrice Wde poids de forme (9, 3).

Ainsi, nous devrons effectuer des opérations de 6x9 = 54multiplication et d' 6x8 = 48addition                     ( nrows x (n-1)_cols), uniquement pour jeter la plupart des résultats calculés car ils seraient 0s (là où nous avons des pads). Le calcul réel requis dans ce cas est le suivant:

 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
   
------------------------------  
#savings: 22-mult & 22-add ops  
          (32-54)  (26-48) 

C'est BEAUCOUP plus d'économies, même pour cet exemple très simple ( jouet ). Vous pouvez maintenant imaginer combien de calcul (éventuellement: coût, énergie, temps, émission de carbone, etc.) peut être économisé en utilisant pack_padded_sequence()de grands tenseurs avec des millions d'entrées, et plus de millions de systèmes partout dans le monde faisant cela, encore et encore.

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

pack-rembourré-seqs

À la suite de l'utilisation pack_padded_sequence(), nous obtiendrons un tuple de tenseurs contenant (i) l'aplatissement (le long de l'axe 1, sur la figure ci-dessus) sequences, (ii) les tailles de lots 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) pourrait alors être passé à des fonctions objectives telles que CrossEntropy pour les calculs de perte.


1 crédit d'image à @sgrvinod

kmario23
la source
2
Excellents diagrammes!
David Waterworth
1
Edit: Je pense que stackoverflow.com/a/55805785/6167850 (ci-dessous) répond à ma question, que je laisserai quand même ici: ~ Cela signifie-t-il essentiellement que les gradients ne sont pas propagés aux entrées rembourrées? Que faire si ma fonction de perte n'est calculée que sur l'état / sortie caché final du RNN? Faut-il alors jeter les gains d'efficacité? Ou la perte sera-t-elle calculée à partir de l'étape précédant le début du remplissage, ce qui est différent pour chaque élément de lot dans cet exemple? ~
nlml
25

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

Prenons un exemple

Remarque: pack_padded_sequencenécessite des séquences triées dans le lot (dans l'ordre décroissant des longueurs de séquence). Dans l'exemple ci-dessous, le lot de séquence a déjà été trié pour moins d'encombrement. Visitez ce lien essentiel pour la mise en œuvre complète.

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

  • Chaque séquence a une taille d'incorporation 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 complétons seq_batchpour obtenir le lot de séquences de longueur égale à 5 (la longueur maximale du lot). Maintenant, le nouveau lot contient 10 éléments au total.

# 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 fichier padded_seq_batch. Il renvoie un tuple de deux tenseurs:

  • Le premier est les données comprenant tous les éléments du lot de séquence.
  • Le second est le batch_sizesqui 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_batchaux modules récurrents dans Pytorch, tels que RNN, LSTM. Cela ne nécessite que des 5 + 2=7calculs dans le module recurrrent.

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 outputle 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 façon standard, il suffit de passer le padded_seq_batchau lstmmodule. Cependant, cela nécessite 10 calculs. Il implique plusieurs calculs plus sur des éléments de remplissage qui seraient inefficaces en termes de calcul .

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

    • Pour LSTM (ou tout module récurrent) avec uniquement une direction avant, si nous souhaitons extraire le vecteur caché de la dernière étape comme représentation d'une séquence, nous devrons récupérer les vecteurs cachés de T (ème) étape, où T est la longueur de l'entrée. La reprise de la dernière représentation sera incorrecte. Notez que T sera différent pour différentes entrées dans le lot.
    • Pour le LSTM bidirectionnel (ou tout module récurrent), c'est encore plus encombrant, 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, cnsont différents de deux manières tandis que outputde deux manières conduisent à des valeurs différentes pour les éléments de remplissage.

David Ng
la source
2
Bonne réponse! Juste une correction si vous faites du remplissage, vous ne devriez pas utiliser le dernier h plutôt que h à l'index égal à la longueur de l'entrée. De plus, pour faire un RNN bidirectionnel, vous voudrez utiliser deux RNN différents --- un avec un rembourrage à l'avant et un autre avec un rembourrage à l'arrière pour obtenir des résultats corrects. Le remplissage et la sélection de la dernière sortie sont "faux". Donc, vos arguments selon lesquels cela conduit à une représentation inexacte sont faux. Le problème avec le remplissage est qu'il est correct mais inefficace (si l'option de séquences compactées est là) et peut être encombrant (par exemple: bi-dir RNN)
Umang Gupta
18

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

Le premier élément du tuple retourné de pack_padded_sequenceest un tenseur de données contenant une séquence condensée. Le deuxième élément est un tenseur d'entiers contenant des informations sur la taille du lot à chaque étape de séquence.

Ce qui est important ici, c'est que le deuxième élément (Tailles de lot) représente le nombre d'éléments à chaque étape de séquence du lot, et non les différentes longueurs de séquence transmises pack_padded_sequence.

Par exemple, des données données abcet x le: class: PackedSequencecontiendraient des données axbcavec batch_sizes=[2,1,1].

Aerin
la source
1
Merci, j'ai totalement oublié ça. et j'ai fait une erreur dans ma réponse en mettant à jour cela. Cependant, j'ai regardé la deuxième séquence comme certaines données nécessaires pour récupérer les séquences et c'est pourquoi j'ai gâché ma description
Umang Gupta
2

J'ai utilisé la séquence rembourrée du 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 selon l'ordre décroissant de longueur dans un lot donné.

vous pouvez consulter un exemple ici .

Et nous faisons un emballage pour que le RNN ne voit pas l'index rembourré indésirable lors du traitement de la séquence, ce qui affecterait les performances globales.

Jibin Mathew
la source