neden dizileri pytorch ile "paketliyoruz"?


94

Rnn için değişken uzunluklu dizi girdileri için paketlemeyi nasıl kullanacağımı kopyalamaya çalışıyordum ama sanırım önce sırayı neden "paketlememiz" gerektiğini anlamam gerekiyor.

Onları neden "doldurmamız" gerektiğini anlıyorum ama "paketlemek" (doldurmak) neden pack_padded_sequencegerekli?

Herhangi bir üst düzey açıklama memnuniyetle karşılanacaktır!


pytorch ile paketlemeyle ilgili tüm sorular: tartış.pytorch.org/t/…
Charlie Parker

Yanıtlar:


89

Ben de bu soruna rastladım ve aşağıda anladığım şey var.

RNN (LSTM veya GRU veya vanilya-RNN) eğitimi alırken, değişken uzunluktaki dizileri gruplamak zordur. Örneğin: 8 numaralı bir partideki dizilerin uzunluğu [4,6,8,5,4,3,7,8] ise, tüm dizileri dolduracaksınız ve bu 8 uzunluk dizisi ile sonuçlanacaktır. 64 hesaplama (8x8) yapmakla sonuçlanırdı, ancak yalnızca 45 hesaplama yapmanız gerekiyordu. Dahası, çift yönlü bir RNN kullanmak gibi süslü bir şey yapmak istiyorsanız, toplu hesaplamaları sadece doldurarak yapmak daha zor olabilir ve sonunda gerekenden daha fazla hesaplama yapmanız gerekebilir.

Bunun yerine, PyTorch diziyi paketlememize izin verir, dahili olarak paketlenmiş dizi iki listeden oluşan bir demettir. Biri dizilerin unsurlarını içerir. Öğeler, zaman adımlarına göre serpiştirilir (aşağıdaki örneğe bakın) ve diğeri , her bir adımdaki parti boyutunu her dizinin boyutunu içerir . Bu, gerçek dizilerin kurtarılmasının yanı sıra her bir adımda parti boyutunun RNN'ye söylenmesinde yardımcı olur. Bu, @Aerin tarafından işaret edilmiştir. Bu, RNN'ye aktarılabilir ve hesaplamaları dahili olarak optimize eder.

Bazı noktalarda net olmamış olabilirim, bu yüzden bana bildirin ve daha fazla açıklama ekleyebilirim.

İşte bir kod örneği:

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

4
Verilen örneğin çıktısının neden PackedSequence (veri = tensör ([1, 3, 2, 4, 3]), batch_sizes = tensor ([2, 2, 1])) olduğunu açıklayabilir misiniz?
ascetic652

3
Veri bölümü, zaman ekseni boyunca birleştirilmiş tüm tensörlerdir. Batch_size, aslında her zaman adımındaki parti boyutları dizisidir.
Umang Gupta

2
Batch_sizes = [2, 2, 1], sırasıyla [1, 3] [2, 4] ve [3] gruplandırmasını temsil eder.
Chaitanya Shivade

@ChaitanyaShivade neden parti boyutu [2,2,1]? [1,2,2] olamaz mı? arkasındaki mantık nedir?
Anonim programcı

1
Çünkü t adımında, vektörleri yalnızca adım t'de işleyebilirsiniz, vektörleri [1,2,2] olarak sıralı tutarsanız, muhtemelen her girişi bir toplu iş olarak koyarsınız, ancak bu paralelleştirilemez ve dolayısıyla gruplandırılamaz
Umang Gupta

53

Burada , işlevsellik için daha iyi sezgi geliştirmeye yardımcı olabilecek bazı görsel açıklamalar 1pack_padded_sequence()

6Toplamda (değişken uzunluklarda) dizilerimiz olduğunu varsayalım . Bu sayıyı hiperparametre 6olarak da düşünebilirsiniz batch_size. (Bu batch_size, dizinin uzunluğuna bağlı olarak değişecektir (aşağıdaki Şekil 2 ile karşılaştırın))

Şimdi, bu dizileri bazı tekrarlayan sinir ağı mimarilerine aktarmak istiyoruz. Bunu yapmak için, 0grubumuzdaki tüm dizileri (tipik olarak s ile) max(sequence_lengths), aşağıdaki şekilde olduğu gibi, grubumuzdaki ( ) maksimum dizi uzunluğuna doldurmalıyız 9.

