深度學習對話系統實戰篇--老版本tf.contrib.legacy_seq2seq API介紹和源碼解析

上一篇文章中我們已經分析了各種seq2seq模型,從理論的角度上對他們有了一定的了解和認識,那麼接下來我們就結合tensorflow代碼來看一下這些模型在tf中是如何實現的,相信有了對代碼的深層次理解,會在我們之後構建對話系統模型的時候有很大的幫助。

tensorflow版本升級之後把之前的tf.nn.seq2seq的代碼遷移到了tf.contrib.legacy_seq2seq下面,其實這部分API估計以後也會被遺棄,因為已經開發出了新的API放在tf.contrib.seq2seq下面,更加靈活,但是目前在網上找到的代碼和模擬實現基本上用的還是legacy_seq2seq下面的代碼,所以我們先來分析一下這部分的函數功能及源碼實現。本次我們會介紹下面幾個函數,這部分代碼的定義都可以在[python/ops/seq2seq.py](github.com/tensorflow/t)文件中找到。

首先看一下這個文件的組成,主要包含下面幾個函數:

可以看到按照調用關係和功能不同可以分成下面的結構:

  • model_with_buckets
    • 1,seq2seq函數
      • basic_rnn_seq2seq
        • rnn_decoder
      • tied_rnn_seq2seq
      • embedding_tied_rnn_seq2seq
      • embedding_rnn_seq2seq
        • embedding_rnn_decoder
      • embedding_attention_seq2seq
        • embedding_attention_decoder
          • attention_decoder
          • attention
      • one2many_rnn_seq2seq
    • 2,loss函數
      • sequence_loss_by_example
      • sequence_loss

在這裡,我會主要介紹一下功能最完備的幾個函數,足以讓我們實現一個基於seq_to_seq模型的對話系統。就讓我們按照函數的調用關係來進行一一介紹吧:

model_with_buckets()函數

首先來說最高層的函數model_with_buckets(),定義如下所示:

def model_with_buckets(encoder_inputs,n decoder_inputs,n targets,n weights,n buckets,n seq2seq,n softmax_loss_function=None,n per_example_loss=False,n name=None):n

首先來說一下這個函數,目的是為了減少計算量和加快模型計算速度,然後由於這部分代碼比較古老,你會發現有些地方還在使用static_rnn()這種函數,其實新版的tf中引入dynamic_rnn之後就不需要這麼做了。但是呢,我們還是來分析一下,其實思路很簡單,就是將輸入長度分成不同的間隔,這樣數據的在填充時只需要填充到相應的bucket長度即可,不需要都填充到最大長度。比如buckets取[(5,10), (10,20),(20,30)...](每個bucket的第一個數字表示source填充的長度,第二個數字表示target填充的長度,eg:『我愛你』-->『I love you』,應該會被分配到第一個bucket中,然後『我愛你』會被pad成長度為5的序列,『I love you』會被pad成長度為10的序列。其實就是每個bucket表示一個模型的參數配置),這樣對每個bucket都構造一個模型,然後訓練時取相應長度的序列進行,而這些模型將會共享參數。其實這一部分可以參考現在的dynamic_rnn來進行理解,dynamic_rnn是對每個batch的數據將其pad至本batch中長度最大的樣本,而bucket則是在數據預處理環節先對數據長度進行聚類操作。明白了其原理之後我們再看一下該函數的參數和內部實現:

encoder_inputs: encoder的輸入一個tensor的列表列表中每一項都是encoder時的一個詞batch)。n decoder_inputs: decoder的輸入同上n targets: 目標值與decoder_input只相差一個<EOS>符號int32型n weights: 目標序列長度值的mask標誌如果是padding則weight=0否則weight=1n buckets: 就是定義的bucket值是一個列表[(510), (1020),(2030)...]n seq2seq: 定義好的seq2seq模型可以使用後面介紹的embedding_attention_seq2seqembedding_rnn_seq2seqbasic_rnn_seq2seq等n softmax_loss_function: 計算誤差的函數(labels, logits)默認為sparse_softmax_cross_entropy_with_logitsn per_example_loss: 如果為真則調用sequence_loss_by_example返回一個列表其每個元素就是一個樣本的loss值如果為假則調用sequence_loss函數對一個batch的樣本只返回一個求和的loss值具體見後面的分析n name: Optional name for this operation, defaults to "model_with_buckets".n

