本文轉載自:http://blog.stupidme.me/2018/08/05/tensorflow-nmt-word-embeddings/,本站轉載出於傳遞更多信息之目的,版權歸原做者或者來源機構全部。網絡
聲明:本文由 羅周楊 stupidme.me.lzy@gmail.com 原創,未經受權不得轉載分佈式
天然語言處理的第一步,就是要將文本表示成計算機能理解的方式。咱們將長文本分詞以後,獲得一個詞典,對於詞典中的每個詞,咱們用一個或者一組數字來表示它們。這樣就實現了咱們的目標。函數
首先, Embedding 即 詞嵌入 ,它的做用是什麼呢?很簡單, 將文本信息轉化成數字 。由於計算機沒法直接處理文字,因此我須要 將文字轉化成數字 這一個技術。學習
文字轉化成數字不是很簡單嗎?最簡單的,對於每個詞,咱們都給一個整數進行表示,這樣不就能夠了嗎?更進一步,對於每個詞,咱們都給定一個定長的向量,讓某一個位置(能夠是前面的整數表示),使這個位置的值爲1,其他位置爲0。也就是說,假設咱們有1000個詞,那麼咱們對於每個詞,都寫成一個1000個元素的列向量,每一個向量裏,只有一個位置的值是1,其他位置都是0,好比:編碼
實際上,上面這種編碼就是 one-hot 編碼,翻譯過來就是 獨熱編碼 。spa
可是咱們的Embedding並非這樣作的,爲何呢?翻譯
主要緣由就是上述 one-hot 編碼有如下幾個嚴重的缺點:code
也就是說,咱們的 Embedding 須要解決以上問題,那麼怎麼辦呢?也很簡單:blog
這樣以來,上述兩個問題也就解決了。token
實際上,你確定發現了一個問題,咱們這寫詞語的數字表示組成的矩陣,全部的值都是能夠變化的,那麼這個變化到底該怎麼變呢?這是一個很關鍵的問題!
答案是: 咱們這個矩陣,實際上就是一個淺層的神經網絡,模型訓練過程當中,會自動更新這些值!
等模型訓練好了,咱們的詞語數字矩陣的值也就肯定下來了。那麼,若是咱們把這個矩陣的值,保存下來,下次不讓模型訓練了,直接加載,這樣能夠嗎?
答案是: 固然能夠! 。
這樣作還能夠減小訓練參數的個數,從而減小訓練時間呢!
實際上,tensorflow/nmt項目有一個參數 --embed_file
指的就是這個所謂的矩陣的值保存的文件!
這就是 Embedding 全部的祕密,一點都不玄乎對不對?
TensorFlow NMT的詞嵌入代碼入口位於 nmt/model.py 文件, BaseModel 有一個 init_embeddings()
方法,NMT模型就是在此處完成詞嵌入的初始化的。
根據上面的介紹,咱們知道,有兩種方式構建Embedding:
接下來,分別介紹一下這兩種方式在 tensorflow/nmt
項目中的構建過程。
須要作詞嵌入,則首先要獲取須要的信息。好比詞典文件,或者說詞嵌入文件(若是已經有訓練好的詞嵌入文件的話)。這些信息,都是經過超參數hparams這個參數傳遞過來的。主要的參數獲取以下:
def _init_embeddings(self, hparams, scope): # 源數據和目標數據是否使用相同的詞典 share_vocab = hparams.share_vocab src_vocab_size = self.src_vocab_size tgt_vocab_size = self.tgt_vocab_size # 源數據詞嵌入的維度,數值上等於指定的神經單元數量 src_embed_size = hparams.num_units # 目標數據詞嵌入的維度,數值上等於指定的神經單元數量 tgt_embed_size = hparams.num_units # 詞嵌入分塊數量,分佈式訓練的時候,須要該值大於1 num_partitions = hparams.num_embeddings_partitions # 源數據的詞典文件 src_vocab_file = hparams.src_vocab_file # 目標數據的詞典文件 tgt_vocab_file = hparams.tgt_vocab_file # 源數據已經訓練好的詞嵌入文件 src_embed_file = hparams.src_embed_file # 目標數據已經訓練好的詞嵌入文件 tgt_embed_file = hparams.tgt_embed_file # 分塊器,用於分佈式訓練 if num_partitions <= 1: # 小於等於1,則不須要分塊,不使用分佈式訓練 partitioner = None else: # 分塊器也是一個張量,其值大小和分塊數量同樣 partitioner = tf.fixed_size_partitioner(num_partitions) # 若是使用分佈式訓練,則不能使用已經訓練好的詞嵌入文件 if (src_embed_file or tgt_embed_file) and partitioner: raise ValueError( "Can't set num_partitions > 1 when using pretrained embedding")
參數的意義我已經寫在註釋裏面了。
獲取到這些參數以後,咱們就能夠建立或者加載詞嵌入的矩陣表示了。
根據超參數,若是提供了 預訓練 的詞嵌入文件,則咱們只須要根據詞典,將詞典中的詞的嵌入表示,從詞嵌入文件取出來便可。若是沒有提供預訓練的詞嵌入文件,則咱們本身建立一個便可。
# 建立詞嵌入的變量域 with tf.variable_scope(scope or "embeddings", dtype=tf.float32, partitioner=partitioner) as scope: # 若是共享詞典 if share_vocab: # 檢查詞典大小是否匹配 if src_vocab_size != tgt_vocab_size: raise ValueError("Share embedding but different src/tgt vocab sizes" " %d vs. %d" % (src_vocab_size, tgt_vocab_size)) assert src_embed_size == tgt_embed_size vocab_file = src_vocab_file or tgt_vocab_file embed_file = src_embed_file or tgt_embed_file # 若是有訓練好的詞嵌入模型,則直接加載,不然建立新的 embedding_encoder = self._create_or_load_embed( "embedding_share", vocab_file, embed_file, src_vocab_size, src_embed_size, dtype=tf.float32) embedding_decoder = embedding_encoder # 不共享詞典的話,須要根據不一樣的詞典建立對應的編碼器和解碼器 else: # 加載或者建立編碼器 with tf.variable_scope("encoder", partitioner=partitioner): embedding_encoder = self._create_or_load_embed( "embedding_encoder", src_vocab_file, src_embed_file, src_vocab_size, src_embed_size, tf.float32) # 加載或建立解碼器 with tf.variable_scope("decoder", partitioner=partitioner): embedding_decoder = self._create_or_load_embed( "embedding_decoder", tgt_vocab_file, tgt_embed_file, tgt_vocab_size, tgt_embed_size, tf.float32) self.embedding_encoder = embedding_encoder self.embedding_decoder = embedding_decoder
如你所見,在獲取詞嵌入表示以前,有一個share_vocab的判斷。這個判斷也很簡單,就是判斷源數據和目標數據是否使用相同的詞典,不論是不是share_vocab,最後都須要建立或者加載詞嵌入表示。這個關鍵的過程在 _create_or_load_embed()
函數中完成。
該函數的主要工做以下:
def _create_or_load_embed(self, embed_name, vocab_file, embed_file, vocab_size, embed_size, dtype=tf.float32): # 若是提供了訓練好的詞嵌入文件,則直接加載 if vocab_file and embed_file: embedding = self._create_pretrained_emb_from_txt(vocab_file, embed_file) else: # 不然建立新的詞嵌入 with tf.device(self._get_embed_device(vocab_size)): embedding = tf.get_variable( embed_name, [vocab_size, embed_size], dtype) return embedding
若是超參數提供了embed_file這個預訓練好的詞嵌入文件,那麼我麼只須要讀取該文件,建立出詞嵌入矩陣,返回便可。
主要代碼以下:
def _create_pretrained_emb_from_txt(self, vocab_file, embed_file, num_trainable_tokens=3, dtype=tf.float32, scope=None): """ 從文件加載詞嵌入矩陣 :param vocab_file: 詞典文件 :param embed_file: 訓練好的詞嵌入文件 :param num_trainable_tokens:詞典文件前3個詞標記爲變量,默認爲"<unk>","<s>","</s>" :param scope: 域 :return: 詞嵌入矩陣 """ # 加載詞典 vocab, _ = vocab_utils.load_vocab(vocab_file) # 詞典的前三行會加上三個特殊標記,取出三個特殊標記 trainable_tokens = vocab[:num_trainable_tokens] utils.print_out("# Using pretrained embedding: %s." % embed_file) utils.print_out(" with trainable tokens: ") # 加載訓練好的詞嵌入 emb_dict, emb_size = vocab_utils.load_embed_txt(embed_file) for token in trainable_tokens: utils.print_out(" %s" % token) # 若是三個標記不在訓練好的詞嵌入中 if token not in emb_dict: # 初始化三個標記爲0.0,維度爲詞嵌入的維度 emb_dict[token] = [0.0] * emb_size # 從訓練好的詞嵌入矩陣中,取出詞典中的詞語的詞嵌入表示,數據類型爲tf.float32 emb_mat = np.array( [emb_dict[token] for token in vocab], dtype=dtype.as_numpy_dtype()) # 常量化詞嵌入矩陣 emb_mat = tf.constant(emb_mat) # 從詞嵌入矩陣的第4行以後的全部行和列(由於num_trainable_tokens=3) # 也就是說取出除了3個標記以外全部的詞嵌入表示 # 這是常量,由於已經訓練好了,不須要訓練了 emb_mat_const = tf.slice(emb_mat, [num_trainable_tokens, 0], [-1, -1]) with tf.variable_scope(scope or "pretrain_embeddings", dtype=dtype) as scope: with tf.device(self._get_embed_device(num_trainable_tokens)): # 獲取3個標記的詞嵌入表示,這3個標記的詞嵌入是能夠變的,經過訓練能夠學習 emb_mat_var = tf.get_variable( "emb_mat_var", [num_trainable_tokens, emb_size]) # 將3個標記的詞嵌入和其他單詞的詞嵌入合併起來,獲得完整的單詞詞嵌入表示 return tf.concat([emb_mat_var, emb_mat_const], 0)
處理過程,我已經在註釋裏面寫得很清楚了。接下來看看新建立詞嵌入表示的過程。
這個過程其實很簡單,就是建立一個可訓練的張量而已:
with tf.device(self._get_embed_device(vocab_size)): embedding = tf.get_variable(embed_name, [vocab_size, embed_size], dtype)
該張量的名字就是 embed_name
,shape即[vocab_size, embed_size],其中 vocab_size
就是詞典的大小,也就是二維矩陣的行數, embed_size
就是詞嵌入的維度,每一個詞用多少個數字來表示,也就是二維矩陣的列數。該張量的數據類型是單精度浮點數。固然, tf.get_variable()
方法還有不少提供默認值的參數,其中一個就是 trainable=True
,這表明這個變量是可變的,也就是咱們的詞嵌入表示在訓練過程當中,數字是會改變的。
這樣就完成了詞嵌入的準備過程。