前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >“你所知道的word2vec都是错的”:论文和代码天壤之别,是普遍现象了?

“你所知道的word2vec都是错的”:论文和代码天壤之别,是普遍现象了?

作者头像
代码医生工作室
发布2019-06-23 20:44:21
9640
发布2019-06-23 20:44:21
举报
文章被收录于专栏:相约机器人相约机器人
栗子 发自 凹非寺 量子位 出品

word2vec是谷歌2013年开源的语言工具。

两层网络,就能把词变成向量,在NLP领域举足轻重,是许多功能实现的基础。

可是现在,有一位叫做bollu (简称菠萝) 的程序员,大声对世界说:

“关于word2vec,你所知道的一切都是错的。”

在他看来,论文里的算法解释,和代码实现一比,讲的根本是两回事。

是不是只要开源了代码,论文写不写清楚都没关系?

一番仔细的论述,引起了许多人的讨论和共鸣,不出半日Hacker News热度已近300点:

那么,菠萝的世界观是怎样崩塌的,他眼里真实的word2vec是什么样子呢?

不一样的天空

word2vec有种经典解释 (在Skip-Gram里、带负采样的那种) ,论文和数不胜数的博客都是这样写的:

只能看出有两个向量。

可程序员说,看了word2vec最原本的C语言实现代码,就会发现完全不一样。

(多数用word2vec做词嵌入的人类,要么是直接调用C实现,要么是调用gensim实现。gensim是从C实现上翻译过来的,连变量的名字都不变。)

C实现长这样

每个单词有两个向量,分别有不同的角色:

一个表示这个词作为中心词 (Focus Word) 时的样子。 一个表示它作为另一个中心词的上下文 (Context Word) 时的样子。

菠萝说:耳熟吧,GloVe就是借用了这里的思路,只是没有谁明确说出来而已。

在C语言的源代码里,设定已经非常完好,这些向量由两个数组 (Array) 分别负责:

syn0数组,负责某个词作为中心词时的向量。是随机初始化的。

代码语言:javascript
复制
1https://github.com/tmikolov/word2vec/blob/20c129af10659f7c50e86e3be406df663beff438/word2vec.c#L369
2  for (a = 0; a < vocab_size; a++) for (b = 0; b < layer1_size; b++) {
3    next_random = next_random * (unsigned long long)25214903917 + 11;
4    syn0[a * layer1_size + b] = 
5       (((next_random & 0xFFFF) / (real)65536) - 0.5) / layer1_size;
6  }

syn1neg数组,负责这个词作为上下文时的向量。是零初始化的。

代码语言:javascript
复制
1https://github.com/tmikolov/word2vec/blob/20c129af10659f7c50e86e3be406df663beff438/word2vec.c#L365
2for (a = 0; a < vocab_size; a++) for (b = 0; b < layer1_size; b++)
3  syn1neg[a * layer1_size + b] = 0;

训练的话,要先选出一个中心词。在正、负样本训练的时候,这个中心词就保持不变 (Constant) 了。

中心词向量的梯度 (Gradients) ,会在缓冲器 (Buffer) 里累积起来。经过正、负样本的作用之后,这些梯度会被应用到中心词上:

代码语言:javascript
复制
 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 0
10    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;
18
19  // find dot product of original vector with negative sample vector
20  // store in f
21  for (c = 0; c < layer1_size; c++) f += syn0[c + l1] * syn1neg[c + l2];
22
23  // 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;
27
28  // 1. update the vector syn1neg,
29  // 2. DO NOT UPDATE syn0
30  // 3. STORE THE syn0 gradient in a temporary buffer neu1e
31  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 neu1e
35https://github.com/tmikolov/word2vec/blob/20c129af10659f7c50e86e3be406df663beff438/word2vec.c#L541
36// Learn weights input -> hidden
37for (c = 0; c < layer1_size; c++) syn0[c + l1] += neu1e[c];

那么问题来了,为什么是随机初始化,为什么是零初始化

关于初始化

这些东西,也没见论文和博客里讲过,菠萝只能自己推测了一下:

因为负样本 (Negative Sample) 来自全文上下,并没太根据词频来定权重,这样选哪个单词都可以,通常这个词的向量还没经过多少训练。 而如果这个向量已经有了一个值,那么它就可以随意移动 (Move Randomly) 中心词了。 解决方法是,把所有负样本设为零,这样依赖只有那些比较高频出现的向量,才会影响到另外一个向量的表征。

程序员说,如果是这样,真的很巧妙。他也从来没想过,初始化策略能有这么重要,读论文也看不出。

直接看代码,不相信论文了

在这之前,菠萝已经花了两个月来复现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

推荐阅读

Google新闻和Leo Tolstoy:使用t-SNE可视化Word2Vec Word嵌入

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 相约机器人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 不一样的天空
    • C实现长这样
      • 关于初始化
      • 直接看代码,不相信论文了
      • 都是这种习惯么?
      • 传送门
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档