最近在使用Keras和Pytorch处理时间序列数据,在变长数据的输入处理上踩了很多坑。一般的通用做法都需要先将一个batch中的所有序列padding到同一长度,然后需要在网路训练时屏蔽掉padding的值。在pytorch和keras中都有各自对padding值的处理,简单做一下总结。

Keras

  1. 使用Masking层

Keras中自带的屏蔽padding值的方式,在网路结构比较简单时使用很方便。

model = Sequential()
model.add(Masking(mask_value=0., input_shape=(timesteps, features)))
model.add(LSTM(32))

缺点是一些层(Conv1D、Global mean pooling或用户自己实现的层)不支持Masking机制,导致无法compile模型。

2. 添加Masking支持

既然Keras原生的层不支持Masking机制,我们可以重新复现该层,并使其支持Masking。

具体需要继承keras.layers.Layer类,在实现时设置self.supports_masking =True,并实现并修改compute_mask和compute_output_shape函数,使其支持Mask的传递。

具体可以参考github.com/CyberZHG/ker中是如何在Attention中实现Mask支持的

3. 使用keras-trans-mask

无意中发现了这个工具,具体效果没试过,但是看介绍使用起来还比较方便。

Pytorch

  1. pack sequence

同样是Pytorch中标准的处理变长输入的操作,常规步骤是pad_sequence -> pack_padded_sequence -> RNN -> pad_packed_sequence

具体可以参考这个回答,对各个函数解释的很详细。

尹相楠:PyTorch 训练 RNN 时,序列长度不固定怎么办??

zhuanlan.zhihu.com
图标

然而这种方法存在缺点,即如果我们要对RNN内部的运算进行修改时(如自己实现LSTM或GRU),就没办法直接使用pack操作了,这时候就需要用到下面的方法

2. 重新实现AutogradRNN,加入pack支持

类似于Keras,我们同样可以自定义RNN,使其支持pack_padded_sequence输入。然而这一操作过于复杂,需要从AutogradRNN开始重构。

根据官方论坛和github上的issue,目前还没有办法灵活地自定义RNNCell(似乎自定义的RNNCell训练慢也与此有关,现在的官方RNN实现和Cudnn耦合程度过高)。

因为这个方法太过复杂,所以我并没有仔细研究,有兴趣的可以自己参考下面代码实现。

https://github.com/huggingface/torchMoji/blob/master/torchmoji/lstm.py?

github.com

通用方法

  1. 按长度划分Batch

既然问题出在同一个batch中序列长度不一致,那么按照序列长度划分batch就没有这一问题了。然而这一方法存在一些缺点/隐患:

  • batch大小不一致,一些batch中样本过少,导致训练很慢
  • 划分训练集/验证集很麻烦,尤其是K折交叉验证
  • 可能会导致batch之间样本分布不一致,会产生performance上的微妙影响

最后一条我也不是特别确定,只不过在实验时发现了可能会出现这一问题,欢迎大佬解释。

2. 手动屏蔽loss

无论是Masking层还是Pack sequence,本质上都是防止padding操作产生错误的梯度影响网路训练,那么我们只需在计算损失函数时将padding对应的timestep输出和target均置0即可。

个人理解pytorch中的pack sequence操作只是为了提升计算效率,真正屏蔽padding仍然需要在最后计算损失函数时将对应的timestep梯度置0。

以上就是对Keras和Pytorch中处理RNN变长序列输入的方法总结,希望能对他人有所帮助。如果有哪些地方我的理解有误,请及时纠正。


推荐阅读:
相关文章