Batch 文本长度不同(pad_sequence)

1. pad_sequence

pad_sequence 函数通过填充的方式,将同一个 batch 中的 sequences 通过默认填充 0 的方式,变成最长 sequence 的长度,如下所示:

上图中,有 3 个句子,我们用数字代表某个词或者字,第一个序列长度为 5,第二个序列长度为 3,第三个序列长度为 4,通过 pad_sequence 函数,对其进行补 0 操作,如下所示:

函数使用代码如下:

import torch
import torch.nn as nn
from torch.nn.utils.rnn import pad_sequence


def test():

    # 1.1 填充一维张量
    a = torch.tensor([10, 20, 30, 40, 50])
    b = torch.tensor([10, 20, 30])
    c = torch.tensor([10, 20, 30, 40])

    # 将三个张量放在列表中,传入到 pad_sequence 函数中
    data = pad_sequence([a, b, c], batch_first=True)
    print(data)

    # 1.2 填充二维张量
    a = torch.randn(2, 3)
    b = torch.randn(4, 3)
    c = torch.randn(3, 3)

    data = pad_sequence([a, b, c], batch_first=True)
    print(data)


if __name__ == '__main__':
    test()

程序输出结果:

tensor([[10, 20, 30, 40, 50],
        [10, 20, 30,  0,  0],
        [10, 20, 30, 40,  0]])
tensor([[[ 1.1548,  0.0590,  1.2352],
         [ 1.1051,  0.3413, -1.4559],
         [ 0.0000,  0.0000,  0.0000],
         [ 0.0000,  0.0000,  0.0000]],

        [[-1.3324, -0.8635,  1.3135],
         [ 0.5169, -0.8685, -2.8555],
         [-0.0589,  0.0805,  1.1203],
         [ 0.4198,  0.8386, -0.0064]],

        [[-0.2444, -0.8604, -0.3829],
         [-0.9048, -1.3597, -1.8759],
         [ 0.4579, -0.7069, -0.5881],
         [ 0.0000,  0.0000,  0.0000]]])

2. pack_sequence 函数

pack_sequence 将同 batch 中长度不同序列封装成 PackedSequence 对象的方式来处理的,需要注意的是,pack_sequence 函数要求将不同的 tensor 放在列表中,并且按照不同 tensor 的长度降序排列。

下面是示例代码:

import torch
import torch.nn as nn
from torch.nn.utils.rnn import pack_sequence


def test():

    # 1.1 填充一维张量
    a = torch.tensor([10, 20, 30, 40, 50])
    b = torch.tensor([10, 20, 30])
    c = torch.tensor([10, 20, 30, 40])

    # 先把 [a, b, c] 按照长度从大到小
    sort_data = sorted([a, b, c], key=lambda x: len(x), reverse=True)
    # 构建压缩后的表示
    data = pack_sequence(sort_data)
    print(data)

    # 1.2 填充二维张量
    a = torch.randn(2, 3)
    b = torch.randn(4, 3)
    c = torch.randn(3, 3)

    # 先把 [a, b, c] 按照长度从大到小
    sort_data = sorted([a, b, c], key=lambda x: len(x), reverse=True)
    # 构建压缩后的表示,输入形状 [L, *]
    data = pack_sequence(sort_data)
    print(data)


if __name__ == '__main__':
    test()

程序输出结果:

PackedSequence(data=tensor([10, 10, 10, 20, 20, 20, 30, 30, 30, 40, 40, 50]), batch_sizes=tensor([3, 3, 3, 2, 1]), sorted_indices=None, unsorted_indices=None)
PackedSequence(data=tensor([[-0.4226,  0.1640, -1.4525],
        [-0.6334, -0.4643, -0.2134],
        [-1.3638, -0.5982, -0.3764],
        [-0.2297, -1.4002, -0.0083],
        [ 1.7213,  0.5896, -0.6520],
        [-0.4708,  1.6265,  1.0932],
        [ 1.9799,  0.2921,  0.7135],
        [ 1.5097, -0.5596, -0.3654],
        [-0.7631,  1.5645, -1.2764]]), batch_sizes=tensor([3, 3, 2, 1]), sorted_indices=None, unsorted_indices=None)
  1. PackedSequence 中的 data 表示压缩的数据
  2. batch_sizes 表示数据的个数,例如:[3, 3, 3, 2, 1] 中的 3, 表示 data 中 [10, 10, 10, 20, 20, 20, 30, 30, 30, 40, 40, 50] 的前 3 个数字是第一个时间步的输入,第二个 3 接着的 3 个数字作为第二个时间步的输入。

