本节课是对word2vec进一步的探讨,主要集中关注于以下的三个问题:
- word2vec究竟从文本中捕获到了什么信息,使其可以有效的用于多种下游NLP任务的解决
- 如何更有效的来捕获这些重要的信息
- 如何分析word2vec所捕获的信息,即如何评估word2vec效果的优劣
在前一篇CS224n lecture 1&2 Introduction and Word Vectors中对于word2vec已经做了介绍,包括其中的CBOW和Skip-gram两种基本模型,以及Hierarchical softmax和Negative sampling两种帮助提高训练效率的方式。如果想要简单的了解有关word2vec的相关内容,可快速的浏览一下前一篇博文;如果想要更深入的了解word2vec可以查阅参考部分~
OK!word2vec所做的就是遍历语料库中的每一个词,然后根据每个中心词来预测它的上下文,计算式为 p ( o ∣ c ) = e x p ( u o T v c ) ∑ w = 1 V e x p ( u w T v c ) p(o|c) = \displaystyle \frac{exp({u_{o}}^Tv_{c})}{\sum_{w=1}^V exp({u_{w}}^Tv_{c})} p(o∣c)=∑w=1Vexp(uwTvc)exp(uoTvc)最后在每个窗口使用随机梯度下降进行更新。
当我们想要分析I like deep learning and NLP"这句话时,假设现在关注的中心词为deep,然后通过上式来计算
p ( I ∣ d e e p ) = exp ( u I T v d e e p ) ∑ . p(I|deep) = \frac{\exp(u_{I}^Tv_{deep})}{\sum.}{} p(I∣deep)=∑.exp(uITvdeep) p ( l i k e ∣ d e e p ) = exp ( u l i k e T v d e e p ) ∑ . p(like|deep) = \frac{\exp(u_{like}^Tv_{deep})}{\sum.}{} p(like∣deep)=∑.exp(ulikeTvdeep)使用其他的词时计算过程是相似的。如果窗口设置为2,那么在每次预测时,模型都需要计算每个中心词前后最多两个词的概率,最后使用SGD更新。
但在计算 p ( o ∣ c ) p(o|c) p(o∣c)是分母上的求和操作需要使用语料库中的每一个词的初始化词向量和中心词的词向量进行点乘操作,最后进行求和。这样的方式使得每一个窗口都最多只有 2 m + 1 2m+1 2m+1个词,所以关于 θ \theta θ的梯度 ∇ θ J t ( θ ) \nabla_{\theta} J_{t}(\theta) ∇θJt(θ)将会十分稀疏,这时使用SGD进行更新时计算量会很大。但在实际情况中,我们并不需要去对中心词和语料库中的每一个词都进行上述概率的计算,而只需要对比窗口中的词,所以在实际更新时只需要更新词向量矩阵 U U U和 V V V中的某些相关的列,或者建立每个词到它的词向量的哈希映射。
为了解决上述的问题,在word2vec原始的paper中使用了负采样的方法。它的想法是:对于某一个中心词来说,语料库中的很多词在大概率情况下是不会做为它的上下文词,因此其处理方式是随机抽取一定数目的随机词(尽量在中心词上下文中没出现过的词),然后最小化中心词和他们出现的概率,最大化中心词和上下文词出现的概率。简单来说就是训练二分类的Logistic Regression,分析给定的词对两者存在上下文关系的概率。
Distributed RepresentaRons of Words and Phrases and their ComposiRonality” (Mikolov et al. 2013)
每个时间步的目标函数为 J t ( θ ) = log σ ( u o T v c ) + ∑ i = 1 k E j ∼ P ( w ) [ log σ ( − u j T v c ) ] J_{t}(\theta)=\log \sigma\left(u_{o}^{T} v_{c}\right)+\sum_{i=1}^{k} \mathbb{E}_{j \sim P(w)}\left[\log \sigma\left(-u_{j}^{T} v_{c}\right)\right] Jt(θ)=logσ(uoTvc)+i=1∑kEj∼P(w)[logσ(−ujTvc)]其中 k k k是采样数, σ \sigma σ表示Sigmoid函数 σ ( x ) = 1 1 + e − x \sigma(x)=\frac{1}{1+e^{-x}} σ(x)=1+e−x1,它的作用就是将计算的值挤压到 [ 0 , 1 ] [0,1] [0,1]之间表示为概率的形式,函数图如下所示
然后在整个语料库上对所有的 J t ( θ ) J_{t}(\theta) Jt(θ)求和取平均 J ( θ ) = 1 T ∑ t = 1 T J t ( θ ) J(\theta)=\frac{1}{T} \sum_{t=1}^{T} J_{t}(\theta) J(θ)=T1∑t=1TJt(θ)。此外, J t ( θ ) J_{t}(\theta) Jt(θ)也可以写为 J t ( θ ) = log σ ( u o T v c ) + ∑ j ∼ P ( w ) [ log σ ( − u j T v c ) ] J_{t}(\theta)=\log \sigma\left(u_{o}^{T} v_{c}\right)+\sum_{j \sim P(w)}\left[\log \sigma\left(-u_{j}^{T} v_{c}\right)\right] Jt(θ)=logσ(uoTvc)+∑j∼P(w)[logσ(−ujTvc)],其中 P ( W ) = U ( w ) 3 4 Z P(W)=\frac{U(w)^{\frac{3}{4}}}{Z} P(W)=ZU(w)43,当然也可以取其他形式的 P ( W ) P(W) P(W),但是这样的方式往往效果就很好了。
我们知道向量之间点积的计算结果可以表示两者的相关性,值越大相关性越强。上述的目标函数 J t ( θ ) J_{t}(\theta) Jt(θ)就是要最大化中心词与上下文的相关概率,最小化与其他词语的概率。
如果我们将使用word2vec得到的所有词向量进行降维,然后将结果平铺到2D空间中,如下所示
因此,word2vec的主要过程为
- 遍历整个语料库的每一个词
- 将每一个词作为中心词,预测其上下文词
- 每一次计算可以捕获到一个共现的词
word2vec在将每一个词作为中心词,然后在整个语料库中每个涉及到它的地方都要进行一次参数更新。而很多词对出现的频次是很高的,因此能否只遍历一遍语料,迅速得到结果呢?YEP~ 早在word2vec之前,就已经出现了很多基于统计词共现矩阵得到词向量的方法,例如潜在语义分析(LSA) 、LDA等。
例如语料库中有:
- I like deep learning.
- I like NLP.
- I enjoy flying.
窗口大小为1,那么就可以得到如下的词共现矩阵
但是这样的词共现矩阵存在着诸多的局限:
- 无法进行高效的更新:当出现新词的时候,需要对整个矩阵重新进行计算
- 高纬度:当语料库很大时,矩阵的维度将极高
- 高稀疏性:很对词并不会一起出现
解决上述局限的一种方法进行进行降维,使用只保留包含重要信息的低维稠密向量进行表示,向量的维度通常为25-1000。其中一种常用的降维方式进行奇异值分解(SVD)
在实际使用中,只需要一行代码就可以使用SVD
import numpy as np
import matplotlib.pyplot as plt
la = np.linalg
words = ["I" , "like" , "enjoy" , "deep" , "learning" , "NLP" , "flying" , "."]
X = np.array([
[0,2,1,0,0,0,0,0],
[2,0,0,1,0,1,0,0],
[1,0,0,0,0,0,1,0],
[0,1,0,0,1,0,0,0],
[0,0,0,1,0,0,0,1],
[0,1,0,0,0,0,0,1],
[0,0,1,0,0,0,0,1],
[0,0,0,0,1,1,1,0]
])
U, s, Vh = la.svd(X, full_matrices=False)
for i in range(len(words)):
plt.scatter(U[i, 0], U[i, 1])
plt.text(U[i, 0], U[i, 1], words[i])
plt.show()
结果
但SVD也存在一些问题
- 计算复杂度高:对 n × m n \times m n×m的矩阵的计算复杂度为 O ( m n 2 ) O(mn^2) O(mn2)
- 不方便处理新词或新文档
- 与其他DL模型训练套路不同
因此有不少研究者对其做了很多的改进工作,例如限制高频词的频次、干脆去掉停用词、根据与中央词的距离衰减词频权重、用皮尔逊相关系数代替词频等。在An Improved Model of SemanRc Similarity Based on Lexical Co-Occurrence Rohde et al. 2005这篇文章中给出了一种可视化的结果
如果同样的在2D平面上表示如下所示,从中可以看出语义相近的词很明显的形成了一个个的簇
基于词频计数的方法和直接预测的方法的对比如下所示:
- 基于计数的方法在中小规模语料训练很快,有效地利用了统计信息,但受限于捕捉词语相似度,也无法拓展到大规模语料
- NNLM, HLBL, RNN, Skip-gram/CBOW这类进行预测的模型必须遍历所有的窗口训练,也无法有效利用单词的全局统计信息,但它们显著地提高了上级NLP任务,其捕捉的不仅限于词语相似度
综合上面两种方法的优势,学者就提出了Glove,它训练时间短、使用了大规模语料库以及在小语料库上同样可取的很好的效果。模型的目标函数为 J ( θ ) = 1 2 ∑ i , j = 1 W f ( P i j ) ( u i T v j − log P i j ) 2 J(\theta)=\frac{1}{2} \sum_{i, j=1}^{W} f\left(P_{i j}\right)\left(u_{i}^{T} v_{j}-\log P_{i j}\right)^{2} J(θ)=21i,j=1∑Wf(Pij)(uiTvj−logPij)2其中的向量 u u u和 v v v都捕获了语料库中的词共现信息,一种很好的方法就是把它们简单的加起来使用 X final = U + V X_{\text { final }}=U+V X final =U+V。而且在真实情况中,这样简单的方式的确可以取得很好的效果,例如使用Glove寻找和frog相近的词,结果有frogs、toad、 litoria、leptodactylidae、rana、lizard、eleutherodactylus,它们对应的图像为
在使用word2vec或是Glove获得词向量后,如何评估它们的优劣呢?一般有两种角度:Intrinsic和extrinsic。对于Intrinsic来说,它使用针对某种具体的任务涉及单独的实验,例如由让专家进行词的标注等工作,然后于模型效果进行对比。这样的方式计算速度快、帮助更好的理解系统,但是无法知道在实际的使用中究竟有多大的帮助。对于extrinsic来说,它是通过实际的应用来体现效果的优劣,但耗时较长,且不能排除是否是新的词向量与旧系统的某种契合度产生。需要至少两个sub-systems同时证明。
其中一种很直观的方式就是Word Vector Analogies,即词向量类比,具体可以使用余弦夹角计算可得。例如man对woman,那么king对应?Queen
如果将使用Glove得到的词向量进行类比的结果可视化到2D平面上,可以看出类比的向量都是近似平行的
下面是一些比较有趣的类比结果
在不同大小的语料上,训练不同维度的词向量,在语义和语法数据集上的结果如下所示,Glove的效果都是最好的。
对于窗口是否对称(还是只考虑前面的单词)、向量维度、窗口大小的实验结果如下所示
从中可以看出维度大约300维,窗口大小8的对称窗口时效果相对最好。此外对于Glove来说,迭代的次数越多,效果越好,但在考虑成本的前提下,往往只需迭代到效果变化不再明显时即可
另外训练数据集不同也会对词向量的效果有很大影响,例如维基百科相对于网络新闻来说拼写错误等更少,因此效果往往更好
最后提到了适合word vector的任务,比如单词分类,不太适合的任务,比如情感分析。此外还有涉及消歧的示例,中心思想是通过对上下文的聚类分门别类地重新训练。