为什么要用pack_padded_sequence
在使用深度学习特别是RNN(LSTM/GRU)进行序列分析时,经常会遇到序列长度不一样的情况,此时就需要对同一个batch中的不同序列使用padding的方式进行序列长度对齐(可以都填充为batch中最长序列的长度,也可以设置一个统一的长度,对所有序列长截短填),方便将训练数据输入到LSTM模型进行训练,填充后一个batch的序列可以统一处理,加快速度。但是此时会有一个问题,LSTM会对序列中非填充部分和填充部分同等看待,这样会影响模型训练的精度,应该告诉LSTM相关序列的padding情况,让LSTM只对非填充部分进行运算。此时,pytorch中的pack_padded_sequence就有了用武之地。
其实有时候,可以填充后直接做,影响有时也不是很大,使用pack_padded_sequence后效果可能会更好。
序列处理简单例子
假设有demo.txt文件,包含下面5段文本/序列:
Some people like to choose those who are different from themselves while others prefer those who are similar to themselves.
People choose friends in differrent ways.
For instance, if an active and energetic guy proposes to his equally active and energetic friends that they should have some activities, it is more likely that his will agree at once.
When people have friends similar to themselves, they and their friends chat, play, and do thing together natually and harmoniously.
The result is that they all can feel relaxed and can trully enjoy each other's company.
使用下面的脚本将单词转换为索引,并填充为统一的长度:
import numpy as np
vocab = {} #词到索引的映射字典
token_id = 1 #token_id=0 预留给填充符号
lengths = [] #存储每个文本的实际长度
with open('demo.txt', 'r') as f:
for l in f:
tokens = l.strip().split() #这里对英文分词 简单的按空格切分。(当然可以使用一些效果更好的分词工具,可以把标点分出来)
lengths.append(len(tokens))
for t in tokens:
if t not in vocab:
vocab[t] = token_id
token_id += 1
x = np.zeros((len(lengths), max(lengths))) #所有文本填充为最大的长度
l_no = 0
with open('demo.txt', 'r') as f:
for l in f:
tokens = l.strip().split()
for i in range(len(tokens)):
x[l_no, i] = vocab[tokens[i]]
l_no += 1
查看填充后的效果,发现文本中的单词都转换为了词典中的索引,并填充为最长文本的长度,填充值为0:
print(x)
print(x.shape)
pack_padded_sequence
import torch
import torch.nn as nn
x = torch.tensor(x,requires_grad=True)
lengths = torch.Tensor(lengths)
print("lenghts:",lengths)
#所有文本长度按从大到小排序 (降序),返回排序后的索引idx_sort
_, idx_sort = torch.sort(torch.Tensor(lengths), dim=0, descending=True)
print("idx_sort:",idx_sort)
#对索引idx_sort进行从小到大排序 ,返回排序后的索引 idx_unsort
_, idx_unsort = torch.sort(idx_sort, dim=0)
print("idx_unsort:",idx_unsort)
x1 = x.index_select(0, idx_sort)#x中的各个文本 随着排序 即最长的文本在第一行...
lengths1 = list(lengths[idx_sort])#此时各个文本对应的长度(从大到小排序后)
print("lenghts1:",lengths1)
print(x1)
print(x1.shape)
x_packed = nn.utils.rnn.pack_padded_sequence(input=x1, lengths=lengths1, batch_first=True)
print(x_packed)
需要注意的是,pack_padded_sequence函数的参数,lengths需要从大到小排序(length1),x1已根据长度大小排好序(最长的序列在第一行…),batch_first如果设置为true,则x的第一维为batch_size,第二维为seq_length,否则相反。
打印x_packed如下:
他把x1的两个维度合并成了一个维度,原本x1(batch_size,max_seq_len)=(5,32),x_packed相当于对x1按列进行访问,并且忽略掉其中的填充值0;下面多出的batch_size有max_seq_len=32个数字,可以理解为对x1进行按列访问时,每一列非填充值的个数,可以看到刚开始的几列没有填充值(每个序列的开始部分),值为batch_size=5,后面由于有的序列不够长,逐渐出现填充值0,所以batch_size的大小逐渐变小<5,直到最后等于1,也就是只有那个batch中最长的序列还有非填充值,其余序列都是填充值0.
pad_packed_sequence
x_packed经后续的LSTM处理之后,再使用pad_packed_sequence转换回padding形式。
假设x_packed经LSTM网络输出后仍为x_packed(注:一般情况下,经LSTM网络输出应该有第三维,但方便起见,x_packed的第三维的维度可看成是1),则相应转换如下:
x_padded = nn.utils.rnn.pad_packed_sequence(x_packed, batch_first=True)
output = x_padded[0].index_select(0, idx_unsort)
需要注意的是,idx_unsort的作用在于将batch中的序列调整为最开始的顺序。
print(output.data.numpy()) #和最初的x一样
print(output.shape)
PackedSequence
其实很简单,当之前的x_packed需要经过dropout等函数处理时,需要传入的是x_packed.data,是一个tensor,经过处理后,要将其重新封装成PackedSequence,再传入LSTM网络,示例如下:
dropout_output = nn.functional.dropout(x_packed.data, p=0.6, training=True)
x_dropout = nn.utils.rnn.PackedSequence(dropout_output, x_packed.batch_sizes)