栗子 發自 凹非寺

量子位 出品 | 公眾號 QbitAI

word2vec是谷歌2013年開源的語言工具。

兩層網絡,就能把詞變成向量,在NLP領域舉足輕重,是許多功能實現的基礎。

可是現在,有一位叫做bollu (簡稱菠蘿) 的程序員,大聲對世界說:

“關於word2vec,你所知道的一切都是錯的。”

谷歌論文和代碼效果天壤之別,遭程序員吐槽 新聞 第1张

在他看來,論文里的算法解釋,和代碼實現一比,講的根本是兩回事。

是不是只要開源了代碼,論文寫不寫清楚都沒關系?

一番仔細的論述,引起了許多人的討論和共鳴,不出半日Hacker News熱度已近300點:

谷歌論文和代碼效果天壤之別,遭程序員吐槽 新聞 第2张

那麼,菠蘿的世界觀是怎樣崩塌的,他眼裡真實的word2vec是什麼樣子呢?

不一樣的天空

word2vec有種經典解釋 (在Skip-Gram里、帶負採樣的那種) ,論文和數不勝數的博客都是這樣寫的:

谷歌論文和代碼效果天壤之別,遭程序員吐槽 新聞 第3张

只能看出有兩個向量。

可程序員說,看了word2vec最原本的C語言實現代碼,就會發現完全不一樣。

(多數用word2vec做詞嵌入的人類,要麼是直接調用C實現,要麼是調用gensim實現。gensim是從C實現上翻譯過來的,連變量的名字都不變。)

C實現長這樣

每個單詞有兩個向量,分別有不同的角色:

一個表示這個詞作為中心詞 (Focus Word) 時的樣子。

一個表示它作為另一個中心詞的上下文 (Context Word) 時的樣子。

菠蘿說:耳熟吧,GloVe就是借用了這里的思路,只是沒有誰明確說出來而已。

在C語言的源代碼里,設定已經非常完好,這些向量由兩個數組 (Array) 分別負責:

syn0數組,負責某個詞作為中心詞時的向量。是隨機初始化的。

syn1neg數組,負責這個詞作為上下文時的向量。是零初始化的。

https://github.com/tmikolov/word2vec/blob/20c129af10659f7c50e86e3be406df663beff438/word2vec.c#L369for (a = 0; a < vocab_size; a++) for (b = 0; b < layer1_size; b++) {next_random = next_random * (unsigned long long)25214903917 + 11;syn0[a * layer1_size + b] = (((next_random & 0xFFFF) / (real)65536) - 0.5) / layer1_size; }

訓練的話,要先選出一個中心詞。在正、負樣本訓練的時候,這個中心詞就保持不變 (Constant) 了。

中心詞向量的梯度 (Gradients) ,會在緩沖器 (Buffer) 里累積起來。經過正、負樣本的作用之後,這些梯度會被應用到中心詞上:

