1. 程式人生 > >NLP中Sequence-to-Sequence model程式碼詳解

NLP中Sequence-to-Sequence model程式碼詳解

在NLP領域,sequence to sequence模型有很多應用,比如機器翻譯、自動應答機器人等。在看懂了相關的論文後,我開始研讀TensorFlow提供的原始碼,剛開始看時感覺非常晦澀,現在基本都弄懂了,我在這裡主要介紹Sequence-to-Sequence Models用到的理論,然後對原始碼進行詳解。

sequence-to-sequence模型

在NLP中最為常見的模型是language model,它的研究物件是單一序列,而本文中的sequence to sequence模型同時研究兩個序列。經典的sequence-to-sequence模型由兩個RNN網路構成,一個被稱為“encoder”,另一個則稱為“decoder”,前者負責把variable-length序列編碼成fixed-length向量表示,後者負責把fixed_length向量表示解碼成variable-length輸出,它的基本網路結構如下,
這裡寫圖片描述

其中每一個小圓圈代表一個cell,比如GRUcell、LSTMcell、multi-layer-GRUcell、multi-layer-GRUcell等。這裡比較直觀的解釋就是,encoder的最終隱狀態c包含了輸入序列的所有資訊,因此可以使用c進行解碼輸出。儘管“encoder”或者“decoder”內部存在權值共享,但encoder和decoder之間一般具有不同的一套引數。在訓練sequence-to-sequence模型時,類似於有監督學習模型,最大化目標函式。

Github原始碼解析

  整個工程主要使用了四個原始檔,seq2seq.py檔案是一個用於建立sequence-to-sequence模型的庫,data_utils.py中包含了對原始資料進行預處理的一些操作,seq2seq_model.py用於定義machine translation模型,translate.py用於訓練和測試所定義的翻譯模型。因為原始碼較長,下面僅針對每個.py檔案,對理解起來可能有困難的程式碼塊進行解析。

seq2seq.py檔案

  這個檔案中比較重要的兩個庫函式basic_rnn_seq2seq和embedding_attention_seq2seq已經在上一部分作了介紹,這裡主要介紹其它的幾個功能函式。

(1)sequence_loss_by_example(logits, targets, weights)

  這個函式用於計算所有examples的加權交叉熵損失,logits引數是一個2D Tensor構成的列表物件,每一個2D Tensor的尺寸為[batch_size x num_decoder_symbols],函式的返回值是一個1D float型別的Tensor,尺寸為batch_size,其中的每一個元素代表當前輸入序列example的交叉熵。另外,還有一個與之類似的函式sequence_loss,它對sequence_loss_by_example函式返回的結果進行了一個tf.reduce_sum運算,因此返回的是一個標稱型float Tensor。

(2)model_with_buckets(encoder_inputs, decoder_inputs, targets, weights, buckets, seq2seq)

for j, bucket in enumerate(buckets):
      with variable_scope.variable_scope(variable_scope.get_variable_scope(),
                                         reuse=True if j > 0 else None):
        # 函式seq2seq有兩個返回值,因為tf.nn.seq2seq.embedding_attention_seq2seq函式有兩個返回值
        bucket_outputs, _ = seq2seq(encoder_inputs[:bucket[0]],
                                    decoder_inputs[:bucket[1]])
        outputs.append(bucket_outputs)
        if per_example_loss:
          losses.append(sequence_loss_by_example(
              outputs[-1], targets[:bucket[1]], weights[:bucket[1]],
              softmax_loss_function=softmax_loss_function))
        else:
          losses.append(sequence_loss(
              outputs[-1], targets[:bucket[1]], weights[:bucket[1]],
              softmax_loss_function=softmax_loss_function))

這個函式建立了一個支援bucketing策略的sequence-to-sequence模型,它仍然屬於Graph的定義階段。具體來說,這段程式定義了length(buckets)個graph,每個graph的輸入為總模型的輸入“佔位符”的一部分,但這些graphs共享模型引數,函式的返回值outputs和losses均為列表物件,尺寸為[length(buckets)],其中每一個元素為當前graph的bucket_outputs和bucket_loss。

data_utils.py檔案

(1)create_vocabulary(vocabulary_path, data_path, max_vocabulary_size)

  這個函式用於根據輸入檔案建立詞庫,在這裡data_path引數表示輸入原始檔的路徑,vocabulary_path表示輸出檔案的路徑,vocabulary_path檔案中每一行代表一個單詞,且按照其在data_path中的出現頻數從大到小排列,比如第1行為r”_EOS”,第2行為r”_UNK”,第3行為r’I’,第4行為r”have”,第5行為r’dream’,……

(2)def data_to_token_ids(data_path, target_path, vocabulary_path)

  這個函式用於把字串為元素的資料檔案轉換為以int索引為元素的檔案,在這裡data_path表示輸入源資料檔案的路徑,target_path表示輸出索引資料檔案的路徑,vocabulary_path表示詞庫檔案的路徑。整個函式把資料檔案中的每一行轉換為在詞庫檔案中的索引值,兩單詞的索引值之間用空格隔開,比如返回值檔案的第一行為’1 123 235’,第二行為‘3 1 234 554 879 355’,……