內部代碼這裡不會全部貼上來,撿關鍵的說一下:

#保存每個bucket對應的loss和output n losses = []n outputs = []n with ops.name_scope(name, "model_with_buckets", all_inputs):n #對每個bucket都要選擇數據進行構建模型n for j, bucket in enumerate(buckets):n #buckets之間的參數要進行復用n with variable_scope.variable_scope(variable_scope.get_variable_scope(), reuse=True if j > 0 else None):nn #調用seq2seq進行解碼得到輸出,這裡需要注意的是,encoder_inputs和decoder_inputs是定義好的placeholder,n #都是長度為序列最大長度的列表(也就是最大的那個buckets的長度),按上面的例子,這兩個placeholder分別是長度為20和30的列表。n #在構建模型時,對於每個bucket,只取其對應的長度個placeholder即可,如對於(5,10)這個bucket,就取前5/10個placeholder進行構建模型n bucket_outputs, _ = seq2seq(encoder_inputs[:bucket[0]], decoder_inputs[:bucket[1]])n outputs.append(bucket_outputs)n #如果指定per_example_loss則調用sequence_loss_by_example,losses添加的是一個batch_size大小的列表n if per_example_loss:n losses.append(n sequence_loss_by_example(n outputs[-1],n targets[:bucket[1]],n weights[:bucket[1]],n softmax_loss_function=softmax_loss_function))n #否則調用sequence_loss,對上面的結果進行求和,losses添加的是一個值n else:n losses.append(n sequence_loss(n outputs[-1],n targets[:bucket[1]],n weights[:bucket[1]],n softmax_loss_function=softmax_loss_function))n

函數的輸出為outputs和losses,其tensor的shape見上面解釋。

1,embedding_attention_seq2seq()函數

上面函數中會調用seq2seq函數進行解碼操作,我們這裡就那一個實現的最完備的例子進行介紹一個seq2seq模型是如何實現的,如下所示,從名字我們就可以看出其實現了embedding和attention兩個功能,而attention則是使用了「Neural Machine Translation by Jointly Learning to Align and Translate」這篇論文里的定義方法:

def embedding_attention_seq2seq(encoder_inputs,n decoder_inputs,n cell,n num_encoder_symbols,n num_decoder_symbols,n embedding_size,n num_heads=1,n output_projection=None,n feed_previous=False,n dtype=None,n scope=None,n initial_state_attention=False):n

在接下來的代碼介紹中,之前函數里說過的參數,如果在本函數中定義上沒有任何差別,不就不會再重複介紹,比如這裡的encoder_inputs和decoder_inputs,下面我們看一下其各個參數的含義:

cell: RNNCell常見的一些RNNCell定義都可以用.n num_encoder_symbols: source的vocab_size大小用於embedding矩陣定義n num_decoder_symbols: target的vocab_size大小用於embedding矩陣定義n embedding_size: embedding向量的維度n num_heads: Attention頭的個數就是使用多少種attention的加權方式用更多的參數來求出幾種attention向量n output_projection: 輸出的映射層因為decoder輸出的維度是output_size所以想要得到num_decoder_symbols對應的詞還需要增加一個映射層參數是W和BW:[output_size, num_decoder_symbols],b:[num_decoder_symbols]n feed_previous: 是否將上一時刻輸出作為下一時刻輸入一般測試的時候置為True此時decoder_inputs除了第一個元素之外其他元素都不會使用n initial_state_attention: 默認為False, 初始的attention是零若為True將從initial state和attention states開始n

