mengapa kita "mengemas" urutan di pytorch?

93

Saya mencoba untuk mereplikasi Bagaimana menggunakan pengepakan untuk input urutan variabel-panjang untuk rnn tapi saya rasa saya harus terlebih dahulu memahami mengapa kita perlu "mengemas" urutan.

Saya mengerti mengapa kita perlu "mengemas" mereka tetapi mengapa "pengepakan" (melalui pack_padded_sequence) perlu?

Penjelasan tingkat tinggi apa pun akan dihargai!

Aerin
sumber
semua pertanyaan tentang pengemasan di pytorch: diskusikan.pytorch.org/t/…
Charlie Parker

Jawaban:

88

Saya telah menemukan masalah ini juga dan di bawah ini adalah yang saya temukan.

Saat melatih RNN (LSTM atau GRU atau vanilla-RNN), sulit untuk menumpuk urutan panjang variabel. Misalnya: jika panjang urutan dalam kelompok ukuran 8 adalah [4,6,8,5,4,3,7,8], Anda akan memasukkan semua urutan dan itu akan menghasilkan 8 urutan panjang 8. Anda akan berakhir dengan melakukan 64 komputasi (8x8), tetapi Anda hanya perlu melakukan 45 komputasi. Selain itu, jika Anda ingin melakukan sesuatu yang mewah seperti menggunakan dua arah-RNN, akan lebih sulit untuk melakukan komputasi batch hanya dengan padding dan Anda mungkin akan melakukan lebih banyak komputasi daripada yang diperlukan.

Sebaliknya, PyTorch memungkinkan kita untuk mengemas urutan, urutan yang dikemas secara internal adalah tupel dari dua daftar. Salah satunya berisi elemen urutan. Elemen disisipkan oleh langkah waktu (lihat contoh di bawah) dan lainnya berisi ukuran setiap urutan ukuran batch di setiap langkah. Ini berguna untuk memulihkan urutan aktual serta memberi tahu RNN apa ukuran batch di setiap langkah waktu. Ini telah ditunjukkan oleh @Aerin. Ini dapat diteruskan ke RNN dan ini akan mengoptimalkan komputasi secara internal.

Saya mungkin kurang jelas di beberapa hal, jadi beri tahu saya dan saya dapat menambahkan lebih banyak penjelasan.

Berikut contoh kodenya:

 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
sumber
4
Bisakah Anda menjelaskan mengapa output dari contoh yang diberikan adalah PackedSequence (data = tensor ([1, 3, 2, 4, 3]), batch_sizes = tensor ([2, 2, 1]))?
pertapa652
3
Bagian data hanyalah semua tensor yang digabungkan sepanjang sumbu waktu. Batch_size sebenarnya adalah larik ukuran batch di setiap langkah waktu.
Umang Gupta
2
Batch_sizes = [2, 2, 1] masing-masing mewakili pengelompokan [1, 3] [2, 4] dan [3].
Chaitanya Shivade
@ChaitanyaShivade mengapa ukuran batch [2,2,1]? tidak bisakah [1,2,2]? apa logika dibaliknya?
Pemrogram anonim
1
Karena pada langkah t, Anda hanya dapat memproses vektor pada langkah t, jika Anda menyimpan vektor yang diurutkan sebagai [1,2,2], Anda mungkin meletakkan setiap input sebagai satu batch, tetapi itu tidak dapat diparalelkan dan karenanya tidak dapat ditumpuk
Umang Gupta
52

Berikut adalah beberapa penjelasan visual 1 yang mungkin membantu mengembangkan intuisi yang lebih baik untuk fungsionalitaspack_padded_sequence()

Mari kita asumsikan kita memiliki 6total urutan (panjang variabel). Anda juga dapat menganggap angka ini 6sebagai batch_sizehyperparameter. ( batch_sizeAkan bervariasi tergantung pada panjang urutan (lihat Gambar 2 di bawah))

Sekarang, kami ingin meneruskan urutan ini ke beberapa arsitektur jaringan neural berulang. Untuk melakukannya, kita harus memasukkan semua urutan (biasanya dengan 0s) dalam batch kita ke panjang urutan maksimum di batch kita ( max(sequence_lengths)), yang pada gambar di bawah ini adalah 9.

