1. 程式人生 > >Tensorflow動態seq2seq使用總結(r1.3)

Tensorflow動態seq2seq使用總結(r1.3)

when tex scalar edi tac 幹什麽 spa googl lse

https://www.jianshu.com/p/c0c5f1bdbb88

動機

其實差不多半年之前就想吐槽Tensorflow的seq2seq了(後面博主去幹了些別的事情),官方的代碼已經拋棄原來用靜態rnn實現的版本了,而官網的tutorial現在還是介紹基於靜態的rnn的模型,加bucket那套,看這裏。

技術分享圖片 tutorial.png
看到了嗎?是legacy_seq2seq的。本來Tensorflow的seq2seq的實現相比於pytorch已經很復雜了,還沒有個正經的tutorial,哎。
好的,回到正題,遇到問題解決問題,想辦法找一個最佳的Tensorflow的seq2seq解決方案

學習的資料

  • 知名博主WildML給google寫了個通用的seq2seq,文檔地址,Github地址。這個框架已經被Tensorflow采用,後面我們的代碼也會基於這裏的實現。但本身這個框架是為了讓用戶直接寫參數就能簡單地構建網絡,因此文檔沒有太多參考價值,我們直接借用其中的代碼構建自己的網絡。
  • 俄羅斯小夥ematvey寫的:tensorflow-seq2seq-tutorials,Github地址。介紹使用動態rnn構建seq2seq,decoder使用raw_rnn,原理和WildML的方案差不多。多說一句,這哥們當時也是吐槽Tensorflow的文檔,寫了那麽個倉庫當第三方的文檔使,現在都400+個star了。真是有漏洞就有機遇啊,哈哈。

Tensorflow的動態rnn

先來簡單介紹動態rnn和靜態rnn的區別。
tf.nn.rnn creates an unrolled graph for a fixed RNN length. That means, if you call tf.nn.rnn with inputs having 200 time steps you are creating a static graph with 200 RNN steps. First, graph creation is slow. Second, you’re unable to pass in longer sequences (> 200) than you’ve originally specified.tf.nn.dynamic_rnn solves this. It uses a tf.While loop to dynamically construct the graph when it is executed. That means graph creation is faster and you can feed batches of variable size.

摘自Whats the difference between tensorflow dynamic_rnn and rnn?。也就是說,靜態的rnn必須提前將圖展開,在執行的時候,圖是固定的,並且最大長度有限制。而動態rnn可以在執行的時候,將圖循環地的復用。

一句話,能用動態的rnn就盡量用動態的吧

Seq2Seq結構分析

技術分享圖片 seq2seq.png

seq2seq由Encoder和Decoder組成,一般Encoder和Decoder都是基於RNN。Encoder相對比較簡單,不管是多層還是雙向或者更換具體的Cell,使用原生API還是比較容易實現的。難點在於Decoder:不同的Decoder對應的rnn cell的輸入不同,比如上圖的示例中,每個cell的輸入是上一個時刻cell輸出的預測對應的embedding。

技術分享圖片 attention.png

如果像上圖那樣使用Attention,則decoder的cell輸入還包括attention加權求和過的context。

通過示例講解

技術分享圖片 slot filling.png
下面通過一個用seq2seq做slot filling(一種序列標註)的例子講解。完整代碼地址:https://github.com/applenob/RNN-for-Joint-NLU

Encoder的實現示例

# 首先構造單個rnn cell
encoder_f_cell = LSTMCell(self.hidden_size)
encoder_b_cell = LSTMCell(self.hidden_size)
 (encoder_fw_outputs, encoder_bw_outputs),
 (encoder_fw_final_state, encoder_bw_final_state) =         tf.nn.bidirectional_dynamic_rnn(cell_fw=encoder_f_cell,
                                            cell_bw=encoder_b_cell,
                                            inputs=self.encoder_inputs_embedded,
                                            sequence_length=self.encoder_inputs_actual_length,
                                            dtype=tf.float32, time_major=True)

上面的代碼使用了tf.nn.bidirectional_dynamic_rnn構建單層雙向的LSTM的RNN作為Encoder。
參數:

  • cell_fw:前向的lstm cell
  • cell_bw:後向的lstm cell
  • time_major:如果是True,則輸入需要是T×B×E,T代表時間序列的長度,B代表batch size,E代表詞向量的維度。否則,為B×T×E。輸出也是類似。

返回:

  • outputs:針對所有時間序列上的輸出。
  • final_state:只是最後一個時間節點的狀態。

一句話,Encoder的構造就是構造一個RNN,獲得輸出和最後的狀態。

Decoder實現示例

下面著重介紹如何使用Tensorflow的tf.contrib.seq2seq實現一個Decoder。
我們這裏的Decoder中,每個輸入除了上一個時間節點的輸出以外,還有對應時間節點的Encoder的輸出,以及attention的context。

Helper

常用的Helper

  • TrainingHelper:適用於訓練的helper。
  • InferenceHelper:適用於測試的helper。
  • GreedyEmbeddingHelper:適用於測試中采用Greedy策略sample的helper。
  • CustomHelper:用戶自定義的helper。

先來說明helper是幹什麽的:參考上面提到的俄羅斯小哥用raw_rnn實現decoder,需要傳進一個loop_fn。這個loop_fn其實是控制每個cell在不同的時間節點,給定上一個時刻的輸出,如何決定下一個時刻的輸入。
helper幹的事情和這個loop_fn基本一致。這裏著重介紹CustomHelper,要傳入三個函數作為參數:

  • initialize_fn:返回finishednext_inputs。其中finished不是scala,是一個一維向量。這個函數即獲取第一個時間節點的輸入。
  • sample_fn:接收參數(time, outputs, state) 返回sample_ids。即,根據每個cell的輸出,如何sample。
  • next_inputs_fn:接收參數(time, outputs, state, sample_ids) 返回 (finished, next_inputs, next_state),根據上一個時刻的輸出,決定下一個時刻的輸入。

