Bootstrap

pytorch中如何在lstm中输入可变长的序列





pytorch中如何在lstm中输入可变长的序列

我在做的时候主要参考了这些文章
https://zhuanlan.zhihu.com/p/59772104
https://blog.csdn.net/u011550545/article/details/89529977


博客中代码参考pytorch_lstm_change_sequence.ipynb

  前两天在做一个情感二分类任务的时候, 使用了lstm. 但是在将句子输入lstm的时候, 发现句子的分布相当的分散, 长度从600个单词到3个单词不等. 本来打算直接截断输入到lstm中, 但是对训练数据分析了一下, 发现数据的分布不是特别的集中, 实际使用截断的方法做的话, 和使用不截断的方法, 差了大概3%左右. 那么pytorch如何使用变长的序列输入呢?

  这里给出其中最主要的3个方法

torch.nn.utils.rnn.pad_sequence()
torch.nn.utils.rnn.pack_padded_sequence()
torch.nn.utils.rnn.pad_packed_sequence()

其中torch.nn.utils.rnn.pad_sequence()把不等长的tensor数据, 补充成等长的tensor数据.

torch.nn.utils.rnn.pack_padded_sequence()把等长的tensor根据所输入的参数压缩成实际的数据, 同时数据格式变成PackedSequence

torch.nn.utils.rnn.pad_packed_sequence()把上面所压缩成PackedSequence的数据还原成tensor类型, 并补成等长的数据, 下面依次介绍一下.

torch.nn.utils.rnn.pad_sequence()

  首先是torch.nn.utils.rnn.pad_sequence(), 我们看一个tensor数据

train_x = [torch.tensor([1, 2, 3, 4, 5, 6, 7]),
           torch.tensor([2, 3, 4, 5, 6, 7]),
           torch.tensor([3, 4, 5, 6, 7]),
           torch.tensor([4, 5, 6, 7]),
           torch.tensor([5, 6, 7]),
           torch.tensor([6, 7]),
           torch.tensor([7])]

可以明显的看到,这个数据是不等长的, 这样的话, 我们是无法将这些数据组成一组数据送入网络进行训练的, 例如我们写一个简单的数据读取的:

class MyData(Dataset):
    def __init__(self, train_x):
        self.train_x = train_x

    def __len__(self):
        return len(self.train_x)

    def __getitem__(self, item):
        return self.train_x[item]
        
train_data = MyData(train_x)
train_dataloader = DataLoader(train_data, batch_size=2)

for i in train_dataloader:
    print(i)

结果我们得到的是:

RuntimeError: invalid argument 0: Sizes of tensors must match except in dimension 0. Got 7 and 6 in dimension 1 at /tmp/pip-req-build-58y_cjjl/aten/src/TH/generic/THTensor.cpp:689

  这个说明, 如果两个数据不一样长, 是没法进行拼接的, 这样的话我们就没法进行批量的数据处理了, 我们需要做的就是将每个批次的数据填充成一样长的, 但是我们要怎么做呢? 在__getitem__(self, item)方法中填充吗?

  在__getitem__(self, item)中填充确实是可以的, 但是这样做的方法有点不好, 就是我们每次进行批次训练的时候, 我们每个批次的数据中最长的那个数据不一定每次都是一样的, 如果在__getitem__(self, item)中进行填充, 我们就要取所有数据集中最长的那个作为基准, 然后把所有的数据都填充成那么长的. 但是这样做肯定是不合适的. 实际上, pytorch中的DataLoader提供了一个参数collate_fn=, 通过这个参数, 我们可以传入一个函数或者类, 进行数据的处理.

  collate_fn参数就是进行处理在选出所需要的数据之后, 如何把这些数据拼接成一个整体的tensor. 平时在默认使用的时候, 这个参数的默认函数会直接把所取到的数据拼接成一个完整的tensor, 但是现在我们的数据是不等长的, 那么在拼接的时候肯定会出问题, 那么我们需要做的其实就是自定义一个collate_fn函数, 然后我们来拼接数据, 例如定义一个下面的collate_fn函数

def collate_fn(train_data):
    train_data.sort(key=lambda data: len(data), reverse=True)
    data_length = [len(data) for data in train_data]
    train_data = rnn_utils.pad_sequence(train_data, batch_first=True, padding_value=0)
    return train_data, data_length

