为什么我们在pytorch中“打包”序列?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么我们在pytorch中“打包”序列?相关的知识,希望对你有一定的参考价值。
我试图复制How to use packing for variable-length sequence inputs for 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允许我们打包序列,内部打包序列是两个列表的元组。一个包含序列的元素。元素按时间步长交错(参见下面的示例),其他元素包含 每个序列的大小 每一步的批量大小。这有助于恢复实际序列以及告知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
元组中的第一项是包含打包序列的数据(张量) - 张量。第二项是整数的张量,在每个序列步骤中保存有关批量大小的信息。
这里重要的是第二项(批量大小)表示批次中每个序列步骤的元素数量,而不是传递给pack_padded_sequence
的变化序列长度。
例如,给定数据abc
和x
:class:PackedSequence
将包含数据axbc
与batch_sizes=[2,1,1]
。
上面的答案很好地解决了这个问题。我只是想添加一个例子来更好地理解pack_padded_sequence
的使用。
Let's take an example
注意:
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。这只需要在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])
"""
Compare this effort with the standard way
- 在标准方式中,我们只需要将
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
从两个方面导致不同的填充元素值。
以下是一些视觉解释1,可能有助于为pack_padded_sequence()
的功能开发出更好的直觉
假设我们总共有6
序列(可变长度)。您也可以将此数字6
视为batch_size
超参数。
现在,我们希望将这些序列传递给一些递归神经网络架构。为此,我们必须在我们的批次中填充所有序列(通常使用0
s)到我们批次中的最大序列长度(max(sequence_lengths)
),在下图中是9
。
那么,数据准备工作现在应该完成了,对吧?不是真的..因为还存在一个紧迫的问题,主要是在与实际需要的计算相比时我们需要做多少计算。
为了便于理解,我们还假设我们将矩阵乘以形状padded_batch_of_sequences
的上述(6, 9)
与形状W
的权重矩阵(9, 3)
。
因此,我们将不得不执行6x9 = 54
乘法和qazxswpoi加法(6x8 = 48
)操作,只是为了抛弃大部分计算结果,因为它们将是nrows x (n-1)_cols
s(我们有垫子)。在这种情况下,实际需要的计算是:
0
即使对于这个玩具示例,这也是更多的节省。您现在可以想象使用 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
可以为具有数百万条目的大张量节省多少计算(成本,能量,时间,碳排
以上是关于为什么我们在pytorch中“打包”序列?的主要内容,如果未能解决你的问题,请参考以下文章