下面來看一下幾個關鍵的代碼片:

# Encoder.先將cell進行deepcopy,因為seq2seq模型是兩個相同的模型,但是模型參數不共享,所以encoder和decoder要使用兩個不同的RnnCelln encoder_cell = copy.deepcopy(cell)n #先將encoder輸入進行embedding操作,直接在RNNCell的基礎上添加一個EmbeddingWrapper即可n encoder_cell = core_rnn_cell.EmbeddingWrapper(encoder_cell,n embedding_classes=num_encoder_symbols,n embedding_size=embedding_size)n #這裡仍然使用static_rnn函數來構造RNN模型n encoder_outputs, encoder_state = rnn.static_rnn(encoder_cell, encoder_inputs, dtype=dtype)nn # First calculate a concatenation of encoder outputs to put attention on.n #將encoder的輸出由列錶轉換成Tensor,shape為[batch_size,encoder_input_length,output_size]。轉換之後n #的tensor就可以作為Attention的輸入了n top_states = [array_ops.reshape(e, [-1, 1, cell.output_size]) for e in encoder_outputs]n attention_states = array_ops.concat(top_states, 1)n

上面的代碼進行了embedding的encoder階段,最終得到每個時間步的隱藏層向量表示encoder_outputs,然後將各個時間步的輸出進行reshape並concat變成一個[batch_size,encoder_input_length,output_size]的tensor。方便計算每個decode時刻的編碼向量Ci。接下來看一下decoder階段的代碼:

# Decoder.n output_size = Nonen #將decoder的輸出進行映射到output_vocab_size維度,直接將RNNCell添加上一個OutputProjectionWrapper包裝即可n if output_projection is None:n cell = core_rnn_cell.OutputProjectionWrapper(cell, num_decoder_symbols)n output_size = num_decoder_symbolsnn #如果feed_previous是bool型的值,則直接調用embedding_attention_decoder函數進行解碼操作n if isinstance(feed_previous, bool):n return embedding_attention_decoder(n decoder_inputs,n encoder_state,n attention_states,n cell,n num_decoder_symbols,n embedding_size,n num_heads=num_heads,n output_size=output_size,n output_projection=output_projection,n feed_previous=feed_previous,n initial_state_attention=initial_state_attention)n

先是對RNNCell封裝了一個OutputProjectionWrapper用於輸出層的映射,然後直接調用embedding_attention_decoder函數解碼。但是當feed_previous不是bool型的變數,而是一個tensor的時候,會執行下面的邏輯:

# 如果feed_previous是一個tensor,則使用tf.cond構建兩個graphn def decoder(feed_previous_bool):n #本函數會被調用兩次,第一次不適用reuse,第二次使用reuse。所以decoder(True),decoder(false)n reuse = None if feed_previous_bool else Truen with variable_scope.variable_scope(variable_scope.get_variable_scope(), reuse=reuse):n outputs, state = embedding_attention_decoder(n decoder_inputs,n encoder_state,n attention_states,n cell,n num_decoder_symbols,n embedding_size,n num_heads=num_heads,n output_size=output_size,n output_projection=output_projection,n feed_previous=feed_previous_bool,n update_embedding_for_previous=False,n initial_state_attention=initial_state_attention)n state_list = [state]n if nest.is_sequence(state):n state_list = nest.flatten(state)n return outputs + state_listnn #????這裡不是很懂n outputs_and_state = control_flow_ops.cond(feed_previous,n lambda: decoder(True),n lambda: decoder(False))n outputs_len = len(decoder_inputs) # Outputs length same as decoder inputs.n state_list = outputs_and_state[outputs_len:]n state = state_list[0]n if nest.is_sequence(encoder_state):n state = nest.pack_sequence_as(n structure=encoder_state, flat_sequence=state_list)n return outputs_and_state[:outputs_len], staten

