144

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

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

任何高级解释将不胜感激!

4

5 回答 5

132

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

在训练 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]))
于 2018-06-25T19:52:48.207 回答
117

这里有一些直观的解释1可能有助于更好地理解pack_padded_sequence().


TL;DR:它主要是为了节省计算。因此,训练神经网络模型所需的时间也(大大)减少了,尤其是在非常大(又名网络规模)数据集上执行时。


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

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

填充序列

那么,数据准备工作应该已经完成​​了吧?不是真的......因为仍然存在一个紧迫的问题,主要是与实际需要的计算相比,我们需要做多少计算。

为了便于理解,我们还假设我们将上面padded_batch_of_sequences的 shape矩阵与 shape(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

于 2019-05-19T19:00:03.497 回答
43

上面的答案很好地解决了这个问题。我只是想添加一个示例以更好地理解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. 在标准方式中,我们只需要传递padded_seq_batchtolstm模块。但是,它需要 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 >))
"""

上面的结果表明hn,cn有两种不同的方式,而output两种方式导致填充元素的值不同。

于 2019-04-23T06:53:30.943 回答
22

添加到 Umang 的答案中,我发现这一点很重要。

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

但这里重要的是第二项(批次大小)表示批次中每个序列步骤的元素数量,而不是传递给pack_padded_sequence.

例如,给定数据 abcx :class:PackedSequence将包含axbc带有 batch_sizes=[2,1,1].

于 2018-06-25T21:46:39.460 回答
3

我使用包填充序列如下。

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

其中 text_lengths 是填充之前单个序列的长度,并且序列根据给定批次中长度的降序排序。

你可以在这里查看一个例子。

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

于 2019-02-07T06:45:18.490 回答