这里我返回了两个参数, 一个是train_data, 另外一个是data_length. 而且我还对train_data进行了一次排序. 这些一会解释. 我们现在需要知道的是, 我们已经对train_data进行了填充, 并将它合并成了一个完整的tensor返回, 这个时候, 我们再把数据传入DataLoader

train_dataloader = DataLoader(train_data, batch_size=2, collate_fn=collate_fn)

for data, length in train_dataloader:
    print(data)
    print(length)

然后我们会看到这样的输出

tensor([[1, 2, 3, 4, 5, 6, 7],
        [2, 3, 4, 5, 6, 7, 0]])
[7, 6]
tensor([[3, 4, 5, 6, 7],
        [4, 5, 6, 7, 0]])
[5, 4]
tensor([[5, 6, 7],
        [6, 7, 0]])
[3, 2]
tensor([[7]])
[1]

torch.nn.utils.rnn.pack_padded_sequence()

  这个时候, pad_sequence的作用也就讲完了, 下面就是pack_padded_sequence. pack_padded_sequence函数的字面意思就是把原来填充过的序列再压缩回去. 它有三个主要的参数, 分别是input, lengths, batch_first. 其中input就是我们上面使用pad_sequence填充过的数据, 而lengths就是我们collate_fn函数返回的length, 也就是我们的数据的实际长度, batch_first就简单了, 就是把数据的batch_first放到最前面.

  但是为啥我们需要使用pack_padded_sequence呢? 直接把填充好的数据输入到RNN中不可以吗?实际上是当然可以的, 但是在实际情况中, 数据是这样输入的, 下面给出一个batch的例子

tensor([[1, 2, 3, 4, 5, 6, 7],
        [2, 3, 4, 5, 6, 7, 0]])

输入到RNN的实际上是按照这样的顺序[1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 0]依次输入到RNN中的. 但是我们发现最后一个是[7, 0], 这里的0输入到RNN中, 实际上并没有输出有用的数据, 这样的话就会浪费算力资源, 所以我们使用pack_padded_sequence进行压缩一下, 例如下面的代码:

for data, length in train_dataloader:
    data = rnn_utils.pack_padded_sequence(data, length, batch_first=True)
    print(data)

输出的结果是:

PackedSequence(data=tensor([1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7]), batch_sizes=tensor([2, 2, 2, 2, 2, 2, 1]), sorted_indices=None, unsorted_indices=None)
PackedSequence(data=tensor([3, 4, 4, 5, 5, 6, 6, 7, 7]), batch_sizes=tensor([2, 2, 2, 2, 1]), sorted_indices=None, unsorted_indices=None)
PackedSequence(data=tensor([5, 6, 6, 7, 7]), batch_sizes=tensor([2, 2, 1]), sorted_indices=None, unsorted_indices=None)
PackedSequence(data=tensor([7]), batch_sizes=tensor([1]), sorted_indices=None, unsorted_indices=None)

  我们可以只看第一个输出, 这个是我们上面举的那个例子, 数据类型已经变成了PackedSequence, 同时数据变成了tensor([1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7]), 你看着肯定很奇怪, 这样的数据还怎么输入到RNN中进行运算呢?

  但是PackedSequence还提供了一个batch_sizes数据, 这个数据其实是用来分割前面的那一串数据的, 例如batch_sizes前面6个都是2, 最后一个是1, 和上面的[1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 0]是对应着的, 只是最后一个0没有了, 所以batch_sizes最后一个变成了1, 其实就相当于把数据分割成了[1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7].
  pack_padded_sequence所起的作用, 其实有点类似于重新对数据的batch_size进行了修改, 根据数据的实际长度, 将每次输入的数据的batch_size值进行修改, 这样也就完成了可变长数据的输入

torch.nn.utils.rnn.pad_packed_sequence()

  pad_packed_sequence又是干什么用的呢? 我们可以看一下, 将上面的可变长序列输入LSTM之后的输出是什么. 我们先定义一个LSTM

net = nn.LSTM(1, 5, batch_first=True)

  定义的这个LSTM是输入的维度是1维, 输出的维度是5维的. 但是它的完整数据输入格式是input (batch, seq_len, input_size). 和我们上面举例的数据稍微有点不太一样, 所以我们需要修改一下collate_fn函数, 让它符合我们的输入要求:

def collate_fn(train_data):
    train_data.sort(key=lambda data: len(data), reverse=True)
    data_length = [len(data) for data in train_data]
    train_data = rnn_utils.pad_sequence(train_data, batch_first=True, padding_value=0)
    return train_data.unsqueeze(-1), data_length  # 对train_data增加了一维数据

然后我们将数据输入LSTM, 看一下输出的第一次的结果:

train_data = MyData(train_x)
train_dataloader = DataLoader(train_data, batch_size=2, collate_fn=collate_fn)

flag = 0
for data, length in train_dataloader:
    data = rnn_utils.pack_padded_sequence(data, length, batch_first=True)
    output, hidden = net(data)
    if flag == 0:
        print(output)
        flag = 1
out:
PackedSequence(data=tensor([[-0.0359, -0.0036,  0.0825,  0.1019, -0.1004],
        [ 0.0155,  0.0222,  0.0926,  0.1369, -0.0548],
        [-0.0054,  0.0196,  0.1241,  0.1759, -0.1449],
        [ 0.0495,  0.0504,  0.1263,  0.2017, -0.0534],
        [ 0.0374,  0.0475,  0.1405,  0.2131, -0.1426],
        [ 0.0729,  0.0720,  0.1338,  0.2225,  0.0114],
        [ 0.0656,  0.0693,  0.1410,  0.2237, -0.0812],
        [ 0.0792,  0.0866,  0.1280,  0.2228,  0.1560],
        [ 0.0743,  0.0844,  0.1319,  0.2203,  0.0601],
        [ 0.0737,  0.0962,  0.1156,  0.2154,  0.3757],
        [ 0.0701,  0.0946,  0.1179,  0.2117,  0.2878],
        [ 0.0630,  0.1021,  0.1004,  0.2058,  0.6103],
        [ 0.0604,  0.1011,  0.1020,  0.2022,  0.5502]], grad_fn=<CatBackward>), batch_sizes=tensor([2, 2, 2, 2, 2, 2, 1]), sorted_indices=None, unsorted_indices=None)

这个结果是PackedSequence类型的, 而且数据格式是[13 * 5]的, 如果我们下面需要经过一个全连接层, 那么我们需要的数据格式应该是[2 * 7 * 5]的形式. 这个时候, 就是pad_packed_sequence发挥作用的时候了. 我们看下面的代码:

train_data = MyData(train_x)
train_dataloader = DataLoader(train_data, batch_size=2, collate_fn=collate_fn)

flag = 0
for data, length in train_dataloader:
    data = rnn_utils.pack_padded_sequence(data, length, batch_first=True)
    output, hidden = net(data)
    if flag == 0:
        output, out_len = rnn_utils.pad_packed_sequence(output, batch_first=True)
        print(output.shape)
        print(output)
        flag = 1

可以看到输出结果是:

torch.Size([2, 7, 5])
tensor([[[-0.0359, -0.0036,  0.0825,  0.1019, -0.1004],
         [-0.0054,  0.0196,  0.1241,  0.1759, -0.1449],
         [ 0.0374,  0.0475,  0.1405,  0.2131, -0.1426],
         [ 0.0656,  0.0693,  0.1410,  0.2237, -0.0812],
         [ 0.0743,  0.0844,  0.1319,  0.2203,  0.0601],
         [ 0.0701,  0.0946,  0.1179,  0.2117,  0.2878],
         [ 0.0604,  0.1011,  0.1020,  0.2022,  0.5502]],

        [[ 0.0155,  0.0222,  0.0926,  0.1369, -0.0548],
         [ 0.0495,  0.0504,  0.1263,  0.2017, -0.0534],
         [ 0.0729,  0.0720,  0.1338,  0.2225,  0.0114],
         [ 0.0792,  0.0866,  0.1280,  0.2228,  0.1560],
         [ 0.0737,  0.0962,  0.1156,  0.2154,  0.3757],
         [ 0.0630,  0.1021,  0.1004,  0.2058,  0.6103],
         [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000]]],
       grad_fn=<TransposeBackward0>)

这里我们可以看到, 输出的数据符合[2 * 7 * 5]的形式, 并且看实际数据, 可看到最后一个数据的不足补的都是0.

博客中代码参考pytorch_lstm_change_sequence.ipynb

;