首先說一下自己對上面這段代碼的理解,希望大神可以指出這段代碼的含義。tf.cond這個函數其實就是一個if else條件控制語句,也就是說如果feed_previous為真則執行decode(True), 否則執行decode(False)。然後再看decode函數,直接調用embedding_attention_decoder函數進行解碼,然後將結果拼接在一起,最後執行完在將結果分開返回。感覺整體實現的功能跟上面那段代碼是一樣的,所以不太清楚目的是什麼==

1.1,embedding_attention_decoder函數

前面的embedding_attention_seq2seq在解碼時會直接調用本函數,那麼我們就來看一下這個函數的定義:

def embedding_attention_decoder(decoder_inputs,n initial_state,n attention_states,n cell,n num_symbols,n embedding_size,n num_heads=1,n output_size=None,n output_projection=None,n feed_previous=False,n update_embedding_for_previous=True,n dtype=None,n scope=None,n initial_state_attention=False):n

因為大多數都是之前函數中的參數或者變數直接傳進來的,想必會比較容易理解各個變數的含義,撿重要的參數簡單說一下:

initial_state: 2D Tensor [batch_size x cell.state_size]RNN的初始狀態n attention_states: 3D Tensor [batch_size x attn_length x attn_size]就是上面計算出來的encoder階段的隱層向量n num_symbols: decoder階段的vocab_sizen update_embedding_for_previous: Boolean; 只有在feed_previous為真的時候才會起作用就是只更新GO的embedding向量其他元素保持不變n

這個函數首先對定義encoder階段的embedding矩陣,該矩陣用於將decoder的輸出轉化為下一時刻輸入向量或者將decoder_inputs轉化為響應的詞向量;然後直接調用attention_decoder函數進入attention的解碼階段。

with variable_scope.variable_scope(scope or "embedding_attention_decoder", dtype=dtype) as scope:n #decoder階段的embedding,n embedding = variable_scope.get_variable("embedding", [num_symbols, embedding_size])n #將上一個cell輸出進行output_projection然後embedding得到當前cell的輸入,僅在feed_previous情況下使用n loop_function = _extract_argmax_and_embed(embedding, output_projection, update_embedding_for_previous) if feed_previous else Nonen #如果不是feed_previous的話,將decoder_inputs進行embedding得到詞向量n emb_inp = [embedding_ops.embedding_lookup(embedding, i) for i in decoder_inputs]n return attention_decoder(n emb_inp,n initial_state,n attention_states,n cell,n output_size=output_size,n num_heads=num_heads,n loop_function=loop_function,n initial_state_attention=initial_state_attention)n

1.1.1,attention_decoder()函數

這個函數可以說是Attention based seq2seq的核心函數了,最重要的attention部分和decoder部分都是在這裡實現的,也就是論文中的公式會在這部分代碼中體現出來:

def attention_decoder(decoder_inputs,n initial_state,n attention_states,n cell,n output_size=None,n num_heads=1,n loop_function=None,n dtype=None,n scope=None,n initial_state_attention=False):nn loop_function: If not None, this function will be applied to i-th outputn in order to generate i+1-th input, and decoder_inputs will be ignored,n except for the first element ("GO" symbol).loop_function(prev, i) = nextn * prev is a 2D Tensor of shape [batch_size x output_size],n * i is an integer, the step number (when advanced control is needed),n * next is a 2D Tensor of shape [batch_size x input_size].n

下面我們看一下具體的代碼實現:

# To calculate W1 * h_t we use a 1-by-1 convolution, need to reshape before.n #為了方便進行1*1卷積,將attention_states轉化為[batch_size, num_steps, 1, attention_size]的四維tensorn #第四個維度是attention_size,表示的是input_channle,n hidden = array_ops.reshape(attention_states, [-1, attn_length, 1, attn_size])nn #用來保存num_heads個讀取頭的相關信息,hidden_states保存的是w*hj,v保存的是v,每個讀取頭的參數是不一樣的n hidden_features = []n v = []n #-----------------------------------接下來計算v*tanh(w*hj+u*zi)來表示二者的相關性--------------------------------------------------------n attention_vec_size = attn_size # Size of query vectors for attention.n #對隱藏層的每個元素計算w*hjn for a in xrange(num_heads):n #卷積核的size是1*1,輸入channle為attn_size,共有attention_vec_size個filtern k = variable_scope.get_variable("AttnW_%d" % a, [1, 1, attn_size, attention_vec_size])n #卷積之後的結果就是[batch_size, num_steps, 1,attention_vec_size]n hidden_features.append(nn_ops.conv2d(hidden, k, [1, 1, 1, 1], "SAME"))n v.append(variable_scope.get_variable("AttnV_%d" % a, [attention_vec_size]))n state = initial_staten

上面的代碼對所有的hidden向量進行了計算,接下來定義一個函數來實現上面的公式,因為每個decode時刻需要輸入相應的query向量,就是解碼RNN的隱層狀態,所以定義一個函數是比較好的選擇。

def attention(query):n """Put attention masks on hidden using hidden_features and query."""n ds = [] # Results of attention reads will be stored here.nn #如果query是tuple,則將其flatten,並連接成二維的tensorn if nest.is_sequence(query): # If the query is a tuple, flatten it.n query_list = nest.flatten(query)n for q in query_list: # Check that ndims == 2 if specified.n ndims = q.get_shape().ndimsn if ndims:n assert ndims == 2n query = array_ops.concat(query_list, 1)nn for a in xrange(num_heads):n with variable_scope.variable_scope("Attention_%d" % a):n #計算u*zi,並將其reshape成[batch_size, 1, 1, attention_vec_size]n y = Linear(query, attention_vec_size, True)(query)n y = array_ops.reshape(y, [-1, 1, 1, attention_vec_size])n # Attention mask is a softmax of v^T * tanh(...).n #計算v * tanh(w * hj + u * zi)n #hidden_features[a] + y的shape為[batch_size, num_steps, 1,attention_vec_size],在於v向量(【attention_vec_size】)相乘仍保持不變n #在2, 3兩個維度上進行reduce_sum操作,最終變成[batch_size,num_steps]的tensor,也就是各個hidden向量所對應的分數n s = math_ops.reduce_sum(v[a] * math_ops.tanh(hidden_features[a] + y), [2, 3])n #使用softmax函數進行歸一化操作n a = nn_ops.softmax(s)n # Now calculate the attention-weighted vector d.n #對所有向量進行加權求和n d = math_ops.reduce_sum(array_ops.reshape(a, [-1, attn_length, 1, 1]) * hidden, [1, 2])n ds.append(array_ops.reshape(d, [-1, attn_size]))n return dsn

定義好了attention的計算函數,接下來就是對輸入進行循環,一次計算每個decode階段的輸出。這裡需要注意的是,attention函數返回的是一個列表,其每個元素是一個讀取頭對應的結果,然後將該列表與每一時刻的decode_input連接在一起輸入到RNNCell中進行解碼。代碼如下所示:

#如果使用全零初始化狀態,則直接調用attention並使用全另狀態。n if initial_state_attention:n attns = attention(initial_state)n #如果不用全另初始化狀態,則對所有decoder_inputs進行遍歷,並逐個解碼n for i, inp in enumerate(decoder_inputs):n if i > 0:n #如果i>0,則復用解碼RNN模型的參數n variable_scope.get_variable_scope().reuse_variables()n # If loop_function is set, we use it instead of decoder_inputs.n #如果要使用前一時刻輸出作為本時刻輸入,則調用loop_function覆蓋inp的值n if loop_function is not None and prev is not None:n with variable_scope.variable_scope("loop_function", reuse=True):n inp = loop_function(prev, i)n # Merge input and previous attentions into one vector of the right size.n input_size = inp.get_shape().with_rank(2)[1]n if input_size.value is None:n raise ValueError("Could not infer input size from input: %s" % inp.name)nn #輸入是將inp與attns進行concat,餵給RNNcelln inputs = [inp] + attnsn x = Linear(inputs, input_size, True)(inputs)n # Run the RNN.n cell_output, state = cell(x, state)n # Run the attention mechanism.n #計算下一時刻的atten向量n if i == 0 and initial_state_attention:n with variable_scope.variable_scope(variable_scope.get_variable_scope(), reuse=True):n attns = attention(state)n else:n attns = attention(state)n

