Keras

Keras中关于Recurrent Network的Padding与Masking

在使用RNN based model处理序列的应用中,如果使用并行运算batch sample,我们几乎一定会遇到变长序列的问题。

通常解决变长的方法主要是将过长的序列截断,将过短序列用0补齐到一个固定长度(例如max_length)。

最后由n个sample组成的dataset能形成一个shape == (n, max_length)的矩阵。然后可以将这个矩阵传递到后续的模型中使用。

然而我们可以很明显,如果用0或者其他整数补齐,势必会影响到模型自身(莫名其妙被输入很多个0,显然是有问题的)。有什么方法能够做到“能够使用一个二维矩阵作为输入数据集,从而达到并行化的同时,还能让RNN模型自行决定真正输入其中的序列的长度”。

和其他很多框架一样,Kears提供了Masking的方法。这个方法要配合Embedding层使用。我们先来看一下Embedding的文档:

Embedding

Arguments

  • input_dim: int > 0. Size of the vocabulary, i.e. maximum integer index + 1.
  • output_dim: int >= 0. Dimension of the dense embedding.
  • embeddings_initializer: Initializer for the embeddings matrix (see initializers).
  • embeddings_regularizer: Regularizer function applied to the embeddings matrix (see regularizer).
  • embeddings_constraint: Constraint function applied to the embeddings matrix (see constraints).
  • mask_zero: Whether or not the input value 0 is a special "padding" value that should be masked out. This is useful when using recurrent layers which may take variable length input. If this is True then all subsequent layers in the model need to support masking or an exception will be raised. If mask_zero is set to True, as a consequence, index 0 cannot be used in the vocabulary (input_dim should equal size of vocabulary + 1).
  • input_length: Length of input sequences, when it is constant. This argument is required if you are going to connect Flatten then Dense layers upstream (without it, the shape of the dense outputs cannot be computed).

其中最关键的就是mask_zero这个参数。当我们构建一个Embedding Layer时,可以设置mask_zero=True。此时你需要让input_dim=|V| + 1。而且要注意,从此以后你的token id从1开始,而id 0则专门留给了padding。如果embedding使用了外部的weight,则需要手动在原有矩阵顶部拼接一行。

下面是一段演示代码:

word_count = 10  # 词表大小
embedding_dim = 3  # embedding 向量维度,为了便于演示,长度设置成3

input_seq = Input(shape=[None])  # 尽管这里是变长序列,但实际上输入还是一个固定大小的序列
embedding_weight = np.vstack([(np.ones(shape=(1, embedding_dim)) * (idx)) for idx in range(word_count + 1)])  # 这里创建了一个 (10 + 1) * 3的矩阵作为embedding的权值。为了方便展示效果,每一行的值都是这行的index。
embedding_layer = Embedding(word_count + 1, embedding_dim, weights=[embedding_weight], mask_zero=True, trainable=False)  # 这里的重点是输入词表大小、mask_zero=True
output_seq = embedding_layer(input_seq)
embedding_model = Model(input=input_seq, output=output_seq)  # 定义了embedding模型:输入序列,直接输出对应的embedding向量。

r_embedding = embedding_model.predict(np.asarray([[1, 2, 0, 0, 0], [2, 0, 0, 0, 0]]))

根据我们的预期,所有index对应等于0的地方,应该不会输出,所以r_embedding里应该是

array([[[1., 1., 1.],
        [2., 2., 2.]],

       [[2., 2., 2.]]], dtype=float32)

然而实际上,代码的执行结果是

array([[[1., 1., 1.],
        [2., 2., 2.],
        [0., 0., 0.],
        [0., 0., 0.],
        [0., 0., 0.]],

       [[2., 2., 2.],
        [0., 0., 0.],
        [0., 0., 0.],
        [0., 0., 0.],
        [0., 0., 0.]]], dtype=float32)

也就是说,这个mask_zero=True并没有生效。

这是由于我们不能只使用Embedding Layer,而是应该将它搭配一个支持Masking的Layer!例如LSTM

# 测试变长输入的reduce

word_count = 10
embedding_dim = 3

input_seq = Input(shape=[None])
embedding_weight = np.vstack([(np.ones(shape=(1, embedding_dim)) * (idx)) for idx in range(word_count + 1)])
embedding_layer = Embedding(word_count + 1, embedding_dim, weights=[embedding_weight], mask_zero=True, trainable=False)
output_seq = embedding_layer(input_seq)

embedding_model = Model(input=input_seq, output=output_seq)

lstm_layer = LSTM(1024, return_sequences=True, return_state=True)  # 构建一个LSTM层,并不需要再指明mask_zero=True了
output_lstm, output_h, output_c = lstm_layer(output_seq)
lstm_model = Model(input=input_seq, output=output_lstm)  # 输出的output_lstm是一个展开的序列。每一个元素都是对应time step的h值。例如[0]表示输入第0个元素后的h,[1]表示输入了第0, 1个元素后的h,[2]表示输入了第0, 1, 2个元素后的h,以此类推

data = np.asarray([[1, 2, 0, 0, 0], [2, 0, 0, 0, 0]])
r_lstm = lstm_model.predict(data)
r_embedding = embedding_model.predict(data)

此时我们看一下l_lstm中的值

array([[[-0.01414275, -0.00412654, -0.00285392, ..., -0.00714629,
          0.00031304,  0.00351544],
        [-0.03493465, -0.01195495, -0.00766455, ..., -0.02046837,
          0.00090127,  0.0104058 ],
        [-0.03493465, -0.01195495, -0.00766455, ..., -0.02046837,
          0.00090127,  0.0104058 ],
        [-0.03493465, -0.01195495, -0.00766455, ..., -0.02046837,
          0.00090127,  0.0104058 ],
        [-0.03493465, -0.01195495, -0.00766455, ..., -0.02046837,
          0.00090127,  0.0104058 ]],

       [[-0.02687557, -0.0085215 , -0.00576595, ..., -0.01428581,
          0.00061124,  0.00697041],
        [-0.02687557, -0.0085215 , -0.00576595, ..., -0.01428581,
          0.00061124,  0.00697041],
        [-0.02687557, -0.0085215 , -0.00576595, ..., -0.01428581,
          0.00061124,  0.00697041],
        [-0.02687557, -0.0085215 , -0.00576595, ..., -0.01428581,
          0.00061124,  0.00697041],
        [-0.02687557, -0.0085215 , -0.00576595, ..., -0.01428581,
          0.00061124,  0.00697041]]], dtype=float32)

在这里,我们会发现,此时输出的结果是一个2 * 5 * 1024的矩阵。2表示两个sample,5表示每个sample输入长度都是5,1024表示h的长度。

以第一个sample为例,输出的r_lstm[0]从第三个元素开始,都和第二个元素一样。这说明,从第三个元素开始,都没有真正地将embedding vector输入到模型中。由此将embedding和支持Masking的Layer输入到一起,是有效果的。

结论:并不能直接使用Embedding的mask_zero参数。而需要配合上支持Masking的Layer(比如LSTM等)。