padded-seqs

Jadi, persiapan data harus sudah selesai sekarang, bukan? Tidak juga .. Karena masih ada satu masalah yang mendesak, terutama dalam hal berapa banyak komputasi yang harus kita lakukan jika dibandingkan dengan perhitungan yang sebenarnya diperlukan.

Demi pemahaman, mari kita asumsikan juga bahwa kita akan mengalikan matriks padded_batch_of_sequencesbentuk di atas (6, 9)dengan matriks bobot Wbentuk (9, 3).

Jadi, kita harus melakukan operasi 6x9 = 54perkalian dan 6x8 = 48penjumlahan                     ( nrows x (n-1)_cols), hanya untuk membuang sebagian besar hasil yang dihitung karena akan menjadi 0s (di mana kita memiliki bantalan). Penghitungan aktual yang diperlukan dalam kasus ini adalah sebagai berikut:

 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) 

Itu BANYAK penghematan lebih banyak bahkan untuk contoh ( mainan ) yang sangat sederhana ini . Anda sekarang dapat membayangkan berapa banyak komputasi (akhirnya: biaya, energi, waktu, emisi karbon, dll.) Yang dapat dihemat menggunakan pack_padded_sequence()tensor besar dengan jutaan entri, dan lebih dari jutaan sistem di seluruh dunia melakukannya, lagi dan lagi.

Fungsionalitas pack_padded_sequence()dapat dipahami dari gambar di bawah ini, dengan bantuan kode warna yang digunakan:

pack-padded-seqs

Sebagai hasil dari penggunaan pack_padded_sequence(), kita akan mendapatkan tupel tensor yang berisi (i) yang diratakan (sepanjang sumbu-1, pada gambar di atas) sequences, (ii) ukuran tumpukan yang sesuai, tensor([6,6,5,4,3,3,2,2,1])untuk contoh di atas.

Tensor data (yaitu urutan yang diratakan) kemudian dapat diteruskan ke fungsi objektif seperti CrossEntropy untuk perhitungan kerugian.


1 kredit gambar untuk @sgrvinod

kmario23
sumber
2
Diagram yang bagus!
David Waterworth
1
Sunting: Saya pikir stackoverflow.com/a/55805785/6167850 (di bawah) menjawab pertanyaan saya, yang akan saya tinggalkan di sini: ~ Apakah ini pada dasarnya berarti gradien tidak disebarkan ke input yang empuk? Bagaimana jika fungsi kerugian saya hanya dihitung pada keadaan / keluaran terakhir tersembunyi dari RNN? Haruskah keuntungan efisiensi dibuang? Atau akankah kerugian dihitung dari langkah sebelumnya tempat padding dimulai, yang berbeda untuk setiap elemen batch dalam contoh ini? ~
nlml
26

Jawaban di atas menjawab pertanyaan mengapa sangat baik. Saya hanya ingin menambahkan contoh untuk lebih memahami penggunaan pack_padded_sequence.

Mari kita ambil contoh

Catatan: pack_padded_sequencemembutuhkan urutan yang diurutkan dalam kelompok (dalam urutan panjang urutan yang menurun). Dalam contoh di bawah ini, sekuens batch sudah diurutkan untuk mengurangi kekacauan. Kunjungi tautan inti ini untuk implementasi penuh.

Pertama, kami membuat kumpulan 2 urutan dengan panjang urutan berbeda seperti di bawah ini. Kami memiliki 7 elemen dalam batch secara total.

  • Setiap urutan memiliki ukuran embedding 2.
  • Urutan pertama memiliki panjang: 5
  • Urutan kedua memiliki panjang: 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]

Kami pad seq_batchuntuk mendapatkan batch urutan dengan panjang yang sama yaitu 5 (Panjang maksimal dalam batch). Sekarang, kumpulan baru memiliki total 10 elemen.

# 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]]])
"""

Kemudian, kami mengemas padded_seq_batch. Ini mengembalikan tupel dari dua tensor:

  • Yang pertama adalah data termasuk semua elemen dalam urutan batch.
  • Yang kedua adalah batch_sizesyang akan memberi tahu bagaimana elemen-elemen tersebut terkait satu sama lain melalui langkah-langkahnya.
# 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]))
"""

