第3章
Word2vec——学习词嵌入
在本章中,我们将讨论NLP中一个至关重要的主题—Word2vec,这是一种学习词嵌入或单词的分布式数字特征表示(即向量)的技术。学习单词表示是许多NLP任务的基础,因为许多NLP任务依赖于能够保留其语义及其在语言中的上下文的单词的良好特征表示。例如,单词“forest”的特征表示应该与“oven”非常不同,因为这些单词在类似的上下文中很少使用,而“forest”和“jungle”的表示应该非常相似。
Word2vec被称为分布式表示,因为单词的语义由表征向量的全部元素的激活状态表示,而不是由表征向量的单个元素表示(例如,对于一个单词,将向量中的单个元素设置为1,其余设为0)。
我们将从解决这一问题的经典方法开始,到在寻找良好的单词表示方面能提供先进性能的基于现代神经网络的方法,逐步介绍词嵌入方法。我们在如图3.1所示的2D画布上可视化(使用t-SNE,一种用于高维数据的可视化技术)从一组单词学习到的单词嵌入。如果仔细观察,你会看到相似的单词互相之间距离较近(例如,中间集群的数字)。
t分布式随机邻域嵌入(t-SNE)
这是一种降维技术,它可将高维数据投影到二维空间。这使我们能够想象高维数据在空间中的分布情况,它非常有用,因为我们无法轻易地在三维之外进行可视化,你将在下一章中更详细地了解t-SNE。
3.1 单词的表示或含义是什么
“含义”本身是什么意思?这更像是一个哲学问题,而不是技术问题。因此,我们不会试图找出这个问题的最恰当答案,而是接受一个折中的答案,即意义是一个单词所表达的想法或某种表示。由于NLP的主要目标是在语言任务中达到和人类一样的表现,因此为机器寻找表示单词的原则性方法是有用的。为了实现这一目标,我们使用可以分析给定文本语料库并给出单词的良好数字表示(即词嵌入)的算法,它可以使属于类似上下文的单词(例如,one和two,I和we)与不相关的单词(例如,cat和volcano)相比有更相似的数字表示。
首先,我们将讨论实现这一目标的一些经典方法,然后继续介绍目前采用的更复杂的方法,后者使用神经网络来学习这些特征表示,并具有最好的性能。
3.2 学习单词表示的经典方法
在本节中,我们将讨论用数字表示单词的一些经典方法。这些方法主要可以分为两类:使用外部资源表示单词的方法和不使用外部资源的方法。首先,我们将讨论WordNet:一种基于外部资源表示单词的最流行的方法。然后,我们会讨论更多的本地化方法(即不依赖外部资源的方法),例如,单热编码和词频率-逆文档频率(TF-IDF)。
3.2.1 WordNet—使用外部词汇知识库来学习单词表示
WordNet是处理单词表示的最流行的经典方法或统计NLP方法之一。它依赖于外部词汇知识库,该知识库对给定单词的定义、同义词、祖先、派生词等信息进行编码。WordNet允许用户推断给定单词的各种信息,比如前一句中讨论的单词的各种信息和两个单词之间的相似性。
3.2.1.1 回顾WordNet
如前所述,WordNet是一个词汇数据库,用于对单词之间的词性标签关系(包括名词、动词、形容词和副词)进行编码。WordNet由美国普林斯顿大学心理学系首创,目前由普林斯顿大学计算机科学系负责。WordNet考虑单词之间的同义性来评估单词之间的关系。用于英语的WordNet目前拥有超过150000个单词和超过100000个同义词组(即synsets)。此外,WordNet不仅限于英语。自成立以来,已经建立了许多不同的WordNet,在http://globalwordnet.org/wordnets-in-the-world/ 上可以查看。
为了理解如何用WordNet,需要对WordNet中使用的术语有坚实的基础。首先,WordNet使用术语synset来表示一群或一组同义词。接下来,每个synset都有一个definition,用于解释synset表示的内容。synset中包含的同义词称为lemmas。
在WordNet中,单词表示是分层建模的,它在给定的synset与另一个synset之间进行关联形成一个复杂的图。有两种不同类别的关联方式:is-a关系或is-made-of关系。首先,我们将讨论is-a关系。
对于给定的synset,存在两类关系:上位词和下位词。synset的上位词是所考虑的synset的一般(更高一层)含义的同义词。例如,vehicle是同义词car的上位词。接下来,下位词是比相应的同义词组更具体的同义词。例如,Toyota car是同义词car的下位词。
现在让我们讨论一个synset的is-made-of关系。一个synset的整体词是可以表示所考虑的这个synset的全部实体的synset。例如,tires的整体词是cars。部分词是is-made-of类别的关系,是整体词的反义词,部分词是组成相应synset的一部分或子部分,我们可以在图3.2中看到它们。
NLTK库是一个Python自然语言处理库,我们可以用它理解WordNet及其机制。完整示例可在ch3文件夹中的ch3_wordnet.ipynb文件中找到。
安装NLTK库
可以用以下python pip命令安装NLTK库:
或者,可以使用IDE(例如PyCharm)通过图形用户界面(GUI)安装库。可以在http://www.nltk.org/install.html 找到更详细的说明。
要将NLTK导入Python并下载WordNet语料库,请首先导入nltk库:
然后可以运行以下命令下载WorldNet语料:
安装并导入nltk库后,我们需要使用以下命令导入WordNet语料库:
然后我们按照以下方式查询WordNet:
在运行这个示例之后,结果如下:
我们还可以通过以下方式获得两个同义词之间的相似性。在NLTK中实现了几种不同的相似性度量,你可以在官方网站(www.nltk.org/howto/wordnet.html )上看到它们的实际应用。在这里,我们使用Wu-Palmer相似性,它根据两个synset在同义词的层次结构中的深度来测量它们之间的相似性:
3.2.1.2 WordNet的问题
虽然WordNet是一个令人惊叹的资源,任何人都可以在NLP任务中用它学习单词的含义,但使用WordNet有很多不足之处。
如下所示:
- 缺少细微差别是WordNet中的一个关键问题。WordNet在理论上和实际应用中都有不可行的原因。从理论的角度来看,对两个实体之间微妙差异的定义进行建模并不恰当或直接。实际上,定义细微差别是主观的。例如,单词want和need具有相似的含义,但其中一个(need)更具有主张性,这被认为是一种细微差别。
- 接下来,WordNet本身就是主观的,因为WordNet是由一个相对较小的社区设计的。因此,取决于你要解决的问题,WordNet可能是合适的,或者你可以通过更宽松的单词定义来提高性能。
- 维护WordNet也存在问题,这是非常需要人力的。维护和添加新的synsets、definitions、lemmas等可能非常昂贵。这会对WordNet的可扩展性产生负面影响,因为人力对更新WordNet至关重要。
- 为其他语言开发WordNet成本可能很高。有一些人努力为其他语言构建WordNet并将其与英语WordNet链接为MultiWordNet(MWN),但它们尚未完成。
接下来,我们将讨论几种不依赖外部资源的单词表示技术。
3.2.2 独热编码表示方式
表示单词的更简单方法是使用独热编码表示。这意味着,如果我们有一个V大小的词汇表,对于第i个词wi,我们将用一个V长度的向量[0, 0, 0, …, 0, 1, 0, …, 0, 0]来表示单词wi,其中第i个元素为1,其他元素为零。举个例子,考虑一下这句话:Bob and Mary are good friends。其每个单词的独热表示如下所示:
但是,正如你可能已经想到的那样,这种表示有许多缺点。该表示并没有用任何方式对单词之间的相似性编码,并且完全忽略了单词的上下文。让我们考虑单词向量之间的点积作为相似性度量方法,两个矢量越相似,这两个矢量的点积越高。例如,单词car和cars的单词表示的相似距离是0,而car和pencil也有相同的值。
对于大型词汇表,此方法变得非常没有效果。此外,对于典型的NLP任务,词汇量很容易超过50000个单词。因此,50000单词的单词表示矩阵将导致非常稀疏的50000×50000矩阵。
然而,即使在最先进的词嵌入学习算法中,独热编码也起着重要作用。我们使用独热编码将单词表示为数字向量,并将其送入神经网络,以便神经网络可以学习单词的更好和更短的数字特征表示。
单热编码也称为局部表示(与分布式表示相反),因为特征表示由向量中的单个元素的激活情况决定。
3.2.3 TF-IDF方法
TF-IDF是一种基于频率的方法,它考虑了单词在语料库中出现的频率。这是一种表示给定文档中特定单词的重要性的单词表示。直观地说,单词的频率越高,该单词在文档中就越重要。例如,在关于猫的文档中,单词cats会出现更多次。然而,仅仅计算频率是行不通的,因为像this和is这样的词是非常频繁的,但是它们并没有携带很多信息。TF-IDF将此考虑在内,并把这些常用单词的值置为零。
同样,TF代表词频率,IDF代表逆文档频率:
下面做个快速练习,考虑两个文件:
- 文件1:This is about cats. Cats are great companions.
- 文件2:This is about dogs. Dogs are very loyal.
现在让我们来处理一些数字:
因此,cat这个词具有丰富的信息,而this这个词不是,这是我们在衡量单词重要性方面所期望的行为。
3.2.4 共现矩阵
与独热编码表示不同,共现矩阵对单词的上下文信息进行编码,但是需要维持V×V矩阵。为了理解共现矩阵,请看两个例句:
- Jerry and Mary are friends.
- Jerry buys flowers for Mary.
共现矩阵看起来像下面的矩阵,我们只显示矩阵的上三角,因为矩阵是对称的如表3.1所示:
然而,不难看出,因为矩阵的大小随着词汇量的大小而多项式地增长,维持这样的共现矩阵是有代价的。此外,上下文窗口扩展到大小大于1并不简单。一种选择是引入加权计数,其中,上下文中的单词的权重随着与中心单词的距离而衰减。
所有这些缺点促使我们研究更有原则、更健壮和更可扩展的推断单词含义的学习方法。
Word2vec是最近推出的分布式单词表示学习技术,目前被用作许多NLP任务的特征工程技术(例如,机器翻译、聊天机器人和图像标题生成)。从本质上讲,Word2vec通过查看所使用的单词的周围单词(即上下文)来学习单词表示。更具体地说,我们试图通过神经网络根据给定的一些单词来预测上下文单词(反之亦然),这使得神经网络*学习良好的词嵌入。我们将在下一节中详细讨论这种方法。Word2vec方法与先前描述的方法相比具有如下许多优点:
- Word2vec方法并不像基于WordNet的方法那样对于人类语言知识具有主观性。
- 与独热编码表示或单词共现矩阵不同,Word2vec表示向量大小与词汇量大小无关。
- Word2vec是一种分布式表示。与表示向量取决于单个元素的激活状态的(例如,独热编码)局部表示不同,分布式表示取决于向量中所有元素的激活状态。这为Word2vec提供了比独热编码表示更强的表达能力。
在下一节中,我们将首先通过一个示例来建立对学习词嵌入的直观感受。然后,我们将定义一个损失函数,以便我们可以使用机器学习方法来学习词嵌入。此外,我们将讨论两种Word2vec算法,即skip-gram和连续词袋(CBOW)算法。
3.3 Word2vec—基于神经网络学习单词表示
"You shall know a word by the company it keeps."
—J.R. Firth
由J.R.Firth于1957年发表的这一陈述是Word2vec的基础,因为Word2vec利用给定单词的上下文来学习它的语义。Word2vec是一种开创性的方法,可以在没有任何人为干预的情况下学习单词的含义。此外,Word2vec通过查看给定单词周围的单词来学习单词的数字表示。
我们可以想象一个真实世界的场景来测试上述说法的正确性。比如,你正在参加考试,你在第一个问题中找到了这句话:“Mary is a very stubborn child. Her pervicacious nature always gets her in trouble.”。现在,除非你非常聪明,否则你可能不知道pervicacious是什么意思。在这种情况下,你会自动查看在感兴趣的单词周围的短语。在我们的例子中,pervicacious的周围是stubborn、nature、和trouble,这三个词就足以说明,pervicacious事实上是指顽固状态。我认为这足以证明语境对于认识一个词的含义的重要性。
现在,让我们讨论Word2vec的基础知识。如前所述,Word2vec通过查看单词上下文并以数字方式表示它,来学习给定单词的含义。所谓“上下文”,指的是在感兴趣的单词的前面和后面的固定数量的单词。假设我们有一个包含N个单词的语料库,在数学上,这可以由以w0,w1,…,wi和wN表示的一系列单词表示,其中wi是语料库中的第i个单词。
接下来,如果我们想找到一个能够学习单词含义的好算法,那么,在给定一个单词之后,我们的算法应该能够正确地预测上下文单词。这意味着对于任何给定的单词wi,以下概率应该较高:
为了得到等式右边,我们需要假设给定目标单词(wi)的上下文单词彼此独立(例如,wi - 2和wi - 1是独立的)。虽然不完全正确,但这种近似使得学习问题切合实际,并且在实际中效果良好。
3.3.1 练习:queen = king – he + she吗
在继续之前,让我们做一个小练习,来了解如何最大化前面提到的概率以找到单词的好的含义(即表示)。考虑以下非常小的语料库:
现在让我们手动做一些预处理并删除标点符号和无信息的单词:
现在,让我们用其上下文单词为每个单词形成一组元组,其格式为:目标单词→上下文单词1,上下文单词2。我们假设两边的上下文窗口大小为1:
请记住,我们的目标是给出左侧的单词能够预测右侧的单词。要做到这一点,对于给定的单词,右侧上下文中的单词应该与左侧上下文中的单词在数值或几何上具有很高的相似性。换句话说,感兴趣的单词应该可以用周围的词来表达。现在,让我们假定实际的数值向量来理解它是如何工作的。为简单起见,我们只考虑以粗体突出显示的元组。让我们首先假设rich这个词有以下数值:
为了能够正确地从rich中预测was和king,was和king应该与rich这个词有很高的相似性。我们假定向量之间的欧几里德距离作为相似性结果。
让我们为单词king和rich尝试以下值:
得到的结果不错:
这里,Dist指的是两个词之间的欧几里德距离,如图3.3所示。
现在让我们考虑以下元组:
我们已经建立了king与rich之间的关系。但是,它还没有完成,我们认为两个词之间的关系越紧密,这两个词就越接近。那么,让我们首先调整king的向量,使它更接近rich:
接下来,我们需要在图片中添加单词he,he应该更接近king这个词,下面是我们现在关于he这个单词的所有信息:
现在,包含这些单词的图与图3.4类似。
现在,让我们继续处理下面两个元组:queen→
beautiful, she和she→queen, was。请注意,我已经交换了元组的顺序,因为这使我们更容易理解该示例:
现在,我们将不得不使用我们先前的英语知识。一个合理的决定是,将单词she放在与单词he同was在距离上同样远的地方,因为它们在单词was的上下文中的用法是等价的。
因此,让我们用这个:
接下来,我们用queen接近单词she:
如图3.5所示。
接下来,我们只有如下元组:
在这里,找到了beautiful这个词。它与queen和she应该有大致相同的距离。我们使用以下表示:
现在,我们用图表描述单词之间的关系。当我们观察图3.6时,词语含义似乎非常直观。
现在,让我们来看看自本练习开始以来潜伏在我们脑海中的问题。这个等式中的数量是等价的吗:queen = king – he + she?好吧,我们现在已经拥有了解决这个谜团所需的所有资源。
让我们先计算方程式的右侧:
最后的确是有用的。如果你看一下我们为单词queen设置的词向量,就会发现这与我们之前推断出的答案完全一致。请注意,这是一个粗略的工作,目的是说明如何学习词嵌入,如果使用算法进行学习,则一词嵌入的确切位置可能不同。
但请记住,对于现实世界的语料库来说,这是一个不切实际的缩小规模之后的练习。因此,仅仅通过处理十几个数字是无法手工计算出这些词向量的值的。这是复杂的函数逼近方法,如神经网络为我们做的那样。但是,要使用神经网络,我们需要以数学语言的方式来表达我们的问题。
3.3.2 为学习词嵌入定义损失函数
即使是简单的现实世界任务,其词汇量也很容易超过10000个单词。因此,我们不能手动为大型文本语料库开发词向量,而需要设计一种方法来使用一些机器学习算法(例如,神经网络)自动找到好的词嵌入,以便有效地执行这项繁重的任务。此外,要在任何类型的任务中使用任何类型的机器学习算法,需要定义损失,这样,完成任务就转化为让损失最小化。让我们为找到好的嵌入向量定义损失。
首先,让我们回想一下在本节开头讨论过的等式:
有了这个等式之后,我们为神经网络定义成本函数:
记住,J (θ)是损失(即成本),而不是奖励。另外,我们想要使P (wj | wi)最大化。因此,我们需要在表达式前面加一个减号将其转换为损失函数。
现在,让我们将其转换对数空间,而不是使用点积运算符。将等式转换为对数空间会带来一致性和数值稳定性:
这种形式的成本函数称为“负对数似然”。现在,因为有一个精心设计的成本函数,我们可以用神经网络来优化这个成本函数。这样做会迫使词向量或词嵌入根据单词含义很好地组织起来。现在,是时候介绍能用这个成本函数找到好的词嵌入的现有算法了。
3.4 skip-gram算法
我们将讨论的第一个算法称为skip-gram算法,它由Mikolov和其他人在2013年提出,该算法是一种利用文本单词上下文来学习好的词嵌入的算法。让我们一步一步地了解skip-gram算法。
首先,我们将讨论数据准备过程,然后介绍理解算法所需要的表示法。最后,我们将讨论算法本身。
正如我们在许多地方所讨论的那样,单词的含义可以从围绕该单词的上下文单词中得到。但是,建立一个利用这种性质来学习单词含义的模型并不是很容易。
3.4.1 从原始文本到结构化的数据
首先,我们需要设计一种方法来提取可以送入学习模型的数据集,这样的数据集应该是格式为(输入,输出)这样的一组元组。而且,这需要以无监督的方式创建。也就是说,人们不应该手动设置数据的标签。总之,数据准备过程应该执行以下操作:
- 获取给定单词的周围单词
- 以无监督的方式执行
skip-gram模型使用以下方法来构建这样的数据集:
1.对于给定的单词wi,假设上下文窗口大小为m。上下文窗口大小,指的是单侧被视为上下文的单词数。因此,对于wi,上下文窗口(包括目标词wi)的大小为2m + 1,如下所示:[wi - m, …, wi - 1, wi, wi + 1, …, wi + m]。
2.接下来,输入输出元组的格式为[…, (wi , wi - m), …, (wi , wi - 1), (wi , wi + 1), …, (wi,wi + m), …];这里,m + 1≤i≤N - m,N是文本中用于获得实际含义的单词数。让我们假设以下句子和上下文窗口大小m为1:
对于此示例,数据集将如下所示:
3.4.2 使用神经网络学习词嵌入
一旦数据是(输入,输出)格式,我们就可以使用神经网络来学习词嵌入。首先,让我们确定学习词嵌入所需的变量。为了存储词嵌入,我们需要一个V×D矩阵,其中V是词汇量大小,D是词嵌入的维度(即向量中表示单个单词的元素数量)。D是用户定义的超参数,D越大,学习到的词嵌入表达力越强。该矩阵将被称为嵌入空间或嵌入层。
接下来,我们有一个softmax层,其权重大小为D×V,偏置大小为V.
每个词将被表示为大小为V的独热编码向量,其中一个元素为1,所有其他元素为0。因此,输入单词和相应的输出单词各自的大小为V。让我们把第i个输入记为xi,xi的对应嵌入为zi,对应的输出为yi。
此时,我们定义了所需的变量。接下来,对于每个输入xi,我们将从对应于输入的嵌入层中查找嵌入向量。该操作向我们提供zi,它是大小为D的向量(即长度为D的嵌入向量)。然后,我们做以下转换以计算xi的预测输出:
这里,logit(xi)表示非标准化分数(即logits),是V大小的预测输出(表示输出是V大小的词汇表的单词的概率),W是D×V权重矩阵,b是V×1偏置矢量,softmax是softmax激活。我们将可视化skip-gram模型的概念(图3.7)和实现(图3.8)图。以下是符号的总结:
- V:这是词汇量的大小
- D:这是嵌入层的维度
- xi:这是第i个输入单词,表示为独热编码向量
- zi:这是与第i个输入单词对应的嵌入(即表示)向量
- yi:这是与xi对应的输出单词的独热编码向量
- :这是xi的预测输出
- logit(xi):这是输入xi的非标准化得分
- Ⅱwj:这是单词wj的独热编码表示
- W:这是softmax权重矩阵
- b:这是softmax的偏置
通过使用现有单词和计算得到的实体,我们现在可以使用负对数似然损失函数来计算给定数据点(xi,yi)的损失。如果你想知道P(wj | wi)是什么,它可以从已定义的实体派生出来。接下来,让我们讨论如何从计算P(wj | wi)并得到一个正式的定义。
为什么原始的词嵌入论文使用两个嵌入层?
原始论文(由Mikolov和其他人于2013年发表)使用两个不同的V×D嵌入空间来表示目标空间中的单词(用作目标时的单词)和上下文空间中的单词(用作上下文的单词)。这样做的一个动机是,同一个单词通常不会出现在自身的上下文中。因此,我们希望尽可能减少此类事件发生的可能性。例如,对于目标单词dog,在其上下文中不太可能找到单词dog(P(dog | dog)~0)。直觉上,如果我们将(xi = dog和yi = dog)数据点提供给神经网络,如果神经网络将dog预测为dog的上下文单词,我们就要求神经网络给出更高的损失。换句话说,我们要求单词dog的词嵌入与单词dog的词嵌入距离非常远。这会产生一个强烈的矛盾,因为相同词嵌入之间的距离将为0。因此,如果我们只有一个嵌入空间,我们就无法实现这一点。但是,为目标词和上下文词提供两个单独的嵌入空间就允许我们拥有此属性,因为这样一来同一个单词就有两个单独的嵌入向量。但是,实际上,只要你避免在输入输出元组中输入和输出是同一个词,使就可以使用单个嵌入空间,并且无须使用两个不同的嵌入层。
3.4.2.1 制定实际的损失函数
让我们更仔细地查看我们的损失函数。我们得出的损失应该如下所示:
但是,根据我们目前掌握的信息,计算这一特定损失并不是很容易。
首先,让我们理解P(wj | wi)代表什么。为此,我们将从单个单词表示法转为单个数据点表示法。也就是说,我们会说P(wj | wi)由第n个数据点给出,其中wi的独热编码向量作为输入(xn),wj的独热编码表示作为真实输出(yn)。这由以下等式给出:
logit(xn)项表示给定输入xn获得的非标准化预测得分(即logit)向量(V大小),而logit(xn)wj是wj的独热编码表示中非零的索引所对应的得分值(从现在开始我们称之为wj的索引)。然后,我们将wj索引处的logit值相对于整个词汇表中所有单词所对应的所有logit值进行标准化。这种特定类型的归一化称为softmax激活(或归一化)。现在,通过将其转换为对数空间,我们得到以下等式:
为了有效地计算logit函数,我们可以调整变量,得出以下表示法:
这里,Ⅱwj是wj的独热编码向量。现在,logit操作缩减为对乘积求和。由于对应于单词wj,Ⅱwj仅有一个非零元素,因此在计算中将仅使用向量的该索引。这比通过扫描词汇量大小的向量找到对应于非零元素的索引的logit向量中的值更有计算效率。
现在,通过将我们获得的计算赋给logit,对于损失函数,我们得到以下结果:
让我们考虑一个例子来理解这个计算:
我们可以如下创建输入输出元组:
现在,我们为上面的单词假定以下独热编码表示:
接下来,让我们考虑输入输出元组(like,I)。当我们通过skip-gram学习模型传播输入like时,让我们假设我们按该顺序获得了Like、I和NLP这些的单词的以下logit:
现在,词汇表中每个单词的softmax输出如下所示:
上面的损失函数表明我们需要最大化P(I | like)以使损失最小化。现在让我们将这个例子应用于这个损失函数:
有了这个损失函数,对于减号之前的项,y向量中只有一个非零元素对应于单词I,因此,我们只考虑概率P(I | like),这就是我们要的。
但是,这不是我们想要的理想解决方案。从实际角度来看,该损失函数的目标是,使预测给定单词的上下文单词的概率最大化,同时使预测给出单词的“所有”非上下文单词的概率最小化。我们很快就会发现,有一个良好定义的损失函数并不能在实践中有效地解决我们的问题,我们需要设计一个更聪明的近似损失函数,来在可行的时间内学习良好的词嵌入。
3.4.2.2 有效的近似损失函数
我们很幸运有一个在数学上和感觉上都很正确的损失函数,但是,困难并没有就此结束。如果我们像前面讨论的那样尝试以封闭形式计算损失函数,我们将不可避免地面对算法执行得非常缓慢的问题,这种缓慢是由于词汇量大而导致的性能瓶颈。我们来看看我们的损失函数:
你会看到计算单个示例的损失需要计算词汇表中所有单词的logit。与通过数百个输出类别就足以解决大多数现有的真实问题的计算机视觉问题不同,skip-gram并不具备这些特性。因此,我们需要在不失去模型效果的前提下寻找有效的损失近似方案。
我们将讨论两种主流的近似选择:
- 负采样
- 分层softmax
(1)对softmax层进行负采样
在这里,我们将讨论第一种方法:对softmax层进行负采样。负采样是对噪声对比估计(NCE)方法的近似。NCE要求,一个好的模型应该通过逻辑回归来区分数据和噪声。
考虑到这个属性,让我们重新设计学习词嵌入的目标。我们不需要完全概率模型,该模型对给定单词给出词汇表中所有单词的确切概率。我们需要的是高质量的词向量。因此,我们可以简化我们的问题,将其变为区分实际数据(即输入输出对)与噪声(即K个虚拟噪声输入输出对)。噪声指的是使用不属于给定单词的上下文的单词所创建的错误输入输出对。我们还将摆脱softmax激活,并将其替换为sigmoid激活(也称为逻辑函数)。这使得我们能够在使输出保持在[0,1]之间的同时,消除损失函数对完整词汇表的依赖。我们可以可视化图3.9中的负采样过程。
确切地说,我们的原始损失函数由以下等式给出:
之前的等式变为:
这里,σ表示sigmoid激活,其中σ (x) = 1/(1 + exp (- x))。请注意,为了清楚起见,我在原始损失函数中用log(exp(logit(xn)wj))替换了logit(xn)wj。你可以看到新的损失函数仅取决于与词汇表中的k项相关的计算。
经过一些简化后,我们得出以下等式:
我们花一点时间来理解这个等式所说的内容。为简化起见,我们假设k = 1,这样得到以下等式:
这里,wj表示wi的上下文单词,wq表示其非上下文单词。这个等式基本上说的是,为了使J?(θ)最小化,我们应该使σ(logit(xn)wi)?≈1,这意味着logit(xn)wj需要是一个大的正值。然后,σ(- logit(xn)wq)?≈1意味着logit(xn)wq需要是一个大的负值。换句话说,对于表示真实目标单词和上下文单词的真实数据点应该获得大的正值,而表示目标单词和噪声的伪数据点应该获得大的负值。这与使用softmax函数获得的效果相同,但具有更高的计算效率。
这里σ表示sigmoid激活函数。直观地看,在计算损失的时候,我们做了如下2步:
- 计算wj的非零列的损失(推向正值)
- 计算K个噪声样本的损失(拉向负值)
(2)分层softmax
分层softmax比负采样略复杂,但与负采样的目标相同,也就是说,近似softmax而不必计算所有训练样本的词汇表中所有单词的激活状态。但是,与负采样不同,分层softmax仅使用实际数据,并且不需要噪声采样。图3.10是可视化的分层softmax模型。
要了解分层softmax,让我们考虑一个例子:
其词汇表如下:
使用这个词汇表,我们构建一个二叉树,其中,词汇表中所有单词都以叶节点的形式出现。我们还将添加一个特殊的标记PAD,以确保所有叶节点都有两个成员。
然后,最后一个隐藏层将完全连接到分层结构中的所有节点(参见图3.11)。注意,与经典的softmax层相比,该模型具有相似的总权重,但是,对于给定的计算,它仅使用其中一部分。
假设我们需要推断P(NLP | like)的概率,其中like是输入词,那么我们只需要权重的子集即可计算概率,如图3.12所示。
具体地,以下是概率计算的过程。
现在我们知道如何计算P(wj | wi),我们可以使用原始的损失函数。
注意,该方法仅使用连接到路径中的节点的权重进行计算,从而提高了计算效率。
(3)学习分层结构
虽然分层softmax是有效的,但一个重要的问题仍然没有答案。
我们如何确定树的分支?更准确地说,哪个词会跟随哪个分支?有几种方法可以实现这一目标:
- 随机初始化层次结构:此方法确实存在一些性能下降,因为随机分配无法保证在单词之间是最佳分支。
- 使用WordNet确定层次结构:WordNet可用于确定树中单词的合适顺序,该方法明显比随机初始化有更好的性能。
(4)优化学习模型
由于我们有了一个精心设计的损失函数,因此优化就是从TensorFlow库调用正确函数。要使用的优化过程是随机优化过程,这意味着我们不会一次输入完整数据集,而只是在许多步中随机提供批量数据。
3.4.3 使用TensorFlow实现skip-gram
我们现在将介绍使用TensorFlow实现skip-gram算法。在这里,我们仅讨论学习嵌入所需要的TensorFlow操作的定义,而不是执行操作。完整练习可在ch3练习目录的ch3_word2vec.ipynb中找到。
首先,让我们定义模型的超参数。你可以*更改这些超参数,以查看它们如何影响最终性能(例如,batch_size = 16或batch_size = 256)。但是,相比于更复杂的实际问题,这是一个简单的问题,你可能看不到任何显著差异(除非你将它们更改为极端值,例如,batch_size = 1或num_sampled = 1):
接着,为训练集输入、标签和验证集输入定义TensorFlow占位符:
然后,定义TensorFlow嵌入层的变量以及softmax层的权重和偏置:
接下来,我们将定义一个嵌入查找操作,用于对给定批次的训练输入收集相应的嵌入:
之后,使用负采样定义softmax损失:
在这里,我们定义一个优化器来优化(最小化)前面定义的损失函数。你可以尝试使用https://www.tensorflow.org/api_guides/python/train 中列出的其他优化器进行试验:
使用余弦距离,计算验证集输入示例和所有嵌入之间的相似性:
在定义了所有TensorFlow变量和操作后,我们现在可以执行操作以获得一些结果。这里我们将概述执行这些操作的基本过程,你可以参考练习文件来得到执行操作的完整概念。
- 首先使用tf.global_variables_initializer().run()初始化TensorFlow变量
-
对于预先定义的总步骤数中的每个步骤,请执行以下操作:
- 使用数据生成器生成一批数据(batch_data - 输入,batch_labels-输出)
- 创建一个名为feed_dict的字典,将训练输入/输出占位符映射到数据生成器生成的数据:
- 执行优化步骤并得到损失值,如下所示:
下面将讨论另一种主流的Word2vec算法,称为连续词袋(CBOW)模型。
3.5 连续词袋算法
CBOW模型的原理类似于skip-gram算法,但是在对问题建模的公式中有一个重大变化。在skip-gram模型中,我们从目标单词预测上下文单词。但是,在CBOW模型中,我们将从上下文单词预测目标单词。让我们通过前面的例句来比较skipgram和CBOW的数:
对于skip-gram,数据元组(即(输入词,输出词))可能如下所示:
(dog, the), (dog, barked), (barked, dog),等等。
对于CBOW,数据元组如下所示:
([the, barked], dog), ([dog, at], barked),等等。
因此,CBOW的输入具有2×m×D的维度,其中m是上下文窗口大小,D是嵌入的维度。CBOW的概念模型如图3.13所示。
我们不会详细介绍CBOW的细节,因为它与skip-gram非常相似。但是,我们会讨论其算法实现(虽然不深入,因为它与skip-gram有许多相似之处),以便清楚地了解如何正确实现CBOW。CBOW的完整实现可以在ch3练习文件夹的ch3_word2vec.ipynb中找到。
在TensorFlow中实现CBOW
首先,我们定义变量,这与skip-gram模型一样:
在这里,我们创建了一组堆叠的嵌入,代表每个上下文单词的位置,所以,我们将有一个大小为[batch_size,embeddings_size,2 * context_window_size]的矩阵。然后,我们使用降维运算符,通过平均最后一个轴上的堆叠嵌入,将堆叠矩阵大小减小到[batch_size,embeddings_size]:
之后,损失和优化的定义与在skip-gram模型中一样:
3.6 总结
词嵌入已成为许多NLP任务不可或缺的一部分,并广泛用于机器翻译、聊天机器人、图像标题生成和语言建模等任务。词嵌入不仅可以作为降维技术(与独热编码相比),而且与其他现有技术相比,它们还提供了更丰富的特征表示。在本章中,我们讨论了两种主流的基于神经网络的学习单词表示的方法,即skip-gram模型和CBOW模型。
首先,我们讨论了经典方法,了解过去如何学习单词表示,例如使用WordNet、构建单词的共现矩阵以及计算TF-IDF。后来,我们讨论了这些方法的局限性。
这促使我们探索基于神经网络的单词表示学习方法。首先,我们手动设计了一个例子来理解如何计算词嵌入或词向量,然后用一个词向量的例子来了解可以用词向量完成的有趣事情。
接下来,我们讨论了第一个词嵌入学习算法:skip-gram模型。之后,我们学习了如何准备用于学习的数据。后来,我们研究了如何设计一个可以让我们根据给定单词的上下文单词来获得词嵌入的损失函数。之后,我们讨论了我们所设计的封闭形式损失函数的一个关键的限制。对于大型词汇表,损失函数无法扩展。后来,我们分析了两种主流的近似损失,它们使我们能够有效地计算损失:负采样和分层softmax。最后,我们讨论了如何使用TensorFlow实现skip-gram算法。
然后,我们介绍了学习单词嵌入的下一个选择:CBOW模型。我们还讨论了CBOW与skip-gram模型的不同之处。最后,我们讨论了CBOW的TensorFlow实现。
在下一章中,我们将分析我们学习过的Word2vec技术的性能,并学习几个可显著提高其性能的扩展方法。此外,我们将学习另一个词嵌入学习技术,称为Global Vectors或GloVe。