【问题标题】:Why do we "pack" the sequences in PyTorch?为什么我们在 PyTorch 中“打包”序列?
【发布时间】:2018-12-04 11:03:16
【问题描述】:

我试图复制How to use packing for variable-length sequence inputs for rnn,但我想我首先需要了解为什么我们需要“打包”序列。

我明白为什么我们需要“填充”它们,但为什么需要“打包”(通过pack_padded_sequence)?

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

【问题讨论】:

标签: deep-learning pytorch recurrent-neural-network tensor zero-padding


【解决方案1】:

在 Umang 的回答中,我发现这一点很重要。

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

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

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

【讨论】:

  • 谢谢,我完全忘记了。并在我的回答中犯了一个错误,要更新它。但是,我将第二个序列视为恢复序列所需的一些数据,这就是为什么搞砸了我的描述
【解决方案2】:

这里有一些视觉解释1,它们可能有助于更好地理解pack_padded_sequence() 的功能。


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


假设我们总共有6 序列(可变长度)。您也可以将此数字 6 视为 batch_size 超参数。 (batch_size 会根据序列的长度而变化(参见下面的图 2))

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

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

为了便于理解,我们还假设我们将对上述形状为(6, 9)padded_batch_of_sequences 与形状为(9, 3) 的权重矩阵W 进行矩阵相乘。

因此,我们必须执行 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) 

即使对于这个非常简单的 (toy) 示例,也可以节省更多。您现在可以想象使用pack_padded_sequence() 可以节省多少计算(最终:成本、能源、时间、碳排放等),用于具有数百万条目的大型张量,以及全世界数百万以上的系统一次又一次地这样做。

pack_padded_sequence()的功能可以从下图中理解,借助使用的颜色编码:

由于使用pack_padded_sequence(),我们将得到一个张量元组,其中包含 (i) 扁平化的(沿轴 1,在上图中)sequences,(ii) 相应的批量大小,@987654350 @上面的例子。

然后可以将数据张量(即展平序列)传递给目标函数,例如 CrossEntropy 进行损失计算。


1 图片归功于 @sgrvinod

【讨论】:

  • 优秀的图表!
  • 编辑:我认为stackoverflow.com/a/55805785/6167850(下)回答了我的问题,无论如何我都会留在这里:~这是否意味着渐变不会传播到填充输入?如果我的损失函数只在 RNN 的最终隐藏状态/输出上计算怎么办?那么是否必须放弃效率收益?还是会从填充开始之前的步骤计算损失,这对于本示例中的每个批处理元素都是不同的?~
  • 我对矩阵乘法的精确度很感兴趣,因为 RNN 馈送应该是顺序的,一次只取一部分压缩向量。这个很棒的教程给出了完整的解释:github.com/sgrvinod/a-PyTorch-Tutorial-to-Sequence-Labeling
【解决方案3】:

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

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

【讨论】:

  • 你能解释一下为什么给定例子的输出是 PackedSequence(data=tensor([ 1, 3, 2, 4, 3]), batch_sizes=tensor([ 2, 2, 1]) ) ?
  • 数据部分只是沿时间轴连接的所有张量。 Batch_size 实际上是每个时间步的批量大小数组。
  • batch_sizes=[2, 2, 1] 分别代表分组[1, 3] [2, 4] 和[3]。
  • 因为在第 t 步,您只能在第 t 步处理向量,如果您将向量保持为 [1,2,2] 的顺序,您可能会将每个输入作为一个批次,但这可以t 是并行化的,因此不可批处理
  • 那么这是否意味着打包序列只是为了节省一些计算(所以速度/能量)?如果在仅填充序列上进行相同的训练/学习,并且在填充处施加 0 的损失,则会发生相同的训练/学习?
【解决方案4】:

以上答案很好地解决了为什么这个问题。我只是想添加一个例子来更好地理解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])
"""

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

  1. 在标准方式中,我们只需要将padded_seq_batch 传递给lstm 模块。但是,它需要 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来自两个方面导致填充元素的值不同。

【讨论】:

  • 不错的答案!如果您进行填充,则只是一个更正,您不应该在等于输入长度的索引处使用最后一个 h 而不是 h。此外,要进行双向 RNN,您需要使用两种不同的 RNN——一种在前面有填充,另一种在后面有填充,以获得正确的结果。填充和选择最后一个输出是“错误的”。因此,您认为它导致不准确表示的论点是错误的。填充的问题是它是正确的但效率低下(如果有打包序列选项)并且可能很麻烦(例如:bi-dir RNN)
  • 我试图了解Note that it does not lead to inaccurate representations 是如何真实的陈述。我认为这个论点是通过 RNN 传递 0 不会改变输出,但似乎只有当偏差都等于 0 时这才是正确的
【解决方案5】:

我使用包填充序列如下。

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

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

您可以查看示例here

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

【讨论】:

    猜你喜欢
    • 2012-09-01
    • 1970-01-01
    • 1970-01-01
    • 2019-02-18
    • 2012-08-31
    • 1970-01-01
    • 1970-01-01
    • 2022-10-23
    • 2017-12-27
    相关资源
    最近更新 更多