【个人整理】循环神经网络RNN对于变长序列的处理方法

【个人整理】循环神经网络RNN对于变长序列的处理方法

前言:我们前面将的各种循环神经网络的实现都是固定的时间步长的,即timesteps的长度是固定的,但是在实际问题中,这个往往是不固定的,为什么呢?因为文本数据在处理的时候,由于各样本的长度并不一样,有的句子长有的句子短 这就导致在timesteps上面是维度不固定的,这种数据该如何处理呢?这就是本文要说的重点了。

目录

一、大胆尝试,直接填充0行不行?

二、tensorflow对于变长序列的处理

三、keras对于变长序列的处理

    3.1 使用Embedding层来实现Word2vector的转化

    3.2 使用embedding层来实现变长序列输入

        3.2.1 当关键字参数mask_zero=False(即默认值)

        3.2.2 当关键字参数mask_zero=True

    3.3 使用Masking层来处理变长序列

    3.4 关于Masking层和SimpleRNN层的理解

        3.4.1 Masking层(其实就是覆盖层)

        3.4.2 SimpleRNN层

     3.5 keras中Embedding层和Masking层实现变长序列的输入的异同点

一、大胆尝试,直接填充0行不行?

比如我有下面的数据X

train_X = np.array([
[[0, 1, 2], [9, 8, 7],[3,6,8]], 
[[3, 4, 5]], 
[[6, 7, 8], [6, 5, 4],[1,7,4]], 
[[9, 0, 1], [3, 7, 4]], 
])
'''
样本数据为(samples,timesteps,features)的形式,其中samples=4,features=3,
但是很明显timesteps是不一样的,它们分别是3,1,3,2
'''

首先确定的是直接将这个train_X放入到RNN里面去,肯定是不行的,因为数据的维度是不同意的,肯定会报错,那我现在简单地处理一下,我都已经最长的timestep作为参考,将不足的全部补充为零,则得到如下样本:

train_X = np.array([
[[0, 1, 2], [9, 8, 7],[3,6,8]], 
[[3, 4, 5], [0, 0, 0],[0,0,0]], 
[[6, 7, 8], [6, 5, 4],[1,7,4]], 
[[9, 0, 1], [3, 7, 4],[0,0,0]], 
])

'''
#样本数据为(samples,timesteps,features)的形式,其中samples=4,timesteps=3,features=3,其中第二个、第四个样本是只有一个时间步长和二个时间步长的,这里自动补零
'''

现在我们搭建一个基本的RNN运算图,代码如下:

import tensorflow as tf
import numpy as np
import pprint
    
#  tensorflow处理变长时间序列的处理方式,首先每一个循环的cell里面有5个神经元
X=tf.placeholder(tf.float32,shape=[None,3,3])
basic_cell=tf.nn.rnn_cell.BasicRNNCell(5)

outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32)

train_X = np.array([
[[0, 1, 2], [9, 8, 7],[3,6,8]], 
[[3, 4, 5], [0, 0, 0],[0,0,0]], 
[[6, 7, 8], [6, 5, 4],[1,7,4]], 
[[9, 0, 1], [3, 7, 4],[0,0,0]]
])
#样本数据为(samples,timesteps,features)的形式,其中samples=4,timesteps=3,features=3,其中第二个、第四个样本是只有一个时间步长的,这里自动补零


with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    outputs_val, states_val = sess.run(
        [outputs, states], feed_dict={X:train_X})
    
    pprint.pprint(outputs_val)
    pprint.pprint(states_val)
    print('==============================================')
    print(np.shape(outputs_val))
    print(np.shape(states_val))


运算结果如下:

array([[[ 0.19004992, -0.5324124 , -0.23640487, -0.78909075,-0.89586043],
        [ 0.9995407 , -0.8933996 ,  0.14939463, -0.49506482,-1.        ],
        [ 0.97084224, -0.9788352 , -0.39607894, -0.9992078 ,-0.99999964]],

       [[ 0.9344785 , -0.8359945 , -0.2250551 , -0.9137756 ,-0.99995327],
        [ 0.08450726, -0.1975519 ,  0.05329951, -0.33512276,-0.3200547 ],
        [-0.02103201,  0.02070498,  0.00529744, -0.11833917,-0.07321271]],

       [[ 0.99663454, -0.94903105, -0.21364392, -0.96614444,-1.        ],
        [ 0.9939659 , -0.6614185 ,  0.29727104, -0.24063313,-0.99999666],
        [ 0.99651307, -0.9850747 , -0.7920838 , -0.98904556,-0.99953353]],

       [[ 0.6827501 ,  0.96307445,  0.9809377 ,  0.99099195,-0.99995494],
        [ 0.99678046, -0.97459066,  0.24722673, -0.98984957,-0.9999654 ],
        [ 0.05819325,  0.12137431,  0.21952726, -0.54427665,-0.4205857 ]]], 
      dtype=float32)

array([[ 0.97084224, -0.9788352 , -0.39607894, -0.9992078 , -0.99999964],
       [-0.02103201,  0.02070498,  0.00529744, -0.11833917, -0.07321271],
       [ 0.99651307, -0.9850747 , -0.7920838 , -0.98904556, -0.99953353],
       [ 0.05819325,  0.12137431,  0.21952726, -0.54427665, -0.4205857 ]],
      dtype=float32)
==============================================
(4, 3, 5)
(4, 5)

从结果上来看这当然是没有问题的,也得到了我们想要的数据维度,但是这是不行的,为什么?

如果就这么简单,不足的地方就补0,那就实在是太简单了,更重要的是,在自然语言处理的时候,我补的0其实也是有特殊的含义的,这就相当于我篡改了一句话的原始意思,这当然是不好的。因为我填充的0也是会参与运算的。

结论:直接填充0,在数据运算上没有问题,但是从序列的整个含义来说,这是不合理的,所以一般情况下不能这么做。

下面介绍不同模型在处理padding上的不同操作,那到底该怎么做呢?这里以tensorflow和keras框架为例加以说明。

二、tensorflow对于变长序列的处理

看过我上一篇文章《【个人整理】tensorflow关于循环神经网络(RNN)的输出与状态的“维度”分析》的应该还有影响,后面在解释dynamic_rnn的参数的时候有一个 sequence_length 的参数,它就是专门用来处理变长序列的,具体怎么做,看代码:

import tensorflow as tf
import numpy as np
import pprint

#样本数据为(samples,timesteps,features)的形式,其中samples=4,features=3,timesteps不固定,第二个样本只有一个步长,第四个样本只有2个步长
train_X = np.array([
[[0, 1, 2], [9, 8, 7],[3,6,8]], 
[[3, 4, 5]], 
[[6, 7, 8], [6, 5, 4],[1,7,4]], 
[[9, 0, 1], [3, 7, 4]]
])
#样本数据为(samples,timesteps,features)的形式,其中samples=4,timesteps=3,features=3,其中第二个、第四个样本所缺的样本自动补零
train_X = np.array([
[[0, 1, 2], [9, 8, 7],[3,6,8]], 
[[3, 4, 5], [0, 0, 0],[0,0,0]], 
[[6, 7, 8], [6, 5, 4],[1,7,4]], 
[[9, 0, 1], [3, 7, 4],[0,0,0]]
])

#  tensorflow处理变长时间序列的处理方式,首先每一个循环的cell里面有5个神经元
basic_cell=tf.nn.rnn_cell.BasicRNNCell(5)

#创建一个容纳训练数据的容器placeholder
X=tf.placeholder(tf.float32,shape=[None,3,3])

# 构建一个向量,这个向量专门用来存储每一个样本中的timesteps的数目,这个是核心所在
seq_length = tf.placeholder(tf.int32, [None])

#在使用dynamic_rnn的时候,传递关键字参数 sequence_length
outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32,sequence_length=seq_length)

#实际上就是没一个样本的步长数所组成的一个数组
seq_length_batch = np.array([3, 1, 3, 2])

#在我们运行RNN的时候,需要将输入X和样本长度seq_length都传输进去,如下:
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    outputs_val, states_val = sess.run(
        [outputs, states], feed_dict={X:train_X,seq_length:seq_length_batch})
    
    pprint.pprint(outputs_val)
    pprint.pprint(states_val)
    print('==============================================')
    print(np.shape(outputs_val))
    print(np.shape(states_val))


