为什么我们将序列“打包”在pytorch中?


98

我试图复制rnn的可变长度序列输入如何使用打包,但是我想我首先需要理解为什么我们需要“打包”序列。

我了解为什么我们需要“填充”它们,但是为什么需要“包装”(通过pack_padded_sequence)呢?

任何高层次的解释将不胜感激!


:关于包装在pytorch所有问题discuss.pytorch.org/t/...
查理帕克

Answers:


91

我也偶然发现了这个问题,下面是我所发现的。

训练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]))

4
你能解释为什么给定示例的输出是PackedSequence(data = tensor([1,3,2,4,3]),batch_sizes = tensor([2,2,1]))吗?
ascetic652 '18

3
数据部分只是沿时间轴连接的所有张量。Batch_size实际上是每个时间步的批次大小数组。
Umang Gupta

3
batch_sizes = [2,2,1]分别代表分组[1,3] [2,4]和[3]。
柴坦雅·希瓦德

@ChaitanyaShivade为什么批处理大小为[2,2,1]?难道不是[1,2,2]吗?它背后的逻辑是什么?
匿名程序员,

1
因为在步骤t中,您只能在步骤t中处理向量,所以如果将向量排序为[1,2,2],则可能会将每个输入作为批处理放置,但是无法并行化,因此不可批处理
Umang Gupta

59

以下是一些视觉上的解释1,可能有助于发展对功能的更好的直觉pack_padded_sequence()

假设6总共有序列(可变长度)。您也可以将此数字6视为batch_size超参数。(这batch_size将取决于序列的长度(请参见下面的图2))

现在,我们希望将这些序列传递给一些递归神经网络体系结构。为此,我们必须将0批次中的所有序列(通常用s)填充到批次(max(sequence_lengths))中的最大序列长度,在下图中为9

填充序列

那么,数据准备工作应该现在就完成了吧?并非如此。因为仍然存在一个紧迫的问题,主要是与实际需要的计算相比,我们必须要做多少计算。

为了便于理解,让我们也假设,我们将矩阵乘法上述padded_batch_of_sequences形状(6, 9)与权重矩阵W形的(9, 3)

因此,我们将必须执行6x9 = 54乘法6x8 = 48加法                     (nrows x (n-1)_cols)操作,仅丢弃大部分计算结果,因为它们将是0s(我们有填充)。在这种情况下,实际所需的计算如下:

 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)以进行损失计算。


1个图片来源@sgrvinod


2
很棒的图表!
David Waterworth

1
编辑:我认为stackoverflow.com/a/55805785/6167850(如下)回答了我的问题,无论如何我都会离开这里:〜这是否实质上意味着梯度不会传播到填充的输入中?如果我的损失函数仅在RNN的最终隐藏状态/输出上计算怎么办?那必须放弃效率的提高吗?还是会从填充开始之前的步骤中计算损失,在此示例中,每个批次元素的损失是不同的?
〜– nlml

28

上面的答案解决了为什么很好的问题。我只想添加一个示例,以更好地了解的使用pack_padded_sequence

让我们举个例子

注意:pack_padded_sequence需要批量处理排序的序列(按序列长度的降序排列)。在以下示例中,已经对序列批次进行了排序,以减少混乱。访问此要点链接以获取完整的实现。

首先,我们如下创建一批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])
"""

将此工作与标准方法进行比较

  1. 以标准方式,我们只需要将to传递padded_seq_batchlstm模块。但是,它需要10次计算。它涉及对填充元素的更多计算,这在计算上效率低下。

  2. 注意,这不会导致不正确的表示,但是需要更多的逻辑来提取正确的表示。

    • 对于仅具有向前方向的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 >))
"""

上述结果表明,hncn在两个方面是不同的,同时output从两种方法导致不同的值用于填充元件。


2
好答案!只是一个更正,如果您进行填充,则不应在索引等于输入长度的最后使用h而不是h。另外,要进行双向RNN,您可能需要使用两种不同的RNN-一种在前面填充,另一种在后面填充以获得正确的结果。填充和选择最后一个输出是“错误的”。因此,您认为这会导致表示不正确的论点是错误的。填充的问题是正确但效率低下(如果存在压缩序列选项)并且可能很麻烦(例如:bi-dir RNN)
Umang Gupta,

18

除了Umang的答案外,我发现这一点很重要。

返回的元组中的第一项pack_padded_sequence是包含打包序列的数据(张量)-张量。第二项是整数张量,其中包含每个序列步骤中有关批次大小的信息。

不过,重要的是第二项(批大小)表示批处理中每个序列步骤的元素数量,而不是传递给的不同序列长度pack_padded_sequence

例如,给定的数据 abcx 在:类:PackedSequence将包含数据axbcbatch_sizes=[2,1,1]


1
谢谢,我完全忘记了。并在我要更新的答案中犯了一个错误。但是,我将第二个序列视为恢复序列所需的一些数据,这就是为什么弄乱了我的描述
Umang Gupta

3

我使用了打包填充序列,如下所示。

packed_embedded = nn.utils.rnn.pack_padded_sequence(seq, text_lengths)
packed_output, hidden = self.rnn(packed_embedded)

其中text_lengths是填充序列之前的各个序列的长度,并根据给定批次中长度的递减顺序对其进行排序。

您可以在此处查看示例。

并且我们进行打包,以使RNN在处理会影响整体性能的序列时不会看到不需要的填充索引。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.