1. 引言
RNN模型一般设定固定的文本长度(text sequence length,可理解为文本序列在时间维度上的步数 time step),以保证网络输出层数据维度的一致性。但在训练和测试时,难以保证输入文本长度的一致性,因此常常需要截断操作(即将超过预设长度的文本截断)和pad操作(即对不足预设长度的文本进行补0填充)。pad操作需满足:
(1)pad后,不足预设长度的文本用相同特征维度的0填充;
(2)pad的部分不参与forward和backward计算。
Pytorch中,在文本数据的transfrom以及RNN网络的输入阶段,均充分考虑了pad操作。其主要体现在:
(1)RNN、LSTM和GRU等网络的输入数据均可为PackedSequence
类型数据;
(2)可通过pad_sequence
、pack_sequence
、pack_padded_sequence
和pad_packed_sequence
等操作,实现pad和pack操作。
2. pack和pad操作
那么,究竟pad和pack操作对原始数据会有何影响?下面通过一个简单的示例来体现。
from torch.nn.utils.rnn import pack_sequence, pad_sequence,pad_packed_sequence, pack_padded_sequence,
text1 = torch.tensor([1,2,3,4]) # 可视为有4个文字的样本
text2 = torch.tensor([5,6,7]) # 可视为有3个文字的样本
text3 = torch.tensor([8,9]) # 可视为有2个文字的样本
sequences = [text1, text2, text3] # 三个文本序列
2.1 pack操作
[Input] pack_sequence(sequences)
[Output] PackedSequence(data=tensor([1, 5, 8, 2, 6, 9, 3, 7, 4]), batch_sizes=tensor([3, 3, 2, 1]))
pack操作将原来的二维数据(batch*sequence)进行了压缩,但其排列是按照列(即sequence的顺序)进行排列,每个时间步一次性输出batch上的所有样本。
pack后的返回值包括两数据。一类为data
,即压缩后的数据;而另一类batch_sizes
表示每个时间步,batch中包含的样本量。
拿本例来说,因为总共有4个时间步(即文本序列长度为4),所以batch_sizes
的长度为4。在第1、2个时间内步,所有样本均有编码,所有对应值为3,而在第三个时间步,只有前两个样本有编码,所以对应值为2。
值的注意的是,sequences
列表内的各元素长度必须按照降序排列,也就是越长的文本应放在前面,整个Batch*Sequence矩阵为上三角阵。
前文提到的RNN网络中可以接收的Input
数据可以为PackedSequence
类型数据,即是类似于这里的返回值。
2.2 pad操作
[Input] pad_sequence(sequences)
[Output] tensor([[1, 5, 8],
[2, 6, 9],
[3, 7, 0],
[4, 0, 0]])
[Input] pad_sequence(sequences, batch_first=True)
[Output] tensor([[1, 2, 3, 4],
[5, 6, 7, 0],
[8, 9, 0, 0]])
pad操作即是将不同长度的文本序列进行对齐的填充过程。默认情况下,参数batch_first=False
,其返回值的第一个维度将变成sequence
,而第二个维度才为batch
。当然,可以通过设置batch_first=True
,使得返回值的第一个维度为batch
,从而保持与输入值的一致性。
与pack
操作不同,pad
操作对于sequences
列表内的各元素长度顺序并无要求。
2.3 关于batch顺序的思考
观察上述pack和pad操作,返回结果均倾向于按照序列sequece的顺序进行输出,而将batch的输出顺序后置,其实这是pytorch中整个RNN网络的统一推荐用法,观察RNN
、LSTM
和GRU
等网络架构,参数batch_first
的默认值均为False
!
这里简单对比下文本数据在batch_first=True
下的数据维度 [Batch, Sequence, Features] 和在batch_first=False
下的**[Sequence, Batch, Features]**两种数据排列方式的异同。
我们可以将第一个维度视为循环维度。对于前者,其循环项为Batch内的每一个样本,因此无法同时喂入RNN网络处理;而对于后者,其循环项为时间步,可以一次性将每个时间步内的全量样本喂入RNN网络处理。所以后者这种cross-batch的方式更为推荐。不过pytorch内做了处理,无论将batch_first
设为True或者False,其内部计算时均采用每个时间步cross-batch的并行方式,但需要注意网络模型的batch_first
设置应与输入数据的batch
维度保持一致性。
3. pack_padded_sequence和pad_packed_sequence
因为RNN网络可以接受的是PackedSequence
类型数据(通过pack操作实现),而pad操作又可以实现不等长文本的填充对齐,所以自然会想到将两个操作联合起来,这就是pytorch提供的pack_padded_sequence
和pad_packed_sequence
功能。
3.1· pack_padded_sequence
顾名思义,将经pad后的文本序列在做pack,从而实现对文本缺失位置的填0和维度压缩。
[Input] pack_padded_sequence(pad_sequence(sequences,batch_first=True),lengths=[4,3,3], batch_first=True)
[Output] PackedSequence(data=tensor([1, 5, 8, 2, 6, 9, 3, 7, 0, 4, 0]), batch_sizes=tensor([3, 3, 3, 2]))
pack_padded_sequence
函数的作用过程可分解为如下步骤:
(1)接收一个padded_sequence
数据;
(2)根据batch_first
参数明确该数据的布局(默认为batch_first=False
);
(3)根据lengths
参数明确batch
内各样本的时间步长,选择数据;
(4)将上述数据按照时间维度进行压缩,得到目标的PackedSequence
类型数据
右上可见,在pack_padded_sequence
函数有两个重要参数:
-
batch_first
:用于明确输入的padded_sequence
数据的布局型式 -
lengths
:用于明确batch内各样本截取的时间长度列表,注意列表内的元素必须为降序
再看一个例子:
[Input] pack_padded_sequence(pad_sequence(sequences,batch_first=True),lengths=[3,2,2,2])
[Output] PackedSequence(data=tensor([1, 2, 3, 4, 5, 6, 7, 0, 8]), batch_sizes=tensor([4, 4, 1]))
是否和想象的不一样?下面来图解下其具体的变换过程:
其与之前例子的最大区别在于batch_first
取默认值False
,因此即使传进来的数据本身是Batch*Sequence的布局,但对于pack_padded_sequence
函数会将其认为是Sequence*Batch的,因此其lenghts
参数的长度必须为4(错误的认为是4个样本),而返回的batch_sizes
的长度为3(错误的认为只有3个时间步长)!
总体来看,在使用pack_padded_sequence
需要牢记各参数的意义(见下图),其中最重要的就是要保证pad_sequence
中batch_first
参数与pack_padded_sequence
中batch_first
参数的一致性!
3.2· 再看pack操作
观察pack_sequence
源码,不难发现,其本质就是调用了pack_padded_sequence
函数:
def pack_sequence(sequences):
return pack_padded_sequence(pad_sequence(sequences), [v.size(0) for v in sequences])
注意到其在pad
和pack
阶段都未设置batch_first
参数(即batch_first=False
),而lenghts
设为各文本的真实长度,所以保证了pack_sequence
的有效性。
3.3· pad_packed_sequence
pad_packed_sequence
函数即为pack_padded_sequence
的逆操作,其在参数设定时也许注意通过batch_first
控制返回值的维度顺序,同时可通过设置total_lengths
来控制pad
后的总步长(该值必须不小于输入PackedSequence
数据的步长数)。
[Input] pad_packed_sequence(pack_sequence(sequences),total_length=5,batch_first=True)
[Output] (tensor([[1, 2, 3, 4, 0],
[5, 6, 7, 0, 0],
[8, 9, 0, 0, 0]]), tensor([4, 3, 2]))
4. 小结
在RNN网络中,文本的pad操作用于各文本长度的对其;而pack操作用于实现文本序列数据的压缩。在使用时,需要注意如下事项:
(1)参数batch_first
用于设定文本数据的布局,在进行pad、pack以及RNN网络训练时,需结合输入端和输出端的要求,进行明确和统一;
(2)参数bacth_sizes
指沿着时间维度,每个时间步的所包含的样本数量;
(3)参数lengths
指沿着样本维度,每个样本需要截断的时间步长;
(4)在实际操作中,最常用的是通过pad_packed_sequence
h和pack_padded_sequence
实现对文本的填充和相互转化。
[References]
1.pytorch中LSTM的细节分析理解