rnnの可変長シーケンス入力にパッキングを使用する方法を複製しようとしましたが、最初にシーケンスを「パック」する必要がある理由を理解する必要があると思います。
なぜそれらを「パッド」する必要があるのか理解していますが、なぜ「パッキング」(スルーpack_padded_sequence
)が必要なのですか?
高レベルの説明をいただければ幸いです。
rnnの可変長シーケンス入力にパッキングを使用する方法を複製しようとしましたが、最初にシーケンスを「パック」する必要がある理由を理解する必要があると思います。
なぜそれらを「パッド」する必要があるのか理解していますが、なぜ「パッキング」(スルーpack_padded_sequence
)が必要なのですか?
高レベルの説明をいただければ幸いです。
回答:
私もこの問題に遭遇しました、そして以下は私が理解したものです。
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]))
の機能についてより良い直感を開発するのに役立つかもしれないいくつかの視覚的な説明1がありますpack_padded_sequence()
6
合計で(可変長の)シーケンスがあると仮定しましょう。この数値6
をbatch_size
ハイパーパラメータと見なすこともできます。(batch_size
シーケンスの長さによって異なります(下の図2を参照))
ここで、これらのシーケンスをいくつかのリカレントニューラルネットワークアーキテクチャに渡します。これを行うに0
は、バッチ内のすべてのシーケンス(通常はsを使用)をバッチ内の最大シーケンス長(max(sequence_lengths)
)までパディングする必要があり9
ます。これは下の図ではです。
では、データ準備作業はこれで完了するはずですよね?実際にはそうではありません。実際に必要な計算と比較した場合、主にどれだけの計算を行う必要があるかという点で、まだ1つの差し迫った問題があるためです。
理解のために、上記padded_batch_of_sequences
の形状に形状(6, 9)
の重み行列W
を行列乗算すると仮定し(9, 3)
ます。
したがって、6x9 = 54
乗算と6x8 = 48
加算
(nrows x (n-1)_cols
)演算を実行する必要がありますが、計算結果のほとんどは0
s(パッドがある場合)になるため、破棄するだけです。この場合に実際に必要な計算は次のとおりです。
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_sequence()
(i)平坦化された(上の図の軸1に沿って)sequences
、(ii)対応するバッチサイズを含むテンソルのタプルが得られtensor([6,6,5,4,3,3,2,2,1])
ます。
次に、データテンソル(つまり、平坦化されたシーケンス)を、損失計算のためにCrossEntropyなどの目的関数に渡すことができます。
@sgrvinodへの1つの画像クレジット
上記の回答は、なぜ非常にうまくいくのかという質問に対処しました。の使用法をよりよく理解するための例を追加したいと思いpack_padded_sequence
ます。
注:
pack_padded_sequence
バッチ内で(シーケンス長の降順で)ソートされたシーケンスが必要です。以下の例では、シーケンスバッチは、煩雑さを軽減するためにすでに並べ替えられています。完全な実装については、この要点リンクにアクセスしてください。
まず、以下のようにシーケンス長の異なる2つのシーケンスのバッチを作成します。バッチには全部で7つの要素があります。
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つのテンソルのタプルを返します。
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のリカレントモジュールにタプルを渡します。これに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])
"""
標準的な方法では、padded_seq_batch
tolstm
モジュールを渡すだけで済みます。ただし、10回の計算が必要です。これには、計算効率が悪いパディング要素に関するいくつかの計算が含まれます。
不正確な表現にはなりませんが、正しい表現を抽出するには、さらに多くのロジックが必要になることに注意してください。
違いを見てみましょう:
# 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 >))
"""
上記の結果はhn
、cn
が2つの方法で異なるのに対しoutput
、2つの方法ではパディング要素の値が異なることを示しています。
Umangの答えに加えて、私はこれに注意することが重要だと思いました。
返されるタプルの最初の項目は、pack_padded_sequence
パックされたシーケンスを含むデータ(テンソル)テンソルです。2番目の項目は、各シーケンスステップでのバッチサイズに関する情報を保持する整数のテンソルです。
ただし、ここで重要なのは、2番目の項目(バッチサイズ)が、に渡されるさまざまなシーケンスの長さではなく、バッチの各シーケンスステップでの要素の数を表すことpack_padded_sequence
です。
例えば、データ与えられた abc
とx
:クラス:PackedSequence
データを含んでいるでしょうaxbc
し
batch_sizes=[2,1,1]
。
次のようにパックパッドシーケンスを使用しました。
packed_embedded = nn.utils.rnn.pack_padded_sequence(seq, text_lengths)
packed_output, hidden = self.rnn(packed_embedded)
ここで、text_lengthsは、パディング前の個々のシーケンスの長さであり、シーケンスは、指定されたバッチ内の長さの降順に従ってソートされます。
また、全体的なパフォーマンスに影響を与えるシーケンスの処理中に、RNNが不要な埋め込みインデックスを認識しないようにパッキングを行います。