Sekarang, kami meneruskan tupel packed_seq_batchke modul berulang di Pytorch, seperti RNN, LSTM. Ini hanya membutuhkan 5 + 2=7perhitungan dalam modul berulang.

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>)))
"""

Kita perlu mengonversi outputkembali ke kumpulan output yang empuk:

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])
"""

Bandingkan upaya ini dengan cara standar

  1. Dengan cara standar, kita hanya perlu meneruskan modul padded_seq_batchke lstm. Namun, itu membutuhkan 10 perhitungan. Ini melibatkan beberapa komputasi lebih lanjut pada elemen padding yang secara komputasi tidak efisien.

  2. Perhatikan bahwa ini tidak mengarah ke representasi yang tidak akurat , tetapi membutuhkan lebih banyak logika untuk mengekstrak representasi yang benar.

    • Untuk LSTM (atau modul berulang) dengan hanya arah maju, jika kita ingin mengekstrak vektor tersembunyi dari langkah terakhir sebagai representasi untuk suatu urutan, kita harus mengambil vektor tersembunyi dari langkah T (th), di mana T adalah panjang input. Mengambil representasi terakhir tidak akan benar. Perhatikan bahwa T akan berbeda untuk input yang berbeda dalam batch.
    • Untuk Bi-directional LSTM (atau modul berulang), ini bahkan lebih rumit, karena seseorang harus memelihara dua modul RNN, satu yang bekerja dengan padding di awal input dan satu dengan padding di akhir input, dan akhirnya mengekstrak dan menggabungkan vektor tersembunyi seperti dijelaskan di atas.

Mari kita lihat perbedaannya:

# 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 >))
"""

Hasil di atas menunjukkan bahwa hn, cnberbeda dalam dua hal sedangkan outputdari dua cara mengarah pada nilai yang berbeda untuk elemen padding.

David Ng
sumber
2
Jawaban bagus! Hanya koreksi jika Anda melakukan padding Anda tidak harus menggunakan h terakhir, bukan h pada indeks yang sama dengan panjang input. Juga, untuk melakukan RNN dua arah, Anda akan ingin menggunakan dua RNN yang berbeda --- satu dengan bantalan di depan dan satu lagi dengan bantalan di belakang untuk mendapatkan hasil yang benar. Mengisi dan memilih keluaran terakhir adalah "salah". Jadi argumen Anda bahwa itu mengarah pada representasi yang tidak akurat adalah salah. Masalah dengan padding adalah benar tetapi tidak efisien (jika ada opsi sekuens yang dikemas) dan dapat menjadi rumit (misalnya: bi-dir RNN)
Umang Gupta
18

Menambah jawaban Umang, saya menemukan ini penting untuk diperhatikan.

Item pertama dalam tupel yang dikembalikan pack_padded_sequenceadalah data (tensor) - tensor yang berisi urutan yang dikemas. Item kedua adalah tensor bilangan bulat yang menyimpan informasi tentang ukuran batch di setiap langkah urutan.

Yang penting di sini adalah item kedua (Ukuran batch) mewakili jumlah elemen di setiap langkah urutan dalam batch, bukan variasi panjang urutan yang diteruskan pack_padded_sequence.

Misalnya, data yang diberikan abcdan x : class: PackedSequenceakan berisi data axbcdengan batch_sizes=[2,1,1].

Aerin
sumber
1
Terima kasih, saya benar-benar lupa. dan membuat kesalahan dalam jawaban saya yang akan memperbaruinya. Namun, saya melihat urutan kedua karena beberapa data diperlukan untuk memulihkan urutan dan itulah mengapa mengacaukan deskripsi saya
Umang Gupta
2

Saya menggunakan urutan paket empuk sebagai berikut.

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

di mana text_lengths adalah panjang urutan individu sebelum padding dan urutan diurutkan sesuai dengan urutan penurunan panjang dalam batch tertentu.

Anda dapat melihat contohnya di sini .

Dan kami melakukan pengepakan sehingga RNN tidak melihat indeks padded yang tidak diinginkan saat memproses urutan yang akan mempengaruhi kinerja keseluruhan.

Jibin Mathew
sumber