Keras学习笔记(三)不利用padding方式解决可变长序列问题

  在处理序列数据时,由于我们需要进行批量处理,所以我们要保证每个序列样本都有相同的序列长度。一般文献中给出的方法是padding:即先确定一个序列长度,然后将每个样本都固定到这个长度上,如果原始序列是长于这个值就截断;如果原始序列是短于这个值就补齐(一般补0)。这样做尽管实现了批量处理,但是存在着数据的丢失和噪声的加入,这在一定程度上相当于是修改了原始数据,那么如何才能不用padding方式既保证能进行批量计算(统一shape),又能实现不对数据进行改动呢(尽管改变了序列长度但却仅利用了原始数据信息)?

1. tensorflow中的解决办法
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)

说明:
  · 在本例中tensorflow是通过构建一个seq_lenght向量,这个向量专门用来存储每一个样本中的timesteps的数目,用来告诉模型每一个样本它有几个时间步,它需要计算几个时间步。在使用dynamic_rnn的时候,通过参数 sequence_length=seq_lenght来传递进入模型。
  · 我们可以看到样本2中的第2、3个时间步,样本4中第3个时间步都是padding得来的,当我们为样本2指定应当计算1个时间步,样本4应当计算2个时间步后,tensorflow就不会去计算样本2的第2、3个时间步以及样本4的第3个时间步(尽管它们有对应的输入但是不会被计算)。最后为了保证shape,这些时间步最终的隐藏状态结果将用0表示,这一点从结果中的0行就能发现。也就是说这些时间步的数据尽管在起初的时候被填充了但是它们是不会被用于计算的,它们的结果直接就是用0表示。
  · (重点) 1.tensorflow是通过指定每个样本需要计算的时间步来处理可变长序列数据的。2.输入模型的数据应当被统一长度,在原始数据中填充的值是不会参与运算的,只不过是一个“空壳子”,仅是为了保持输入数据的维度完整性而存在。这样就既能实现shape的统一又能保证只是利用原始数据信息。3.对于输出:没有被计算的时间步它的输出结果是0向量。(注意这点设计与keras中不同)

2. Keras中的解决办法

  Keras中不用padding方式来解决可变长序列问题的地方有两个1.Embedding层2.Masking层。不过Embedding层它仅仅是有这个功能不是一个通用的解决方法,Masking层才是Keras提供的通用解决方法。这里只是为了总结而已。接下来一个个举例说明。

2.1 Embedding层
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=Ture))
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.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来告诉keras将输入中的0进行遮盖(即忽略值为0的这个时间步,不计算它们)。
  · 因为输入是像这样的[2,0,0](第二个样本用0填充了后两个时间步),Embedding中的参数mask_zero它将为0的时间步进行忽略,所以0这个值这个索引不能作为原始数据中的索引比如在nlp中预料库中的单词索引不能是0,应当从1开始,0将被用来padding使用。那么同样模型的输入数据应当被统一长度,不足长度的用0补充。
  · 对于输出,keras中是不参与计算的时间步的结果是用它上一个需要计算的时间步的结果来填充的,这一点可以从输出结果中发现(在第二个样本结果中第2、3时间步的结果与第1时间步的结果一样;在第四个样本结果中第3个时间步的结果与第2个时间步的结果一样)。注意keras的输出设计与tensorflow的输出设计不一样,一个是用上一个有效结果填充,一个是用0向量填充。

2.2 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 ]]]

说明:
  · keras.layers.Masking(mask_value=0),指定mask_value,通过判断mask_value以跳过时间步。对于每一个时间步上的张量, 如果该张量的每一个元素都与mask_value 相等, 那么这个时间步将会被跳过即mask掉。而且这个时间步将在所有下游层被覆盖 (跳过) 。由此我们可以发现1. Masking层与Embeding层一样都是用在模型的首层,我想这也是为啥Embeding层有mask_zero参数而其他层却没有的原因。2.下游层需要支持mask操作,如果任何下游层不支持覆盖这也就意味着它在这个下游层中会去计算那些被padding来的时间步,这是不合理的。所以,如果不支持但仍然收到此类输入覆盖信息,将会引发编译异常。所以我们在自定义层时需要考虑这个层是否支持masking(遮盖、不计算),若不支持则不能用于Masking层的下游层。
  · 实际上输入模型前数据还是需要padding操作,目的是为了维持统一的shape,masking操作可以看成是一种过滤,把padding后的数据按照指定的mask_value来过滤掉特定的值对应的时间步,不让它参与计算,最终被忽略步的结果由其最近邻有效步的结果给出(注意不像TensorFlow是用的0向量给出)。
  · 由于在每一个layer层计算时框架都要去判断哪些时间步需要计算哪些不需要计算结果应怎样给出。这样做会增大计算量,所以有些人还是倾向于选择用padding方式来解决可变长问题,不忽略任何一个时间步,去计算每一个时间步的结果甚至是有的框架都不支持这种忽略计算。

参考:
[1] https://blog.csdn.net/qq_27825451/article/details/88991529
[2] Keras Masking
[3] Keras Embedding

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章