本文轉載自:http://blog.stupidme.me/2018/08/05/tensorflow-nmt-word-embeddings/,本站轉載出於傳遞更多信息之目的,版權歸原作者或者來源機構所有。
聲明:本文由 羅周楊 stupidme.me.lzy@gmail.com 原創,未經授權不得轉載
自然語言處理的第一步,就是要將文本表示成計算機能理解的方式。我們將長文本分詞之后,得到一個詞典,對於詞典中的每一個詞,我們用一個或者一組數字來表示它們。這樣就實現了我們的目標。
Embedding(詞嵌入)到底是什么
首先, Embedding 即 詞嵌入 ,它的作用是什么呢?很簡單, 將文本信息轉化成數字 。因為計算機無法直接處理文字,所以我需要 將文字轉化成數字 這一個技術。
文字轉化成數字不是很簡單嗎?最簡單的,對於每一個詞,我們都給一個整數進行表示,這樣不就可以了嗎?更進一步,對於每一個詞,我們都給定一個定長的向量,讓某一個位置(可以是前面的整數表示),使這個位置的值為1,其余位置為0。也就是說,假設我們有1000個詞,那么我們對於每一個詞,都寫成一個1000個元素的列向量,每個向量里,只有一個位置的值是1,其余位置都是0,比如:
- hello –> [1,0,0,0,…,0]
- world –> [0,1,0,0,…,0]
實際上,上面這種編碼就是 one-hot 編碼,翻譯過來就是 獨熱編碼 。
但是我們的Embedding並不是這樣做的,為什么呢?
主要原因就是上述 one-hot 編碼有以下幾個嚴重的缺點:
- 維度爆炸,如果我有30萬個詞,那么每一個詞就需要[1,300000]的向量表示。詞越多,維度越高
- 無法表示詞語之間的關系。
也就是說,我們的 Embedding 需要解決以上問題,那么怎么辦呢?也很簡單:
- 對於每一個詞,我們使用一個固定長度的向量來表示,比如長度為256
- 對於每一個的表示,不是使用非0即1這種表示,我們使用浮點數,向量的每一個值都可以是一個浮點數
這樣以來,上述兩個問題也就解決了。
實際上,你肯定發現了一個問題,我們這寫詞語的數字表示組成的矩陣,所有的值都是可以變化的,那么這個變化到底該怎么變呢?這是一個很關鍵的問題!
答案是: 我們這個矩陣,實際上就是一個淺層的神經網絡,模型訓練過程中,會自動更新這些值!
等模型訓練好了,我們的詞語數字矩陣的值也就確定下來了。那么,如果我們把這個矩陣的值,保存下來,下次不讓模型訓練了,直接加載,這樣可以嗎?
答案是: 當然可以! 。
這樣做還可以減少訓練參數的個數,從而減少訓練時間呢!
實際上,tensorflow/nmt項目有一個參數 --embed_file
指的就是這個所謂的矩陣的值保存的文件!
這就是 Embedding 所有的秘密,一點都不玄乎對不對?
NMT項目中Embedding的構建過程
TensorFlow NMT的詞嵌入代碼入口位於 nmt/model.py 文件, BaseModel 有一個 init_embeddings()
方法,NMT模型就是在此處完成詞嵌入的初始化的。
根據上面的介紹,我們知道,有兩種方式構建Embedding:
- 從已經訓練好的文件(embed_file)直接加載
- 構建一個矩陣,讓模型自己訓練
接下來,分別介紹一下這兩種方式在 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
,這代表這個變量是可變的,也就是我們的詞嵌入表示在訓練過程中,數字是會改變的。
這樣就完成了詞嵌入的准備過程。