rnnの可変長シーケンス入力にパッキングを使用する方法 を複製しようとしていましたが、まずシーケンスを「パック」する必要がある理由を理解する必要があると思います。
なぜそれらを「パディング」する必要があるのか理解していますが、なぜ「パッキング」(pack_padded_sequence
を介して)が必要ですか?
高度な説明をいただければ幸いです!
私もこの問題に出くわしましたが、以下が私が見つけたものです。
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]))
Umangの答えに加えて、これは重要なことに気付きました。
pack_padded_sequence
の返されたタプルの最初の項目は、パックされたシーケンスを含むデータ(テンソル)テンソルです。 2番目の項目は、各シーケンスステップでのバッチサイズに関する情報を保持する整数のテンソルです。
ここで重要なのは、2番目の項目(バッチサイズ)は、pack_padded_sequence
に渡されるさまざまなシーケンスの長さではなく、バッチの各シーケンスステップでの要素の数を表すことです。
たとえば、データabc
とx
を指定すると、:class:PackedSequence
にはbatch_sizes=[2,1,1]
のデータaxbc
が含まれます。
上記の回答は、質問whyに非常によく対処しました。 pack_padded_sequence
の使用法をよりよく理解するための例を追加したいだけです。
注:
pack_padded_sequence
では、バッチ内でソートされたシーケンスが必要です(シーケンスの長さの降順)。以下の例では、乱雑さを軽減するためにシーケンスバッチは既にソートされています。完全な実装については、 このGistリンク にアクセスしてください。
最初に、以下のように異なるシーケンス長の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のリカレントモジュールに渡します。これには、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])
"""
標準的な方法では、padded_seq_batch
をlstm
モジュールに渡すだけです。ただし、10回の計算が必要です。 computationally非効率的であるパディング要素に関する複数の計算が含まれます。
inaccurate表現につながることはありませんが、正しい表現を抽出するためにはさらに多くのロジックが必要です。
違いを見てみましょう:
# 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つの方法で異なるため、パディング要素の値が異なることを示しています。
いくつかの視覚的な説明があります1 pack_padded_sequence()
の機能のより良い直観を開発するのに役立つかもしれません
合計で(可変長の)6
シーケンスがあると仮定します。この番号6
をbatch_size
ハイパーパラメーターと見なすこともできます。
次に、これらのシーケンスをいくつかのリカレントニューラルネットワークアーキテクチャに渡します。そのためには、バッチ内のすべてのシーケンス(通常は0
s)を、バッチ内の最大シーケンス長(max(sequence_lengths)
)にパディングする必要があります(下図では9
)。
それで、データの準備作業は今までに完了しているはずですよね?実際はそうではありません。実際に必要な計算と比較した場合、主にどれだけの計算を行う必要があるかという点で、まだ差し迫った問題が1つあります。
理解のために、形状padded_batch_of_sequences
の上記(6, 9)
に形状(9, 3)
の重み行列W
を行列乗算すると仮定します。
したがって、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
これは、このおもちゃの例でもさらに節約できます。これで、数百万のエントリを持つ大きなテンソルに対してpack_padded_sequence()
を使用してどれだけの計算(コスト、エネルギー、時間、炭素排出量など)を節約できるか想像できます。
pack_padded_sequence()
の機能は、使用されている色分けの助けを借りて、次の図から理解できます。
pack_padded_sequence()
を使用した結果、(i)平坦化された(上図のaxis-1に沿って)sequences
、(ii)対応するバッチサイズ、上記の例のtensor([6,6,5,4,3,3,2,2,1])
を含むテンソルのタプルを取得します。
データテンソル(フラット化されたシーケンス)は、損失計算のためにCrossEntropyなどの目的関数に渡すことができます。
1 @ sgrvinod への画像クレジット
次のようにパックパッドシーケンスを使用しました。
packed_embedded = nn.utils.rnn.pack_padded_sequence(seq, text_lengths)
packed_output, hidden = self.rnn(packed_embedded)
ここで、text_lengthsは、特定のバッチ内の長さの降順に従ってパディングとシーケンスがソートされる前の個々のシーケンスの長さです。
例 here を確認できます。
そして、全体的なパフォーマンスに影響するシーケンスの処理中に、RNNが不要なパディングインデックスを認識しないようにパッキングを行います。
他の回答に追加:ここに、シーケンスパッキングの概念を理解するのに非常に適した詳細な最小コード例を示します。 https://github.com/HarshTrivedi/packing-unpacking-pytorch-minimal-tutorial/tree/master