Warum „packen“ wir die Sequenzen in Pytorch?

98

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!

Aerin
quelle
Alle Fragen zum Verpacken in Pytorch: diskutieren.pytorch.org/t/…
Charlie Parker

Antworten:

91

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 und die 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:

 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
quelle
4
Können Sie erklären, warum die Ausgabe des angegebenen Beispiels PackedSequence (Daten = Tensor ([1, 3, 2, 4, 3]), batch_sizes = Tensor ([2, 2, 1]) ist?
Asket652
3
Der Datenteil besteht nur aus allen Tensoren, die entlang der Zeitachse verkettet sind. Batch_size ist das Array der Stapelgrößen zu jedem Zeitschritt.
Umang Gupta
3
Die batch_sizes = [2, 2, 1] stehen für die Gruppierung [1, 3] [2, 4] bzw. [3].
Chaitanya Shivade
@ChaitanyaShivade warum ist die Chargengröße [2,2,1]? kann es nicht [1,2,2] sein? Was ist die Logik dahinter?
Anonymer Programmierer
1
Da Sie in Schritt t nur Vektoren in Schritt t verarbeiten können, wenn Sie die Vektoren als [1,2,2] geordnet halten, setzen Sie wahrscheinlich jede Eingabe als Stapel, aber das kann nicht parallelisiert und daher nicht stapelbar sein
Umang Gupta
58

Hier sind einige visuelle Erklärungen 1 , die helfen könnten, eine bessere Intuition für die Funktionalität von zu entwickelnpack_padded_sequence()

Nehmen wir an, wir haben insgesamt 6Sequenzen (variabler Länge). Sie können diese Zahl auch 6als batch_sizeHyperparameter betrachten. (Die batch_sizevariieren 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 0s) in unserem Stapel auf die maximale Sequenzlänge in unserem Stapel ( max(sequence_lengths)) auffüllen , wie in der folgenden Abbildung dargestellt 9.

gepolsterte seqs

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_sequencesForm (6, 9)mit einer Gewichtsmatrix Wder Form multiplizieren (9, 3).

Daher müssen wir 6x9 = 54Multiplikations- und 6x8 = 48Additionsoperationen                     ( nrows x (n-1)_cols) ausführen, um nur die meisten berechneten Ergebnisse wegzuwerfen, da es sich um 0s handelt (wo wir Pads haben). Die tatsächlich erforderliche Berechnung in diesem Fall lautet wie folgt:

 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) 

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:

Pack-Padded-Seqs

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ößen tensor([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

kmario23
quelle
2
Hervorragende Diagramme!
David Waterworth
1
Bearbeiten: Ich denke, stackoverflow.com/a/55805785/6167850 (unten) beantwortet meine Frage, die ich hier trotzdem belassen werde: ~ Bedeutet dies im Wesentlichen, dass die Farbverläufe nicht an die gepolsterten Eingaben weitergegeben werden? Was ist, wenn meine Verlustfunktion nur für den endgültigen verborgenen Zustand / Ausgang des RNN berechnet wird? Müssen die Effizienzgewinne dann weggeworfen werden? Oder wird der Verlust aus dem Schritt vor dem Beginn des Auffüllens berechnet, der für jedes
Stapelelement
27

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

Hinweis: pack_padded_sequenceErfordert sortierte Sequenzen im Stapel (in absteigender Reihenfolge der Sequenzlängen). Im folgenden Beispiel wurde der Sequenzstapel bereits nach weniger Unordnung sortiert. Besuchen Sie diesen Hauptlink für die vollständige Implementierung.

Zuerst erstellen wir einen Stapel von 2 Sequenzen mit unterschiedlichen Sequenzlängen wie unten. Wir haben insgesamt 7 Elemente in der Charge.

  • Jede Sequenz hat eine Einbettungsgröße von 2.
  • Die erste Sequenz hat die Länge: 5
  • Die zweite Sequenz hat die Länge: 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]

Wir füllen seq_batchauf, 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:

  • Das erste sind die Daten, die alle Elemente im Sequenzstapel enthalten.
  • Die zweite ist die, batch_sizesdie 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_batchan die wiederkehrenden Module in Pytorch, wie z. B. RNN, LSTM. Dies erfordert nur 5 + 2=7Berechnungen 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>)))
"""

Wir müssen outputzurück in den gepolsterten Stapel der Ausgabe konvertieren :

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

  1. In dem Standard - Weg, wir brauchen nur das weitergeben padded_seq_batchzu lstmModul. Es sind jedoch 10 Berechnungen erforderlich. Es werden mehrere Berechnungen mehr für Füllelemente durchgeführt, die rechnerisch ineffizient wären .

  2. Beachten Sie, dass dies nicht zu ungenauen Darstellungen führt, sondern viel mehr Logik benötigt, um korrekte Darstellungen zu extrahieren.

    • Wenn wir für LSTM (oder wiederkehrende Module) mit nur Vorwärtsrichtung den verborgenen Vektor des letzten Schritts als Darstellung für eine Sequenz extrahieren möchten, müssten wir verborgene Vektoren aus dem T (th) -Schritt aufnehmen, wobei T. ist die Länge der Eingabe. Das Aufnehmen der letzten Darstellung ist falsch. Beachten Sie, dass T für verschiedene Eingaben im Stapel unterschiedlich ist.
    • Für bidirektionales LSTM (oder wiederkehrende Module) ist es noch umständlicher, da zwei RNN-Module verwaltet werden müssten, eines mit Auffüllung am Anfang der Eingabe und eines mit Auffüllung am Ende der Eingabe und Schließlich werden die verborgenen Vektoren wie oben erläutert extrahiert und verkettet.

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, cnunterscheiden sich in zwei Wege , während outputvon zwei Möglichkeiten , auf unterschiedliche Werte für die Polsterelemente führen.

David Ng
quelle
2
Gute Antwort! Nur eine Korrektur, wenn Sie auffüllen, sollten Sie nicht das letzte h, sondern h am Index verwenden, der der Länge der Eingabe entspricht. Um eine bidirektionale RNN durchzuführen, sollten Sie zwei verschiedene RNN verwenden - eine mit Polsterung vorne und eine mit Polsterung hinten, um korrekte Ergebnisse zu erzielen. Das Auffüllen und Auswählen der letzten Ausgabe ist "falsch". Ihre Argumente, dass dies zu einer ungenauen Darstellung führt, sind also falsch. Das Problem beim Auffüllen ist, dass es korrekt, aber ineffizient ist (wenn die Option für gepackte Sequenzen vorhanden ist) und umständlich sein kann (zum Beispiel: Bidir RNN)
Umang Gupta
18

Als Ergänzung zu Umangs Antwort fand ich dies wichtig zu beachten.

Das erste Element im zurückgegebenen Tupel von pack_padded_sequenceist 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 abcund x die: class: PackedSequenceDaten axbcmit enthalten batch_sizes=[2,1,1].

Aerin
quelle
1
Danke, das habe ich total vergessen. und habe einen Fehler in meiner Antwort gemacht, um das zu aktualisieren. Ich betrachtete die zweite Sequenz jedoch als einige Daten, die zur Wiederherstellung der Sequenzen erforderlich sind, und deshalb habe ich meine Beschreibung durcheinander gebracht
Umang Gupta
3

Ich habe die gepackte Pack-Sequenz wie folgt verwendet.

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

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.

Jibin Mathew
quelle