我明白为什么我们需要将它们“填充”,但是为什么需要通过
pack_padded_sequence
进行“打包”呢?pack_padded_sequence
进行“打包”呢?我也遇到了这个问题,以下是我找出的解决方法。
在训练 RNN(LSTM、GRU 或 vanilla-RNN)时,很难对变长序列进行批处理。例如:如果大小为 8 的一组序列的长度是 [4,6,8,5,4,3,7,8],那么您将填充所有序列,这将导致 8 个长度为 8 的序列。这样您将会做 64 次计算(8x8),但实际上只需要做 45 次计算。此外,如果您想要像使用双向 RNN 这样的花哨操作,仅通过填充进行批量计算就更难了,您可能会做比所需更多的计算。
相反,PyTorch 允许我们打包序列,内部打包的序列是一个包含两个列表的元组。其中一个包含序列的元素。元素按时间步骤交错排列(请参见下面的示例),另一个包含每个步骤的批次大小。这有助于恢复实际序列,并告诉 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()
的功能。
TL;DR: 这主要是为了节省计算资源。因此,训练神经网络模型所需的时间也会(大幅)缩短,特别是在处理非常大的(也就是 Web 规模的)数据集时。
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) 扁平化的(沿着axis-1,如上图所示)sequences
,(ii) 相应的批次大小,对于上面的示例为tensor([6,6,5,4,3,3,2,2,1])
。1 图片由@sgrvinod提供
pack_padded_sequence
的用法。pack_padded_sequence
需要对批次中的序列进行排序(按序列长度降序排列)。在下面的示例中,序列批次已经排序,以避免混乱。请访问此gist链接以获取完整实现。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次计算。它还涉及在填充元素上进行了更多的计算,这会导致计算效率低下。
请注意,这不会导致不准确的表示,但需要更多的逻辑来提取正确的表示。
让我们看看区别:
# 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
对于填充元素的值也是不同的。请注意,它不会导致不准确的表示
"是如何成立的。我认为这个论点是通过将0传递到RNN中不会改变输出,但这似乎只有在所有偏差都等于0的情况下才成立。 - financial_physician补充Umang的回答,我认为这一点很重要。
pack_padded_sequence
返回元组的第一项是一个数据(张量)——包含打包序列的张量。第二项是一个整数张量,保存每个序列步骤的批处理大小信息。
这里重要的是第二项(批处理大小)代表批处理中每个序列步骤中的元素数量,而不是传递给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)