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!
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]))
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
.
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.
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:
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])
"""
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.
Notez que cela ne conduit pas à des représentations inexactes , mais nécessite beaucoup plus de logique pour extraire des 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érentes de deux manières, tandis que output
de deux manières conduisent à des valeurs différentes pour les éléments de remplissage.
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]
.
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
_.
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é:
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
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.
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