字詞的向量表示
Vector Representations of Words
在本教程我們來看一下Mikolov et al中提到的word2vec模型。該模型是用於學習文字的向量表示,稱之爲“word embedding”。
艾伯特(http://www.aibbt.com/)國內第一家人工智能門戶
亮點
本教程意在展現出在TensorfLow中構建word2vec模型有趣、本質的部分。
- 我們從我們爲何需要使用向量表示文字開始。
- 我們通過直觀地例子觀察模型背後的本質,以及它是如何訓練的(通過一些數學方法評估)。
- 同時我們也展示了TensorFlow對該模型的簡單實現。
- 最後,我們着眼於讓給這個簡單版本的模型表現更好。
我們會在教程的推進中循序漸進地解釋代碼,但是如果你更希望直入主題,可以在tensorflow/g3doc/tutorials/word2vec/word2vec_basic.py查看到一個最簡單的實現。這個基本的例子提供的代碼可以完成下載一些數據,簡單訓練後展示結果。一旦你覺得已經完全掌握了這個簡單版本,你可以查看tensorflow/models/embedding/word2vec.py,這裏提供了一些更復雜的實現,同時也展示了TensorFlow的一些更進階的特性,比如如何更高效地使用線程將數據送入文本模型,再比如如何在訓練中設置檢查點等等。
但是首先,讓我們來看一下爲何需要學習word embeddings。如果你對word embeddings相關內容已經是個專家了,那麼請安心跳過本節內容,直接深入細節幹一些髒活吧。
動機: 爲什麼需要學習 Word Embeddings?
通常圖像或音頻系統處理的是由圖片中所有單個原始像素點強度值或者音頻中功率譜密度的強度值,把它們編碼成豐富、高緯度的向量數據集。對於物體或語音識別這一類的任務,我們所需的全部信息已經都存儲在原始數據中(顯然人類本身就是依賴原始數據進行日常的物體或語音識別的)。然後,自然語言處理系統通常將詞彙作爲離散的單一符號,例如 "cat" 一詞或可表示爲 Id537
,而 "dog" 一詞或可表示爲 Id143
。這些符號編碼毫無規律,無法提供不同詞彙之間可能存在的關聯信息。換句話說,在處理關於 "dogs" 一詞的信息時,模型將無法利用已知的關於 "cats" 的信息(例如,它們都是動物,有四條腿,可作爲寵物等等)。可見,將詞彙表達爲上述的獨立離散符號將進一步導致數據稀疏,使我們在訓練統計模型時不得不尋求更多的數據。而詞彙的向量表示將克服上述的難題。
向量空間模型 (VSMs)將詞彙表達(嵌套)於一個連續的向量空間中,語義近似的詞彙被映射爲相鄰的數據點。向量空間模型在自然語言處理領域中有着漫長且豐富的歷史,不過幾乎所有利用這一模型的方法都依賴於 分佈式假設,其核心思想爲出現於上下文情景中的詞彙都有相類似的語義。採用這一假設的研究方法大致分爲以下兩類:基於計數的方法 (e.g. 潛在語義分析), 和 預測方法 (e.g. 神經概率化語言模型).
其中它們的區別在如下論文中又詳細闡述 Baroni et al.,不過簡而言之:基於計數的方法計算某詞彙與其鄰近詞彙在一個大型語料庫中共同出現的頻率及其他統計量,然後將這些統計量映射到一個小型且稠密的向量中。預測方法則試圖直接從某詞彙的鄰近詞彙對其進行預測,在此過程中利用已經學習到的小型且稠密的嵌套向量。
Word2vec是一種可以進行高效率詞嵌套學習的預測模型。其兩種變體分別爲:連續詞袋模型(CBOW)及Skip-Gram模型。從算法角度看,這兩種方法非常相似,其區別爲CBOW根據源詞上下文詞彙('the cat sits on the')來預測目標詞彙(例如,‘mat’),而Skip-Gram模型做法相反,它通過目標詞彙來預測源詞彙。Skip-Gram模型採取CBOW的逆過程的動機在於:CBOW算法對於很多分佈式信息進行了平滑處理(例如將一整段上下文信息視爲一個單一觀察量)。很多情況下,對於小型的數據集,這一處理是有幫助的。相形之下,Skip-Gram模型將每個“上下文-目標詞彙”的組合視爲一個新觀察量,這種做法在大型數據集中會更爲有效。本教程餘下部分將着重講解Skip-Gram模型。
處理噪聲對比訓練
神經概率化語言模型通常使用極大似然法 (ML) 進行訓練,其中通過 softmax function 來最大化當提供前一個單詞 h(代表 "history"),後一個單詞的概率(代表 "target"),
當 score(w_t,h) 計算了文字 w_t 和 上下文 h 的相容性(通常使用向量積)。我們使用對數似然函數來訓練訓練集的最大值,比如通過:
這裏提出了一個解決語言概率模型的合適的通用方法。然而這個方法實際執行起來開銷非常大,因爲我們需要去計算並正則化當前上下文環境 h 中所有其他 V 單詞 w' 的概率得分,在每一步訓練迭代中。
從另一個角度來說,當使用word2vec模型時,我們並不需要對概率模型中的所有特徵進行學習。而CBOW模型和Skip-Gram模型爲了避免這種情況發生,使用一個二分類器(邏輯迴歸)在同一個上下文環境裏從 k 虛構的 (噪聲) 單詞區分出真正的目標單詞。我們下面詳細闡述一下CBOW模型,對於Skip-Gram模型只要簡單地做相反的操作即可。
從數學角度來說,我們的目標是對每個樣本最大化:
其中代表的是數據集在當前上下文 h ,根據所學習的嵌套向量,目標單詞 w 使用二分類邏輯迴歸計算得出的概率。在實踐中,我們通過在噪聲分佈中繪製比對文字來獲得近似的期望值(通過計算蒙特卡洛平均值)。
當真實地目標單詞被分配到較高的概率,同時噪聲單詞的概率很低時,目標函數也就達到最大值了。從技術層面來說,這種方法叫做 負抽樣,而且使用這個損失函數在數學層面上也有很好的解釋:這個更新過程也近似於softmax函數的更新。這在計算上將會有很大的優勢,因爲當計算這個損失函數時,只是有我們挑選出來的 k 個 噪聲單詞,而沒有使用整個語料庫 V。這使得訓練變得非常快。我們實際上使用了與noise-contrastive estimation (NCE)介紹的非常相似的方法,這在TensorFlow中已經封裝了一個很便捷的函數tf.nn.nce_loss()
。
讓我們在實踐中來直觀地體會它是如何運作的!
Skip-gram 模型
下面來看一下這個數據集
the quick brown fox jumped over the lazy dog
我們首先對一些單詞以及它們的上下文環境建立一個數據集。我們可以以任何合理的方式定義‘上下文’,而通常上這個方式是根據文字的句法語境的(使用語法原理的方式處理當前目標單詞可以看一下這篇文獻 Levy et al.,比如說把目標單詞左邊的內容當做一個‘上下文’,或者以目標單詞右邊的內容,等等。現在我們把目標單詞的左右單詞視作一個上下文, 使用大小爲1的窗口,這樣就得到這樣一個由(上下文, 目標單詞)
組成的數據集:
([the, brown], quick), ([quick, fox], brown), ([brown, jumped], fox), ...
前文提到Skip-Gram模型是把目標單詞和上下文顛倒過來,所以在這個問題中,舉個例子,就是用'quick'來預測 'the' 和 'brown' ,用 'brown' 預測 'quick' 和 'brown' 。因此這個數據集就變成由(輸入, 輸出)
組成的:
(quick, the), (quick, brown), (brown, quick), (brown, fox), ...
目標函數通常是對整個數據集建立的,但是本問題中要對每一個樣本(或者是一個batch_size
很小的樣本集,通常設置爲16 <= batch_size <= 512
)在同一時間執行特別的操作,稱之爲隨機梯度下降 (SGD)。我們來看一下訓練過程中每一步的執行。
假設用 t 表示上面這個例子中quick
來預測 the
的訓練的單個循環。用 num_noise
定義從噪聲分佈中挑選出來的噪聲(相反的)單詞的個數,通常使用一元分佈,P(w)。爲了簡單起見,我們就定num_noise=1
,用 sheep
選作噪聲詞。接下來就可以計算每一對觀察值和噪聲值的損失函數了,每一個執行步驟就可表示爲:
整個計算過程的目標是通過更新嵌套參數來逼近目標函數(這個這個例子中就是使目標函數最大化)。爲此我們要計算損失函數中嵌套參數的梯度,比如,(幸好TensorFlow封裝了工具函數可以簡單調用!)。對於整個數據集,當梯度下降的過程中不斷地更新參數,對應產生的效果就是不斷地移動每個單詞的嵌套向量,直到可以把真實單詞和噪聲單詞很好得區分開。
我們可以把學習向量映射到2維中以便我們觀察,其中用到的技術可以參考 t-SNE 降緯技術。當我們用可視化的方式來觀察這些向量,就可以很明顯的獲取單詞之間語義信息的關係,這實際上是非常有用的。當我們第一次發現這樣的誘導向量空間中,展示了一些特定的語義關係,這是非常有趣的,比如文字中 male-female,gender 甚至還有 country-capital的關係, 如下方的圖所示 (也可以參考 Mikolov et al., 2013論文中的例子)。
這也解釋了爲什麼這些向量在傳統的NLP問題中可作爲特性使用,比如用在對一個演講章節打個標籤,或者對一個專有名詞的識別 (看看如下這個例子 Collobert et al.或者 Turian et al.)。
不過現在讓我們用它們來畫漂亮的圖表吧!
建立圖形
這裏談得都是嵌套,那麼先來定義一個嵌套參數矩陣。我們用唯一的隨機值來初始化這個大矩陣。
embeddings = tf.Variable(
tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))
對噪聲-比對的損失計算就使用一個邏輯迴歸模型。對此,我們需要對語料庫中的每個單詞定義一個權重值和偏差值。(也可稱之爲輸出權重
與之對應的 輸入嵌套值
)。定義如下。
nce_weights = tf.Variable(
tf.truncated_normal([vocabulary_size, embedding_size],
stddev=1.0 / math.sqrt(embedding_size)))
nce_biases = tf.Variable(tf.zeros([vocabulary_size]))
我們有了這些參數之後,就可以定義Skip-Gram模型了。簡單起見,假設我們已經把語料庫中的文字整型化了,這樣每個整型代表一個單詞(細節請查看 tensorflow/g3doc/tutorials/word2vec/word2vec_basic.py)。Skip-Gram模型有兩個輸入。一個是一組用整型表示的上下文單詞,另一個是目標單詞。給這些輸入建立佔位符節點,之後就可以填入數據了。
# 建立輸入佔位符
train_inputs = tf.placeholder(tf.int32, shape=[batch_size])
train_labels = tf.placeholder(tf.int32, shape=[batch_size, 1])
然後我們需要對批數據中的單詞建立嵌套向量,TensorFlow提供了方便的工具函數。
embed = tf.nn.embedding_lookup(embeddings, train_inputs)
好了,現在我們有了每個單詞的嵌套向量,接下來就是使用噪聲-比對的訓練方式來預測目標單詞。
# 計算 NCE 損失函數, 每次使用負標籤的樣本.
loss = tf.reduce_mean(
tf.nn.nce_loss(nce_weights, nce_biases, embed, train_labels,
num_sampled, vocabulary_size))
我們對損失函數建立了圖形節點,然後我們需要計算相應梯度和更新參數的節點,比如說在這裏我們會使用隨機梯度下降法,TensorFlow也已經封裝好了該過程。
# 使用 SGD 控制器.
optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0).minimize(loss)
訓練模型
訓練的過程很簡單,只要在循環中使用feed_dict
不斷給佔位符填充數據,同時調用 session.run
即可。
for inputs, labels in generate_batch(...):
feed_dict = {training_inputs: inputs, training_labels: labels}
_, cur_loss = session.run([optimizer, loss], feed_dict=feed_dict)
完整地例子可參考 tensorflow/g3doc/tutorials/word2vec/word2vec_basic.py.
嵌套學習結果可視化
使用t-SNE來看一下嵌套學習完成的結果。
http://www.aibbt.com/a/16370.html
Et voila! 與預期的一樣,相似的單詞被聚類在一起。對word2vec模型更復雜的實現需要用到TensorFlow一些更高級的特性,具體是實現可以參考 tensorflow/models/embedding/word2vec.py。
嵌套學習的評估: 類比推理
詞嵌套在NLP的預測問題中是非常有用且使用廣泛地。如果要檢測一個模型是否是可以成熟地區分詞性或者區分專有名詞的模型,最簡單的辦法就是直接檢驗它的預測詞性、語義關係的能力,比如讓它解決形如king is to queen as father is to ?
這樣的問題。這種方法叫做類比推理 ,可參考Mikolov and colleagues,數據集下載地址爲:https://word2vec.googlecode.com/svn/trunk/questions-words.txt。
To see how we do this evaluation如何執行這樣的評估,可以看build_eval_graph()
和 eval()
這兩個函數在下面源碼中的使用 tensorflow/models/embedding/word2vec.py.
超參數的選擇對該問題解決的準確性有巨大的影響。想要模型具有很好的表現,需要有一個巨大的訓練數據集,同時仔細調整參數的選擇並且使用例如二次抽樣的一些技巧。不過這些問題已經超出了本教程的範圍。
優化實現
以上簡單的例子展示了TensorFlow的靈活性。比如說,我們可以很輕鬆得用現成的tf.nn.sampled_softmax_loss()
來代替tf.nn.nce_loss()
構成目標函數。如果你對損失函數想做新的嘗試,你可以用TensorFlow手動編寫新的目標函數的表達式,然後用控制器執行計算。這種靈活性的價值體現在,當我們探索一個機器學習模型時,我們可以很快地遍歷這些嘗試,從中選出最優。
一旦你有了一個滿意的模型結構,或許它就可以使實現運行地更高效(在短時間內覆蓋更多的數據)。比如說,在本教程中使用的簡單代碼,實際運行速度都不錯,因爲我們使用Python來讀取和填裝數據,而這些在TensorFlow後臺只需執行非常少的工作。如果你發現你的模型在輸入數據時存在嚴重的瓶頸,你可以根據自己的實際問題自行實現一個數據閱讀器,參考 新的數據格式。對於Skip-Gram 模型,我們已經完成了如下這個例子tensorflow/models/embedding/word2vec.py。
如果I/O問題對你的模型已經不再是個問題,並且想進一步地優化性能,或許你可以自行編寫TensorFlow操作單元,詳見添加一個新的操作。相應的,我們也提供了Skip-Gram模型的例子tensorflow/models/embedding/word2vec_optimized.py。請自行調節以上幾個過程的標準,使模型在每個運行階段有更好地性能。
總結
在本教程中我們介紹了word2vec模型,它在解決詞嵌套問題中具有良好的性能。我們解釋了使用詞嵌套模型的實用性,並且討論瞭如何使用TensorFlow實現該模型的高效訓練。總的來說,我們希望這個例子能夠讓向你展示TensorFlow可以提供實驗初期的靈活性,以及在後期優化模型時對模型內部的可操控性。