以上答案很好地解决了为什么这个问题。我只是想添加一个例子来更好地理解pack_padded_sequence的使用。
举个例子
注意:pack_padded_sequence 要求批处理中的排序序列(按序列长度的降序排列)。在下面的示例中,已经对序列批次进行了排序,以减少混乱。访问this gist link 了解完整实施。
首先,我们创建一批 2 个不同序列长度的序列,如下所示。我们批次总共有 7 个元素。
- 每个序列的嵌入大小为 2。
- 第一个序列的长度为:5
- 第二个序列的长度为: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。它返回两个张量的元组:
- 首先是包含序列批次中所有元素的数据。
- 第二个是
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 传递给 Pytorch 中的循环模块,例如 RNN、LSTM。这只需要在循环模块中进行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 次计算。它涉及更多关于填充元素的计算,这将是计算低效的。
-
请注意,它不会导致不准确表示,但需要更多的逻辑来提取正确的表示。
- 对于只有前向的 LSTM(或任何循环模块),如果我们想提取最后一步的隐藏向量作为序列的表示,我们必须从 T(th) 步中提取隐藏向量,其中 T 是输入的长度。拿起最后一个表示将是不正确的。请注意,对于批次中的不同输入,T 会有所不同。
- 对于双向 LSTM(或任何循环模块),它更加麻烦,因为必须维护两个 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 >))
"""
以上结果表明hn、cn在两个方面不同,而output来自两个方面导致填充元素的值不同。