在 PyTorch 中的 RNN、LSTM、GRU 支持输入 PackedSequence 类型的数据作为输入。

3. pad 和 pack 在应用时的区别

import torch
import torch.nn as nn
from torch.nn.utils.rnn import pad_sequence
from torch.nn.utils.rnn import pack_sequence
from torch.nn.utils.rnn import pack_padded_sequence


# 固定随机数种子
torch.manual_seed(0)


# 1. 使用 pad_sequence
def test01():

    inputs = [torch.tensor([10, 20, 30, 40, 50]),
              torch.tensor([10, 20, 30]),
              torch.tensor([10, 20, 30, 40])]

    # 使用 pad_sequence 对 batch 数据长度处理
    inputs = pad_sequence(inputs)
    # print(inputs.shape)

    # 经过词嵌入层
    embed = nn.Embedding(num_embeddings=100, embedding_dim=2)
    inputs = embed(inputs)
    # print(inputs.shape)

    gru = nn.GRU(input_size=2, hidden_size=4)
    # GRP 输入形状: [sentence_length, batch_size, input_size]
    output, h = gru(inputs)
    print(h.data)


# 2. 使用 pack_padded_sequence
def test02():

    inputs = [torch.tensor([10, 20, 30, 40, 50]),
              torch.tensor([10, 20, 30]),
              torch.tensor([10, 20, 30, 40])]

    # 先对 inputs 根据长度排序
    inputs.sort(key=lambda x: len(x), reverse=True)

    # 先对输入序列进行0填充
    inputs = pad_sequence(inputs)

    # 经过词嵌入层,由于很多0经过,词嵌入之后也有很多0
    embed = nn.Embedding(num_embeddings=100, embedding_dim=2, padding_idx=0)
    inputs = embed(inputs)
    # print(inputs.shape)
    # print(inputs.transpose(0, 1))

    # 接下来通过 pack_padded_sequence 对输入进行压缩
    inputs = pack_padded_sequence(inputs, lengths=[5, 4, 3])
    # print(inputs)

    gru = nn.GRU(input_size=2, hidden_size=4)
    # GRP 输入形状: [sentence_length, batch_size, input_size]
    output, h = gru(inputs)
    print(h.data)


if __name__ == '__main__':
    test01()
    print('-' * 50)
    test02()

程序输出结果:

tensor([[[ 0.0466, -0.4807, -0.0078, -0.0605],
         [-0.0605, -0.3388,  0.1844, -0.2294],
         [ 0.0828, -0.3468,  0.0599, -0.2231]]])
--------------------------------------------------
tensor([[[-0.2108,  0.3736,  0.0056, -0.4251],
         [-0.0305,  0.0509, -0.1880, -0.6150],
         [ 0.4270, -0.0893,  0.0460, -0.5448]]])

上述函数中,test01 和 test02 函数中先使用 pad_sequence 函数进行 0 填充,变成相同长度的张量,然后经过 Embedding 转换为词嵌入。

在 test01 中直接将词嵌入结果送入 GRU 进行序列信息提取,过程如下图所示:

在 test02 中词嵌入的结果使用 pack_padded_sequence 进行压缩,然后送入 GRU 中进行序列信息提取,过程如下:

也就是说,通过 pack_padded_sequence 这种方式可以避免送入很多的 0 到循环神经网络中,进行某种层面上的无意义的计算。

3. pad_sequences 函数

pad_sequences 函数可以将文本截断、填充到指定长度.

from keras_preprocessing.sequence import pad_sequences
sentence_content = pad_sequences(sentence_content, maxlen=100, padding='post')

未经允许不得转载:一亩三分地 » Batch 文本长度不同(pad_sequence)