1. 程式人生 > >Alink漫談(十六) :Word2Vec原始碼分析 之 建立霍夫曼樹

Alink漫談(十六) :Word2Vec原始碼分析 之 建立霍夫曼樹

# Alink漫談(十六) :Word2Vec原始碼分析 之 建立霍夫曼樹 [ToC] ## 0x00 摘要 Alink 是阿里巴巴基於實時計算引擎 Flink 研發的新一代機器學習演算法平臺,是業界首個同時支援批式、流式演算法的機器學習平臺。本文和下文將帶領大家來分析Alink中 Word2Vec 的實現。 因為Alink的公開資料太少,所以以下均為自行揣測,肯定會有疏漏錯誤,希望大家指出,我會隨時更新。 ## 0x01 背景概念 ### 1.1 詞向量基礎 #### 1.1.1 獨熱編碼 one-hot編碼就是保證每個樣本中的單個特徵只有1位處於狀態1,其他的都是0。 具體編碼舉例如下,把語料庫中,杭州、上海、寧波、北京每個都對應一個向量,向量中只有一個值為1,其餘都為0。 ``` 杭州 [0,0,0,0,0,0,0,1,0,……,0,0,0,0,0,0,0] 上海 [0,0,0,0,1,0,0,0,0,……,0,0,0,0,0,0,0] 寧波 [0,0,0,1,0,0,0,0,0,……,0,0,0,0,0,0,0] 北京 [0,0,0,0,0,0,0,0,0,……,1,0,0,0,0,0,0] ``` 其缺點是: - 向量的維度會隨著句子的詞的數量型別增大而增大;如果將世界所有城市名稱對應的向量合為一個矩陣的話,那這個矩陣過於稀疏,並且會造成維度災難。 - 城市編碼是隨機的,向量之間相互獨立,無法表示語義層面上詞彙之間的相關資訊。 所以,人們想對獨熱編碼做如下改進: - 將vector每一個元素由整形改為浮點型,變為整個實數範圍的表示; - 轉化為低維度的連續值,也就是稠密向量。將原來稀疏的巨大維度壓縮嵌入到一個更小維度的空間。並且其中意思相近的詞將被對映到向量空間中相近的位置。 簡單說,要尋找一個空間對映,把高維詞向量嵌入到一個低維空間。然後就可以繼續處理。 #### 1.1.2 分散式表示 分散式表示(Distributed Representation)其實Hinton 最早在1986年就提出了,基本思想是將每個詞表達成 n 維稠密、連續的實數向量。而實數向量之間的關係可以代表詞語之間的相似度,比如向量的夾角cosine或者歐氏距離。 有一個專門的術語來描述詞向量的分散式表示技術——詞嵌入【word embedding】。從名稱上也可以看出來,獨熱編碼相當於對詞進行編碼,而分散式表示則是將詞從稀疏的大維度壓縮嵌入到較低維度的向量空間中。 Distributed representation 最大的貢獻就是讓相關或者相似的詞,在距離上更接近了。其核心思想是:**上下文相似的詞,其語義也相似。**這就是著名的**詞空間模型(word space model)**。 Distributed representation 相較於One-hot方式另一個區別是維數下降極多,對於一個100W的詞表,我們可以用100維的實數向量來表示一個詞,而One-hot得要100W維。 為什麼對映到向量空間當中的詞向量就能表示是確定的哪個詞並且還能知道它們之間的相似度呢? - 關於為什麼能表示詞這個問題。分散式實際上就是求一個對映函式,這個對映函式將每個詞原始的one-hot表示壓縮投射到一個較低維度的空間並一一對應。所以分散式可以表示確定的詞。 - 關於為什麼分散式還能知道詞之間的關係。就必須要了解分散式假設(distributional hypothesis)。 其基於的分散式假設就是出現在相同上下文(context)下的詞意思應該相近。所有學習word embedding的方法都是在用數學的方法建模詞和context之間的關係。 詞向量的分散式表示的核心思想由兩部分組成: - 選擇一種方式描述上下文; - 選擇一種模型刻畫目標詞與其上下文之間的關係。 事實上,不管是神經網路的隱層,還是多個潛在變數的概率主題模型,都是應用分散式表示。 ### 1.2 CBOW & Skip-Gram 在word2vec出現之前,已經有用神經網路DNN來用訓練詞向量進而處理詞與詞之間的關係了。採用的方法一般是一個三層的神經網路結構(當然也可以多層),分為輸入層,隱藏層和輸出層(softmax層)。 這個模型是如何定義資料的輸入和輸出呢?一般分為CBOW(Continuous Bag-of-Words Model) 和 Skip-gram (Continuous Skip-gram Model)兩種模型。 #### 1.2.1 CBOW CBOW通過上下文來預測當前值。相當於一句話中扣掉一個詞,讓你猜這個詞是什麼。CBOW就是根據某個詞前面的C個詞或者前後C個連續的詞,來計算某個詞出現的概率。 CBOW的訓練過程如下: 1. Input layer輸出層:是上下文單詞的one hot。假設單詞向量空間的維度為V,即整個詞庫corpus大小為V,上下文單詞視窗的大小為C。 2. 假設最終詞向量的維度大小為N,則權值共享矩陣為W。W 的大小為 V * N,並且初始化。 3. 假設語料中有一句話"我愛你"。如果我們現在關注"愛"這個詞,令C=2,則其上下文為"我",“你”。模型把"我" "你"的onehot形式作為輸入。易知其大小為1V。C個1V大小的向量分別跟同一個V * N大小的權值共享矩陣W相乘,得到的是C個1N大小的隱層hidden layer。 4. C個1N大小的hidden layer取平均,得到一個1N大小的向量,即Hidden layer。 5. 輸出權重矩陣 W’ 為N V,並進行相應的初始化工作。 6. 將得到的Hidden layer向量 1N與 W’ 相乘,並且用softmax處理,得到1V的向量,此向量的每一維代表corpus中的一個單詞。概率中最大的index所代表的單詞為預測出的中間詞。 7. 與groud truth中的one hot比較,求loss function的的極小值。 8. 通過DNN的反向傳播演算法,我們可以求出DNN模型的引數,同時得到所有的詞對應的詞向量。這樣當我們有新的需求,要求出某8個詞對應的最可能的輸出中心詞時,我們可以通過一次DNN前向傳播演算法並通過softmax啟用函式找到概率最大的詞對應的神經元即可。 #### 1.2.2 Skip-gram Skip-gram用當前詞來預測上下文。相當於給你一個詞,讓你猜前面和後面可能出現什麼詞。即根據某個詞,然後**分別**計算它前後出現某幾個詞的各個概率。從這裡可以看出,對於每一個詞,Skip-gram要訓練**C**次,這裡C是預設的視窗大小,而CBOW只需要計算一次,因此CBOW計算量是Skip-gram的**1/C**,但也正因為Skip-gram同時擬合了C個詞,因此在避免過擬合上比CBOW效果更好,因此在訓練大型語料庫的時候,Skip-gram的效果比CBOW更好。 Skip-gram的訓練方法與CBOW如出一轍,唯一區別就是Skip-gram的輸入是單個詞的向量,而不是C個詞的求和平均。同時,訓練的話對於一箇中心詞,要訓練C次,每一次是一個不同的上下文詞,比如中心詞是**北京**,視窗詞是**來到**、**天安門**這兩個,那麼Skip-gram要對**北京-來到**、**北京-天安門**進行分別訓練。 目前的實現有一個問題:從隱藏層到輸出的softmax層的計算量很大,因為要計算所有詞的softmax概率,再去找概率最大的值。比如Vocab大小有10^5,那麼每算一個概率都要計算10^5次矩陣乘法,不現實。於是就引入了Word2vec。 ### 1.3 Word2vec #### 1.3.1 Word2vec基本思想 所謂的語言模型,就是指對自然語言進行假設和建模,使得能夠用計算機能夠理解的方式來表達自然語言。word2vec採用的是**n元語法模型**(n-gram model),即假設一個詞只與周圍n個詞有關,而與文字中的其他詞無關。 如果 **把詞當做特徵,那麼就可以把特徵對映到 K 維向量空間,可以為文字資料尋求更加深層次的特徵表示** 。所以 Word2vec的基本思想是 **通過訓練將每個詞對映成 K 維實數向量**(K 一般為模型中的超引數),通過詞之間的距離(比如 cosine 相似度、歐氏距離等)來判斷它們之間的語義相似度。 其採用一個 **三層的神經網路** ,輸入層-隱層-輸出層。有個核心的技術是 **根據詞頻用Huffman編碼** ,使得所有詞頻相似的詞隱藏層啟用的內容基本一致,出現頻率越高的詞語,他們啟用的隱藏層數目越少,這樣有效的降低了計算的複雜度。 這個三層神經網路本身是 **對語言模型進行建模** ,但也同時 **獲得一種單詞在向量空間上的表示**,而這個副作用才是Word2vec的真正目標
。 word2vec對之前的模型做了改進, - 首先,對於從輸入層到隱藏層的對映,沒有采取神經網路的線性變換加啟用函式的方法,而是採用簡單的對所有輸入詞向量求和並取平均的方法。比如輸入的是三個4維詞向量:**(1,2,3,4),(9,6,11,8),(5,10,7,12)**,那麼我們word2vec對映後的詞向量就是**(5,6,7,8)**。由於這裡是從多個詞向量變成了一個詞向量。 - 第二個改進就是從隱藏層到輸出的softmax層這裡的計算量個改進。為了避免要計算所有詞的softmax概率,word2vec取樣了霍夫曼樹來代替從隱藏層到輸出softmax層的對映。 #### 1.3.2 Hierarchical Softmax基本思路 Word2vec計算可以用 **層次Softmax演算法** ,這種演算法結合了Huffman編碼,其實藉助了分類問題中,使用一連串二分類近似多分類的思想。例如我們是把所有的詞都作為輸出,那麼“桔子”、“汽車”都是混在一起。給定w_t的上下文,先讓模型判斷w_t是不是名詞,再判斷是不是食物名,再判斷是不是水果,再判斷是不是“桔子”。 取一個適當大小的視窗當做語境,輸入層讀入視窗內的詞,將它們的向量(K維,初始隨機)加和在一起,形成隱藏層K個節點。輸出層是一個巨大的二叉樹,葉節點代表語料裡所有的詞(語料含有V個獨立的詞,則二叉樹有|V|個葉節點)。而這整顆二叉樹構建的演算法就是Huffman樹。 這樣,語料庫中的某個詞w_t 都對應著二叉樹的某個葉子節點,這樣每個詞 w 都可以從樹的根結點root沿著唯一一條路徑被訪問到,其路徑也就形成了其全域性唯一的二進位制編碼code,如"010011"。 不妨記左子樹為1,右子樹為0。接下來,隱層的每一個節點都會跟二叉樹的內節點有連邊,於是對於二叉樹的每一個內節點都會有K條連邊,每條邊上也會有權值。假設 n(w, j)為這條路徑上的第 j 個結點,且 L(w)為這條路徑的長度, j 從 1 開始編碼,即 n(w, 1)=root,n(w, L(w)) = w。對於第 j 個結點,層次 Softmax 定義的Label 為 1 - code[j]。 在訓練階段,當給定上下文,要預測後面的詞w_t的時候,我們就從二叉樹的根節點開始遍歷,這裡的目標就是預測這個詞的二進位制編號的每一位。即對於給定的上下文,我們的目標是使得預測詞的二進位制編碼概率最大
。形象地說,對於 "010011",我們希望在根節點,詞向量和與根節點相連經過logistic計算得到bit=1的概率儘量接近0,在第二層,希望其bit=1的概率儘量接近1,這麼一直下去,我們把一路上計算得到的概率相乘,即得到目標詞w_t在當前網路下的概率P(w_t),那麼對於當前這個sample的殘差就是1-P(w_t),於是就可以使用梯度下降法訓練這個網路得到所有的引數值了。顯而易見,按照目標詞的二進位制編碼計算到最後的概率值就是歸一化的。 在訓練過程中,模型會賦予這些抽象的中間結點一個合適的向量,這個向量代表了它對應的所有子結點。因為真正的單詞公用了這些抽象結點的向量,所以Hierarchical Softmax方法和原始問題並不是等價的,但是這種近似並不會顯著帶來效能上的損失同時又使得模型的求解規模顯著上升。 #### 1.3.3 Hierarchical Softmax 數學推導 傳統的Softmax可以看成是一個線性表,平均查詢時間O(n)。HS方法將Softmax做成一顆平衡的滿二叉樹,維護詞頻後,變成Huffman樹。 ![img](https://images2017.cnblogs.com/blog/1042406/201707/1042406-20170727105752968-819608237.png) 由於我們把之前所有都要計算的從輸出softmax層的概率計算變成了一顆二叉霍夫曼樹,那麼我們的softmax概率計算只需要沿著樹形結構進行就可以了。我們可以沿著霍夫曼樹從根節點一直走到我們的葉子節點的詞**w2**。 和之前的神經網路語言模型相比,我們的霍夫曼樹的所有內部節點就類似之前神經網路隱藏層的神經元,其中,根節點的詞向量對應我們的投影后的詞向量,而所有葉子節點就類似於之前神經網路softmax輸出層的神經元,葉子節點的個數就是詞彙表的大小。在霍夫曼樹中,隱藏層到輸出層的softmax對映不是一下子完成的,而是沿著霍夫曼樹一步步完成的,因此這種softmax取名為"Hierarchical Softmax"。 如何“沿著霍夫曼樹一步步完成”呢?在word2vec中,我們採用了二元邏輯迴歸的方法,即規定沿著左子樹走,那麼就是負類(霍夫曼樹編碼1),沿著右子樹走,那麼就是正類(霍夫曼樹編碼0)。判別正類和負類的方法是使用sigmoid函式即: $$ P(+) = \sigma(x_w^T\theta) = \frac{1}{1+e^{-x_w^T\theta}} $$ 其中**xw**是當前內部節點的詞向量,而θ則是我們需要從訓練樣本求出的邏輯迴歸的模型引數
。 使用霍夫曼樹有什麼好處呢? - 首先,由於是二叉樹,之前計算量為V,現在變成了log2V。 - 第二,由於使用霍夫曼樹是高頻的詞靠近樹根,這樣高頻詞需要更少的時間會被找到,這符合我們的貪心優化思想。 容易理解,被劃分為左子樹而成為負類的概率為**P(−)=1−P(+)**。在某一個內部節點,要判斷是沿左子樹還是右子樹走的標準就是看**P(−),P(+)**誰的概率值大。而控制**P(−),P(+)**誰的概率值大的因素一個是當前節點的詞向量,另一個是當前節點的模型引數**θ**。 對於上圖中的**w2**,如果它是一個訓練樣本的輸出,那麼我們期望對於裡面的隱藏節點**n(w2,1)**的**P(−)**概率大,**n(w2,2)**的**P(−)**概率大,**n(w2,3)**的**P(+)**概率大。 回到基於Hierarchical Softmax的word2vec本身,我們的目標就是找到合適的所有節點的詞向量和所有內部節點**θ**, 使訓練樣本達到最大似然。 定義 w 經過的霍夫曼樹某一個節點j的邏輯迴歸概率為: $$ P(d_j^w|x_w, \theta_{j-1}^w)= \begin{cases} \sigma(x_w^T\theta_{j-1}^w)& {d_j^w=0}\\ 1-\sigma(x_w^T\theta_{j-1}^w) & {d_j^w = 1} \end{cases} $$ 那麼對於某一個目標輸出詞w,其最大似然為: $$ \prod_{j=2}^{l_w}P(d_j^w|x_w, \theta_{j-1}^w) = \prod_{j=2}^{l_w} [\sigma(x_w^T\theta_{j-1}^w)] ^{1-d_j^w}[1-\sigma(x_w^T\theta_{j-1}^w)]^{d_j^w} $$ 在word2vec中,由於使用的是隨機梯度上升法,所以並沒有把所有樣本的似然乘起來得到真正的訓練集最大似然,僅僅每次只用一個樣本更新梯度,這樣做的目的是減少梯度計算量。 可以求出![x_w](https://math.jianshu.com/math?formula=x_w)的梯度表示式如下: $$ \frac{\partial L}{\partial x_w} = \sum\limits_{j=2}^{l_w}(1-d_j^w-\sigma(x_w^T\theta_{j-1}^w))\theta_{j-1}^w $$ 有了梯度表示式,我們就可以用梯度上升法進行迭代來一步步的求解我們需要的所有的θwj−1和xw。 注意!word2vec要訓練兩組引數:一個是網路隱藏層的引數,一個是輸入單詞的引數(1 * dim) 在skip gram和CBOW中,中心詞詞向量在迭代過程中是不會更新的,只更新視窗詞向量,這個中心詞對應的詞向量需要下一次在作為非中心詞的時候才能進行迭代更新。 ## 0x02 帶著問題閱讀 Alink的實現核心是以 https://github.com/tmikolov/word2vec 為基礎進行修改,實際上如果不是對C語言非常牴觸,建議先閱讀這個程式碼。因為Alink的並行處理程式碼真的挺難理解,尤其是資料預處理部分。 以問題為導向: - 哪些模組用到了Alink的分散式處理能力? - Alink實現了Word2vec的哪個模型?是CBOW模型還是skip-gram模型? - Alink用到了哪個優化方法?是Hierarchical Softmax?還是Negative Sampling? - 是否在本演算法內去除停詞?所謂停用詞,就是出現頻率太高的詞,如逗號,句號等等,以至於沒有區分度。 - 是否使用了自適應學習率? ## 0x03 示例程式碼 我們把Alink的測試程式碼修改下。需要說明的是Word2vec也吃記憶體,所以我的機器上需要配置VM啟動引數:-`Xms256m -Xmx640m -XX:PermSize=128m -XX:MaxPermSize=512m`。 ```java public class Word2VecTest { public static void main(String[] args) throws Exception { TableSchema schema = new TableSchema( new String[] {"docid", "content"}, new TypeInformation [] {Types.LONG(), Types.STRING()} );