测试得到的结果为:

array([[[ 0.19467512, -0.79010636, -0.57950014,  0.6630075 ,-0.391196  ],
        [ 0.9999744 , -1.        ,  0.99997497,  0.12559196,-0.9973965 ],
        [ 0.9755946 , -0.9999939 ,  0.8523646 ,  0.9613497 ,-0.9103804 ]],

       [[ 0.9631605 , -0.99987656,  0.6419198 ,  0.85546035,-0.8805427 ],
        [ 0.        ,  0.        ,  0.        ,  0.        ,0.        ],
        [ 0.        ,  0.        ,  0.        ,  0.        ,0.        ]],

       [[ 0.9989558 , -0.99999994,  0.9749926 ,  0.94184864,-0.9817268 ],
        [ 0.9955585 , -0.9999952 ,  0.99994856,  0.25942597,-0.9250224 ],
        [ 0.79531014, -0.9992963 ,  0.99106103, -0.82377946,0.9658859 ]],

       [[ 0.9995462 , -0.99997026,  0.9998705 ,  0.7101562 ,-0.9996873 ],
        [ 0.95413935, -0.99994653,  0.99955887, -0.7478514 ,0.759941  ],
        [ 0.        ,  0.        ,  0.        ,  0.        ,0.        ]]], dtype=float32)


array([[ 0.9755946 , -0.9999939 ,  0.8523646 ,  0.9613497 , -0.9103804 ],
       [ 0.9631605 , -0.99987656,  0.6419198 ,  0.85546035, -0.8805427 ],
       [ 0.79531014, -0.9992963 ,  0.99106103, -0.82377946,  0.9658859 ],
       [ 0.95413935, -0.99994653,  0.99955887, -0.7478514 ,  0.759941  ]],
      dtype=float32)
==============================================
(4, 3, 5)
(4, 5)

原因分析:通过比较上面两个的测试结果,发现一个问题,第二次的结果中,第二组样本、第四组样本,分别出现了两个全为0的,一个全为0的,这是为什么呢?

这个地方我看见很多地方都解释错误了,就拿第二组样本来说,因为第二组样本来说,因为第二个时刻和第三个时刻的样本都是0,所以对应的输出也是0,很多人说这是因为,传入的X既然都是0,那运算出来不就是0嘛,这种解释是错误的,因为那这不就是上面的测试一中的结果吗?那一中为什么不是0,而且如果是这些0参与了运算,输出的结果也不会是0,为什么,因为RNN在每一个cell里面的运算为:

【个人整理】循环神经网络RNN对于变长序列的处理方法

即使我输入的Xt=1这一个时刻的全部是0,由于前面的状态St=0不是全部为0的,所以Yt=1,Yt=2都是不会为零的,事实上这就是测试一中的结果的由来。

而这个地方之所以第二组样本的后面两个时间点的输出全部为0,第四组样本的第3个时刻输出为零,是因为这一个为0的输入根本就没有参与运算,通过参数

seq_length_batch = np.array([3, 1, 3, 2])

指定了第一组样本计算3个timestep,第二组样本计算1个timestep,第三组样本计算3个timestep,第四组样本计算2个timestep。那个全为0的样本根本就没有参与运算,只不过是一个“空壳子”,为了保持输入数据的维度完整性而存在的

而每一个样本最后的输出状态也不再是固定的第3个时间步的输出了,它会默认对应最后一个参与了运算的时间步的输出,在这里,第一组样本对应的state为第3个timestep的输出、第二组样本对应的state为第1个timestep的输出、第三组样本对应的state为第3个timestep的输出、第四组样本对应的state为第2个timestep的输出,可以参照下面的不同颜色。