yastıklı diziler

Yani veri hazırlama işi şimdiye kadar tamamlanmış olmalı, değil mi? Pek değil .. Çünkü aslında gerekli hesaplamalara kıyasla ne kadar hesaplama yapmamız gerektiğiyle ilgili hala bir acil sorun var.

Anlayış uğruna, en ayrıca çarpma yukarıda matris edeceğini varsayalım padded_batch_of_sequencesşeklin (6, 9)bir ağırlık matrisi ile Wşeklin (9, 3).

Bu nedenle, 6x9 = 54çarpma ve 6x8 = 48toplama                     ( nrows x (n-1)_cols) işlemlerini gerçekleştirmemiz gerekecek , yalnızca hesaplanan sonuçların çoğunu atmamız gerekecek çünkü bunlar 0s (pedlerimiz olduğu yerde) olacaktır. Bu durumda gerekli olan gerçek hesaplama aşağıdaki gibidir:

 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) 

Bu çok basit ( oyuncak ) örnek için bile çok fazla tasarruf . Artık pack_padded_sequence()milyonlarca giriş içeren büyük tensörler ve bunu tekrar tekrar yapan tüm dünyada milyondan fazla sistem kullanılarak ne kadar hesaplama (sonunda: maliyet, enerji, zaman, karbon emisyonu vb.) Tasarruf edilebileceğini hayal edebilirsiniz .

pack_padded_sequence()Kullanılan renk kodlaması yardımıyla aşağıdaki şekilden işlevselliği anlaşılabilir:

pack-padded-seqs

Kullanmanın bir sonucu olarak , yukarıdaki örnek için pack_padded_sequence()(i) düzleştirilmiş (yukarıdaki şekilde eksen-1 boyunca) sequences, (ii) karşılık gelen parti boyutlarını içeren bir tensör demeti elde edeceğiz tensor([6,6,5,4,3,3,2,2,1]).

Veri tensörü (yani düzleştirilmiş diziler) daha sonra kayıp hesaplamaları için CrossEntropy gibi nesnel işlevlere geçirilebilir.


@Sgrvinod için 1 resim kredisi


2
Mükemmel diyagramlar!
David Waterworth

1
Düzenleme: Sanırım stackoverflow.com/a/55805785/6167850 (aşağıda) sorumu yanıtlıyor, ki bunu yine de burada bırakacağım: ~ Bu, temelde gradyanların doldurulmuş girişlere yayılmadığı anlamına mı geliyor? Ya kayıp fonksiyonum yalnızca RNN'nin son gizli durumu / çıktısında hesaplanırsa? O zaman verimlilik kazanımları bir kenara atılmalı mı? Yoksa kayıp, bu örnekteki her parti öğesi için farklı olan doldurmanın başladığı adımdan önceki adımdan mı hesaplanacak? ~
nlml

26

Yukarıdaki cevaplar neden çok iyi sorusuna yanıt verdi . Sadece kullanımını daha iyi anlamak için bir örnek eklemek istiyorum pack_padded_sequence.

Bir örnek alalım

Not: pack_padded_sequencetoplu işlemde sıralı diziler gerektirir (sıra uzunluklarının azalan sırasına göre). Aşağıdaki örnekte, daha az dağınıklık için sekans grubu zaten sıralanmıştır. Tam uygulama için bu temel bağlantıyı ziyaret edin .

İlk olarak, aşağıdaki gibi farklı dizi uzunluklarına sahip 2 diziden oluşan bir grup oluşturuyoruz. Toplamda 7 elementimiz var.

  • Her dizinin gömme boyutu 2'dir.
  • İlk sıranın uzunluğu: 5
  • İkinci sıranın uzunluğu: 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]

Bu ped seq_batch5 eşit uzunlukta (toplu olarak maksimum uzunluk) sahip dizilerin toplu alır. Şimdi, yeni partide toplam 10 eleman var.

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

Ardından padded_seq_batch,. İki tensörden oluşan bir demet döndürür:

  • Birincisi, dizi grubundaki tüm öğeleri içeren verilerdir.
  • İkincisi, batch_sizesadımlarla öğelerin birbirleriyle nasıl ilişkili olduğunu söyleyecektir.
