なぜシーケンスをpytorchに「パック」するのですか?


98

rnnの可変長シーケンス入力にパッキングを使用する方法を複製しようとしましたが、最初にシーケンスを「パック」する必要がある理由を理解する必要があると思います。

なぜそれらを「パッド」する必要があるのか​​理解していますが、なぜ「パッキング」(スルーpack_padded_sequence)が必要なのですか?

高レベルの説明をいただければ幸いです。


pytorchでの梱包に関するすべての質問:discuss.pytorch.org/t/...
チャーリー・パーカー

回答:


91

私もこの問題に遭遇しました、そして以下は私が理解したものです。

RNN(LSTMまたはGRUまたはバニラ-RNN)をトレーニングする場合、可変長シーケンスをバッチ処理することは困難です。例:サイズ8のバッチのシーケンスの長さが[4,6,8,5,4,3,7,8]の場合、すべてのシーケンスをパディングすると、長さ8の8つのシーケンスになります。最終的に64回の計算(8x8)を実行しますが、必要な計算は45回だけです。さらに、双方向RNNを使用するような凝ったことをしたい場合、パディングだけでバッチ計算を行うのは難しく、必要以上の計算を行うことになります。

代わりに、PyTorchを使用すると、シーケンスをパックできます。内部でパックされたシーケンスは、2つのリストのタプルです。1つはシーケンスの要素を含みます。要素はタイムステップによってインターリーブされ(以下の例を参照)、その他には各シーケンスのサイズと各ステップのバッチサイズが含まれます。これは、実際のシーケンスを回復するだけでなく、各タイムステップでのバッチサイズをRNNに伝えるのに役立ちます。これは@Aerinによって指摘されています。これはRNNに渡すことができ、内部で計算を最適化します。

どこか不明だったかもしれませんので、お知らせください。さらに説明を追加させていただきます。

コード例は次のとおりです。

 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
与えられた例の出力がPackedSequence(data = tensor([1、3、2、4、3])、batch_sizes = tensor([2、2、1]))である理由を説明できますか?
ascetic652 2018年

3
データ部分は、時間軸に沿って連結されたすべてのテンソルです。Batch_sizeは、実際には各タイムステップでのバッチサイズの配列です。
ウマングプタ2018

3
batch_sizes = [2、2、1]は、それぞれグループ化[1、3] [2、4]および[3]を表します。
ChaitanyaShivade19年

@ChaitanyaShivadeなぜバッチサイズ[2,2,1]なのですか?[1,2,2]ではないですか?その背後にあるロジックは何ですか?
匿名プログラマー

1
ステップtで、あなたは[1,2,2]として注文ベクトルを続ける場合は、あなたはおそらく、バッチとして各入力をステップtにおけるプロセスのみベクトル入れていることができますが、それは並列化されていないので、バッチ可能することはできませんので
ウマングプタ

59

の機能についてより良い直感を開発するのに役立つかもしれないいくつかの視覚的な説明1がありますpack_padded_sequence()

6合計で(可変長の)シーケンスがあると仮定しましょう。この数値6batch_sizeハイパーパラメータと見なすこともできます。(batch_sizeシーケンスの長さによって異なります(下の図2を参照))

ここで、これらのシーケンスをいくつかのリカレントニューラルネットワークアーキテクチャに渡します。これを行うに0は、バッチ内のすべてのシーケンス(通常はsを使用)をバッチ内の最大シーケンス長(max(sequence_lengths))までパディングする必要があり9ます。これは下の図ではです。

padded-seqs

では、データ準備作業はこれで完了するはずですよね?実際にはそうではありません。実際に必要な計算と比較した場合、主にどれだけの計算を行う必要があるかという点で、まだ1つの差し迫った問題があるためです。

理解のために、上記padded_batch_of_sequencesの形状に形状(6, 9)の重み行列Wを行列乗算すると仮定し(9, 3)ます。

したがって、6x9 = 54乗算6x8 = 48加算                     (nrows x (n-1)_cols)演算を実行する必要がありますが、計算結果のほとんどは0s(パッドがある場合)になるため、破棄するだけです。この場合に実際に必要な計算は次のとおりです。

 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) 

これは、この非常に単純な(おもちゃの)例でも、はるかに多くの節約になります。これでpack_padded_sequence()、何百万ものエントリを持つ大きなテンソルと、世界中で何度も何度もそれを実行する数百万以上のシステムを使用して、どれだけの計算(最終的にはコスト、エネルギー、時間、炭素排出量など)を節約できるか想像できます。

の機能はpack_padded_sequence()、使用されている色分けの助けを借りて、次の図から理解できます。

pack-padded-seqs

を使用した結果、上記の例では、pack_padded_sequence()(i)平坦化された(上の図の軸1に沿って)sequences、(ii)対応するバッチサイズを含むテンソルのタプルが得られtensor([6,6,5,4,3,3,2,2,1])ます。

次に、データテンソル(つまり、平坦化されたシーケンス)を、損失計算のためにCrossEntropyなどの目的関数に渡すことができます。


@sgrvinodへの1つの画像クレジット



