解析Tensorflow官方English-Franch翻譯器demo


今天我們來解析下Tensorflow的Seq2Seq的demo。繼上篇博客的PTM模型之后,Tensorflow官方也開放了名為translate的demo,這個demo對比之前的PTM要大了很多(首先,空間上就會需要大約20個G,另外差點把我的硬盤給運行死),但是也實用了很多。模型采用了encoder-decoder的框架結果,佐以attention機制來實現論文中的英語法語翻譯功能。同時,模型的基礎卻來自之前的PTM模型。下面,讓我們來一起來了解一下這個神奇的系統吧!

 

論文介紹及基礎描寫:

這個英語法語翻譯器融合了多篇論文的核心內容,所以在學習的過程中其實我們可以變相的了解這些技巧。首先,Cho在論文Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation一文中指出可以通過encoder-decoder這種類似於編碼-解碼的框架來做機器翻譯。這個框架在后續論文中掀起了不小的波瀾,很多作品延續了這種sequence-to-sequence的框架在其他的一些領域,比如image caption。該框架說白了就是兩個RNN,一個作為編碼器,一個作為解碼器。至於RNN cell的結構,雖然本項目支持使用LSTM, default的模型為GRU(Gated Recurrent Uint),一個LSTM的簡化版模型。另外,為了使翻譯的效果達到最好,2014年Bahdanau在論文Neural Machine Translation By Jointy Learning to Align and Translate中提出的attention機制也被運用了起來。背景介紹到此,下面我們趕快來看看代碼吧!

 

代碼:

較之PTM模型,機器翻譯的demo代碼有足足個file之多,可見其難度。(其實也沒那么難,都是些猶如紙老虎般的存在,充其量也就在數量上嚇唬嚇唬人罷了。)項目源代碼可以在Tensorflow官方github中找到,地址點擊這里。打開項目后我們發現了3個文件,分別是data_utils.py, seq2seq_model.py以及translate.py。這三個文件里,data_utils.py是類似於helper function般的存在,如其名,是一個集合了對輸入raw的數據做處理的一個file。file本身不是用來運行的,只是被translate.py所使用而已。這個文件本身也可以在你自己的Tensorflow框架的本地庫內找到,所需要做的只是輸入from tensorflow.models.rnn.translate import data_utils,就可以取得庫里的函數了。為方便用戶,這個data_utils文件里的函數還自帶檢測數據庫是否存在於本地路徑的功能,如果你沒來得及或不知道如何下載數據庫,只需要指定路徑后運行庫里的函數即可,是不是很方便?

Encoder-decoder模型存在於第二個文件中,即seq2seq_model.py文件。文件作為一個類包含了組建機器翻譯所需要的神經網絡圖框架。與data_utils文件一樣,他們都是被我們的“main”文件,translate.py使用的。

了解了三個文件的關系后,我們也了解了系統運行的過程以及解讀順序,那么,我們先來了解一下程序運行的大體順序。運行該程序的方式為cd到文件所在的目錄下后在terminal里面輸入“python translate.py --data_dir /tmp/ --train_dir /tmp/”就可以了。 其中tmp為默認的tmp文件夾,如果你希望長期保留運行結果,建議在別的文件夾里進行實驗。另外,你可以改變一些模型的參數,如把模型改為兩層,每層改為256個神經元等。方法為在剛才的comment后面加上兩橫杠,更改參數名,等於號及更改后參數量。如改變模型為兩層的框架的方法是“--num_layers=2”。那么在輸入參數后系統是怎么運行的呢?我們在translate.py里找到了main函數的代碼如下:

def main(_):
  if FLAGS.self_test:
    self_test()
  elif FLAGS.decode:
    decode()
  else:
    train()

 該代碼顯示,一般情況下如果你沒表明要運行decode模式或者測試模式,訓練模式將自動開始。那么很明顯,我們的主程序在訓練模式,也就train函數里。那么我們就順藤摸瓜來看看train函數吧。

首先,train函數的開始為運行data_utils庫的prepare_wmt_data函數,這里的輸入為我們在之前手動輸入的data_dir外還有英語和法語的單詞數。這里除了data_dir是我們之前手動輸入的外,英法語的單詞數默認都是4萬。通過這個神奇的prepare_wmt_data函數,我們可以得到英語,法語兩種語言的訓練以及測試資料外,還可以獲得英語及法語單詞的單詞表存放路徑。那么這個函數是如何工作的呢?我們這就來一探究竟。

