Ich habe versucht zu replizieren, wie man das Packen für Sequenzeingaben variabler Länge für rnn verwendet, aber ich denke, ich muss zuerst verstehen, warum wir die Sequenz "packen" müssen.
Ich verstehe, warum wir sie "auffüllen" müssen, aber warum ist "Packen" (durch pack_padded_sequence
) notwendig?
Jede Erklärung auf hoher Ebene wäre willkommen!
Antworten:
Ich bin auch auf dieses Problem gestoßen und unten habe ich herausgefunden.
Beim Training von RNN (LSTM oder GRU oder Vanille-RNN) ist es schwierig, die Sequenzen variabler Länge zu stapeln. Beispiel: Wenn die Länge der Sequenzen in einem Stapel der Größe 8 [4,6,8,5,4,3,7,8] beträgt, werden alle Sequenzen aufgefüllt, was zu 8 Sequenzen der Länge 8 führt würde am Ende 64 Berechnungen (8x8) durchführen, aber Sie mussten nur 45 Berechnungen durchführen. Wenn Sie etwas Besonderes wie die Verwendung eines bidirektionalen RNN ausführen möchten, ist es außerdem schwieriger, Stapelberechnungen nur durch Auffüllen durchzuführen, und Sie führen möglicherweise mehr Berechnungen als erforderlich durch.
Stattdessen können wir mit PyTorch die Sequenz packen. Die intern gepackte Sequenz ist ein Tupel aus zwei Listen. Eine enthält die Elemente von Sequenzen. Elemente werden durch Zeitschritte verschachtelt (siehe Beispiel unten) und andere enthalten die
Größe jeder Sequenz unddie Stapelgröße bei jedem Schritt. Dies ist hilfreich, um die tatsächlichen Sequenzen wiederherzustellen und RNN bei jedem Zeitschritt die Stapelgröße mitzuteilen. Dies wurde von @Aerin gezeigt. Dies kann an RNN übergeben werden und optimiert intern die Berechnungen.Ich war an einigen Stellen möglicherweise unklar. Lassen Sie es mich wissen und ich kann weitere Erklärungen hinzufügen.
Hier ist ein Codebeispiel:
quelle
Hier sind einige visuelle Erklärungen 1 , die helfen könnten, eine bessere Intuition für die Funktionalität von zu entwickeln
pack_padded_sequence()
Nehmen wir an, wir haben insgesamt
6
Sequenzen (variabler Länge). Sie können diese Zahl auch6
alsbatch_size
Hyperparameter betrachten. (Diebatch_size
variieren je nach Länge der Sequenz (vgl. Abb. 2 unten))Nun möchten wir diese Sequenzen an einige wiederkehrende neuronale Netzwerkarchitekturen übergeben. Dazu müssen wir alle Sequenzen (normalerweise mit
0
s) in unserem Stapel auf die maximale Sequenzlänge in unserem Stapel (max(sequence_lengths)
) auffüllen , wie in der folgenden Abbildung dargestellt9
.Die Datenvorbereitungsarbeiten sollten also jetzt abgeschlossen sein, oder? Nicht wirklich. Weil es immer noch ein dringendes Problem gibt, hauptsächlich in Bezug darauf, wie viel Rechenaufwand wir im Vergleich zu den tatsächlich erforderlichen Berechnungen leisten müssen.
Nehmen wir zum besseren Verständnis auch an, dass wir die obige
padded_batch_of_sequences
Form(6, 9)
mit einer GewichtsmatrixW
der Form multiplizieren(9, 3)
.Daher müssen wir
6x9 = 54
Multiplikations- und6x8 = 48
Additionsoperationen (nrows x (n-1)_cols
) ausführen, um nur die meisten berechneten Ergebnisse wegzuwerfen, da es sich um0
s handelt (wo wir Pads haben). Die tatsächlich erforderliche Berechnung in diesem Fall lautet wie folgt:Das ist viel mehr Einsparungen, selbst für dieses sehr einfache ( Spielzeug- ) Beispiel. Sie können sich jetzt vorstellen, wie viel Rechenaufwand (letztendlich: Kosten, Energie, Zeit, Kohlenstoffemission usw.)
pack_padded_sequence()
für große Tensoren mit Millionen von Einträgen und über Millionen von Systemen auf der ganzen Welt eingespart werden kann, die dies immer wieder tun.Die Funktionalität von
pack_padded_sequence()
kann anhand der folgenden Abbildung anhand der verwendeten Farbcodierung verstanden werden:Als Ergebnis der Verwendung erhalten
pack_padded_sequence()
wir ein Tupel von Tensoren, die (i) die abgeflachten (entlang der Achse 1 in der obigen Abbildung)sequences
, (ii) die entsprechenden Chargengrößentensor([6,6,5,4,3,3,2,2,1])
für das obige Beispiel enthalten.Der Datentensor (dh die abgeflachten Sequenzen) könnte dann zur Verlustberechnung an objektive Funktionen wie CrossEntropy übergeben werden.
1 Bildnachweis an @sgrvinod
quelle
Die obigen Antworten haben die Frage warum sehr gut beantwortet . Ich möchte nur ein Beispiel hinzufügen, um die Verwendung von besser zu verstehen
pack_padded_sequence
.Nehmen wir ein Beispiel
Zuerst erstellen wir einen Stapel von 2 Sequenzen mit unterschiedlichen Sequenzlängen wie unten. Wir haben insgesamt 7 Elemente in der Charge.
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]
Wir füllen
seq_batch
auf, um den Stapel von Sequenzen mit der gleichen Länge von 5 zu erhalten (die maximale Länge im Stapel). Jetzt hat die neue Charge insgesamt 10 Elemente.# 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]]]) """
Dann packen wir die
padded_seq_batch
. Es wird ein Tupel von zwei Tensoren zurückgegeben:batch_sizes
die zeigt, wie die Elemente durch die Schritte miteinander in Beziehung stehen.# 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])) """
Jetzt übergeben wir das Tupel
packed_seq_batch
an die wiederkehrenden Module in Pytorch, wie z. B. RNN, LSTM. Dies erfordert nur5 + 2=7
Berechnungen im wiederkehrenden Modul.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]) """
Vergleichen Sie diesen Aufwand mit dem Standard
In dem Standard - Weg, wir brauchen nur das weitergeben
padded_seq_batch
zulstm
Modul. Es sind jedoch 10 Berechnungen erforderlich. Es werden mehrere Berechnungen mehr für Füllelemente durchgeführt, die rechnerisch ineffizient wären .Beachten Sie, dass dies nicht zu ungenauen Darstellungen führt, sondern viel mehr Logik benötigt, um korrekte Darstellungen zu extrahieren.
Mal sehen, den Unterschied:
# 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 >)) """
Die obigen Ergebnisse zeigen , dass
hn
,cn
unterscheiden sich in zwei Wege , währendoutput
von zwei Möglichkeiten , auf unterschiedliche Werte für die Polsterelemente führen.quelle
Als Ergänzung zu Umangs Antwort fand ich dies wichtig zu beachten.
Das erste Element im zurückgegebenen Tupel von
pack_padded_sequence
ist ein Daten (Tensor) - Tensor, der eine gepackte Sequenz enthält. Das zweite Element ist ein Tensor von ganzen Zahlen, die Informationen über die Stapelgröße bei jedem Sequenzschritt enthalten.Was hier jedoch wichtig ist, ist, dass das zweite Element (Stapelgrößen) die Anzahl der Elemente bei jedem Sequenzschritt im Stapel darstellt, nicht die variierenden Sequenzlängen, an die übergeben wird
pack_padded_sequence
.Zum Beispiel würden gegebene Daten
abc
undx
die: class:PackedSequence
Datenaxbc
mit enthaltenbatch_sizes=[2,1,1]
.quelle
Ich habe die gepackte Pack-Sequenz wie folgt verwendet.
Dabei sind text_lengths die Länge der einzelnen Sequenzen, bevor das Auffüllen und die Sequenz nach abnehmender Reihenfolge der Länge innerhalb eines bestimmten Stapels sortiert werden.
Ein Beispiel finden Sie hier .
Und wir packen so, dass der RNN den unerwünschten aufgefüllten Index während der Verarbeitung der Sequenz nicht sieht, was die Gesamtleistung beeinträchtigen würde.
quelle