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

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