BasicDecoder

有了自定義的helper以後,可以使用tf.contrib.seq2seq.BasicDecoder定義自己的Decoder了。再使用tf.contrib.seq2seq.dynamic_decode執行decode,最終返回:(final_outputs, final_state, final_sequence_lengths)。其中:final_outputstf.contrib.seq2seq.BasicDecoderOutput類型,包括兩個字段:rnn_outputsample_id

回到示例

        # 傳給CustomHelper的三個函數
        def initial_fn():
            initial_elements_finished = (0 >= decoder_lengths)  # all False at the initial step
            initial_input = tf.concat((sos_step_embedded, encoder_outputs[0]), 1)
            return initial_elements_finished, initial_input

        def sample_fn(time, outputs, state):
            # 選擇logit最大的下標作為sample
            prediction_id = tf.to_int32(tf.argmax(outputs, axis=1))
            return prediction_id

        def next_inputs_fn(time, outputs, state, sample_ids):
            # 上一個時間節點上的輸出類別,獲取embedding再作為下一個時間節點的輸入
            pred_embedding = tf.nn.embedding_lookup(self.embeddings, sample_ids)
            # 輸入是h_i+o_{i-1}+c_i
            next_input = tf.concat((pred_embedding, encoder_outputs[time]), 1)
            elements_finished = (time >= decoder_lengths)  # this operation produces boolean tensor of [batch_size]
            all_finished = tf.reduce_all(elements_finished)  # -> boolean scalar
            next_inputs = tf.cond(all_finished, lambda: pad_step_embedded, lambda: next_input)
            next_state = state
            return elements_finished, next_inputs, next_state

        # 自定義helper
        my_helper = tf.contrib.seq2seq.CustomHelper(initial_fn, sample_fn, next_inputs_fn)

        def decode(helper, scope, reuse=None):
            with tf.variable_scope(scope, reuse=reuse):
                memory = tf.transpose(encoder_outputs, [1, 0, 2])
                attention_mechanism = tf.contrib.seq2seq.BahdanauAttention(
                    num_units=self.hidden_size, memory=memory,
                    memory_sequence_length=self.encoder_inputs_actual_length)
                cell = tf.contrib.rnn.LSTMCell(num_units=self.hidden_size * 2)
                attn_cell = tf.contrib.seq2seq.AttentionWrapper(
                    cell, attention_mechanism, attention_layer_size=self.hidden_size)
                out_cell = tf.contrib.rnn.OutputProjectionWrapper(
                    attn_cell, self.slot_size, reuse=reuse
                )
                # 使用自定義helper的decoder
                decoder = tf.contrib.seq2seq.BasicDecoder(
                    cell=out_cell, helper=helper,
                    initial_state=out_cell.zero_state(
                        dtype=tf.float32, batch_size=self.batch_size))
                # 獲取decode結果
                final_outputs, final_state, final_sequence_lengths = tf.contrib.seq2seq.dynamic_decode(
                    decoder=decoder, output_time_major=True,
                    impute_finished=True, maximum_iterations=self.input_steps
                )
                return final_outputs

        outputs = decode(my_helper, ‘decode‘)

Attntion

上面的代碼,還有幾個地方沒有解釋:BahdanauAttentionAttentionWrapperOutputProjectionWrapper

先從簡單的開始:OutputProjectionWrapper即做一個線性映射,比如之前的cell的ouput是T×B×D,D是hidden size,那麽這裏做一個線性映射,直接到T×B×S,這裏S是slot class num。wrapper內部維護一個線性映射用的變量:Wb

技術分享圖片 attention.png

BahdanauAttention是一種AttentionMechanism,另外一種是:BahdanauMonotonicAttention。具體二者的區別,讀者請自行深入調查。關鍵參數:

  • num_units:隱層維度。
  • memory:通常就是RNN encoder的輸出
  • memory_sequence_length=None:可選參數,即memory的mask,超過長度數據不計入attention。

繼續介紹AttentionWrapper:這也是一個cell wrapper,關鍵參數:

  • cell:被包裝的cell。
  • attention_mechanism:使用的attention機制,上面介紹的。
技術分享圖片 attention.png

memory對應公式中的h,wrapper的輸出是s。

那麽一個AttentionWrapper具體的操作流程如何呢?看官網給的流程:

技術分享圖片 AttentionWrapper.png

Loss Function

tf.contrib.seq2seq.sequence_loss可以直接計算序列的損失函數,重要參數:

  • logits:尺寸[batch_size, sequence_length, num_decoder_symbols]
  • targets:尺寸[batch_size, sequence_length],不用做one_hot。
  • weights[batch_size, sequence_length],即mask,濾去padding的loss計算,使loss計算更準確。

後記

這裏只討論了seq2seq在序列標註上的應用。seq2seq還廣泛應用於翻譯和對話生成,涉及到生成的策略問題,比如beam search。後面會繼續研究。除了sample的策略,其他seq2seq的主要技術,本文已經基本涵蓋,希望對大家踩坑有幫助。
完整代碼:https://github.com/applenob/RNN-for-Joint-NLU



作者:Cer_ml
鏈接:https://www.jianshu.com/p/c0c5f1bdbb88
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並註明出處。

Tensorflow動態seq2seq使用總結(r1.3)