本文章内容主要翻译自Word2Vec Tutorial - The Skip-Gram Model,英文水平还可以的建议去看原文。
word2vec是word embedding(词向量)的一种浅层神经网络训练方法。通俗的讲就是把一个词变成一个词向量。这篇文章主要介绍word2vec 中的skip gram模型。word2vec还用到了CBOW模型,这个在本文不做介绍。
Part 1
The Model
skip-gram模型结构实际上出乎意料的简单。
我们先从比较宏观的角度来看看我们到底要干嘛。Word2Vec使用了一个可能你在机器学习的其他地方也见过的trick。我们打算训练一个单隐藏层的简单网络模型来完成一个明确的任务。但是我们实际上并不用这个我们训练的神经网络来完成这个任务。我们的目的是为了学习到隐藏层的权重。实际上这些权重实际上就是 word vector。所以说我们构建这个神经网络只是个假任务,真任务是要它的隐藏层权重。
另一个你能够看到这个trick的地方是非监督特征的学习,你在隐藏层训练一个auto-encode用来压缩输入的vector。然后在输出的时候再进行解压回原样。当训练好后,直接去掉输出层(解压的部分),只使用隐藏层。这是在没有标签的训练数据的情况下学习好的图片特征的一个方法。
The Fake Task
我们先来谈谈这个假任务,然后再反过来再看看它是如何间接的给了我们所需的word vector。
我们打断这样来进行训练。首先给定一个sentence中的一个word(input word),然后随机取一个这个word上下文的word。这个网络用来告诉我们,对于词表中的每个单词,我们随机挑一个“nearby word”,每个词的概率。
当我们提到“nearby”的时候,实际上是这个算法中的窗口大小。一个典型的窗口大小是5,即前后各5个word,一共10个words。
输出的概率代表了在input word的nearby能够找到这个word的可能性大小。例如你给已经训练好的模型输入一个单词‘Soviet’,输出的各个单词,像‘Union’及‘Russia’这类的单词的概率要比‘watemelon’这类不相干的单词要高。
我们通过给网络输入一堆训练文本中的单词对来对其进行训练。下图展示了一些我们从句子”The quick brown fox jumps over the lazy dog.“中提取出来用来训练网络模型的单词对。在这个例子中,窗口大小设置为2,蓝色的word表示input word。
这个网络是用来统计单词对出现的频次的。所以,加入这个网络输入很多(“Soviet”, “Union”)的单词对以及比较少的 (“Soviet”, “Sasquatch”) 单词对。那么当训练结束后,我们输入“Soviet”,输出结果中“Union”的概率要比“Sasquatch”高。
Model Detail
所以这一切到底是如何表示的呢?
首先,我们知道我们给神经网络输入的是一些文本信息,比如一个word。那么我们必须有一个表示这些word的方式。所以首先我们得从我们的训练文本里构建一个词典,我们假设词典大小是10000个不一样的单词。
我们打算把input word比如“ants”表示成一个one-hot vector(比如说00001,只有一个1其他都是0)。我们的每个vector将有10000个components, “ants” 这个单词对应的位置设为1,其他的都为0。
这个网络的输出是一个vector(也是10000个components),对于我们词典种的每个单词,都有一个对应的概率。
下入是我们的网络的结构。
隐藏层没有用到激活函数,但是输出层用到了softmax函数,这个事情我们呆会再谈。
当我们训练这个网络的时候,input是一个one-hot vector表似乎一个input word,输出也是一个one-hot vector表示一个word。但是当我们进行评估的时候,输出实际上是一个概率的分布,一个bunch的float数值,而不是一个one-hot vector。
The Hidden Layer
在我们的例子种,我们打算学习word的300个特征。所以隐藏层是用一个有10000*300(300个神经元)的矩阵构成的。
Google在他们公布的训练谷歌新闻数据的模型里使用了300个特征。特征的数目是一个超参,你得尝试不同的值来获得好的结果。
看看权重矩阵的行,你会发现,卧槽,这个实际上就是我们想要的word vectors。
所以我们真正的最后的目标是获得这个隐藏层的权重矩阵,等训练完成了,我们就把输出层给扔了!
现在,是时候回头看看了,解决一下遗留的一些问题,模型种的一些概念。
现在,你可能问你自己,one-hot vector几乎所有都为0…那么,这有什么用呢?当你将1个1 * 10000的one-hot vector乘上一个10000 * 300的矩阵时候。它会很有效率的选择1对应的那一行。如下如所示:
这意味着这个模型的隐藏层实际上就像是一个lookup table。隐藏层的输出实际上就是input word的word vector。
The Output Layer
然后代表“ants”的1 * 300的word vector作为输出层的输入。输出层是一个softmax回归分类器。这里有关于softmax回归更深入的讲解。但是这里的要点是每一个输出层的神经元会产生一个范围为0到1的数(每一个都对应我们字典中的一个word),并且这些所有的值加起来和为1。
特别的是,每一个输出层神经元有一个权重向量,将它乘上word vector,然后代入函数exp(x)
中。最后,我们再除以所有10000个输出节点的结果的和,使得所有输出的和为1。
下图是计算单词“car”的输出的一个示意图。
需要注意的是这个神经网络并不知道输出的word相对于输入word的偏移。它并没有学习到这个输出word是在输入word的前面或者是后面。假设说我们的训练语料中,每一个“York”前面都有一个单词“New”,这个概率是100%。但是,当我们的窗口大小是10,我们随机选择附近单词,“New”的概率并不是100%,你可能选择别的单词。
Intuition
好的,你准备好更深入的了解这个网络的令人兴奋的部分了吗?
假如两个不同的word有着非常类似的上下文,那么我们的模型就会输出非常相似的两个word vector。这个网络输出相同的上下文预测的一种情况就是这两个word vector非常相似。所以,假如两个words有着相似的上下文,那么我们的网络就会倾向于学习出两个相似的word vector。Ta da!(不知道啥意思haha)
这意味着什么呢?我认为你会期望说像“intelligent”和“smart”这种同义词会有着相似的上下文。或者说像“engine”和“transmission”这种相关的单词,可能也会有相同的上下文。
以及,对于“ant”和“ants”这种有着相似上下文的单词,这个网络也会输出相似的word vector。
Next Up
你可能会注意到skip-gram神经网络有着一个巨大的权重。就我们的例子来看,300个特征,10000个words,输入层到隐藏层以及隐藏层到输出层的权重都达到了3M。训练这个大dataset代价比较高的,所以word2vec的作者介绍了一些微调策略来使得这个训练是可行的。接下来我们就讲一讲这些。
Part 2
在这个部分,我们主要谈一谈一些基于basic skip-gram model的改进,这些改进使得模型的训练变得可行。
上文中我们提到,这个网络是一个很大的网络。在这么一个巨大的网络里,跑梯度下降是非常慢的。更糟糕的是,要训练好这么多的权重并且避免过拟合,你需要非常多的数据。百万级别的权重,十亿级别的训练样本,意味着训练这么一个模型,完全是在扯淡。
word2vec的作者在他的论文里提出了三个创新点:
- 在模型中把正常的单词对或者短语当做一个“words”;
- 对那些频繁出现的单词进行子采样(subsample),来减少所需要的训练样本;
- 用一个“ Negative Sampling”的技术来改变优化目标,这使得每个训练样本只更新模型中一部分的权重。
值得注意的是subsampling frequent words和Negative Sampling不仅仅减少了运算负担,并且还提升了结果的质量。
Word Pairs and “Phrases”(单词对和词组)
作者指出,像“Boston Globe”(一份报纸的名称)这种合起来和分开的意思大相径庭的词组,最好把它当作一个单词,不管是在上下文中还是在word vector中。
你可以看看他们公布的在100billion谷歌新闻数据上训练的模型的结果。加入词组(phrases)使得词汇量高达3M。假如你对这些词汇感兴趣你可以看看这里。
我认为他们的这种把词汇当作一个word的方法不是他们论文的主要贡献,我在这里提一下因为实际上方法很直接。
在每一轮中,他们的工具只看相邻的两个words,但是你可以跑多次,以此来获得更多单词的词组。比如说,第一轮的时候,会选取New_York,然后再跑一遍,会选取New_York_City。
这个工具计算每两个连接的words出现的次数,然后根据次数带入方程式中来判断应该把哪些归类为词组。这个方程式是为了避免把那些独立的但是经常一起出现的word也归类为词组。它也比较倾向于把那些低频word的组合归类为词组,避免诸如“and the”或者“this is”之类的词组。如果你想更详细的了解细节,你可以看这里。
我曾经想过的一个替代策略是采用wikipedia articles的标题来作为词汇。
Subsampling Frequent Words
再Part 1中,我展示了训练样本是如何从源文件中产生的,我会在这里重复一下。下面的例子展示了从“The quick brown fox jumps over the lazy dog.”里获得的一次训练样本(单词对)。我在这个例子中使用的窗口大小为2。高亮的蓝色单词是输入单词。
对于那些高频单词比如“the”,这里存在两个问题:
- 当我们观察单词对的时候,(“fox”, “the”) 并没有告诉我们太多关于“fox”的信息。“the”几乎出现在每一个单词的上下文中。
- 我们有着太多的 (“the”, …) 这类的单词对,我们并不需要那么多来学习出the一个好的vector。
word2vec采用一种“subsampling”的策略来解决这个问题。对于我们在训练文本中遇到的每一个单词,我们有一定的几率会把它删掉。这个概率是和这个单词出现的频率相关的。
假如我们设定的窗口大小为10,并且我们从文本中移除掉“the”的一个实例:
- 当我们利用剩下的单词进行训练的时候,“the”不会出现在它们的上下文窗口中;
- 当the是输入单词的时候,我们会有着小于10的训练样本。
思考一下这两个结果对我们解决上面提出的问题有什么帮助。
Sampling rate
word2vec的C代码中利用一个方程式来计算是否丢弃词表中的word的概率。
wi是一个单词,z(wi)表示这个单词出现的次数相对于总次数的比例。比如,“peanut”在包含1billion的语料中出现了1000次,那么z(“peanut”)就是1000/10 0000 0000 = 1/100 0000。
还有一个参数叫做“sample”用来控制subsampling出现的频率,默认值是0.001。words的“sample”越小表示越容易被丢弃。
P(wi)表示keeping这个单词的概率:
你可以把它画出来来看看形状:
没有一个word占整个语料的比例会很高,所以我们来看看当x比较小的时候是什么样子的。
这是在这个函数中一些关键的点(重申下sample采用的是默认的0.001):
你可能会注意到paper中定义的函数和C代码中采用的有些不太一样,但是我觉得C代码中的是更加权威的版本。
Negativa Sampling
训练一个神经网络指的是拿一个训练样本进行训练,然后细微的调整神经元中的权重,使得对训练样本的于此更加精确。换句话说,每一个训练样本对神经网络中的所有权重都有影响。
就我们前面提到的,巨大的词汇表意味着神经网络中巨大的权重,这些权重每次都会因我们的训练样本做出轻微改动,而我们的样本数量是十亿级别的。
Negativa Sampling通过每个训练样本只改变某些权重的方法来解决这个问题。接下来我们看看它是怎么工作的。
当我们用单词对 (“fox”, “quick”) 来训练网络的时候,输入和输出都是one-hot vector。也就是说,神经元的输出,对应“quick”的是1,其他的都是0。
采用negative sampling,我们只是随机选取少量的“negative” word(比如说5个)来更新它们的权重。(“negative” word在这里指的是那些输出对应为0的单词)。对于“positive” word也就是“quick”,我们仍然进行更新。
paper中说选择5-20个words在校的数据集中工作的很好。在大的数据集中,你可以选择更少,比如2-5个。
回忆一下,输出层的权重矩阵是300 * 10000的。假如我们只更新我们的“positive” word (“quick”),加上5个随机选择的negative单词。一共也才6个神经元,1800个权重值。只占了3M 权重的0.06%。
在隐藏层中,只有input word对应的权重进行更新,这个不管采不采用Negative Sampling都一样。
Selecting Negative Samples
刚才说的5个“negative samples”怎么确定呢,实际上不是完全随机的,采用“unigram distribution”的方法来确定。
实际上,选择一个word作为negative word的概率是和它出现的频率相关的,也就是说它出现的频率越高,那么它就越可能被选中。
在word2vec的C代码的实现中,你可以看到以下这个方程式。f(wi)表示wi出现次数的权重。所以选择一个word的概率就是它的权重除以所有words权重和。
选择3/4作为指数项应该是凭经验选择的,在他们的paper中,他们说这个选择比别的效果更好。你可以在google中画图:“plot y = x^(3/4) and y = x” 并且把x的范围设定为[0, 1],就可以看它的形状了。
这个选择的方式在C代码中的实现挺有趣的。他们有着个非常大的array,多大100M个元素(也就是unigram table)。他们根据word的index多次填充这个table,然后一个word的index在这个table中出现的次数等于P(wi) * table_size。所以实际上你选择一个negative sample的过程就是产生一个从0-100M的随机数,然后使用table中对应位置的word。因为高频单词在这个table中出现了多次,所以你有更大的概率选择它。