NLP笔记(1)——word2vec

NLP笔记(1)——word2vec

word2vec

简介

NLP中一个基本的问题就是如何得到一个合理的word embedding,或者说representation. 这里就介绍word2vec中的一种名为skip-gram的模型。

目标 & Insights

首先我们需要明确我们的目标:学习一个合理的word embedding.
这里就有两个问题:

  1. 什么是word embedding
  2. 怎样的embedding算作是"合理"的,或者说是"好"的。

word embedding其实就是一个用来表示词语的一维向量,可以反映这个词的语义信息,用于完成下游任务(downstream task),比如文本的情感分析或者是对对联之类的任务。

假设词汇表(Vocabulary)的大小是\(|V|\),那么表中第\(i\)个词用one-hot向量\(e_i\)表示就可以将每个词区分开,但是完全不能反映它们的语义信息,不同词语的embedding都互相正交;而且还有另一个缺点,就是embedding的维数太高,存储代价太大。所以我们希望可以用一个维数较低的向量(dense vector)来表示,单词w的word embedding我们可以认为是一个d维向量,记作\(z_w\).

这时就会有一个自然的想法:意思相近的词语的embedding应该也是相近的。这里embedding之间的距离可以用cos距离,也就是向量之间的点乘(限制embedding的模长为1).

另一个朴素的想法是一个词的意思可以由句子里在它附近经常出现的那些词决定。比如我们提到“牛奶”的句子里,“牛奶”附近经常出现“倒”“喝”“一杯”等词语,而“咖啡”附近也经常出现这些词语,我们就可以认为“牛奶”和“咖啡”这两个词语的意思是比较相近的。

skip-gram的基本方法

这样基本的方法就已经有了雏形:
假设我们有一个语料数据集,其中有很多的句子。
现在我们重点考虑一个句子中互相靠得比较近的词语。假设我们有一个句子\(w_1, w_2, w_3, w_4, ..., w_n\)我们考虑词语\(w_j\)附近的一个“窗口”(window),这里假设窗口大小(window size)为2,那么这个窗口中其他的词语有\(w_{j-2}, w_{j-1}, w_{j+1}, w_{j+1}\),这些词语称为context word,\(w_j\)这时被称为center word. 我们希望我们学到的word embedding可以很好的预测数据集中的句子,这里就有两种选择:

  1. 由context word预测center word
  2. 由center word预测context word

我们这里考虑后者,当然前者也可以导出CBOW模型,不过我们这里仅介绍后者导出的skip-gram模型。

假设窗口大小为m,我们希望最小化negative log likelihood \(L(\theta)=-\frac{1}{T}\Sigma_{t=1}^{T}\Sigma_{-m\leq j \leq m, j\neq0} log(P(w_{t+j}|w_t;\theta))\) .

其中\(T\)为总的窗口个数,\(\theta\)为模型的所有参数,实际上就是所有词的向量的每个分量放在一起构成的一维向量。
这里有个小问题,我们知道实际上\(P(a, b|c)\neq P(a|c)P(b|c)\),应该是\(P(a, b|c)=P(a|c)P(b|a, c)\),有的文章解释到可以假设给定词c的情况下,c附近出现词a和出现词b两事件是独立的,但是这显然不符合实际。我个人偏向于理解为希望词a和词b在词c附近出现的概率都比较大,所以乘在一起后取log还是有一定合理性的(可能这就是ML中的玄学吧)。

好了,那么问题来了,\(P(o|c)\)要如何计算呢?(这里o是一个context word(个人认为来源于"outside"的"o"),c是center word)

回想之前我们提到的\(z_o \cdot z_c=z_o^Tz_c\),然后我们利用\(softmax\)就可以得到\(P(o|c)=\frac{z^T_oz_c}{\Sigma_{w\in V}(z_w^Tz_c)}\).

为了方便训练,这里我们把作为context word和center word的embedding分开,分别用\(u\)\(v\)表示,训练完后再取均值作为这个词的embedding. 当然只用一个embedding来训练也是可以的。

于是我们有\(P(o|c)=\frac{u^T_ov_c}{\Sigma_{w\in V}(u_w^Tv_c)},\)
\(log(P(o|c))=log(u^T_ov_c)-log({\Sigma_{w\in V}(u_w^Tv_c)})\),从而可以计算出negative log likelihood并用SGD进行优化(注意,这时梯度很稀疏,可以利用稀疏矩阵或hash)。

负采样

但是这样做有一个缺点,计算\(softmax\)时的复杂度是\(O(|V|)\),计算代价太高。我们可以采用负采样技术(negative sampling)来解决这个问题。

原论文是这样说的:\(D=1\)表示\((o,c)\)是正样本(来自于合理的句子中的一个样本),\(D=0\)表示是负样本。假设\(P(D=1|w, c, \theta)=\sigma(u_o^Tv_c)\)(其中\(\sigma\)\(sigmoid\)激活函数,\(\sigma(x)=\frac{1}{1+e^{-x}}\),满足\(\sigma(x)+\sigma(-x)=1\)

那么我们希望
\(\theta =argmax_{\theta}\Pi_{(o,c)\in D}P(D=1|o,c,\theta)\Pi_{(o,c)\in \widetilde{D}}P(D=0|o,c,\theta)\)

\(=argmax_{\theta}(\Sigma_{(o,c)\in D}log(\sigma(u_o^Tv_c)) + \Sigma_{(o,c)\in \widetilde{D}}log(\sigma(-u_o^Tv_c)))\)

于是对于每一对\((o, c)\),我们再同时从\(\widetilde{D}\)中采样出\(K\)个负样本来就可以了。
也就是说\((c, o)\)贡献的目标函数,或者说损失函数(loss)为\(J_t(\theta)=-log(\sigma(u_o^Tv_c)) + \Sigma_{i=1}^K\mathbb{E}_{j \sim P(w)}(log(\sigma(-u_j^Tv_c)))\).
\(P(w)\)为负样本的分布,我们随机的取一串词语就可以近似看作是负样本。原文中采用的分布是\(P(w)=\frac{f(w)^\frac{3}{4}}{\Sigma_{\widetilde{w}\in V}f(\widetilde{w})^\frac{3}{4}}\),这样可以稍微提高出现频率较小的词语被采样到的概率。

这里\(K\)是负采样的个数,取得越大越robust,计算量也越大。

我们也可以从另一个角度来看这个损失函数:
原来的损失函数为\(-u_o^Tv_c + log\Sigma_{w=1}^{|V|}exp(u_k^Tv_c)\)

现在的损失函数为\(-log\sigma(u_o^Tv_c)-\Sigma_{w=1}^K log\sigma(-u_w^Tv_c)\)

两者的形式较为相近,可以把后者看作是前者的一种近似。也就是优化它们最后达到的效果相近,但是后者的计算量更小。

skip-gram总结

最后我们就学到了所有词的embedding \(W\in \mathbb{R}^{|V|\times d}\).每一行就是每个词对应的embedding,也就是说从每个词获得对应的embedding只需要简单的lookup,也可以看成是\(W\)乘上一个one-hot向量。在pyTorch中可以使用nn.Embedding实现。

当然word2vec不仅仅只有skip-gram这一种方法,比如我们可能会问“为什么不直接用co-occurence matrix作为word embedding?” 这些内容我以后会更新在这里。这方面我也是初学者,如果以上内容有typo或者事实错误,希望读者可以指出,我将感激不尽。希望可以和大家一同进步( ̄▽ ̄)/

参考资料

cs224n课程前两次课的PPT

猜你喜欢

转载自www.cnblogs.com/dwxrycb123/p/12312716.html