# 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]))
"""

Şimdi packed_seq_batchtuple'ı Pytorch'taki RNN, LSTM gibi tekrarlayan modüllere geçiriyoruz. Bu sadece 5 + 2=7recurrrent modülünde hesaplamalar gerektirir .

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

outputYastıklı çıktı kümesine geri dönmemiz gerekiyor :

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

Bu çabayı standart yöntemle karşılaştırın

  1. Standart şekilde, sadece geçmesi gerekiyor padded_seq_batchiçin lstmmodül. Ancak, 10 hesaplama gerektirir. Hesaplama açısından verimsiz olan dolgu öğelerinde daha fazla sayıda hesaplama içerir .

  2. Yanlış temsillere yol açmadığını , ancak doğru temsilleri çıkarmak için çok daha fazla mantığa ihtiyaç duyduğunu unutmayın.

    • Yalnızca ileri yönlü LSTM (veya herhangi bir tekrarlayan modül) için, son adımın gizli vektörünü bir dizinin temsili olarak çıkarmak istersek, T (th) adımından gizli vektörleri almamız gerekir; burada T girişin uzunluğudur. Son temsili almak yanlış olacaktır. Toplu işteki farklı girişler için T'nin farklı olacağını unutmayın.
    • İki yönlü LSTM (veya herhangi bir tekrarlayan modül) için, biri girişin başında dolgu ile ve diğeri girişin sonunda dolgu ile çalışan iki RNN modülünü sürdürmek zorunda kalacağı için daha da zahmetlidir ve son olarak yukarıda açıklandığı gibi gizli vektörlerin çıkarılması ve birleştirilmesi.

Farkı görelim:

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

Yukarıdaki sonuçlar hn, cniki yönden farklı olduğunu , iki yoldan ise outputdolgu öğeleri için farklı değerlere yol açtığını göstermektedir.


2
Güzel cevap! Sadece bir düzeltme, eğer doldurma yaparsanız, giriş uzunluğuna eşit dizinde son h yerine h'yi kullanmamalısınız. Ayrıca, çift yönlü RNN yapmak için, iki farklı RNN kullanmak isteyeceksiniz - doğru sonuçlar elde etmek için biri önde, diğeri arkada dolgu ile. Son çıktıyı doldurma ve toplama "yanlış". Yani bunun yanlış temsile yol açtığına dair argümanlarınız yanlış. Dolgu ile ilgili sorun doğru ancak verimsiz olması (eğer paketlenmiş diziler seçeneği varsa) ve külfetli olabilir (örneğin: bi-dir RNN)
Umang Gupta

18

Umang'ın cevabına ek olarak, bunu not etmeyi önemli buldum.

Döndürülen demetindeki ilk öğe pack_padded_sequencebir veri (tensör) - paketlenmiş dizi içeren tensördür. İkinci öğe, her bir dizi adımında parti boyutu hakkında bilgi tutan tam sayıların bir tensörüdür.

Burada önemli olan, ikinci öğenin (Parti boyutları), aktarılan değişen dizi uzunluklarını değil, partideki her dizi adımındaki öğelerin sayısını temsil etmesidir pack_padded_sequence.

Örneğin, verilen veriler abcve x : class:, ile PackedSequenceverileri içerir .axbcbatch_sizes=[2,1,1]


1
Teşekkürler, bunu tamamen unutmuşum. ve cevabımda bir hata yaptım ve bunu güncelleyeceğim. Bununla birlikte, sekansları kurtarmak için bazı veriler gerekli olduğundan ikinci sekansa baktım ve bu yüzden açıklamamı karıştırdı
Umang Gupta

2

Aşağıdaki gibi paket yastıklı sıralamayı kullandım.

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

burada metin_uzunlukları, doldurma işleminden önceki bireysel dizinin uzunluğudur ve dizi, belirli bir toplu iş içindeki azalan uzunluk sırasına göre sıralanır.

buradan bir örneğe bakabilirsiniz .

RNN'nin genel performansı etkileyecek diziyi işlerken istenmeyen dolgulu indeksi görmemesi için paketleme yapıyoruz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.