Próbowałem powtórzyć Jak używać pakowania dla danych wejściowych sekwencji o zmiennej długości dla rnn, ale myślę, że najpierw muszę zrozumieć, dlaczego musimy „spakować” sekwencję.
Rozumiem, dlaczego musimy je „wypełniać”, ale dlaczego konieczne jest „pakowanie” (przez pack_padded_sequence
)?
Wszelkie wyjaśnienia na wysokim poziomie będą mile widziane!
Odpowiedzi:
Natknąłem się również na ten problem i poniżej jest to, co odkryłem.
Podczas szkolenia RNN (LSTM lub GRU lub vanilla-RNN) trudno jest grupować sekwencje o zmiennej długości. Na przykład: jeśli długość sekwencji w partii o rozmiarze 8 wynosi [4,6,8,5,4,3,7,8], wypełnisz wszystkie sekwencje, co da 8 sekwencji o długości 8. Możesz skończyłoby się na wykonaniu 64 obliczeń (8x8), ale trzeba było wykonać tylko 45 obliczeń. Co więcej, jeśli chciałbyś zrobić coś wymyślnego, na przykład użycie dwukierunkowego RNN, trudniej byłoby wykonać obliczenia wsadowe tylko przez wypełnienie i możesz w końcu wykonać więcej obliczeń niż jest to wymagane.
Zamiast tego PyTorch pozwala nam spakować sekwencję, wewnętrznie spakowana sekwencja jest krotką dwóch list. Jedna zawiera elementy sekwencji. Elementy są przeplatane krokami czasowymi (patrz przykład poniżej), a inne zawierają
rozmiar każdej sekwencji,rozmiar wsadu na każdym etapie. Jest to pomocne w odtwarzaniu rzeczywistych sekwencji, a także w informowaniu RNN o wielkości partii na każdym etapie czasowym. Wskazał na to @Aerin. Można to przekazać do RNN i wewnętrznie zoptymalizuje obliczenia.W niektórych momentach mogłem być niejasny, więc daj mi znać, a będę mógł dodać więcej wyjaśnień.
Oto przykład kodu:
źródło
Oto kilka wizualnych wyjaśnień 1, które mogą pomóc w opracowaniu lepszej intuicji w zakresie działania
pack_padded_sequence()
Załóżmy, że mamy
6
w sumie sekwencje (o zmiennej długości). Możesz również uznać tę liczbę6
zabatch_size
hiperparametr.Teraz chcemy przekazać te sekwencje do niektórych powtarzających się architektur sieci neuronowych. Aby to zrobić, musimy dopełnić wszystkie sekwencje (zwykle
0
s) w naszej partii do maksymalnej długości sekwencji w naszej batch (max(sequence_lengths)
), która na poniższym rysunku wynosi9
.Zatem prace nad przygotowaniem danych powinny być już zakończone, prawda? Niezupełnie ... Ponieważ wciąż istnieje jeden palący problem, głównie w kwestii tego, ile obliczeń musimy wykonać w porównaniu z faktycznie wymaganymi obliczeniami.
W trosce o zrozumienie, przyjmijmy, że będziemy matrix pomnożyć powyższe
padded_batch_of_sequences
kształtu(6, 9)
z matrycą wagiW
kształtu(9, 3)
.W związku z tym będziemy musieli wykonywać operacje
6x9 = 54
mnożenia i6x8 = 48
dodawania (nrows x (n-1)_cols
), tylko po to, aby wyrzucić większość obliczonych wyników, ponieważ byłyby one0
s (gdzie mamy pola ). Rzeczywiste wymagane obliczenia w tym przypadku są następujące:To DUŻO więcej oszczędności, nawet jak na ten bardzo prosty ( zabawkowy ) przykład. Możesz sobie teraz wyobrazić, ile mocy obliczeniowej (ostatecznie: koszt, energia, czas, emisja dwutlenku węgla itp.) Można zaoszczędzić, używając
pack_padded_sequence()
dużych tensorów z milionami wpisów i milionów systemów na całym świecie, które robią to wielokrotnie.Funkcjonalność
pack_padded_sequence()
można zrozumieć na poniższym rysunku, za pomocą zastosowanego kodowania kolorami:W wyniku użycia
pack_padded_sequence()
otrzymamy krotkę tensorów zawierającą (i) spłaszczone (wzdłuż osi-1, na powyższym rysunku)sequences
, (ii) odpowiednie rozmiary partii,tensor([6,6,5,4,3,3,2,2,1])
dla powyższego przykładu.Tensor danych (tj. Spłaszczone sekwencje) można następnie przekazać do funkcji celu, takich jak CrossEntropy, w celu obliczenia strat.
1 kredyty obrazkowe dla @sgrvinod
źródło
Powyższe odpowiedzi odpowiadały na pytanie, dlaczego bardzo dobrze. Chcę tylko dodać przykład, aby lepiej zrozumieć użycie
pack_padded_sequence
.Weźmy przykład
Najpierw tworzymy partię 2 sekwencji o różnych długościach sekwencji, jak poniżej. Mamy w zestawie łącznie 7 elementów.
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]
Podkładamy,
seq_batch
aby uzyskać partię sekwencji o równej długości 5 (maksymalna długość w partii). Teraz nowa partia ma w sumie 10 elementów.# 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]]]) """
Następnie pakujemy
padded_seq_batch
. Zwraca krotkę dwóch tensorów:batch_sizes
który powie, w jaki sposób elementy są ze sobą powiązane za pomocą kroków.# 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])) """
Teraz przekazujemy krotkę
packed_seq_batch
do powtarzających się modułów w Pytorch, takich jak RNN, LSTM. Wymaga to jedynie5 + 2=7
obliczeń w module rekurencyjnym.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]) """
Porównaj ten wysiłek ze standardowym sposobem
W standardowy sposób musimy tylko przekazać moduł
padded_seq_batch
dolstm
. Wymaga jednak 10 obliczeń. Obejmuje kilka obliczeń więcej na elementach wypełniających, co byłoby nieefektywne obliczeniowo .Należy zauważyć, że nie prowadzi to do niedokładnych reprezentacji, ale wymaga znacznie więcej logiki, aby wyodrębnić prawidłowe reprezentacje.
Zobaczmy różnicę:
# 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 >)) """
Powyższe wyniki wskazują, że
hn
,cn
są różne w obu sposobów, podczas gdyoutput
z dwóch sposobów prowadzić do różnych wartości elementów wypełniających.źródło
Dodając do odpowiedzi Umanga, uznałem to za ważne.
Pierwsza pozycja w zwróconej krotce
pack_padded_sequence
to data (tensor) - tensor zawierający upakowany ciąg. Drugą pozycją jest tensor liczb całkowitych przechowujących informacje o wielkości partii w każdym kroku sekwencji.Ważna jest tutaj jednak druga pozycja (rozmiary partii) reprezentująca liczbę elementów w każdym kroku sekwencji w partii, a nie różne długości sekwencji przekazane do
pack_padded_sequence
.Na przykład podane dane
abc
ix
: class:PackedSequence
zawierałyby daneaxbc
z rozszerzeniembatch_sizes=[2,1,1]
.źródło
Użyłem sekwencji wyściełanej plecaka w następujący sposób.
gdzie długość_tekstu to długość pojedynczej sekwencji przed wypełnieniem, a sekwencja jest sortowana według malejącej kolejności długości w danej partii.
możesz sprawdzić przykład tutaj .
I robimy pakowanie, aby RNN nie widział niechcianego, wypełnionego indeksu podczas przetwarzania sekwencji, która wpłynęłaby na ogólną wydajność.
źródło