array([[[ 0.19467512, -0.79010636, -0.57950014,  0.6630075 ,-0.391196  ],
        [ 0.9999744 , -1.        ,  0.99997497,  0.12559196,-0.9973965 ],
        [ 0.9755946 , -0.9999939 ,  0.8523646 ,  0.9613497 ,-0.9103804 ]],

       [[ 0.9631605 , -0.99987656,  0.6419198 ,  0.85546035,-0.8805427 ],
        [ 0.        ,  0.        ,  0.        ,  0.        ,0.        ],
        [ 0.        ,  0.        ,  0.        ,  0.        ,0.        ]],

       [[ 0.9989558 , -0.99999994,  0.9749926 ,  0.94184864,-0.9817268 ],
        [ 0.9955585 , -0.9999952 ,  0.99994856,  0.25942597,-0.9250224 ],
        [ 0.79531014, -0.9992963 ,  0.99106103, -0.82377946,0.9658859 ]],

       [[ 0.9995462 , -0.99997026,  0.9998705 ,  0.7101562 ,-0.9996873 ],
        [ 0.95413935, -0.99994653,  0.99955887, -0.7478514 ,0.759941  ],
        [ 0.        ,  0.        ,  0.        ,  0.        ,0.        ]]], dtype=float32)

array([[ 0.9755946 , -0.9999939 ,  0.8523646 ,  0.9613497 , -0.9103804 ],
       [ 0.9631605 , -0.99987656,  0.6419198 ,  0.85546035, -0.8805427 ],
       [ 0.79531014, -0.9992963 ,  0.99106103, -0.82377946,  0.9658859 ],
       [ 0.95413935, -0.99994653,  0.99955887, -0.7478514 ,  0.759941  ]],
      dtype=float32)

结论总结:tensorflow中对变长序列的输入是通过在dynamic_rnn()函数中的sequence_length参数来指定的,这个参数是一个一维数组,每一个数组的元素为所对应的那一组样本的timesteps数目,而填充的01根本就没有参与运算,只不过是一个“空壳子”,为了保持输入数据的维度完整性而存在的

还有一个需要注意的,就是那完整的第一组、第三组样本,他们的输出好像也不一样啊,这是因为初始状态和初始全职是随机设置的,这里只进行一次运算,没有进行迭代,自然不一样了。

三、keras对于变长序列的处理

keras相较于tensorflow而言,封装更加高层。使用keras有几种方式可以实现变长序列的输入

3.1 使用Embedding层来实现Word2vector的转化

先看一下Embedding层有什么作用,直接看代码:

import keras as ks
import numpy as np

'''
#这是原始的输入数据,一共四组样本(四个句子),没组样本的时间跨度为3,即timesteps=3,每一个数字表示一个单词
#现在我想把每一个数字(即单词)转化成一个三维向量
#即
4->[#,#,#]
10->[#,#,#]
5->[#,#,#]
2->[#,#,#]
.
..依次下去
'''
input_array=np.array([[4,10,5],[2,1,6],[3,7,9],[2,5,3]])

model = ks.models.Sequential()
model.add(ks.layers.Embedding(100, 3, input_length=3))

model.compile('rmsprop', 'mse')
output_array = model.predict(input_array)
print(output_array)
print('==========================================')
print(np.shape(output_array))

运行结果如下:

[[[ 0.00993429  0.04398254 -0.0125849 ]
  [-0.02356382 -0.02504394 -0.01354498]
  [-0.02061195  0.01945392 -0.02339504]]

 [[ 0.03188958  0.02244325  0.04611005]
  [-0.02672956  0.02242425  0.0270121 ]
  [-0.03706253  0.03849537  0.02586788]]

 [[ 0.01334128  0.0285162  -0.04092228]
  [ 0.01520776  0.04370985 -0.00255948]
  [-0.03107758 -0.03339813 -0.02454655]]

 [[ 0.03188958  0.02244325  0.04611005]
  [-0.02061195  0.01945392 -0.02339504]
  [ 0.01334128  0.0285162  -0.04092228]]]
==========================================
(4, 3, 3)

现在我的输入样本数据变成了和前面的例子中一样的(samples,timesteps,features)这个形式了,其中

samples=4,timesteps=3,features=3

下面来看一下Embedding层的参数定义:

keras.layers.Embedding(input_dim, output_dim, embeddings_initializer='uniform', embeddings_regularizer=None, activity_regularizer=None, embeddings_constraint=None, mask_zero=False, input_length=None)
  • input_dim: int > 0。词汇表大小, 即,最大整数 index + 1。这个是整个词汇表的最大数量,本例中写的500,是随便写的,一般这个数据需要经过分词之后通过统计词的数量求得
  • output_dim: int >= 0。词向量的维度。即每一个单词表示成多少维向量,这里是3
  • embeddings_initializer: embeddings 矩阵的初始化方法 (详见 initializers)。
  • embeddings_regularizer: embeddings matrix 的正则化方法 (详见 regularizer)。
  • embeddings_constraint: embeddings matrix 的约束函数 (详见 constraints)。
  • mask_zero: 是否把 0 看作为一个应该被遮蔽的特殊的 "padding" 值。 这对于可变长的 循环神经网络层 十分有用。 如果设定为 True,那么接下来的所有层都必须支持 masking,否则就会抛出异常。 如果 mask_zero 为 True,作为结果,索引 0 就不能被用于词汇表中 (input_dim 应该与 vocabulary + 1 大小相同)。
  • input_length: 输入序列的长度,当它是固定的时。 如果你需要连接 FlattenDense 层,则这个参数是必须的 (没有它,dense 层的输出尺寸就无法计算)。这其实就是所谓的timesteps,只不过换了一个说法,这里取值为3

注意:这里关键的参数是mask_zero,后面再详说。

3.2 使用embedding层来实现变长序列输入

上面的例子中,由于原始输入就没有缺少,每一个的长度都是固定的3,只是为了看一下Embedding层的作用是什么,现在假设有一些不定长的数据,我同样需要做的就是填充0,代码如下:

import keras as ks
import numpy as np

'''
#这是原始的输入数据,一共四组样本(四个句子),没组样本的时间跨度不一样,
第一组为3
第二组为1
第三组为3
第四组为2
'''
input_array=np.array([[4,10,5],[2],[3,7,9],[2,5]])

'''
#使用序列与处理函数pad_sequences()填充0,
这里的maxlen=3就是指timesteps最大为3,不足的全部补充0,
post表示在后面填充,pre表示在前面填充,默认为pre
'''
X=ks.preprocessing.sequence.pad_sequences(input_array,maxlen=3,padding='post')
print(X)
print('==========================================')

model = ks.models.Sequential()
model.add(ks.layers.Embedding(100, 3, input_length=3))

model.compile('rmsprop', 'mse')
output_array = model.predict(X)
print(output_array)
print('==========================================')
print(np.shape(output_array))

运行结果为:

[[ 4 10  5]
 [ 2  0  0]
 [ 3  7  9]
 [ 2  5  0]]
==========================================
[[[-0.04528923 -0.00923926  0.01403354]
  [ 0.01515898  0.01483223 -0.00947972]
  [-0.01472244 -0.02926698  0.03155271]]

 [[ 0.02277514  0.02044438  0.03303898]
  [-0.0392134  -0.00636433 -0.02115151]
  [-0.0392134  -0.00636433 -0.02115151]]

 [[ 0.00736283  0.04576111  0.01646307]
  [-0.00783598  0.0376038  -0.03824564]
  [-0.0125035  -0.04934802  0.00373475]]

 [[ 0.02277514  0.02044438  0.03303898]
  [-0.01472244 -0.02926698  0.03155271]
  [-0.0392134  -0.00636433 -0.02115151]]]
==========================================
(4, 3, 3)

从上面我们发现,虽然数据维度上现在已经没有问题了,但是,通过Embedding后,单词0所对应的向量并不是全为0呢?

事实上,这就是这个地方的一个误区之一,此处在使用Embedding层的时候并没有传递关键字参数mask_zero=True,很多人觉得如果是传入了关键字参数mask_zero=True,那么0所对应的单词转化成向量之后应该全部是0,这是不正确的,这个地方不管有没有传入关键字参数mask_zero=True,输出的结果都是一样的,单词0都不会全部为0向量。

