代码之家  ›  专栏  ›  技术社区  ›  aerin

为什么我们要把序列“打包”在pytorch里?

  •  40
  • aerin  · 技术社区  · 7 年前

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

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

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

    5 回复  |  直到 6 年前
        1
  •  46
  •   David Beauchemin Sammaye    6 年前

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

    在训练rnn(lstm或gru或vanilla rnn)时,很难对可变长度序列进行批处理。例如:如果8号批量中的序列长度为[4,6,8,5,4,3,7,8],则将填充所有序列,这将导致8个序列长度为8。你最终会做64次计算(8x8),但是你只需要做45次计算。此外,如果你想用双向RNN做一些有趣的事情,那么只需通过填充就很难进行批量计算,最终可能会比需要做更多的计算。

    相反,pytorch允许我们打包序列,内部打包序列是两个列表的元组。一个包含序列元素。元素被时间步交错(参见下面的示例)和其他包含 每个序列的大小 每一步的批量大小。这有助于恢复实际序列,以及告诉RNN在每一个时间步长是什么批次大小。艾琳指出了这一点。这可以传递给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]))
    
        2
  •  13
  •   aerin    7 年前

    加上Umang的回答,我发现这一点很重要。

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

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

    例如,给定的数据 abc x 班级: PackedSequence 将包含数据 axbc 具有 batch_sizes=[2,1,1] 是的。

        3
  •  9
  •   Umang Gupta    6 年前

    上面的答案回答了这个问题 为什么? 很好。我只想添加一个示例,以便更好地理解 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. 以标准的方式,我们只需要通过 填充序列号 lstm 模块。然而,它需要10次计算。它涉及到几个关于padding元素的计算 计算上的 效率低下。

    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 在两个方面是不同的 输出 从两种方式导致填充元素的值不同。

        4
  •  5
  •   kmario23 Mazdak    6 年前

    这里有一些 视觉解释 1个 这可能有助于为 pack_padded_sequence()

    假设我们有 6 (可变长度的)序列。你也可以考虑这个号码 6个 作为 batch_size 超参数。

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

    padded-seqs

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

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

    因此,我们必须 6x9 = 54 乘法 6x8 = 48 附加 ( nrows x (n-1)_cols )操作,只会丢弃大部分计算结果,因为它们将 S(我们有护垫的地方)。在这种情况下,实际需要的计算是:

     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
    

    即使是这个玩具的例子也能省很多钱。现在你可以想象可以节省多少计算量(成本,能量,时间,碳排放等)。 压缩填充序列() 对于有数百万条记录的大张量。

    功能性 压缩填充序列() 通过使用的颜色编码,可以从下图中理解:

    pack-padded-seqs

    由于使用 压缩填充序列() 我们将得到包含(i)平坦的张量的元组(沿图中的AxIS-1)。 sequences ,(ii)相应的批量大小, tensor([6,6,5,4,3,3,2,2,1]) 对于上面的例子。

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


    图像学分 @sgrvinod

        5
  •  1
  •   Jibin Mathew    6 年前

    我用了如下的填充顺序。

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

    其中,文本长度是填充前单个序列的长度,序列根据给定批次内长度的递减顺序排序。

    你可以看看一个例子 here 是的。

    我们做包装,使RNN没有看到不必要的填充指数,同时处理序列,这将影响整体性能。