def prepare_wmt_data(data_dir, en_vocabulary_size, fr_vocabulary_size, tokenizer=None):
  """Get WMT data into data_dir, create vocabularies and tokenize data.
  Args:
    data_dir: directory in which the data sets will be stored.
    en_vocabulary_size: size of the English vocabulary to create and use.
    fr_vocabulary_size: size of the French vocabulary to create and use.
    tokenizer: a function to use to tokenize each data sentence;
      if None, basic_tokenizer will be used.
  Returns:
    A tuple of 6 elements:
      (1) path to the token-ids for English training data-set,
      (2) path to the token-ids for French training data-set,
      (3) path to the token-ids for English development data-set,
      (4) path to the token-ids for French development data-set,
      (5) path to the English vocabulary file,
      (6) path to the French vocabulary file.
  """
  # Get wmt data to the specified directory.
  # 建立訓練集並取得他們的路徑
  train_path = get_wmt_enfr_train_set(data_dir)
  dev_path = get_wmt_enfr_dev_set(data_dir)

  # Create vocabularies of the appropriate sizes.
  # 建立英語及法語的單詞表
  fr_vocab_path = os.path.join(data_dir, "vocab%d.fr" % fr_vocabulary_size)
  en_vocab_path = os.path.join(data_dir, "vocab%d.en" % en_vocabulary_size)
  create_vocabulary(fr_vocab_path, train_path + ".fr", fr_vocabulary_size, tokenizer)
  create_vocabulary(en_vocab_path, train_path + ".en", en_vocabulary_size, tokenizer)

  # Create token ids for the training data.
  # 將輸入數據里的單詞數字化以方便運算
  fr_train_ids_path = train_path + (".ids%d.fr" % fr_vocabulary_size)
  en_train_ids_path = train_path + (".ids%d.en" % en_vocabulary_size)
  data_to_token_ids(train_path + ".fr", fr_train_ids_path, fr_vocab_path, tokenizer)
  data_to_token_ids(train_path + ".en", en_train_ids_path, en_vocab_path, tokenizer)

  # Create token ids for the development data.
  # 將測試數據里的單詞數字化以方便運算
  fr_dev_ids_path = dev_path + (".ids%d.fr" % fr_vocabulary_size)
  en_dev_ids_path = dev_path + (".ids%d.en" % en_vocabulary_size)
  data_to_token_ids(dev_path + ".fr", fr_dev_ids_path, fr_vocab_path, tokenizer)
  data_to_token_ids(dev_path + ".en", en_dev_ids_path, en_vocab_path, tokenizer)

  return (en_train_ids_path, fr_train_ids_path,
          en_dev_ids_path, fr_dev_ids_path,
          en_vocab_path, fr_vocab_path)

在data_utils庫里,prepare_wmt_data整合了其他的helper函數,其中,get_wmt_enfr_train_set和get_wmt_enfr_dev_set兩個函數測試了訓練集的路徑是否存在后,視情況下載訓練集(如果訓練集還未下載),之后便是解壓了訓練包中所用的具體集並提供其路徑。雖然對於測試集有不同的應對方法,其邏輯大同小異,具體實現過程大家可以閱讀這兩個函數去一探究竟,這里將不做細說。

之后函數建立了兩個不同的單詞表,即英語單詞表及法語單詞表。在建立表的過程中,邏輯近似於之前討論過的Word2Vec的create_dataset函數,即從海量數據里建立一個對應的字典來統計輸入詞。其中,值得注意的是這里有一個tokenizer輸入的選項,默認的tokenizer是在符號處截斷句子,其代碼如下:

_WORD_SPLIT = re.compile(b"([.,!?\"':;)(])")

def basic_tokenizer(sentence):
  """Very basic tokenizer: split the sentence into a list of tokens."""
  words = []
  for space_separated_fragment in sentence.strip().split():
    words.extend(re.split(_WORD_SPLIT, space_separated_fragment))
  return [w for w in words if w]

 該項目鼓勵讀者們去采用更好的tokenizer去取得更好的結果。最后,不同於Word2Vec模型的最大地方在於我們將這個單詞集保存后備用,也就是說,在第一次運算耗時可能很長后,之后在運行將會比較方便。

