前言
最近在由 TensorFlow 迁移至 Pytorch, 不得不说,真的香啊。 在写模型的时候发现 Pytorch 中处理变长序列与 TensorFlow 有很大的不同, 因此此处谈谈我自己的理解。
此外, 我对 LSTM, GRU 进行了二次加工, 将对变长序列的处理封装到内部细节中,感兴趣的可以看看:NLP-Pytorch
从 LSTM 谈起[1]
首先, 注意到这里LSTM的计算公式与我们常见的LSTM有所区别,虽然区别不大,但还是要提一下,因为后面的参数初始化会有所不同:
class torch.nn.LSTM(*args, **kwargs)
-- 参数列表:
-- input_size: x 的特征维度
-- hidden_size: 隐层的特征维度
-- num_layers: LSTM 层数,默认为1
-- bias: 是否采用 bias, 如果为False,则不采用。默认为True
-- batch_first: True, 则输入输出的数据格式为 [batch_size, seq_len, feature_dim],默认为False
-- dropout: dropout会在除最后一层外都进行dropout, 默认为0
-- bidirectional: 是否采用双向,默认为False
-- 输入数据:
-- input: [seq_len, batch_size, input_size], 输入的特征矩阵
-- h_0: [num_layers * num_directions, batch_size, hidden_size], 初始时 h 状态, 默认为0
-- c_0: [num_layers * num_directions, batch_size, hidden_size], 初始时 cell 状态, 默认为0
-- 输出数据:
-- output: [seq_len, batch_size, num_directions * hidden_size], 最后一层的所有隐层输出
-- h_n : [num_layers * num_directions, batch, hidden_size], 所有层的最后一个时刻隐层状态
-- c_n : [num_layers * num_directions, batch, hidden_size], 所有层的最后一格时刻的 cell 状态
-- W,b参数:
-- weight_ih_l[k]: 与输入x相关的第k层权重 W 参数, W_ii, W_if, W_ig, W_io
-- weight_hh_l[k]: 与上一时刻 h 相关的第k层权重参数, W_hi, W_hf, W_hg, W_ho
-- bias_ih_l[k]: 与输入x相关的第k层 b 参数, b_ii, b_if, b_ig, b_io
-- bias_hh_l[k]: 与上一时刻 h 相关的第k层 b 参数, b_hi, b_hf, b_hg, b_ho
需要注意的一点是, LSTM中所有的W,b 参数默认采用均匀分布 :
, 有一些初始化方法能够加速收敛过程, 因此,很多情况下我们需要自己初始化这些参数,我在 NLP-Pytorch: LSTM 对LSTM 进行了简要封装。
对比 GRU [1]
同样为了更鲜明的表明参数的初始化, 这里将 GRU 搬过来:
class torch.nn.GRU(*args, **kwargs)
-- 参数列表:与 LSTM 的一致, 不赘述了
-- 输入序列:input, h_0; 与 LSTM 差不多,只是省略了 cell 状态
-- 输出序列:output, h_n; 与 LSTM 差不多,只是省略了 cell 状态
-- W,b参数:
-- weight_ih_l[k]: 与输入x相关的第k层权重 W 参数, W_ir, W_iz, W_in
-- weight_hh_l[k]: 与上一时刻 h 相关的第k层权重参数, W_hr, W_hz, W_hn
-- bias_ih_l[k]: 与输入x相关的第k层 b 参数, b_ir, b_iz, b_in
-- bias_hh_l[k]: 与上一时刻 h 相关的第k层 b 参数, b_hr, b_hz, b_hn
与LSTM 一样, W,b参数的初始化默认采用均匀分布 :
如何处理变长序列?[2]
我们知道,在文本的处理过程中,句子的长度是不一的,对于这种数据,我们往往采用 将每个句子扩充到一样的长度, 然后我们就可以使用LSTM或GRU来处理了。
但仔细一想, 又有些不对,我们将句子扩充,那么扩充的信息必然会对我们的结果产生影响,这与我们正常的思路完全不同,虽然我自己做比较实验表明,二者之间差距并不明显,但我个人认为这是数据集与初始化的关系。
那么,Pytorch 中如何处理这种变长的情况,去掉 呢,答案就是 torch.nn.utils.rnn.pack_padded_sequence()以及 torch.nn.utils.rnn.pad_packed_sequence()
压缩序列
压缩序列所使用的API为torch.nn.utils.rnn.pack_padded_sequence(), 其目的是将多余的 去除,获得一个干净的,最初的序列。
pack_padded_sequence(...)
-- 参数列表:
-- input: 有 的 batch 序列
-- lengths: input 中每个序列的长度
-- batch_first: 如果为True, input 必须是 [batch_size, seq_len, input_size], 参见LSTM
-- enforce_sorted: 如果为True, 那么 input 中的序列需要按照 长度递减排列
-- 返回值:
一个 PackedSequence 对象
我们获得干净的序列之后,就可以将其放入 LSTM 中了, 具体可参见我的实现:NLP-Pytorch: LSTM
解压序列
我们通过 LSTM 对压缩后的序列处理后,还需要将压缩后的信息解压缩,本质上是将数据从PackedSequence 类型转化为Tensor ,此部分主要是做 Attention 的时候会用到。
pad_packed_sequence(...)
-- 参数列表:
-- sequence: 一个PackedSequence 对象
-- batch_first:
-- padding_value: padding 该序列的values
-- total_length: Padding 到多长, 一般为None
-- Returns:
-- output: 有 padding 信息的序列输出
-- output_lengths: 每个序列没有Padding之前的长度
最后
最后,知乎上有一个很有趣的问题:你在训练RNN的时候有哪些特殊的trick, 十分值得一看,感兴趣的可以自己做一做相关的实验,ok, 就酱。
Reference