1
編集:stackoverflow.com/a/55805785/6167850(下記)が私の質問に答えると思いますが、とにかくここに残しておきます:〜これは本質的に、グラデーションがパッド付き入力に伝播されないことを意味しますか?損失関数がRNNの最終的な非表示状態/出力でのみ計算される場合はどうなりますか?その場合、効率の向上は捨てられなければなりませんか?または、パディングが開始される前のステップから損失が計算されますか?これは、この例のバッチ要素ごとに異なりますか?〜
nlml20年

28

上記の回答は、なぜ非常にうまくいくのという質問に対処しました。の使用法をよりよく理解するための例を追加したいと思いpack_padded_sequenceます。

例を見てみましょう

注:pack_padded_sequenceバッチ内で(シーケンス長の降順で)ソートされたシーケンスが必要です。以下の例では、シーケンスバッチは、煩雑さを軽減するためにすでに並べ替えられています。完全な実装については、この要点リンクにアクセスしてください。

まず、以下のようにシーケンス長の異なる2つのシーケンスのバッチを作成します。バッチには全部で7つの要素があります。

  • 各シーケンスの埋め込みサイズは2です。
  • 最初のシーケンスの長さは次のとおりです:5
  • 2番目のシーケンスの長さは次のとおりです: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]

seq_batch等しい長さの5(バッチの最大長)のシーケンスのバッチを取得するためにパディングします。現在、新しいバッチには合計10個の要素があります。

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

次に、をパックしpadded_seq_batchます。2つのテンソルのタプルを返します。

  • 1つ目は、シーケンスバッチ内のすべての要素を含むデータです。
  • 2つ目は、batch_sizes要素がステップによってどのように相互に関連しているかを示すものです。
# 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]))
"""

ここで、packed_seq_batchRNN、LSTMなどのPytorchのリカレントモジュールにタプルを渡します。これに5 + 2=7は、recurrrentモジュールでの計算のみが必要です。

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

output出力のパディングされたバッチに戻す必要があります。

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

この取り組みを標準的な方法と比較してください

  1. 標準的な方法では、padded_seq_batchtolstmモジュールを渡すだけで済みます。ただし、10回の計算が必要です。これには、計算効率が悪いパディング要素に関するいくつかの計算が含まれます。

  2. 不正確な表現にはなりませんが、正しい表現を抽出するには、さらに多くのロジックが必要になることに注意してください。

    • 順方向のみのLSTM(または任意の反復モジュール)の場合、シーケンスの表現として最後のステップの非表示ベクトルを抽出する場合は、T(th)ステップから非表示ベクトルを取得する必要があります。ここでT入力の長さです。最後の表現を拾うことは正しくありません。Tは、バッチ内の入力ごとに異なることに注意してください。
    • 双方向LSTM(または任意のリカレントモジュール)の場合、2つのRNNモジュールを維持する必要があるため、さらに面倒です。1つは入力の最初にパディングを使用し、もう1つは入力の最後にパディングを使用します。最後に、上で説明したように、非表示のベクトルを抽出して連結します。

違いを見てみましょう:

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

上記の結果はhncnが2つの方法で異なるのに対しoutput、2つの方法ではパディング要素の値が異なることを示しています。


2
いい答えだ!パディングを行う場合の単なる修正であり、入力の長さに等しいインデックスで最後のhではなくhを使用する必要があります。また、双方向RNNを実行するには、2つの異なるRNNを使用する必要があります。1つは前面にパディングがあり、もう1つは背面にパディングがあり、正しい結果が得られます。最後の出力のパディングとピッキングは「間違っています」。したがって、それが不正確な表現につながるというあなたの議論は間違っています。パディングの問題は、それは正しいが非効率的であり(パックされたシーケンスオプションがある場合)、面倒になる可能性があることです(例:bi-dir RNN)
UmangGupta19年

18

Umangの答えに加えて、私はこれに注意することが重要だと思いました。

返されるタプルの最初の項目は、pack_padded_sequenceパックされたシーケンスを含むデータ(テンソル)テンソルです。2番目の項目は、各シーケンスステップでのバッチサイズに関する情報を保持する整数のテンソルです。

ただし、ここで重要なのは、2番目の項目(バッチサイズ)が、に渡されるさまざまなシーケンスの長さではなく、バッチの各シーケンスステップでの要素の数を表すことpack_padded_sequenceです。

例えば、データ与えられた abcx :クラス:PackedSequenceデータを含んでいるでしょうaxbcbatch_sizes=[2,1,1]


1
おかげで、私はそれを完全に忘れました。それを更新しようとしている私の答えを間違えました。しかし、私は2番目のシーケンスを、シーケンスを回復するために必要ないくつかのデータと見なしました。そのため、私の説明を台無しにしました
UmangGupta18年

3

次のようにパックパッドシーケンスを使用しました。

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

ここで、text_lengthsは、パディング前の個々のシーケンスの長さであり、シーケンスは、指定されたバッチ内の長さの降順に従ってソートされます。

ここで例を確認できます

また、全体的なパフォーマンスに影響を与えるシーケンスの処理中に、RNNが不要な埋め込みインデックスを認識しないようにパッキングを行います。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.