173 votes

pourquoi "emballons-nous" les séquences en 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 "garnir" mais pourquoi est-il nécessaire de les "emballer" (par pack_padded_sequence )?

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

156voto

Umang Gupta Points 3100

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

Lors de la formation RNN (LSTM ou GRU ou à la vanille-RNN), il est difficile de lot de la variable longueur des séquences. Ex: si la longueur des séquences dans une taille 8 lot est [4,6,8,5,4,3,7,8], vous pavé de toutes les séquences et que les résultats en 8 séquences de longueur 8. Vous allez finir par faire de la 64 calcul (8x8), mais vous avez besoin de faire seulement 45 calculs. En outre, si vous voulais faire quelque chose de fantaisie, comme à l'aide d'une bidirectionnel-RNN il serait plus difficile de faire des lots calculs seulement par le rembourrage et vous pourriez faire plus de calculs que nécessaire.

Au lieu de cela, pytorch nous permet d'emballer la séquence, en interne, des paniers de séquence 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 dans la récupération de l'effectif des séquences ainsi que de raconter RNN quelle est la taille de lot à chaque pas de temps. Cela a été signalé par @Aerin. Ce peut être passé à RNN et il va en interne d'optimiser les calculs.

Je pourrais avoir été clair sur certains points, alors laissez-moi savoir et je peux 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]))

152voto

mario23 Points 322

Voici quelques explications visuelles1 qui peuvent aider à développer une meilleure intuition de la fonctionnalité de l' pack_padded_sequence()

Imaginons que nous avons 6 des séquences (de longueur variable) au total. Vous pouvez également considérer ce nombre 6 le batch_size hyper-paramètre.

Maintenant, nous voulons passer ces séquences à certaines réseau de neurones récurrent de l'architecture(s). Pour ce faire, nous avons à la map toutes les séquences (généralement avec 0s) dans notre lot au maximum de la longueur de la séquence dans notre lot (max(sequence_lengths)), qui, dans la figure ci-dessous est 9.

padded-seqs

Ainsi, la préparation des données, le travail devrait être terminé d'ici là, non? Pas vraiment.. Car il y a encore un problème urgent, principalement en termes de combien de calcul devons-nous faire lorsque comparé à la réalité calculs requis.

Pour des raisons de compréhension, supposons également que nous allons multiplier la matrice ci-dessus padded_batch_of_sequences forme (6, 9) avec une matrice de poids W forme (9, 3).

Ainsi, nous aurons à effectuer 6x9 = 54 de la multiplication et de l' 6x8 = 48 plus (nrows x (n-1)_cols) à des opérations de jeter la plupart des résultats calculés, car ils seraient 0s (où nous avons plaquettes). La réelle requise calculer 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 ce jouet exemple. Vous pouvez maintenant imaginer combien de calcul (coût, l'énergie, le temps, les émissions de carbone, etc.) peuvent être enregistrés à l'aide de pack_padded_sequence() pour les grandes tenseurs avec des millions d'entrées.

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

pack-padded-seqs

En tant que résultat de l'utilisation de pack_padded_sequence(), nous obtenons un tuple de tenseurs contenant (i) la aplati (le long de l'axe-1, dans la figure ci-dessus) sequences , (ii) le correspondant des tailles de lots, tensor([6,6,5,4,3,3,2,2,1]) pour l'exemple ci-dessus.

Les données du tenseur (c'est à dire la aplati séquences) pourrait alors être transmis à des fonctions objectives telles que CrossEntropy pour les calculs de perte.


1 crédits image de @sgrvinod

50voto

David Ng Points 743

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

Prenons un exemple

Remarque: pack_padded_sequence nécessite séquences triées dans le lot (dans l'ordre décroissant de séquence de longueur). Dans l'exemple ci-dessous, la séquence de lots ont déjà été triés pour moins encombrer. Visitez ce gist lien pour la pleine mise en œuvre.

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

  • Chaque séquence a l'incorporation de la taille 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 pad seq_batch pour obtenir le lot de séquences avec la même longueur de 5 (La longueur maximum 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 les emballons l' padded_seq_batch. Elle renvoie un tuple de deux tenseurs:

  • La première est que les données, y compris tous les éléments dans la séquence de lot.
  • La deuxième est l' batch_sizes qui vous dira comment les éléments liés les uns aux autres par les différentes é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 à la récurrente des modules de Pytorch, tels que les RNN, LSTM. Cela nécessite seulement 5 + 2=7 des calculs dans le recurrrent module.

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 avons besoin de convertir output dos au collier de lot de production:

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

Comparer cet effort avec le standard

  1. De façon standard, nous avons seulement besoin de passer l' padded_seq_batch de lstm module. Cependant, elle nécessite 10 calculs. Il s'agit de plusieurs calcule plus sur le remplissage des éléments de calcul inefficace.

  2. Notez que cela ne conduise pas à inexacte des représentations, mais il faut beaucoup plus de logique à l'extrait de corriger les représentations.

    • Pour LSTM (ou toute récurrent modules) avec seulement vers l'avant, si nous tenons à extraire le caché vecteur de la dernière étape en tant que représentation d'une séquence, nous aurions à ramasser caché des vecteurs à partir de T(th) de l'étape, où T est la longueur de l'entrée. La cueillette jusqu'à la dernière représentation sera incorrect. Notez que T sera différent pour les différentes entrées dans le lot.
    • Pour le Bi-directionnelle LSTM (ou toute récurrent modules), il est même de plus en plus lourde, que l'on pourrait avoir à maintenir deux RNN des modules, l'un qui fonctionne avec un rembourrage au début de l'input et l'autre avec un rembourrage à la fin de l'entrée, et enfin l'extraction et la concaténation de l'caché vecteurs comme expliqué ci-dessus.

Nous allons voir 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 l' hn, cn sont différentes dans les deux façons, tandis que output à partir de deux chemins conduisent à des valeurs différentes pour le rembourrage des éléments.

23voto

Aaron Points 2993

L'ajout de Umang de réponse, j'ai trouvé cet important à noter.

Le premier élément dans le retour de l'tuple de pack_padded_sequence de données (tenseur)- tenseur de paniers contenant la séquence. Le deuxième élément est un tenseur d'entiers contenant les informations sur la taille du lot à chaque étape de la séquence.

Ce qui est important ici est que le deuxième élément (tailles de Lots) représente le nombre d'éléments à chaque étape de la séquence dans le lot, pas le variant de la séquence de longueurs passé de pack_padded_sequence.

Par exemple, données en abc et x l' :classe:PackedSequence contiendrait des données axbcavec batch_sizes=[2,1,1].

3voto

Jibin Mathew Points 1

J'ai utilisé le pack collier de la séquence comme suit.

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

où text_lengths sont la durée de chaque séquence avant de rembourrage et de la séquence sont triés selon l'ordre décroissant de longueur à l'intérieur d'un lot donné.

vous pouvez consulter un exemple ici.

Et nous ne l'emballage, de sorte que la RNN ne voit pas non rembourré index lors du traitement de la séquence qui aurait une incidence sur la performance globale.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X