1if (negative > 0) for (d = 0; d < negative + 1; d++) { 2 // if we are performing negative sampling, in the 1st iteration, 3 // pick a word from the context and set the dot product target to 1 4 if (d == 0) { 5 target = word; 6 label = 1; 7 } else { 8 // for all other iterations, pick a word randomly and set the dot 9 //product target to 010 next_random = next_random * (unsigned long long)25214903917 + 11;11 target = table[(next_random >> 16) % table_size];12 if (target == 0) target = next_random % (vocab_size - 1) + 1;13 if (target == word) continue;14 label = 0;15 }16 l2 = target * layer1_size;17 f = 0;1819 // find dot product of original vector with negative sample vector20 // store in f21 for (c = 0; c < layer1_size; c++) f += syn0[c + l1] * syn1neg[c + l2];2223 // set g = sigmoid(f) (roughly, the actual formula is slightly more complex)24 if (f > MAX_EXP) g = (label - 1) * alpha;25 else if (f < -MAX_EXP) g = (label - 0) * alpha;26 else g = (label - expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha;2728 // 1. update the vector syn1neg,29 // 2. DO NOT UPDATE syn030 // 3. STORE THE syn0 gradient in a temporary buffer neu1e31 for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1neg[c + l2];32 for (c = 0; c < layer1_size; c++) syn1neg[c + l2] += g * syn0[c + l1];33}34// Finally, after all samples, update syn1 from neu1e35https://github.com/tmikolov/word2vec/blob/20c129af10659f7c50e86e3be406df663beff438/word2vec.c#L54136// Learn weights input -> hidden37for (c = 0; c < layer1_size; c++) syn0[c + l1] += neu1e[c];

那麼問題來了,為什麼是隨機初始化,為什麼是零初始化?

關於初始化

這些東西,也沒見論文和博客里講過,菠蘿只能自己推測了一下:

因為負樣本 (Negative Sample) 來自全文上下,並沒太根據詞頻來定權重,這樣選哪個單詞都可以,通常這個詞的向量還沒經過多少訓練。

而如果這個向量已經有了一個值,那麼它就可以隨意移動 (Move Randomly) 中心詞了。

解決方法是,把所有負樣本設為零,這樣依賴只有那些比較高頻出現的向量,才會影響到另外一個向量的表徵。

程序員說,如果是這樣,真的很巧妙。他也從來沒想過,初始化策略能有這么重要,讀論文也看不出。

直接看代碼,不相信論文了

谷歌論文和代碼效果天壤之別,遭程序員吐槽 新聞 第4张

在這之前,菠蘿已經花了兩個月來復現word2vec,也讀了無數文章,就是不成功。

不管試了多少次,還是得不到論文說的分數。又不能認為分數是論文作者編的。

最後,他決定去仔細讀源代碼。初讀還以為打開方式錯了,因為和之前看過的資料都不一樣:

我不明白,為什麼原始論文和網上的博客,都不去寫word2vec真正是怎麼工作的。所以就想自己寫出來。

也是在這個過程中,他才像上文提到的那樣,發現GloVe給上下文 (Context) 一個單獨的向量這種做法,是從word2vec那裡來的。

而GloVe的作者並沒有提到過這一點。

想到這里,程序員又有了新的質疑:

這樣不算學術不誠實 (Academic Dishonesty) 么?我也不知道算不算,但覺得至少是個很嚴重的問題。

傷感之餘,菠蘿作出了一個機智的決定:以後先不看論文對算法的解釋,直接去讀源代碼。

都是這種習慣么?

探討起論文和實現不一致的情況,一個用編譯器讀了40年論文的資深程序員 (DannyBee) ,佔據了Hacker News評論區的頂樓。

他細數了這些年來,論文作者的習慣變化:

早期許多算法的實現,原理都和描述相符,性能也和描述相符。只是論文會用偽代碼 (Pseudocode) ,用偽代碼的部分,和實現的差別到底在哪,也會詳細說明。後來,人們便開始走遠了。有些論文的算法,要麼是工作原理不像描述那樣,要麼是效率低到沒法用。看源碼的時候也會發現,不是論文說的那回事。SSAPRE就是一個典型。時至今日,大家讀起它的論文還是會覺得難懂。把源碼放進Open64編譯器去讀,也發現和論文大相徑庭 (Wildly Different) 。再後來,有了github這類社區,事情好像又朝着早期的健康方向發展了。在這樣的環境里,word2vec算個反例吧,可能他們覺得已經把代碼開源了,論文里寫不清也沒關系。

緊接着,樓下便有人 (nullwasamistake) 表示,反例不止這一個:

我在實現一個哈希表排序算法的時候,發現一篇近期的論文也有類似的問題。論文里從來沒提到過,表格尺寸必須是2的n次方。而這篇研究的全部意義,似乎就是比現有的其他算法,內存效率更高。我做了2/3才發現,根本沒有比現有方法更高效,反而更差了,除非把表的尺寸調成2^n。雖然不是徹頭徹尾的騙人,但這個疏漏算是很有創意了。

不過,當有人勸ta把那篇論文掛出來,這位吐槽的網友也實誠地表示:

現在批評科技巨頭有風險,以後可能還想去工作呢。

由此觀之,菠蘿是個有勇氣的少年。

傳送門

菠蘿對word2vec的完整意見發表在github上,有興趣可前去觀賞:

https://github.com/bollu/bollu.github.io

另外,還有Hacker News評論區,便於尋找更多同感:

https://news.ycombinator.com/item?id=20089515

— 完 —

誠摯招聘

量子位 QbitAI · 頭條號簽約作者

վ'ᴗ' ի 追蹤AI技術和產品新動態