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!
Réponses:
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équencela 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:
la source
Voici quelques explications visuelles 1 qui pourraient aider à développer une meilleure intuition pour la fonctionnalité de
pack_padded_sequence()
Supposons que nous ayons des
6
séquences (de longueurs variables) au total. Vous pouvez également considérer ce nombre6
comme l'batch_size
hyperparamè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
0
s) dans notre lot à la longueur de séquence maximale dans notre lot (max(sequence_lengths)
), ce qui est dans la figure ci-dessous9
.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_sequences
de forme(6, 9)
avec une matriceW
de poids de forme(9, 3)
.Ainsi, nous devrons effectuer des opérations de
6x9 = 54
multiplication et d'6x8 = 48
addition (nrows x (n-1)_cols
), uniquement pour jeter la plupart des résultats calculés car ils seraient0
s (là où nous avons des pads). Le calcul réel requis dans ce cas est le suivant: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é:À 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
la source
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
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.
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_batch
pour 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: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 dans Pytorch, tels que RNN, LSTM. Cela ne nécessite que des5 + 2=7
calculs 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>))) """
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
De façon standard, il suffit de passer le
padded_seq_batch
aulstm
module. Cependant, cela nécessite 10 calculs. Il implique plusieurs calculs plus sur des éléments de remplissage qui seraient inefficaces en termes de calcul .Notez que cela ne conduit pas à des représentations inexactes , mais nécessite beaucoup plus de logique pour extraire les représentations correctes.
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érents de deux manières tandis queoutput
de deux manières conduisent à des valeurs différentes pour les éléments de remplissage.la source
Ajoutant à la réponse d'Umang, j'ai trouvé cela important à noter.
Le premier élément du tuple retourné de
pack_padded_sequence
est 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
abc
etx
le: class:PackedSequence
contiendraient des donnéesaxbc
avecbatch_sizes=[2,1,1]
.la source
J'ai utilisé la séquence rembourrée du pack comme suit.
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.
la source