既然关键字参数mask_zero=True有没有都一样,那还需要他有什么用?这其实是后面的使用要用到的地方,因为Embedding层只能放在网络的第一层,用来对数据进行处理,当后面要跟循环层的时候,关键字参数mask_zero=True就发挥出作用了。

3.2.1 当关键字参数mask_zero=False(即默认值)

此时的代码如下:

import keras as ks
import numpy as np

'''
#这是原始的输入数据,一共四组样本(四个句子),没组样本的时间跨度为3,即timesteps=3,每一个数字表示一个单词
#现在我想把每一个数字(即单词)转化成一个三维向量
#即
4->[#,#,#]
10->[#,#,#]
5->[#,#,#]
2->[#,#,#]
.
..依次下去
'''
input_array=np.array([[4,10,5],[2],[3,7,9],[2,5]])
X=ks.preprocessing.sequence.pad_sequences(input_array,maxlen=3,padding='post')
print(X)

model = ks.models.Sequential()
model.add(ks.layers.Embedding(100, 3, input_length=3,mask_zero=False))
rnn_layer=ks.layers.SimpleRNN(5,return_sequences=True)
model.add(rnn_layer)


model.compile('rmsprop', 'mse')
output_array = model.predict(X)
print(output_array)

运行结果为:

[[[-0.01507465 -0.0192386  -0.03286014  0.0430268  -0.01492362]
  [ 0.04894346 -0.03795945 -0.03045793 -0.06069282  0.00017872]
  [-0.04956069  0.03926758 -0.02741077 -0.02429961 -0.00730546]]

 [[-0.01881071  0.00454673 -0.01398094  0.02748031 -0.02079656]
  [ 0.0318772  -0.03267315 -0.01361655 -0.02769214  0.00790467]
  [-0.02251629  0.03369562 -0.00906445 -0.04744482  0.00141174]]

 [[ 0.00473799 -0.0216711  -0.04201381 -0.01117982  0.0191639 ]
  [-0.00850029  0.01261848 -0.06416406  0.02927677 -0.03776023]
  [ 0.03353037 -0.04458039 -0.05099481 -0.01612281 -0.02791761]]

 [[-0.01881071  0.00454673 -0.01398094  0.02748031 -0.02079656]
  [ 0.03428226 -0.0450107  -0.00934676 -0.00833553  0.00501254]
  [-0.01707621  0.02320341 -0.00025231 -0.06497627 -0.00428138]]]

从上面可以看出,第二组样本、第四组样本的补充的单词0,也是参与了循环层的运算了的,这改变了输入的句子的意思。

3.2.2 当关键字参数mask_zero=True

代码如下,只需要修改上面代码的一个地方,将false改为True即可:

model.add(ks.layers.Embedding(100, 3, input_length=3,mask_zero=True))

运行结果如下:

[[[-0.00566185 -0.01395454  0.03897382 -0.00447031 -0.01689496]
  [ 0.01163974 -0.007847    0.00704868  0.0319964  -0.01156033]
  [ 0.02103921  0.03141655  0.01596024  0.00670511 -0.05503707]]

 [[ 0.015795   -0.02212714 -0.00166886 -0.00120822  0.01417502]
  [ 0.015795   -0.02212714 -0.00166886 -0.00120822  0.01417502]
  [ 0.015795   -0.02212714 -0.00166886 -0.00120822  0.01417502]
]

 [[ 0.04398683 -0.02056707  0.012842   -0.00317691 -0.02015743]
  [-0.05454749  0.03934489 -0.02353742  0.03340311 -0.0235149 ]
  [ 0.02906755  0.07557297  0.0048439  -0.00078752 -0.00623714]]

 [[ 0.015795   -0.02212714 -0.00166886 -0.00120822  0.01417502]
  [ 0.01466416 -0.00909013  0.00990194 -0.01179877 -0.05580193]
  [ 0.01466416 -0.00909013  0.00990194 -0.01179877 -0.05580193]
]]

从上面我们发现,第二组样本、第四组样本他们的单词0,并没有参与运算,输出就是前一个时间步的输出。

