訓練語料每一個句子呈一行。ReadWord()逐個對輸入流讀字符。html
特判的換行符,第一次遇到換行符,會把換行符退流。這樣下一次單獨遇到換行符,node
此時a=0,直接生成結尾符單詞$</s>$,這個詞在Hash表中處於0號位置。算法
只要Hash到0,說明一個句子處理完了,跳過這個詞。進入下一個句子。編程
爲了執行速度,Word2Vec放棄了C++,於是不能用map作Hash。數組
於是裏面手寫了Hash表進行詞Hash。使用線性探測,默認Hash數組30M。網絡
AddWordToVocab()會根據狀況自動擴空間。app
亞系字符,由於編碼默認使用UTF8,也能夠被Hash。可是須要分詞。負載均衡
因爲構建Huffman樹的須要,作好Vocab庫後,對Hash表,按詞頻從大到小排序。函數
並剔除低頻詞(min_count=5),從新調整Hash表空間。優化
除此以外,在從文件建立Vocab時候,會自動用ReduceVocab()剪枝。
當VocabSize達到Hash表70%容量時,先剪掉頻率1的所有詞。而後下次再負荷,則對頻率2下手。
在Hierarchical Softmax優化方法中使用。
直接計算Softmax函數是不切實際的,$P(W_{t}|W_{t-N}....,W_{t-1},W_{t+1}....,W_{t+N})=\frac{e^{W_{t}X+b_{t}}}{\sum_{i=1}^{V}e^{W_{i}X+b_{i}}}$
底部的歸一化因子直接關係到Vocab大小,有10^5,每算一個機率都要計算10^5次矩陣乘法,不現實。
最先的Solution是Hierarchical Softmax,即把Softmax作成一顆二叉樹,計算一次機率,最壞要跑$O(logV)$結點(矩陣乘法)。
至於爲何採用Huffman樹,緣由有二:
①Huffman樹是滿二叉樹,從BST角度來看,平衡性最好。
②Huffman樹能夠構成優先隊列,對於非隨機訪問每每奇效。
爲何是非隨機訪問?由於根據生活常識,高頻詞常常被訪問(廢話=。=)
這樣,按照詞頻降序創建Huffman樹,保證了高頻詞接近Root,這樣高頻詞計算少,低頻詞計算多,貪心優化思想。
Word2Vec的構建代碼很是巧妙,利用數組下標的移動就完成了構建、編碼。
它最重要的是隻用了parent_node這個數組來標記生成的Parent結點(範圍$[VocabSize,VocabSize*2-2]$)
最後對Parent結點減去VocabSize,獲得從0開始的Point路徑數組。剩餘細節在下文描述。
syn0數組存着Vocab的所有詞向量,大小$|V|*|M|$,初始化範圍$[\frac{-0.5}{M},\frac{0.5}{M}]$,經驗規則。
syn1數組存着Hierarchical Softmax的參數,大小$|V|*|M|$,初始化全爲0,經驗規則。實際使用$|V-1|$組。
syn1neg數組存着Negative Sampling的參數,大小$|V|*|M|$,初始化全爲0,經驗規則。
在One-Hot Represention中,若是一個句子中出現相同的詞,那麼只用0/1來編碼明顯不許確。
詞袋模型(BOW),容許將重複的詞疊加,就像把重複的詞裝進一個袋子同樣,以此來增長句子的信度。
它基於樸素貝葉斯的獨立性假設,將不一樣位置出現的相同詞,看做是等價的,可是仍然無視了語義、語法、關聯性。
Bengio的模型中,爲了讓詞向量獲得詞序信息,輸入層對N-Gram的N個詞進行拼接,增長了計算壓力。
CBOW中取消了訓練詞序信息,將N個詞各個維度求和,而且取平均,構成一個新的平均詞向量$W_{{\tilde{x}}}$。
同時,爲了獲得更好的語義、語法信息,採用窗口掃描上下文法,即預測第$i$個詞,不只和前N個詞有關,還和後N個詞有關。
對於單個句子,須要優化的目標函數:$arg\max \limits_{Vec\&W}\frac{1}{T}\sum_{t=1}^{T}logP(W_{t}|W_{{\tilde{x}}})$
T爲滑動窗口數。
傳統的Softmax能夠當作是一個線性表,平均查找時間$O(n)$
HS方法將Softmax作成一顆平衡的滿二叉樹,維護詞頻後,變成Huffman樹。
這樣,本來的Softmax問題,被近似退化成了近似$log(K)$個Logistic迴歸組合成決策樹。
Softmax的K組$\theta$,如今變成了K-1組,表明着二叉樹的K-1個非葉結點。在Word2Vec中,由syn1數組存放,。
範圍$[0*layerSize\sim(vocabSize - 2)*layerSize]$。由於Huffman樹是倒着編碼的,因此數組尾正好是樹的頭。
如:syn1數組中,$syn1[(vocabSize - 2)*layerSize]$就是Root的參數$\theta$。(不要問我爲何要-2,由於下標從零開始)
Word2Vec規定,每次Logistic迴歸,$Label=1-HuffmanCode$,Label和編碼正好是相反的。
好比如今要利用$W_{{\tilde{x}}}$預測$love$這個詞, 從Root到love這個詞的路徑上,有三個結點(Node 一、二、3),兩個編碼01。
那麼(注意,Node指的是Huffman編碼,然後面Sigmoid算出的是標籤,因此和Logistic迴歸正好相反):
Step1: $P(Node_{2}=0|W_{{\tilde{x}}},\theta_{1})=\sigma(\theta_{1}W_{{\tilde{x}}})$
Step2: $P(Node_{3}=1|W_{{\tilde{x}}},\theta_{2})=1-\sigma(\theta_{2}W_{{\tilde{x}}})$
則$P(W_{love}|W_{{\tilde{x}}})=P(Node_{2}=0|W_{{\tilde{x}}},\theta_{1}) \cdot P(Node_{3}=1|W_{{\tilde{x}}},\theta_{2})$
將每一個Node寫成完整的判別模型機率式: $P(Node\mid W_{{\tilde{x}}},\theta)=\sigma(\theta W_{{\tilde{x}}})^{1-y} \cdot (1-\sigma(\theta W_{{\tilde{x}}}))^{y}$
將路徑上全部Node連鎖起來,獲得機率積:
$P(W_{obj}|W_{{\tilde{x}}})=\prod_{i=0}^{len(Code)-1}\sigma(\theta_{i}W_{{\tilde{x}}})^{1-y} \cdot (1-\sigma(\theta_{i}W_{{\tilde{x}}}))^{y}\qquad y\propto \begin{Bmatrix}{{0,1}}\end{Bmatrix}\quad and \quad y=HuffmanCode$
Word2Vec中vocab_word結構體有兩個數組變量負責這部分:
$int *point\quad--\quad Node\\char *code\quad--\quad HuffmanCode$
一個容易混掉的地方:
$vocab[word].code[d]$ 指的是,當前單詞word的,第d個編碼,編碼不含Root結點
$vocab[word].point[d]$ 指的是,當前單詞word,第d個編碼下,前置結點。
好比$vocab[word].point[0]$ 確定是Root結點,而 $vocab[word].code[0]$ 確定是Root結點走到下一個點的編碼。
正好錯開了,這樣就能夠一步計算出 $P(Node\mid W_{{\tilde{x}}},\theta)=\sigma(\theta W_{{\tilde{x}}})^{1-y} \cdot (1-\sigma(\theta W_{{\tilde{x}}}))^{y}$
這種避免回溯搜索對應路徑的預處理trick在 $CreateBinaryTree()$ 函數中實現。
判別模型 $P(W_{obj}|W_{{\tilde{x}}})$ 須要更新的是 $W_{{\tilde{x}}}$,因爲 $W_{{\tilde{x}}}$ 是個平均項。
源碼中的作法是對於原始SUM的所有輸入,逐一且統一更新 $W_{{\tilde{x}}}$ 的梯度項。(注意,這種近似不是一個好主意)
先對目標函數取對數:
$\zeta =\frac{1}{T}\sum_{t=1}^{T}\sum_{i=0}^{len(Code)-1}\quad(1-y)\cdot\log[\sigma(\theta_{i}W_{{\tilde{x}}})]+y\cdot\log[1-\sigma(\theta_{i}W_{{\tilde{x}}})]$
固然,Word2Vec中沒有去實現麻煩的批梯度更新,而是對於每移動到一箇中心詞t,就更新一下,單樣本目標函數:
$\zeta^{'} =\sum_{i=0}^{len(Code)-1}\quad(1-y)\cdot\log[\sigma(\theta_{i}W_{{\tilde{x}}})]+y\cdot\log[1-\sigma(\theta_{i}W_{{\tilde{x}}})]$
對$W_{{\tilde{x}}}$的梯度:
$\frac{\partial \zeta^{'}}{\partial W_{{\tilde{x}}}}=\sum_{i=0}^{len(Code)-1}\frac{\partial [\,(1-y)\cdot\log[\sigma(\theta_{i}W_{{\tilde{x}}})]+y\cdot\log[1-\sigma(\theta_{i}W_{{\tilde{x}}})]\,]}{\partial W_{{\tilde{x}}}}\\\\\quad \, \, \, \, =\,\sum_{i=0}^{len(Code)-1}(1-y)\cdot\theta_{i}\cdot[(1-\sigma(\theta_{i}W_{{\tilde{x}}})]-y\cdot\theta_{i}\cdot\sigma(\theta_{i}W_{{\tilde{x}}})\\\\\quad \, \, \, \, =\,\sum_{i=0}^{len(Code)-1}(1-y-\sigma(\theta_{i}W_{{\tilde{x}}}))\cdot\theta_{i}$
對$\theta_{i}$的梯度,和上面有點對稱,只不過沒有 $\sum$ 了,因此源碼裏,每pass一個Node,就更新:
$\frac{\partial \zeta^{'}}{\partial \theta_{i}}=\,(1-y-\sigma(\theta_{i}W_{{\tilde{x}}}))\cdot W_{{\tilde{x}}}$
更新流程:
$UPDATE\_CBOW\_HIERARCHICAL\,SOFTMAX(W_{t})\\neu1e=0\\W_{{\tilde{x}}}\leftarrow Sum\&Avg(W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\for \quad i=0 \quad to \quad len(W_{t}.code-1)\\\qquad f=\sigma(W_{{\tilde{x}}}\theta_{i})\\\qquad g=(1-code-f)\cdot LearningRate\\\qquad neu1e=neu1e+g\cdot\theta_{i}\\\qquad \theta_{i}=\theta_{i}+g\cdot W_{{\tilde{x}}}\\for \quad W \quad in \quad (W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\ \qquad\qquad W=W+neu1e$
引入噪聲的Negative Examples,能夠用來替代$P(W_{t}|W_{t-N}....,W_{t-1},W_{t+1}....,W_{t+N})$。[licstar的Blog]
最先這麼作的是[Collobert&Weston08]中的,替代Bengio的Softmax的單個詞打分目標函數:
$\sum\limits_{x\in \mathfrak{X}} { \sum\limits_{w\in \mathfrak{D}} {\max \{0 , 1-f(x)+f(x^{(w)})\} } }$
他們的作法是,對於一個N-Gram的輸入$x$,枚舉整個語料庫$\mathfrak{D}$, 每次取出一個詞$w$,替換N-Gram的中心詞,build成$x^{(w)}$
這樣,每次爲正樣本打$f(x)$分,噪聲負樣本打$f(x^{(w)})$,訓練使得,正樣本得分高,負樣本得分低而負分滾粗。
理論上的工做,來自[Gutmann12],論文提出NCE方法,用原始集X,生成噪聲集Y,經過Logistic迴歸訓練出機率密度函數之比:
$\frac{p_{d}}{p_{n}} \quad where \quad d \in Positive,n\in Negative$
而$\frac{p_{d}}{p_{n}}\approx Softmax$ (我的理解,[Gutmann12]中並無這麼說,可是[Mikolov13]中一提而過)
[Gutmann12]中,Logistic迴歸總體的目標函數爲:
$J(\theta)=\frac{1}{T_{d}}\begin{Bmatrix} \sum_{t=1}^{T_{d}}ln[h(x_{t};\theta)]+\sum_{t=1}^{T_{n}}ln[1-h(y_{t};\theta)]] \end{Bmatrix} \quad \quad \\where \quad T_{d}=Num(Positive),T_{n}=v*T_{d}=Num(Negative)$
對於Softmax函數而言,每次只須要一個給定的預測正樣本$W_{t}$,數個隨機抽取的詞做爲預測負樣本(源碼裏默認設定是5)。
隨機抽取負樣本是件苦差事。
隨機數生成知足是均勻分佈,而取詞機率可不是均勻分佈,其機率應當隨着詞頻大小變化。
詞頻高的詞容易被隨機到,而詞頻低的詞不容易被隨機到。
按照 peghoty 中的理解,源碼中作法是將詞頻轉換爲線段長度:
$len(W)=\frac{W.cnt}{\sum\limits_{U\in Vocab}U.cnt}$
這樣就生成了一條詞頻線段,接下來就是隨機Roll點了:
源碼中有幾點說明:
①詞頻默認被冪了3/4 ,這樣縮短了每一個詞線段的長度,加強了Roll出的每一個點的分佈性。
固然,冪不能太小,不然會致使詞線段長度總體不夠,後面的Roll出點都映射到最後一個詞線段上。
②沒有刻意去鏈接全部詞線段,而是動態鏈接。一旦Roll點進度超出詞線段總長度,就擴展一條詞線段。
Roll點長度=當前Roll點序號/所有Roll點數(VocabSize)
源碼中的syn1neg數組存着負採樣的參數,該參數和HS的數組syn1是獨立的。
用6個Logistic迴歸退化Softmax後,獲得:
$P(W_{obj}|W_{{\tilde{x}}})\approx\prod_{i=0}^{5}P(W_{Sampling}^{i}|W_{{\tilde{x}}})\\\\\left\{\begin{matrix}W_{Sampling}^{i}=Positive \quad (i=0)\\
W_{Sampling}^{i}=Negative \quad (i>0)\end{matrix}\right.$
接下來的Logistic迴歸就比較簡單了,因爲此時不是Huffman編碼,因此標籤不用顛倒了,有單樣本對數似然函數:
$\zeta^{'}=\sum_{i=0}^{5}y\cdot log [\sigma(\theta_{neg}^{i}W_{{\tilde{x}}})]+(1-y)\cdot log[1-\sigma(\theta_{neg}^{i}W_{{\tilde{x}}})]$
若是你熟悉Logistic迴歸,應該已經熟記Logistic迴歸參數梯度的優美式子。
對 $W_{{\tilde{x}}}$ 的梯度:
$\frac{\partial \zeta^{'}}{\partial W_{{\tilde{x}}}}=\sum_{i=0}^{5}[y-\sigma(\theta_{neg}^{i}W_{{\tilde{x}}})]\cdot\theta_{neg}^{i}$
對 $\theta_{neg}^{i}$ 的梯度:
$\frac{\partial \zeta^{'}}{\partial \theta_{neg}^{i}}=[y-\sigma(\theta_{neg}^{i}W_{{\tilde{x}}})]\cdot W_{{\tilde{x}}}$
更新流程:
$UPDATE\_CBOW\_NEGATIVE\,SAMPLING(W_{t})\\neu1e=0\\W_{{\tilde{x}}}\leftarrow Sum\&Avg(W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\for \quad i=0 \quad to \quad negative\\\qquad f=\sigma(W_{{\tilde{x}}}\theta_{neg}^{i})\\\qquad g=(label-f)\cdot LearningRate\\\qquad neu1e=neu1e+g\cdot\theta_{neg}^{i}\\\qquad \theta_{neg}^{i}=\theta_{neg}^{i}+g\cdot W_{{\tilde{x}}}\\for \quad W \quad in \quad (W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\ \qquad\qquad W=W+neu1e$
Skip-Gram是Mikolov在Word2Vec中祭出的大殺器,用於解決CBOW的精度問題。
它再也不將輸入求和平均化,而是對稱預測。
須要優化目標函數:$arg\max \limits_{Vec\&W}\frac{1}{T}\sum_{t=1}^{T}\sum_{-c<=j<=c,j\neq 0}logP(W_{t+j}|W_{t})$
Skip-Gram是個容易讓人誤解的模型,主要是由於 $P(W_{t+j}|W_{t})$ ,以及上面那張圖。
你可能會不屑一笑:"啊,Skip-Gram不就是用中心詞預測兩側詞嘛,不就是CBOW的顛倒版!」
實際上,Skip-Gram可不是簡單的顛倒版,它是用 每一個詞,預測窗口內,除它之外的詞。
先看一下二年級小朋友寫的做文,~Link~:
握手遊戲
二年級108班 朱紫曦
今天上數學課咱們學了《數學廣角》。下課的時候,鍾老師帶咱們作握手遊戲。首先,我和廖志傑、董恬恬,還有鍾老師,咱們四我的來握手。我和他們3我的每人 握了一次手就是3次。鍾老師笑眯眯地和廖志傑、董恬恬每人握了一次手。最後,董恬恬和廖志傑友好地我了一次手。鍾老師問同窗們:「咱們4人每兩人握一次手 一共握了幾回手?」你們齊聲說:「6次!」接着,老師讓咱們5我的,6我的……都來作握手遊戲,並能說出每兩人握一次手一共握了幾回手。在快樂的遊戲中鍾 老師還教咱們一個計算握手次數的計算方法。4我的每兩我的握一次手,握手次數是:1+2+3;5我的這樣握手次數是:1+2+3+4:;10我的這樣握手 次數是1+2+3+4+5+6+7+8+9;100個這樣握手次數是1+2+3+4……+98+99。
在這個數學握手遊戲,不只讓咱們開學,還讓咱們學到了知識。
Skip-Gram模型是握手遊戲的規則修改版,它假設A和B握手、B與A握手是不一樣的( 貝葉斯條件機率公式不一樣 )
仔細回想一下目標函數中的t循環了一個句子中的每一個詞,對於每一個t,它和其餘的詞都握了一次手。
假設一個句子有10個詞,第一個詞和剩餘9個詞握手,第二個詞和剩餘9個詞握手.....,請問一共握了多少次手?
噢喲,10*9=90次嘛!對,目標函數裏就有90個條件機率。
迷人的Skip-Gram,以致於讓 http://blog.csdn.net/itplus/article/details/37969979 這篇很是棒的文章都寫反寫錯了。
直接貼算法流程了:
$UPDATE\_SKIPGRAM\_HIERARCHICAL\,SOFTMAX(W_{t})\\for \quad W \quad in \quad (W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\\qquad neu1e=0\\\qquad for \quad i=0 \quad to \quad len(W_{t}.Code)-1\\\qquad\qquad f=\sigma(W\theta_{i})\\\qquad\qquad g=(1-code-f)\cdot LearningRate\\\qquad\qquad neu1e=neu1e+g\cdot\theta_{i}\\\qquad\qquad \theta_{i}=\theta_{i}+g\cdot W\\\qquad W=W+neule$
再貼下做者 peghoty 理解錯誤的算法流程:
$UPDATE\_SKIPGRAM\_HIERARCHICAL\,SOFTMAX(W_{t})\\for \quad W \quad in \quad (W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\\qquad neu1e=0\\\qquad for \quad i=0 \quad to \quad len(W.Code)-1\\\qquad\qquad f=\sigma(W_{t}\theta_{i})\\\qquad\qquad g=(1-code-f)\cdot LearningRate\\\qquad\qquad neu1e=neu1e+g\cdot\theta_{i}\\\qquad\qquad \theta_{i}=\theta_{i}+g\cdot W_{t}\\\qquad W_{t}=W_{t}+neule$
至於做者 peghoty 爲何會犯這樣的錯誤,正如前面所說:
你們都被$P(W_{t+j}|W_{t})$給誤導了,認爲是中心詞預測兩側詞,因此每次就更新中心詞。這是錯誤的。
引用我在原文裏的評論:
回覆neopenx:補充一下,LZ的方法主要錯在,梯度更新順序錯了。
主要緣由是word2vec源碼中使用了隨機梯度訓練,之因此不採樣徹底梯度,是由於梯度矩陣太難算(Theano等能夠直接一步求導,可是無法作HS)
正常狀況下,若是是徹底梯度的話,須要等這個句子跑完以後,再更新。因此對於一個句子,先算P(4|3)仍是P(3|4)其實無所謂,反正都沒更新。
也就是說P(w|context(w))=P(context(w)|w)是能夠顛倒的。
可是隨機梯度則是在每跑一個pos後更新,自己就是一種近似。
LZ的作法是對於每一個窗口,老是在更新當前pos的詞向量。如P(2|4)、P(3|4)、P(5|4)、P(6|4),更新的都是4,這是一種DP思想,關鍵真的無後效性嘛?
明顯P(5|4)時,5就沒更新,卻仍是算了,這是錯誤的。
源碼中則是依次更新P(4|2)、P(4|3)、P(4|5)、P(4|6),這樣,對於每一個pos,保證窗口詞都能更新一遍,而不是盯在一個詞上,有點負載均衡的味道。
最 後就是,不能簡單認爲Skip-Gram就是CBOW的顛倒。其實二者的差異主要在於,CBOW利用詞袋模型的貝葉斯獨立性假設,近似將n-gram中的 n個單詞sum&avg當作一個。而Skip-Gram則是當作n個。若是徹底梯度,至於P(中心|兩側)仍是P(兩側|中心)其實無所謂,反正 都是一對一,和握手遊戲同樣,最後是對稱的,確定全被覆蓋到。
若是隨機梯度,那麼必須先算P(中心|兩側),P(兩側|中心)是沒有道理的。
對於批梯度來講,其實先更新誰都無所謂。可是若是是隨機梯度,應該一樣按照CBOW中的作法:
對於每一個t,每次應該把握手的9個詞給更新掉,否則就有點負載不均衡了。
這樣,更新的時候,實際上用的是$P(W_{t}|W_{t+j})$,而不是$P(W_{t+j}|W_{t})$。
有趣的是,這卻偏偏和目標函數相反,然而目前網上竟然還沒人從這個角度理解源碼。
在Negative Sampling中, peghoty 仍是沒有意識到他的錯誤,他誤認爲:
源碼沒有按照目標函數編程,而後Balabala一大堆,實際上,NS只是在HS基礎上把Huffman樹去掉,主要是他HS理解錯了。
可是此次卻寫對了算法流程。
源碼的算法流程:
$UPDATE\_SKIPGRAM\_NEGATIVE\,SAMPLING(W_{t})\\for \quad W \quad in \quad (W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\\qquad neu1e=0\\\qquad for \quad i=0 \quad to \quad negative\\\qquad\qquad f=\sigma(W\theta_{neg}^{i})\\\qquad\qquad g=(label-f)\cdot LearningRate\\\qquad\qquad neu1e=neu1e+g\cdot\theta_{neg}^{i}\\\qquad\qquad \theta_{neg}^{i}=\theta_{neg}^{i}+g\cdot W\\\qquad W=W+neule$