在建立完詞典后,通過運用data_to_token_ids函數,我們可以將輸入轉化為數字序列並將其保存,這樣可以方便我們系統的運用。其原理也可以在Word2Vec的demo代碼里找到邏輯,即運用單詞在詞典里的位置來代替單詞。具體內容請參考博客Python Tensorflow下的Word2Vec代碼解釋。所得的結果在保存在各自的文件中已備后用后,我們可以直接運用這些資料來訓練我們的系統了。接下來,讓我們重新回到translation.py文件的train()函數來繼續了解它的機制。在得到了輸入及訓練資料后,我們看到了熟悉的with tf.Session() as sess,這里大家都明白怎么回事了吧,我們進入session了,可以開始建立模型並運行了。那么很明顯,我們現在的任務就是建立模型。這里,我們看到了model = create_model(sess, False), 這個函數的具體內容就在train()函數之上,很好找。那么,它是怎么建立模型的呢?走進該函數后我們發現它其實就是個包裝盒子,運用了系統的另一個文件,即seq2seq_model.py庫里Seq2SeqModel類來得到模型,之后便是取得模型目前的狀態。如果模型已經訓練並保存,我們即呼喚之前訓練的模型並返回。反之則初始化所有參數並返回模型。

現在,讓我們來看看這個模型本身。在seq2seq_model.py文件里有三個函數:init函數,step函數及get_batch函數。目前,我們在制造模型階段,所以先來看看這個init函數。

def __init__(self, source_vocab_size, target_vocab_size, buckets, size,
               num_layers, max_gradient_norm, batch_size, learning_rate,
               learning_rate_decay_factor, use_lstm=False,
               num_samples=512, forward_only=False):

 這個函數的parameter列很長,有源語言和目標語言各自的單詞數量,框架的層數,每層的神經元數,用於clipped的梯度的最大數值,訓練batch的大小,learning rate,learning rate減少的比例及sampled softmax所接收的sample數量。這些都是常見的訓練神經網絡的參數。參數use_lstm也比較好理解,默認的false表明我們將會運用到GRU,即簡易版本的lstm。設置為True時便是使用傳統的LSTM cell了。現在,有兩個參數我沒講解到,buckets參數和forward_only參數。這兩個參數挺有意思。bucket參數的存在是針對機器翻譯的,他的格式為一個充滿(I,O)的list,I代表這最大輸入長度,O代表着輸出的最大長度。當輸入或輸出超出這個距離后,我們將超出的部分放入下一個batch。至於forward_only參數,其存在是因為兩種不同的訓練方式。一種方式為在訓練中根據兩種語言各自訓練input及output,這是默認方式,即該參數設為Flase。如果我們設為True后,將會在訓練output語言時運用output目標的開頭后由輸入語言取得剩下的數據。兩種方法在官方的document里有詳細講解,這里附上鏈接供有興趣的讀者加深了解。

之后,在assign了變量后,我們又見到了熟悉的RNN模型框架,即

    # If we use sampled softmax, we need an output projection.
    output_projection = None
    softmax_loss_function = None
    # Sampled softmax only makes sense if we sample less than vocabulary size.
    if num_samples > 0 and num_samples < self.target_vocab_size:
      w = tf.get_variable("proj_w", [size, self.target_vocab_size])
      w_t = tf.transpose(w)
      b = tf.get_variable("proj_b", [self.target_vocab_size])
      output_projection = (w, b)

      def sampled_loss(inputs, labels):
        labels = tf.reshape(labels, [-1, 1])
        return tf.nn.sampled_softmax_loss(w_t, b, inputs, labels, num_samples,
                self.target_vocab_size)
      softmax_loss_function = sampled_loss

    # Create the internal multi-layer cell for our RNN.
    single_cell = tf.nn.rnn_cell.GRUCell(size)
    if use_lstm:
      single_cell = tf.nn.rnn_cell.BasicLSTMCell(size)
    cell = single_cell
    if num_layers > 1:
      cell = tf.nn.rnn_cell.MultiRNNCell([single_cell] * num_layers)

    # The seq2seq function: we use embedding for the input and attention.
    def seq2seq_f(encoder_inputs, decoder_inputs, do_decode):
      return tf.nn.seq2seq.embedding_attention_seq2seq(
          encoder_inputs, decoder_inputs, cell,
          num_encoder_symbols=source_vocab_size,
          num_decoder_symbols=target_vocab_size,
          embedding_size=size,
          output_projection=output_projection,
          feed_previous=do_decode)

 這里,我們先了解到如果我們的參數num_samples大於0但小於目標單詞量時,我們再次如同PTB模型那樣運用一個projection layer來減少空間的占據。之后,我們默認celll類型是GRU,但是如果用戶設定了用lstm,我們即更新single_cell變量到lstm cell框架。當網絡層大於1時(默認為3層),我們的cell變成了一個多層RNN框架,由單層cell累積組成。這個邏輯已經在之前關於PTM模型的博客里有過介紹,如果你不熟悉這個設定,請看之前PTB模型的博客。在之后,我們發現設計了一個seq2seq_f的內部函數,這個函數為模型加入attention機制,之后我們會用到。