到這為止我們就介紹完了所有關於attention seq2seq模型的代碼。至於剩下幾個seq2seq模型都是本模型的子集,就不過多進行贅述,然後接下來我們再來看一看關於loss計算的代碼:

2 loss計算函數

我們先來看第一個函數sequence_loss_by_example的定義,代碼比較簡單,就是計算decode結果與targets之間的差別,注意本函數的返回結果是一個shape為batch_size的1-D tensor,其中每個值都是一個樣本的loss:

def sequence_loss_by_example(logits,n targets,n weights,n average_across_timesteps=True,n softmax_loss_function=None,n name=None):n log_perp_list = []n #對每個時間步的數據進行計算loss,並添加到log_perp_list列表當中n for logit, target, weight in zip(logits, targets, weights):n #如果沒有指定softmax_loss_function,則默認調用sparse_softmax_cross_entropy_with_logits函數計算lossn if softmax_loss_function is None:n target = array_ops.reshape(target, [-1])n crossent = nn_ops.sparse_softmax_cross_entropy_with_logits(n labels=target, logits=logit)n else:n crossent = softmax_loss_function(labels=target, logits=logit)n #weight是0或者1,用於標明該詞是否為填充,如果是為0,則loss也為0,不計算n log_perp_list.append(crossent * weight)n #對所有時間步的loss進行求和,add_n就是對一個列表元素進行求和n log_perps = math_ops.add_n(log_perp_list)n #如果的話,求平均,注意除以的是weights的和,為不是n_step的和n if average_across_timesteps:n total_size = math_ops.add_n(weights)n total_size += 1e-12 # Just to avoid division by 0 for all-0 weights.n log_perps /= total_sizen return log_perpsn

接下來再看一下sequence_loss這個函數的定義,很簡單,就是調用上面的函數,然後對batch個樣本的loss進行求和或者求平均。返回的結果是一個標量值。

def sequence_loss(logits,n targets,n weights,n average_across_timesteps=True,n average_across_batch=True,n softmax_loss_function=None,n name=None):n with ops.name_scope(name, "sequence_loss", logits + targets + weights):n #對batch個樣本的loss進行求和n cost = math_ops.reduce_sum(n sequence_loss_by_example(n logits,n targets,n weights,n average_across_timesteps=average_across_timesteps,n softmax_loss_function=softmax_loss_function))n #如果要對batch進行求平均,則除以batch_sizen if average_across_batch:n batch_size = array_ops.shape(targets[0])[0]n return cost / math_ops.cast(batch_size, cost.dtype)n else:n return costn

以上,我們分析了tf中seq2seq的代碼,相比看完之後大家應該有了一個大致的了解,如何調用這些函數應該也很清楚明白了,下一篇博客中會結合實際的對話系統的代碼進行分析。後續計劃也會去研究tf最新的seq2seq的API介面tf.contrib.seq2seq,用更規範的代碼來構造seq2seq模型~~

參考鏈接

  1. 官網代碼
  2. Tensorflow源碼解讀(一):Attention Seq2Seq模型
  3. Chatbots with Seq2Seq
  4. tensorflow的legacy_seq2seq
  5. Neural Machine Translation (seq2seq) Tutorial

推薦閱讀:

拔了智齒,疼滴想屎。
【博客存檔】深度學習之Neural Image Caption
[乾貨|實踐] TensorBoard可視化
YJango的前饋神經網路--代碼LV1

TAG:深度学习DeepLearning | 自然语言处理 | TensorFlow |