seq2seq_model.py檔案

  機器學習模型的定義過程,一般包括輸入變數定義、輸入資訊的forward propagation和誤差資訊的backward propagation三個部分,這三個部分在這個程式檔案中都得到了很好的體現,下面我們結合程式碼分別進行介紹。

(1)輸入變數的定義

 # Feeds for inputs.
    self.encoder_inputs = []
    self.decoder_inputs = []
    self.target_weights = []
    for i in xrange(buckets[-1][0]):  # Last bucket is the biggest one.
      self.encoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
                                                name="encoder{0}".format(i)))
    for i in xrange(buckets[-1][1] + 1):
      self.decoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
                                                name="decoder{0}".format(i)))
      self.target_weights.append(tf.placeholder(dtype, shape=[None],
                                                name="weight{0}".format(i)))

    # Our targets are decoder inputs shifted by one.
    targets = [self.decoder_inputs[i + 1]
               for i in xrange(len(self.decoder_inputs) - 1)]

與前面的幾個樣例不同,這裡輸入資料採用的是最常見的“佔位符”格式,以self.encoder_inputs為例,這個列表物件中的每一個元素表示一個佔位符,其名字分別為encoder0, encoder1,…,encoder39,encoder{i}的幾何意義是編碼器在時刻i的輸入。這裡需要注意的是,在訓練階段執行sess.run()函式時會再次用到這些變數名字。另外,跟language model類似,targets變數是decoder inputs平移一個單位的結果,讀者可以結合當前模型的損失函式進行理解。

(2)輸入資訊的forward propagation

 # Training outputs and losses.
    if forward_only:
      # 返回每一個bucket子圖模型對應的output和loss
      self.outputs, self.losses = tf.nn.seq2seq.model_with_buckets(
          self.encoder_inputs, self.decoder_inputs, targets,
          self.target_weights, buckets, lambda x, y: seq2seq_f(x, y, True),
          softmax_loss_function=softmax_loss_function)
      # If we use output projection, we need to project outputs for decoding.
      if output_projection is not None:
        for b in xrange(len(buckets)):
          self.outputs[b] = [
              tf.matmul(output, output_projection[0]) + output_projection[1]
              for output in self.outputs[b]
          ]
    else:
      self.outputs, self.losses = tf.nn.seq2seq.model_with_buckets(
          self.encoder_inputs, self.decoder_inputs, targets,
          self.target_weights, buckets,
          lambda x, y: seq2seq_f(x, y, False),
          softmax_loss_function=softmax_loss_function)

從程式碼中可以看到,輸入資訊的forward popagation分成了兩種情況,這是因為整個sequence to sequence模型在訓練階段和測試階段資訊的流向是不一樣的,這一點可以從seq2seqf函式的do_decode引數值體現出來,而do_decoder取值對應的就是tf.nn.seq2seq.embedding_attention_seq2seq函式中的feed_previous引數,forward_only為True也即feed_previous引數為True時進行模型測試,為False時進行模型訓練。這裡還應用到了一個很重要的函式tf.nn.seq2seq.model_with_buckets,我麼在seq2seq檔案中對其進行講解。

(3)誤差資訊的backward propagation

# 返回所有bucket子graph的梯度和SGD更新操作,這些子graph共享輸入佔位符變數encoder_inputs,區別在於,
    # 對於每一個bucket子圖,其輸入為該子圖對應的長度。
    params = tf.trainable_variables()
    if not forward_only:
      self.gradient_norms = []
      self.updates = []
      opt = tf.train.GradientDescentOptimizer(self.learning_rate)
      for b in xrange(len(buckets)):
        gradients = tf.gradients(self.losses[b], params)
        clipped_gradients, norm = tf.clip_by_global_norm(gradients,
                                                         max_gradient_norm)
        self.gradient_norms.append(norm)
        self.updates.append(opt.apply_gradients(
            zip(clipped_gradients, params), global_step=self.global_step))

  這一段程式碼主要用於計算損失函式關於引數的梯度。因為只有訓練階段才需要計算梯度和引數更新,所以這裡有個if判斷語句。並且,由於當前定義除了length(buckets)個graph,故返回值self.updates是一個列表物件,尺寸為length(buckets),列表中第i個元素表示graph{i}的梯度更新操作。

 # Input feed: encoder inputs, decoder inputs, target_weights, as provided.
    input_feed = {}
    for l in xrange(encoder_size):
      input_feed[self.encoder_inputs[l].name] = encoder_inputs[l]
    for l in xrange(decoder_size):
      input_feed[self.decoder_inputs[l].name] = decoder_inputs[l]
      input_feed[self.target_weights[l].name] = target_weights[l]
    ......
    if not forward_only:
      output_feed = [self.updates[bucket_id],  # Update Op that does SGD.
                     self.gradient_norms[bucket_id],  # Gradient norm.
                     self.losses[bucket_id]]  # Loss for this batch.
    else:
      output_feed = [self.losses[bucket_id]]  # Loss for this batch.
      for l in xrange(decoder_size):  # Output logits.
        output_feed.append(self.outputs[bucket_id][l])

    outputs = session.run(output_feed, input_feed)

 模型已經定義完成了,這裡便開始進行模型訓練了。上面的兩個for迴圈用於為之前定義的輸入佔位符賦予具體的數值,這些具體的數值源自於get_batch函式的返回值。當session.run函式開始執行時,當前session會對第bucket_id個graph進行引數更新操作。