如同往常,我們為源語言,目標語言的訓練輸入以及目標權重建立placeholder,因為訓練目標是輸入目標的下一句,我們設定目標為目標語言輸入的現在位置+1。之后便是建立訓練輸出及計算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, False),
          softmax_loss_function=softmax_loss_function)

我們將會運用Tensorflow庫seq2seq里的model_with_buckets函數來完成。這個函數是什么呢?讓我們來一探究竟。該函數的目標是建立一個bucket版本的seq2seq模型。模型取得encoder, decoder的輸入,目標和權重的Tensor,輸入和輸出大小配對列表叫做buckets的參數后,要求一個sequence to sequence模型,softmax_loss_function函數(default是None),每例子的loss(default None)及名字(default None)。輸出是(output, losses)tuple,output指的是每一個bucket的輸出,losses這該bucket的loss數值。了解了這些后,我們發掘這里的sequence to sequence函數輸入我們運用了lambda匿名函數,核心是我們的attention模型。這是我們訓練的基礎,但是當forward_only被設為True時,訓練后,我們又多了一步,及重新編寫buckets的output為揉合輸出及之前projection的output。代碼如下:

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

 之后便是傳統的RNN訓練方法,即運用GradientDecentOptimizer,並運用clip_by_global_norm及appy_gradient函數。這些知識點在之前的PTB模型里以詳細介紹,這里將不再重復。

 建立模型之后,在train()函數里,我們讀入測試和訓練的數據,計算訓練的bucket並選擇輸入的句子屬於哪個bucket (= [sum(train_bucket_sizes[:i + 1]) / train_total_size for i in xrange(len(train_bucket_sizes))]),之后便是具體訓練的循環步驟。

步驟里,系統在運用seq2seq_model類里的get_batch和step兩個函數,在取得了一個batch的數據后訓練一個步驟的數據,並在一定的步驟后展出結果。代碼的邏輯還是很清晰的,只是值得注意的是第205行的sys.stdout.flush()代碼,這代碼的存在是為了實時把運算結果展示在terminal的。除此之外,大家可以仔細閱讀代碼來加深了解,這里將不再細說。

就此,系統訓練的步驟講完了。但當我們准備測試我們的訓練結果時,我們該怎么辦呢?這里,我們就要講講這個decode()函數了。根據官方的說明,我們在訓練模型后模型參數等全部資料全部都是有好好的保存的,所以我們不需要再次訓練了,我們只需要運行“python translate.py --decode --data_dir /tmp/ --train_dir /tmp/"即可,一個interactive session將會打開供我們運作。

這個decode函數本身是用來測試系統的,所以在初始化英語法語的單詞表后,系統讀取我們輸入的一行句子后,運行一下邏輯:

    # 讀取一行話
    sentence = sys.stdin.readline()
    while sentence:
      # Get token-ids for the input sentence.
      # 把輸入轉換成token_ids
      token_ids = data_utils.sentence_to_token_ids(tf.compat.as_bytes(sentence), en_vocab)
      # Which bucket does it belong to?
      # 選擇輸入所對應的bucket大小
      bucket_id = min([b for b in xrange(len(_buckets))
                       if _buckets[b][0] > len(token_ids)])
      # Get a 1-element batch to feed the sentence to the model.
      # 取得一個一個element的batch並通過step函數來取得運行結果
      encoder_inputs, decoder_inputs, target_weights = model.get_batch(
          {bucket_id: [(token_ids, [])]}, bucket_id)
      # Get output logits for the sentence.
      _, _, output_logits = model.step(sess, encoder_inputs, decoder_inputs,
                                       target_weights, bucket_id, True)
      # This is a greedy decoder - outputs are just argmaxes of output_logits.
      outputs = [int(np.argmax(logit, axis=1)) for logit in output_logits]
      # If there is an EOS symbol in outputs, cut them at that point.
      if data_utils.EOS_ID in outputs:
        outputs = outputs[:outputs.index(data_utils.EOS_ID)]
      # Print out French sentence corresponding to outputs.
      print(" ".join([tf.compat.as_str(rev_fr_vocab[output]) for output in outputs]))
      print("> ", end="")
      sys.stdout.flush()
      sentence = sys.stdin.readline()

 通過這種方式,我們可以驗證我們系統的好壞。可惜的是第一,我不懂法語。第二,系統在讀取第1770000行句子時系統卡死了,差點廢了我的硬盤,我沒有得到測試結果,可能是模型過大我的電腦承受不起的緣故吧。對於這個錯誤我會研究一下,不過如果讀者們有類似的情況並知道為何如此,請務必讓我知道!謝謝大家!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM