为什么我们要在 PyTorch 中“包装”序列?

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

我明白为什么我们“垫”他们,但为什么“包装”(通过 pack_padded_sequence)是必要的?

62437 次浏览

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

在训练 RNN (LSTM 或 GRU 或香草 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的不同序列长度。

例如,给定数据 abcx Class: 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)

其中 text _ length 是填充之前单个序列的长度,序列根据给定批处理中长度的递减顺序进行排序。

你可以查看一个例子 给你

我们做填充,以便 RNN 不会看到不需要的填充索引,同时处理序列,这将影响整体性能。

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

现在,我们将 tuple 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在两个方面导致填充元素的值不同。

这里有一些 视觉解释1,可能有助于发展更好的直觉功能的 强 > pack_padded_sequence()


DR : 主要用于保存计算。因此,训练神经网络模型所需的时间也大大减少,特别是在非常大的(也称为网络规模)数据集上进行时。


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

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

padded-seqs

那么现在数据准备工作应该已经完成了吧?没有。.因为还有一个紧迫的问题,主要是我们需要进行多少计算,与实际需要的计算相比。

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

因此,我们必须执行 6x9 = 54乘法6x8 = 48加法 (nrows x (n-1)_cols)操作,只是扔掉大部分计算结果,因为它们将是 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
   

------------------------------
#savings: 22-mult & 22-add ops
(32-54)  (26-48)

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

pack_padded_sequence()的功能可以从下图中理解,通过使用彩色编码的帮助:

pack-padded-seqs

作为使用 pack_padded_sequence()的结果,我们将得到一个张量元组,其中包含(i)平坦的(在上图中沿轴 -1) sequences,(ii)相应的批量大小,以及上面例子中的 tensor([6,6,5,4,3,3,2,2,1])

然后,数据张量(即平坦序列)可以传递给目标函数,例如用于损失计算的 Cross熵。


1 图片归功于 < a href = “ https://github.com/sgrvinod/”rel = “ norefrer”>@sgrvinod