web-dev-qa-db-ja.com

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

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

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

高度な説明をいただければ幸いです!

45
Aerin

私もこの問題に出くわしましたが、以下が私が見つけたものです。

RNN(LSTMまたはGRUまたはVanilla-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]))
42
Umang Gupta

Umangの答えに加えて、これは重要なことに気付きました。

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

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

たとえば、データabcxを指定すると、:class:PackedSequenceにはbatch_sizes=[2,1,1]のデータaxbcが含まれます。

11
Aerin

上記の回答は、質問whyに非常によく対処しました。 pack_padded_sequenceの使用法をよりよく理解するための例を追加したいだけです。

例を見てみましょう

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

最初に、以下のように異なるシーケンス長の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_batchをRNN、LSTMなどのPytorchのリカレントモジュールに渡します。これには、recurrrentモジュールでの5 + 2=7計算のみが必要です。

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_batchlstmモジュールに渡すだけです。ただし、10回の計算が必要です。 computationally非効率的であるパディング要素に関する複数の計算が含まれます。

  2. inaccurate表現につながることはありませんが、正しい表現を抽出するためにはさらに多くのロジックが必要です。

    • 順方向のみのLSTM(または任意の反復モジュール)の場合、最後のステップの隠しベクトルをシーケンスの表現として抽出する場合、T(th)から隠しベクトルを選択する必要があります。ステップ。ここで、Tは入力の長さです。最後の表現を選択するのは正しくありません。 Tはバッチの入力ごとに異なることに注意してください。
    • 双方向LSTM(または任意の反復モジュール)の場合、2つのRNNモジュールを維持する必要があるため、さらに扱いにくくなります。最後に、上記で説明したように非表示のベクトルを抽出して連結します。

違いを見てみましょう:

# 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つの方法で異なるため、パディング要素の値が異なることを示しています。

7
David Ng

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

合計で(可変長の)6シーケンスがあると仮定します。この番号6batch_sizeハイパーパラメーターと見なすこともできます。

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

padded-seqs

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

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

したがって、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

これは、このおもちゃの例でもさらに節約できます。これで、数百万のエントリを持つ大きなテンソルに対してpack_padded_sequence()を使用してどれだけの計算(コスト、エネルギー、時間、炭素排出量など)を節約できるか想像できます。

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

pack-padded-seqs

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

データテンソル(フラット化されたシーケンス)は、損失計算のためにCrossEntropyなどの目的関数に渡すことができます。


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

4
kmario23

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

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

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

here を確認できます。

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

1
Jibin Mathew

他の回答に追加:ここに、シーケンスパッキングの概念を理解するのに非常に適した詳細な最小コード例を示します。 https://github.com/HarshTrivedi/packing-unpacking-pytorch-minimal-tutorial/tree/master

1
MicPie