总结:Embedding的关键字参数mask_zero=True不会改变Word2vector的结果,即不是讲所有补充的0全部变为0向量,这个很重要,关键字参数mask_zero=True的作用是决定了后面的“循环层”是否会将补充的0单词参与运算,如果设置为True,就是“覆盖掉0”的意思,自然就不参与运算,如果设置为False,就是不覆盖0,0也会参与运算的意思。

补充:既然Word2vector之后的结果根本就不是0向量,那后面循环层在运算的时候怎么会知道哪个是补充的单词0,哪个不是补充的单词0?

事实上,在对每一个单词进行Word2vector的时候,会存在一个单词与向量之间的映射关系在里面,比如

3->[#,#,#]

4->[#,#,#]

5->[#,#,#]

0->[#,#,#]

所以即使向量并不是0向量,但是哪个与单词0对应的向量依然是清楚地,所以只要有这样一个映射关系,循环层在遇到与0相对应的向量时,就“覆盖掉不参与运算”,这就是其中的原理了。后面要介绍的Masking层实际上也是要达到同样的效果,二者异曲同工。

3.3 使用Masking层来处理变长序列

直接看代码:

import keras as ks
import numpy as np

#样本数据为(samples,timesteps,features)的形式,其中samples=4,features=3,timesteps不固定,第二个样本只有一个步长,第四个样本只有2个步长
train_X = np.array([
[[0, 1, 2], [9, 8, 7],[3,6,8]], 
[[3, 4, 5]], 
[[6, 7, 8], [6, 5, 4],[1,7,4]], 
[[9, 0, 1], [3, 7, 4]]
])

#样本数据为(samples,timesteps,features)的形式,其中samples=4,timesteps=3,features=3,其中第二个、第四个样本所缺的样本自动补零
train_X = np.array([
[[0, 1, 2], [9, 8, 7],[3,6,8]], 
[[3, 4, 5], [0, 0, 0],[0,0,0]], 
[[6, 7, 8], [6, 5, 4],[1,7,4]], 
[[9, 0, 1], [3, 7, 4],[0,0,0]]
])


model = ks.models.Sequential()

#添加一个Masking层,这个层的input_shape=(timesteps,features)
model.add(ks.layers.Masking(mask_value=0,input_shape=(3,3)))

#添加一个普通RNN层,注意这里的return_sequence参数,后面会说
rnn_layer=ks.layers.SimpleRNN(5,return_sequences=True)

model.add(rnn_layer)

model.compile('rmsprop', 'mse')
output_array = model.predict(train_X)
print(output_array)
print('==========================================')

运行结果为:

[[[ 0.38400522 -0.07637138  0.80482227  0.5201789   0.8758846 ]
  [ 0.9732032  -0.5962235   0.5779228  -0.6468719   0.99999994]
  [ 0.9232968   0.3515737   0.9787954   0.7505297   0.9999945 ]]

 [[ 0.8733732  -0.33613908  0.962015    0.17185715  0.9998116 ]
  [ 0.8733732  -0.33613908  0.962015    0.17185715  0.9998116 ]
  [ 0.8733732  -0.33613908  0.962015    0.17185715  0.9998116 ]
]

 [[ 0.9796784  -0.5531762   0.993092   -0.22548307  0.9999997 ]
  [ 0.69188297  0.36132666 -0.59311306 -0.58000374  0.9999302 ]
  [ 0.92864347 -0.89851534  0.40440124 -0.99098665  0.99703205]]

 [[ 0.6512652   0.79500616 -0.69901264  0.5302738   0.9928446 ]
  [ 0.9593913  -0.94486046  0.5666493  -0.9513761   0.9993295 ]
  [ 0.9593913  -0.94486046  0.5666493  -0.9513761   0.9993295 ]]]

从上面的结果可以看到,输出的形状依然是(samples,timesteps,num_units)的格式,即(4,3,5),但是前面在使用tensorflow的时候,上面结果中的“蓝色标记”和“红色标记”应该是0才对啊,这里怎么不是0,仔细观察可以发现,原来它们其实也没有经过运算,他跟前面的那个经过运算的是重复的输出,即重复输出最后一个经过运算的时间步的结果,所以依然是正确的,只不过tensorflow中将没经过运算的时间步全部填充0,而keras将没经过运算的时间步全部填充前面重复的输出结果而已

那现在来看一看状态:将上面的程序,修改一句话即可,将:

#添加一个普通RNN层,注意这里的return_sequence参数,后面会说
rnn_layer=ks.layers.SimpleRNN(5,return_sequences=True)

修改为(后面会介绍为什么):

#添加一个普通RNN层,注意这里的return_sequence参数,后面会说
rnn_layer=ks.layers.SimpleRNN(5,return_sequences=False)

结果为:

[[-0.13516335  0.99992615 -0.98370534  0.959703   -0.9999998 ]
 [-0.82386047  0.99106854 -0.9709585   0.96902865 -0.9998157 ]
 [-0.9918523   0.99992615  0.06573126  0.8771769  -0.99996233]
 [-0.99787956  0.9999137  -0.10109197  0.95941854 -0.99998754]]

可以看出结果依然为(samples,num_units),此处即(4,5),本来应该和上面的对应的最后一个时间步的输出是对应一样的,但是由于这是重新运行的,参数初始化不一样,所以不一样而已。

3.4 关于Masking层和SimpleRNN层的理解

3.4.1 Masking层(其实就是覆盖层)

定义如下:

ks.layers.Masking(mask_value=0,input_shape=(timesteps,features))

使用覆盖值覆盖序列,以跳过时间步。

对于输入张量的每一个时间步(张量的第一个维度), 如果所有时间步中输入张量的值与 mask_value 相等, 那么这个时间步将在所有下游层被覆盖 (跳过) (只要它们支持覆盖)。

如果任何下游层不支持覆盖但仍然收到此类输入覆盖信息,会引发异常。

其实就是,因为mask_value=0,所以在每一个全部特征features都是0的那一个timesteps都会被覆盖掉,即像前面tensorflow中介绍的那样,不会对它进行任何计算,即所谓的覆盖掉了。

需要注意的是,Masking层默认是覆盖0,当然还可以屏蔽其它的值,比如我要覆盖1,则mask_value=1即可。

3.4.2 SimpleRNN层

定义如下:

keras.layers.SimpleRNN(units, activation='tanh', use_bias=True, kernel_initializer='glorot_uniform', recurrent_initializer='orthogonal', bias_initializer='zeros', kernel_regularizer=None, recurrent_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, recurrent_constraint=None, bias_constraint=None, dropout=0.0, recurrent_dropout=0.0, return_sequences=False, return_state=False, go_backwards=False, stateful=False, unroll=False)

它有一个很重要的参数,即

return_sequence:

它是布尔值。是返回输出序列中的最后一个输出,还是全部序列。什么意思呢?

  • 如果 return_sequences=True:返回 3D 张量, 尺寸为 (batch_size, timesteps, num_units)。即所谓的输出值;
  • 如果 return_sequences=False:返回 2D 张量,返回尺寸为 (batch_size, num_units) 。即所谓的状态值。

这里和前面tensorflow的实现是一致的。

3.5 keras中Embedding层和Masking层实现变长序列的输入的异同点

(1)不同点

Embedding层和Masking层都有覆盖过滤的功能,但与Masking层不同的是,Embedding它只能过滤0,不能指定其他字符,并且因为是embedding层,它会将序列映射到一个固定维度的空间中。但是Masking层可以通过mask_value关键字参数覆盖过滤掉其它的单词,因此,如果诉求仅仅是让keras中LSTM能够处理边长序列,使用Masking层会比使用Embedding层更加适合。

Embedding层的功能:Word2vector+mask覆盖

Masking层的功能:专门的mask覆盖

(2)相同点

Embedding层和Masking层都看可以实现填充的单词0的覆盖过滤,在使用的时候,后面所添加的循环层一定要支持mask操作(覆盖过滤操作)才行,否则没有办法compile,会报错。一般keras中不使用SimpleRNN()、LSTM()或GRU(),它们都是支持mask操作的,但是更快的CuDNNLSTM()和CuDNNGRU(),这两者是不支持mask的,如果Embedding()的参数mash_zero设为True,那model.compile()时就会报错。