Fork me on GitHub

#! https://zhuanlan.zhihu.com/p/463766834

title: NLP重大发展历程:从word2vec到Bert的演变
date: 2022-01-29
categories: 深度学习
cover: true
toc: true
tags:
- summary
- NLP


NLP发展至今,Bert成了一个里程碑式的模型,它类似与CV中的ImageNet,将NLP带入了一个全新的时代。很多新入坑的NLP萌新,包括我,在学习Bert模型的时候,可能都会先接触一下Transformer,或者深入点的会接触一下ELMo,最后再接触Bert,但是看完模型架构后会陷入迷茫,好像懂了数据是怎么流动的,训练策略也略知一二,但是为什么要这么做,Transformer和Bert为什么引起那么大的轰动,预训练模型在CV也经常用,很多领域的任务都用在imagenet上预训练的Resnet、VGG等模型作为backbond,为什么在NLP一直那么强调预训练?

总而言之,就是一种似懂非懂的状态,如果你也跟我一样对Bert久仰大名,但又是刚刚入门NLP,那么我推荐你读一下这篇博客,这篇文章不会先不讲解那些锱铢必较的模型细节,而是从一个较为宏观的角度,告诉你Transform、Bert等模型的设计理念。之后你再去看那些模型,可能会更有体会。

这篇文章会先从词向量入手,从前的word2vec和Glove等词向量已经无法满足需求,需要一种更加灵活的词向量作为下游任务的输入,随后会介绍TagLM和ELMo对于这方面的贡献,顺便带过和ELMo同期出现的ULMfit模型;然后介绍Transformer,它开创性地抛弃掉LSTM和RNN这类过于耗时的网络结构,仅仅保留Attention,相比起ELMo它显得更加简约高效;最后再介绍GPT和Bert,它们都是在Transformer的基础上进行不同的改进,尤其是Bert,在推出的时候可谓是NLP领域的集大成之作。

这篇文章需要你对传统的词向量至少是word2vec有所了解,知道什么是语言模型,对RNN、LSTM、Transformer结构有大概的认识。

多义词难题的出路——动态词向量

NLP领域不管是进行文本分类、翻译、文本生成等任务,词向量都是绕不开的一个话题,要实现目标任务,往往需要先训练一份词向量,作为目标任务模型的输入,比起采用随机初始化的词向量作为模型输入,一份预先训练过的良好的词向量总是能给目标任务带来一些增益。

在2018年之前,一般都是用word2vec、Glove这类词向量。先回忆一下这类词向量的原理,在训练的时候,会设定一个大小合适的窗口,挖去窗口中心的词,用周边的词来预测该词,或者是用中心词去预测周边词;而在训练完成后,一个词就对应一个特定的词向量,不再随上下文会改变,即使有时候用于下游任务,可能会跟模型一起进行finetune,但是最终仍然是一个词对应一个特定的词向量,我们称这种词向量是一种静态的词向量。

静态词向量的最大问题在于它无法解决一词多义问题,这里举个例子加深理解“静态”:

  1. 这本书很深,不太容易理解
  2. 这衣服颜色太深,不太好看

在这两句话中都有“深”字,第一句话的表示“深奥”,第二句话的表示一种“程度”,人是怎么判断这两句话的”深“要怎么理解的呢?自然是通过上下文的一些语境。

现在假设已经训练好了一份word2vec词向量,通过查词典,可以很容易地找到“深”这个字对应的词向量是vRdv \isin R^{d}。如果我们要利用这份词向量再进行下游任务,比如情感分类,采用最简单的方式——将一句话中的所有词向量做加权平均后送入分类层,这时候弊端就显现出来了,两句话中“深”这个字明明不是一个意思,但是我们却都用vv去表示它,这自然是不太符合常理。

这便是静态词向量的弊端,同一个词不管处于什么语境,都只能对应同一个固定的词向量。我们希望能有更灵活的表示,即一个词在不同的语境中,是可以对应不同词向量的,套用到上面那个例子,即两句话的“深”可能分别对应v1,v2v_1,v_2两个词向量,它们编码了“深”的不同意思,我们称这种词向量为动态词向量。

那么动态词向量怎么获得?一般还是会用word2vec作为初始词向量,将它送入到一个模型中,设定某种模型优化目标,让词向量随着模型一起优化,模型的输出不仅有预测目标,还有每个初始词向量对应的新词向量,新的词向量是与上下文相关的,对于不同的上下文,输出的新词向量是不一样的。当优化目标达到后,这个训练好的模型就是一个转化器,它将原始word2vec词向量转换为与上下文相关的词向量。

当然这里的模型优化目标是什么并没有限定,可以是机器翻译,也可以是语言模型(给定一段话预测下一个词)等等,我们只是想得到一份词向量,训练词向量时的模型优化目标跟我们下游任务的优化目标,本质上是没有关系的,需要特别注意这一点。另外,语言模型的初始词向量从哪里来?一般来说,可以一切静态词向量比如word2vec,这个初始词向量也被称为原型向量(prototype vector),一个词的一切动态变化都是从这个初始的原型向量衍生出来的。

对于动态词向量的尝试,最早可以追溯到CoVe,他们在序列到序列的机器翻译任务上训练了一个深度 LSTM 编码器,然后用它生成根据上下文变化的词向量,再在下游任务中应用这些词向量。随后就是接下来要讲的TagLM。

TagLM——ELMo的前身

CoVe在训练词向量时用的是一个机器翻译模型,机器翻译需要大量标注的双语预料,这是很耗费人力的一项工作。TagLM简单得多,我们想要的只是一份能随着上下文语义动态变化的词向量,一个简单的语言模型是不是也能实现类似的功能?再重温一下语言模型的定义——给定一段话,预测下一个词。最重要的是,如果是训练语言模型,也就不需要人为地做什么标注了,可以说是一种无监督的训练方式。

milestone/Untitled.png

上图右侧就是TagLM在训练词向量,它是一个双向RNN模型,模型的输入是Token representation,可以是word2vec这类静态词向量,输出是编码了上下文语义的词向量。在完成训练后,它的参数会被固定住,输入一句话New York is located …中每个词的Token representation,输出的是每个词的重新编码的词向量h1LMh_{1}^{LM}(LM就是language model 语言模型的意思),当然还有一个细节就是这个h1LMh_{1}^{LM}是将两个方向RNN中输出concat在一起。

图的左侧是用于目标任务——命名实体识别的模型,除了将右侧的上下文相关词向量h1LMh_{1}^{LM}作为输入外,还将Token representation和字符级词向量也作为输入,个人感觉后面这两种输入主要是为了堆性能,TagLM最大的贡献是发现可以用无监督方法训练语言模型获得与上下文相关的动态词向量,该词向量对下游任务也有帮助。

ELMO

ELMo是TagLM的升级版,实际上也是同一批人做的。总的来说,它只是改变了TagLM的预训练词向量方法,即原TagLM示意图的右侧pretrained bi-LM:

milestone/Untitled%201.png

具体来说做了以下改变:

  1. bi-rnn 变成 bi-lstm,而且是多层的bi-lstm
  2. 不止取bi-lstm的顶层作为词向量,而是将多层的输出合并在一起作为词向量

ELMo取得了很好的性能,在TagLM基础上又更进一步,但是其最大的意义仍然是告诉大家用无监督预训练语言模型能得到随上下文变化的动态词向量,对于绝大部分下游的NLP任务都能获得一致地提升,因为训练词向量是无监督的,这几乎是白给的增益。

ULMfit

ULMfit是和ELMo同期的模型,都是在Tag LM上改进,它改进的思路和ELMo不一致,它考虑了两点:

  1. 预训练的词向量可能和目标域有差别,考虑在目标域的数据集上对词向量进行finetune
  2. 进行下游任务真的需要另外搭一个模型吗?可以考虑在训练词向量的模型上直接进行下游任务的finetune

milestone/Untitled%202.png

Transformer

前面CoVe、TagLM、ELMo、ULMfit基本定下了NLP任务的范式,先预训练好一份随上下文动态变化的词向量,然后用该词向量作为下游任务模型的输入。预训练词向量可以是用无监督策略,节省人力,但又能带来良好的效果,从功能层面上看它也确实能随上下文变化,能解决一词多义的问题。

Transformer开始对模型的基本组件动刀——即LSTM和RNN,这两者都是时序相关的模型,很难充分利用GPU的并行计算,因此时间效率很差。Transformer的作者做出了大胆的设计,LSTM和RNN的模型大多都会加上Attention机制,这能带来很好的增益,那么可不可以只保留Attention这种机制就行了,LSTM和RNN的架构全部推倒重来,专门设计一种能够实现并行计算的Attention模型。

此时再想想Transformer的论文题目——Attention Is All You Need,不是强调Attention很重要,而是很激进地认为,有Attention就够了,LSTM和RNN等结构都可以丢弃了。

文章最重要的贡献就是设计了一种较为复杂的Attention子模块,整个模型就是这种子模块的堆叠,但是却能取得很好的效果,这也印证了作者所说的只要有Attention就行了。该模型由于是一次性将一整段预料一起丢进去的,也就没有LSTM和RNN那种t时刻推理t+1时刻的机制,GPU的并行计算得以更充分地发挥。当然一整段一起丢进去会损失每个词位置的位置,因此又加了了位置编码这个的输入。这些都是模型的细节层面了,这里就不展开叙述了。

milestone/Untitled%203.png

GPT

有了Transformer简单高效的模块,财大气粗的OpenAI开始整活了,既然Transformer这么好用,而且都不用设计什么模型结构直接堆就行了,那么就堆多点让网络更深点吧。

Bert

作为Transformer的提出者,Google也不甘示弱,谁家还没个庞大的GPU群,我也堆Transformer,Transformer很好,加深一点吧;Bi-LSTM双向机制在Transformer中用不了了,我们加入一个Mask训练机制,强行上双向机制;finetune时似乎只要encoder就够了,decoder都可以丢了。

可以看到,GPT和Bert都是在Transformer上做一些修修补补,没有再有大的变化,他们的贡献是提供了用大量数据预训练的大模型,使得整个NLP领域的下游任务都可以获得性能增益。

参考文献:

  1. http://blog.itpub.net/69946223/viewspace-2685459/

CS224N笔记(五) Lecture8 机器翻译、Seq2Seq以及Attention注意力机制

NLP连载系列:

  1. NLP入门:文本特征的表示方式
  2. 命名实体分类NER初识
  3. n-gram的原理
  4. CS224N笔记(一) Lecture1~2——word2vec超详细解析
  5. CS224N笔记(二) Lecture1~2 :深入理解Glove原理
  6. CS224N笔记(三) Lecture 6~7——深入理解循环神经网络RNN模型
  7. CS224N笔记(四) Lecture 7:循环神经网络RNN的进阶——LSTM与GRU
  8. CS224N笔记(五) Lecture8 机器翻译、Seq2Seq以及Attention注意力机制

一、背景知识

机器翻译,Machine Translation,简称MT,是一种运用计算机将一个句子从一种语言翻译成另一种语言的技术,是自然语言处理领域中一项重要的任务。机器翻译最早可以追溯到19世纪50年代的冷战时期,其目的是实现英语和俄语的翻译,但那个时候的技术手段主要是基于规则。之后到90年代才出现了基于统计学的机器翻译技术,而在2010后乘着深度学习的风,神经网络也被大量应用于机器翻译中,并且取得了长足发展。下文将对统计机器翻译和神经网络机器翻译进行详细介绍。

假设要将某种语言的一个句子xx翻译成另一种语言最合适的句子yy,可以用下面式子来表达:

argmaxyP(yx)(1)argmax_{y} P(y|x) \tag{1}

无论是统计机器翻译,还是神经网络机器翻译都是以公式(1)为目标,建立模型,只是寻找模型参数的过程有些不同。

二、统计机器翻译

统计机器翻译,英文全称Statistical Machine Translation,简称SMT。它是以贝叶斯概率为基础,将公式(1)拆解成两个部分:

argmaxyP(xy)P(y)(2)argmax_y P(x|y)P(y) \tag{2}

公式(2)中的P(y)P(y)是先验概率,实际上可以理解成一个统计语言模型,它表征句子yy有多大的概率是合法(即正常人说的),这一项保障了翻译的流畅度(fluency),而P(xy)P(x|y)可以理解成一个翻译模型,它保障翻译的保真度(fidelity)。这样我们就能分解出两个模型,逐一攻破,其中统计语言模型P(y)只需要在目标语言的语料库上训练即可,而翻译模型P(x|y)则需要两种语言的配对语料库上训练。

1. 单词对齐

翻译模型P(xy)P(x|y)需要在配对语料上训练,具体该如何实现?这其中最重要的一个步骤就是单词对齐。所谓单词对齐,就是找到源句子中的每个单词对应目标句子的哪个单词。只要模型能够找准单词对齐的关系,也能顺理成章地正确翻译句子,但是单词对齐是一个较为复杂的任务,因为它包含很多种情况:

  1. 一个词对应一个词
  2. 多个词对应一个词或者一个词对应多个词
  3. 多个词对应多个词(短语级别的对齐)

一般在建立模型P(xy)P(x|y)时会引入隐变量aa,它表示单词的配对关系,这样我们所要建立的模型可以表示为P(x,ay)P(x,a|y),在对含有隐变量的模型进行参数估计时,可以采用EM算法(Expectation Maximization),课上只是简单提了一下。EM算法在李航的《统计学习方法》和周志华的《机器学习》中都有介绍,这里就不展开了。

2. SMT的解码

我们在回头看一下公式(2):

argmaxyP(xy)P(y)(2)argmax_y P(x|y)P(y) \tag{2}

假设我们已经建立起了翻译模型P(xy)P(x|y)和统计语言模型P(y)P(y),那么接公式(2)需要将所有可能的翻译句子yy都代入式子中,然后求得能使得概率值最大的yy,可想而知这是非常耗时耗力的。一般会采用维比特算法(动态规划)来计算,这一过程也叫做解码。

3. SMT的缺点

  1. SMT需要考虑的细节过多
  2. 包含了太多独立设计的子模块
  3. 需要许多特征工程上的工作
  4. 需耗费大量人力去做短语配对、语言配对

三、神经网络机器翻译Seq2Seq

神经网络机器翻译,英文全称Neural Machine Translation,简称NMT,是使用单个神经网络进行机器翻译的方法,这种神经网络称为Sequence-to-sequence,简称seq2seq,下面将对其模型结构以及相关原理展开介绍。

1. seq2seq的结构与基本原理

图1展示的是seq2seq的结构以及测试推理过程,它是由两个RNN组成,分别作为编码器和解码器,将源句子输入到编码器RNN中提取出源句子的特征,将该特征输入到解码器RNN中进行翻译,得到目标句子。注意图中的粉红色虚线,解码器中每一步的输出都会作为下一步的输入,一直向前推进,直到某一步输出句子结束标示

图1:seq2seq的测试阶段

在训练阶段数据的流动稍有不同,如图2所示,在解码时没有了图1的粉红色箭头,每一步的输入不是从上一步的输出获得,而是输入了groundtruth单词。我们希望每一步的输出能与groundtruth句子的下一个单词吻合,可以用交叉熵损失来约束。

图2:seq2seq的训练阶段

图2:seq2seq的训练阶段

按照课程中的说法,在训练时解码器的输入一般是用groundtruth,这种训练方式叫做Teacher Forcing,把正确答案告诉解码强迫它学习,但是其实这并不是唯一做法。有学者提出应当在训练时采用和推理时一样的策略,即把解码器自己的输出,作为下一时刻的输入,即使这个词可能是错的。这种方法的好处在于更好地模拟推理时的情况,我们希望模型能够对抗扰动,即使某一个词出错,后面还能正确输出,不要一旦出错就越来越偏。这种想法有其道理,但是会非常难训练,模型在一开始训练一定是大概率出错的,训练过程会很缓慢且不稳定。

为此,有学者提出了Scheduled Resampling的训练策略,翻译可以叫“有计划的采样”,或者是“有计划的学习”,简而言之就是训练要先易后难。具体做法以一定概率选择要用上一时刻的输出,还是真是的groudtruth,而这个概率是动态变化的,一开始可以用高概率选择groundtruth,让模型先快速学习,后面再以高概率选择上一时刻的输出,以模型实际推理时的扰动,使模型更加鲁棒。

由于seq2seq用一个整体模型就实现了机器翻译,它跟统计机器翻译中将模型分解出翻译模型和统计语言模型的方法有本质区别,此时我们求解公式(1)的方式为:

argmaxyP(yx)=argmaxyP(y1x)P(y2y1,x)P(y3y1,y2,x)...P(yTy1,...,yT1,x)(3)argmax_y P(y|x)= argmax_y P(y_1|x)P(y_2|y_1,x)P(y_3|y_1,y_2,x)...P(y_T|y_1,...,y_{T-1},x) \tag{3}

公式(3)的含义是是以xx源句子作为条件,预测输出的第一个单词y1y_1,保留对应的概率,然后以xxy1y_1,预测第二个单词y2y_2,保留对应的概率,依此类推,将所有概率值乘起来,就是给定源句子xx的情况下,预测序列为y=[y1,y2,y3....,yT]y=[y_1,y_2,y_3....,y_T]的概率。

2. 贪婪编码与波束搜索

上面的seq2seq模型一个最大问题是它采用了贪婪解码,何谓贪婪?即求解每一步的输出都是基于当前状况的最优解,而没有考虑全局的最优解。在这个场景下,解码器每一步的输出都是取argmax,即预测当前时刻最可能的单词,但是向前推进后不能再回过头,即使发现后面的单词预测说不通。当然,我们可以计算所有可能的yy序列的概率,然后取其中概率值最大的作为结果。但是这会相当耗费计算资源,假设词典大小为V,yy序列的长度是T,那么yy序列一共有VTV^T中可能,复杂度过高。

为了解决这个问题,我们可以用波束搜索(Beam search),波束搜索是在每一步都保留kk个分数最高的输出,用这kk个输出作为下一步的输入,又再发散出kkk*k种可能的输出,保留其中分数最高的kk个,再进行下一步的预测。这里分数的定义如下:

score(y1,...,yt)=logP(y1,...,ytx)=i=1tlogP(yiy1,...,yi1,x)(4)score(y_1,...,y_t)=logP(y_1,...,y_t|x)=\sum_{i=1}^{t}logP(y_i|y_1,...,y_{i-1},x) \tag{4}

这个分数定义本质上就是对公式(3)的P(yx)P(y|x)取对数,但是需要注意的是,由于概率都是0到1之间的数值,取对数后为负数,因此分数都是负数,但是仍然是分数越高越好,即越靠近0越好。

光从定义上解释不够,下面结合图例进行解释。假设k=2k=2,那在预测第一步,我们保留其中两个分数最高的预测he和I

translate/Untitled%202.png

接下来以he和I分别作为下一步的输入,它们分别又能计算得到V个分数(V为整个词典的大小),但是我们仍然只保留各自分数最高的两个单词,即以单词he作为输入时,保留了分数最高的两个输出hit和struck,以单词I作为输入时,保留了分数最高的两个输出was和got:

translate/Untitled%203.png

因此到第二步时有4个备选单词,我们保留分数最高的2个即hit和was,让它们继续以同样的方式发散出下一步4个可能的输出a、me、hit、struck:

translate/Untitled%204.png

一直这样发散下去,那么什么时候停止?一般可以选择两中停止方式:

  1. 设定最大时间T,预测T次后就必须停止。
  2. 一直发散直到收集到至少n个以结尾的预测。

波束搜索存在的最大问题是它偏向于取序列最短的预测结果,因为序列越短,分数越高,解决这个问题的方式是对长度进行归一化,在公式(4)的前方除以长度t,即:

1ti=1tlogP(yiy1,...,yi1,x)(5)\frac{1}{t} \sum_{i=1}^{t}logP(y_i|y_1,...,y_{i-1},x) \tag{5}

最后有四点注意事项:

  1. k的大小是人为设定的,一般取值为5~10。
  2. 如果中途保留的某个高分输出是,那该路径就截止了,但它仍会占用kk中的一个名额。
  3. 波束搜索不能找到全局最优解,它是准确率与效率折中的方法。

3. NMT的优缺点

相比起统计机器翻译SMT,神经网络机器翻译NMT的优点在于:

  1. 性能更好,体现在句子更流畅、更好地利用到上下文和短语相似性。
  2. 可以进行端到端的优化,不像SMT那样具有太多独立的子模块只能逐个优化。
  3. 不需要特征工程,节省人力。

但是它也有缺点:

  1. 可解释性较差,很难debug
  2. 很难人为控制,很难添加人设计的特殊规则或翻译指导

四、机器翻译的评价方法

Lecture9 介绍了机器翻译的一种评价方法BLEU(Bi-Lingual Evaluation Understudy),它是计算机器翻译和结果和人自己翻译的结果间的相似性,具体地,它是以n-gram的匹配度来衡量两个机器预测和人工翻译间的相似性,并且会对过短的句子作出惩罚。BLEU的缺点在于翻译并没有标准答案,同一个句子可以用不同的单词组合来描述,所以可能出现一个翻译得不错的句子,其BLEU分数却很低。

五、注意力机制

1. seq2seq中的注意力机制

seq2seq还存在一个问题——瓶颈问题,即源句子中每个词的特征最后都集中单一个特征向量上,要求该特征向量能够表征整个句子以及每个词的含义,这造成了一种信息瓶颈,会影响翻译的效果。注意力机制是用来解决该问题的重要方法,即核心思想是在解码时,将解码器与编码器每一步的输出直接连接,引入权重,让模型学会在解码时的每一步关注编码器特定位置上的输出。如下图所示:

图3:seq2seq的注意力机制

图3:seq2seq的注意力机制

在解码时将当前时刻的隐状态和编码器每一步的输出分别做内积,得到每个位置注意力分数,用softmax函数对分数进行归一化得到最终的注意力分数,将这些分数作为一种权重,对编码器各个位置上的特征进行加权求和得到最终的注意力输出,它是一个向量,与隐状态的尺寸一致,将该向量与解码器的隐状态向量做拼接,再去预测词汇。解码时每一步都进行这样的操作,这样就能利用上源句子每个位置上的信息,并且获得一种关注句子特定部分的能力。

注意力机制的好处在于:

  1. 提升模型性能
  2. 解决了信息瓶颈问题
  3. 有利于缓解梯度消失问题
  4. 提高了模型的可解释性,这可以看作一个单词软对齐,并且是由模型自己习得的

2. 注意力机制的常见形式

注意力机制应用广泛,不仅仅是在机器翻译中可以用,具有普适性。一般注意力机制中会涉及几个变量:

  1. 用来计算分数或权重的特征向量h1,...,hNRd1h_1,...,h_N \isin R^{d_1}sRd2s \isin R^{d_2}
  2. 分数,或者说权重eRNe \isin R^N
  3. 通过softmax函数归一化后的分数或权重 α=softmax(e)RN\alpha = softmax(e) \isin R^N
  4. 用分数或权重对特征向量进行加权求和得到的最终的注意力输出 a=i=1NαihiRd1a=\sum_{i=1}^{N} \alpha_i h_i \isin R^{d_1}

不同的注意力机制通常是在计算分数时采用了不一样的方式,主要有三种方法:

  1. 基本的内积运算,即ei=sThiRe_i = s^Th_i \isin R,这时候一般要求向量长度一致即d1=d2d_1=d_2
  2. 积性运算(Multiplicative):ei=sTWhiRe_i = s^TWh_i \isin R,这里会引入一个可学习的权值矩阵WRd1×d2W \isin R^{d_1 \times d_2}
  3. 加性运算(Additive):ei=vTtanh(W1hi+W2s)Re_i = v^Ttanh(W_1h_i+W_2s) \isin R,这里引入了两个矩阵W1Rd3×d1W_1 \isin R^{d_3 \times d_1}W2Rd3×d2W_2 \isin R^{d_3 \times d_2},还引入了一个权值向量v3Rd3v_3 \isin R^{d_3},这里d3d_3是一个超参数

六、机器翻译当前的难点

尽管借助深度学习,机器翻译在近年来得到迅猛发展,但是还存在许多难点没有被完全解决:

  1. 词典外的单词难以翻译,尤其是那些新兴流行词
  2. 训练集与测试集间的域间差异
  3. 长文本翻译难以保持上下文信息,比如说翻译一本书
  4. 依赖与大量的训练数据,语言对可能不足
  5. 模型没有常识
  6. 成语俚语很难翻译
  7. 训练集样本不够丰富,使得模型在翻译时有偏差
  8. 会输出一些不可解释的翻译

六、参考文献

  1. CS224N Lecture 8、9 slides

CS224N笔记(四) Lecture 7:循环神经网络RNN的进阶——LSTM与GRU

NLP连载系列:

  1. NLP入门:文本特征的表示方式
  2. 命名实体分类NER初识
  3. n-gram的原理
  4. CS224N笔记(一) Lecture1~2——word2vec超详细解析
  5. CS224N笔记(二) Lecture1~2 :深入理解Glove原理
  6. CS224N笔记(三) Lecture 6~7——深入理解循环神经网络RNN模型
  7. CS224N笔记(四) Lecture 7:循环神经网络RNN的进阶——LSTM与GRU
  8. CS224N笔记(五) Lecture8 机器翻译、Seq2Seq以及Attention注意力机制

本文将介绍两种比RNN更好地应对梯度消失问题的模型结构——LSTM和GRU,文章以CS224N的课件和材料为基础,以图解的形式帮助大家更好地理解这两种模型的结构,并进一步分析他们的优缺点和应用场景。

一、背景知识

循环神经网络RNN由于模型结构上的缺陷,很容易引起梯度爆炸和梯度消失,梯度爆炸可以用梯度截断方法在一定程度上缓解其影响,但是梯度消失几乎是致命缺陷,没有什么好办法可以解决它,这使得训练变得困难,模型很可能只受短时约束,长时约束的作用被大大削弱,学习不到相隔较远的两个词之间的联系。本文介绍的两种新的神经网络结构LSTM和RNN,可以很好地应对这个问题。

二、LSTM的原理与结构

1.模型结构

LSTM在模型结构上相对于RNN而言有两大变动:

  1. 新增了三个独特的门结构,用来控制信息地流动
  2. 增添了细胞状态cell state,同时也保留了原来的隐状态hiden state

其整体的模型结构图如下所示,由多个结构相同的LSTM模块组成:

LSTM/Untitled.png

LSTM结构的细节图:

LSTM/Untitled%201.png

课件上这张图的来源于参考文献2,大家可以去看看那篇文章对LSTM每个步骤进行拆解,下面的公式讲以图中的符号为准,可能会与课件中有一点出入。

符号解释

细胞状态 CtC_tCt=ftCt1+itC~tC_t = f_t \otimes C_{t-1} + i_t \otimes \tilde{C}_{t}

细胞状态新内容 C~t\tilde{C}_tC~t=tanh(Wcht1+Ucxt+bc)\tilde{C}_{t} = tanh(W_ch_{t-1}+U_cx_t+b_c)

隐状态 hth_tht=ottanh(Ct)h_t=o_t \otimes tanh(C_t)

遗忘门 ftf_tft=σ(Whht1+Ufxt+bf)f_t=\sigma(W_hh_{t-1}+U_fx_t+b_f)

输入门 iti_tfi=σ(Wiht1+Uixt+bi)f_i=\sigma(W_ih_{t-1}+U_ix_t+b_i)

输出门 oto_tft=σ(Whht1+Uoxt+bo)f_t=\sigma(W_hh_{t-1}+U_ox_t+b_o)

三个门结构

LSTM的门结构充当信息的关口,它们决定了信息是否能够完全流通,取值范围都是(0, 1),0则完全不让通过,1则完全通过。三个门结构的计算方法是一模一样的,只是用了相互独立的参数,LSTM的参数量相比于RNN多了许多,一定程度上提高了模型容量。注意在参考文献2中的写法不太一样,但其实只是将两个参数WWUU给合并了,本质上是一样的。

遗忘门会作用到上一时刻的细胞状态Ct1C_{t-1},将句子中的一些历史内容遗忘掉,举个例子,一个句子中如果出现了he,那么模型可能会记住该信息,后面的谓语要用单数形式比如is,如果紧接着出现了they,那么模型可能需要忘掉之前的主语he,后面的谓语需要用复数形式are,当然这只是一个理想化的例子,真实模型具体编码了什么我们很难得知,这只是以人的思维赋予了模型它可能需要的能力。

输入门作用到细胞新内容C~t\tilde{C}_t,要添加到细胞状态的新内容也许不是全都需要,所以用输入门减小部分元素或者清零。这部分就相对抽象,因为细胞新内容C~t\tilde{C}_t和遗忘门一样也是通过ht1h_{t-1}xtx_t计算出来的,只是选用的激活函数不同,为什么要这么分两步走。可以这么想:细胞新内容C~t\tilde{C}_t是计算出了一些备选的新信息,输入门对这些信息进行挑选后再添加到细胞状态中。

输出门则是作用到细胞状态CtC_t中,从细胞状态中挑选出信息作为隐状态的输出。

细胞状态

LSTM中一个重要结构为细胞状态,值得详细展开,它贯穿整个LSTM模型,用来存储句子上下文信息,相当于RNN中将上下文信息编码在隐状态中,LSTM的细胞状态具有更强的信息保存能力,内容不容易被完全清除,也即能更好地捕捉长距离词语间的关系。为什么说它的内容不容易被完全清除,我们回顾它的计算方法:

Ct=ftCt1+itC~t(1)C_t = f_t \otimes C_{t-1} + i_t \otimes \tilde{C}_{t} \tag{1}

抛开遗忘门和输入门的作用不谈,当前时刻的细胞状态CtC_t,是上一时刻的细胞状态Ct1C_{t-1}与新添加的细胞内容C~t\tilde C_t的以相加的形式获得的,而RNN中上下文信息都放在hth_t中,它的计算过程中会通过参数矩阵WhW_h与上一隐状态ht1h_{t-1}矩阵相乘的形式获得,并不断重复该过程,如果参数矩阵WhW_h的特征值都很小(或者模很小),那么在多次矩阵相乘过程中,hth_t可能变得越来越小,上下文信息都已经丢失了。

那么有人可能会问,细胞状态一直这么加下去,CtC_t不会到后面变得异常地大吗?确实是会这样,在初代的LSTM中,没有设置遗忘门,细胞状态的计算方式是:

Ct=Ct1+itC~tC_t = C_{t-1} + i_t \otimes \tilde{C}_t

这种形式的确非常容易使得细胞状态到后面异常地大,所以才设置了遗忘门ftf_t,让它与上一时刻的细胞状态进行元素级相乘,有机会减小某些元素的值,甚至清零,这样就保证了细胞状态没有无节制地增长。

2. 如何解决梯度消失

LSTM的模型结构讲述完毕,但是仅从模型结构来看,还是很难解释为什么LSTM能够应对梯度消失。其实上面已经涉及到一点点,关键就是LSTM的细胞状态,它存储着句子的上下文信息,像一条传送带一样贯穿整个模型,而且是以相加元素级形式获得的。我们可以先感性地理解为什么不会梯度消失:

  1. LSTM中存在多条通路,多条通路的梯度以相加的形式汇聚,一条路的梯度为0不至于全部梯度为0
  2. LSTM中存在遗忘门和细胞状态,可以保证历史信息不那么容易被清除。

但是这么说还是还有抽象,我们来真正计算一下梯度。回顾前一篇文章中说RNN(链接文章)梯度消失主要是因为两个时刻间隐状态的梯度是WhW_h的幂次这种形式,WhW_h如果很小,时间距离又很远的话,梯度就消失了:

h(t)h(i)=j=i+1th(j)h(j1)=j=i+1tdiag(σ(Whh(j1)+Wee(t)+b1))×Wh(2)\frac{\partial h^{(t)}}{\partial h^{(i)}} = \prod_{j=i+1}^t \frac{\partial h^{(j)}}{\partial h^{(j-1)}} = \prod_{j=i+1}^t diag(\sigma'(W_hh^{(j-1)}+W_ee^{(t)} + b_1)) \times W_h \tag{2}

由于LSTM的上下文信息存储在细胞状态,我们重点来看下前后两个时刻细胞状态的梯度CtCt1\frac{\partial C_t}{\partial C_{t-1}}。公式(1)(1)表明,C_t是关于ftf_tCt1C_{t-1}iti_tC~t\tilde{C}_{t}的函数,而它们都是元素级乘法和加法,所以梯度相对好求,可以套用(uv)=uv+uv(uv)'=uv'+u'v

CtCt1=ft×Ct1Ct1+Ct1×ftCt1+it×C~tCt1+C~t×itCt1=ft+Ct1×ftCt1+it×C~tCt1+C~t×itCt1(3)\begin{aligned} \frac{\partial C_t}{\partial C_{t-1}} =& f_t \times \frac{\partial C_{t-1}}{\partial C_{t-1}} + C_{t-1} \times \frac{\partial f_t}{\partial C_{t-1}} + i_t \times \frac{\partial \tilde{C}_t }{\partial C_{t-1}} + \tilde{C}_t \times \frac{\partial i_t}{\partial C_{t-1}} \\ =& f_t + C_{t-1} \times \frac{\partial f_t}{\partial C_{t-1}} + i_t \times \frac{\partial \tilde{C}_t}{\partial C_{t-1}} + \tilde{C}_t \times \frac{\partial i_t}{\partial C_{t-1}} \end{aligned} \tag{3}

上面关键就是第一项,由于Ct1Ct1\frac{\partial C_{t-1}}{\partial C_{t-1}}的结果是单位矩阵,所以第一项只剩下一个遗忘门ftf_t,它不需要与其他矩阵相乘,所以只要遗忘门是1,可以保证CtCt1\frac{\partial C_t}{\partial C_{t-1}}至少是一个1向量,这样损失函数JtJ_t关于C1C_1的梯度JtCt\frac{\partial J_t}{\partial C_t}可以沿着细胞状态的通路无损地传送到下去,而不会在中途因为存在0向量所使得传到前面时梯度已经消失,即:

JtC1=JtCtCtCt1Ct1Ct2...C2C10(4)\frac{\partial J_t}{\partial C_1} = \frac{\partial J_t}{\partial C_t} \frac{\partial C_t}{\partial C_{t-1}} \frac{\partial C_t-1}{\partial C_{t-2}} ...\frac{\partial C_2}{\partial C_{1}} \not= \bold{0} \tag{4}

这里需要提醒大家注意,在知乎等平台上看到很多文章都喜欢引用或翻译文献4中的说法,那里也是计算了梯度CtCt1\frac{\partial C_t}{\partial C_{t-1}},通过ftf_t这一项说明梯度不至于完全消失,本文也是借鉴了这种说法,但是那篇文章中,CtCt1\frac{\partial C_t}{\partial C_{t-1}}的计算是错误的:

图中等式两边红色方框的项都是CtCt1\frac{\partial C_t}{\partial C_{t-1}},两项完全一致,直接就消掉了,更离谱的是后边的CtCt1\frac{\partial C_t}{\partial C_{t-1}}计算等于ftf_t,这样等号左边的CtCt1\frac{\partial C_t}{\partial C_{t-1}}还有什么好算的。只能说歪打正着,尽管CtCt1\frac{\partial C_t}{\partial C_{t-1}}是会出现ftf_t这独立的一项,但不是这样来的。

LSTM/Untitled%202.png

真正的计算方法应该是这样,从公式(3)(3)触发,iti_tftf_tC~t\tilde{C}_{t}都是关于ht1h_{t-1}的函数,ht1h_{t-1}是又关于Ct1C_{t-1}的函数,这样我们根据链式法则,可以计算得到:

CtCt1=ft+Ct1×ftCt1+it×C~tCt1+C~t×itCt1=ft+Ct1×ftht1ht1Ct1+it×C~tht1ht1Ct1+C~t×itht1ht1Ct1(5)\begin{aligned} \frac{\partial C_t}{\partial C_{t-1}} =& f_t + C_{t-1} \times \frac{\partial f_t}{\partial C_{t-1}} + i_t \times \frac{\partial \tilde{C}_t}{\partial C_{t-1}} + \tilde{C}_{t} \times \frac{\partial i_t}{\partial C_{t-1}} \\ =&f_t + C_{t-1} \times \frac{\partial f_t}{\partial h_{t-1}}\frac{\partial h_{t-1}}{\partial C_{t-1}} + i_t \times \frac{\partial \tilde{C}_t}{\partial h_{t-1}}\frac{\partial h_{t-1}}{\partial C_{t-1}} + \tilde{C}_t \times \frac{\partial i_t}{\partial h_{t-1}}\frac{\partial h_{t-1}}{\partial C_{t-1}}\end{aligned} \tag{5}

但是这条公式最关键的还是第一项遗忘门ftf_t,当它为1是梯度不至于消失,但是需要注意的是,它是否为1是由模型自己学习的,我们只能从结构上保证它有联系长距离上下文的能力,但也许长距离的上下文真的没有很强的关系呢?而在模型训练初始化时,一般还是会将遗忘门初始化为1,保证梯度能够无损地传递,从功能来理解,是认为所有上下文信息都需要保留,至于是不是真的要保留,交由模型在后续的训练中学习。

最后还有两点需要注意:

  1. 上面计算的是细胞状态通路的梯度,它不那么容易梯度消失,但是其他通路跟RNN很像,在梯度计算中仍然会出现参数矩阵的幂次,也是很有可能出现梯度消失的。LSTM解决梯度消失的最重要途径就是顶上细胞状态这一条传送带。
  2. LSTM并不保证完全不发生梯度消失,只是相比起RNN更加稳定。

三、GRU的原理与结构

LSTM中存在三个门结构,参数量较大,计算缓慢,因此有学者对它进行了以下精简:

  1. 将细胞状态和隐状态又重新合并成了单独的隐状态
  2. 将遗忘门和输入门合并成了更新门(update gate),它控制哪些信息需要进行更新,哪些信息进行保留
  3. 设置了重置门(reset gate),作用是控制旧的隐状态中的哪些内容可以参与新隐状态的计算
  4. 由于细胞状态和隐状态合二为一了,也就没有必要设置输出门了,输出门被删除

最终的模型结构如下,注意这幅图来自参考文献4,其中的符号和CS224N中所采用的不一致:

LSTM/Untitled%203.png

符号解释:

重置门 rtr_trt=σ(Wrht1+Urxt+br)r_t = \sigma(W_rh_{t-1} + U_rx_t +b_r)

更新门 ztz_t: zt=σ(Wzht1+Uzxt+bz)z_t = \sigma(W_zh_{t-1} + U_zx_t +b_z)

隐状态的新内容h~t\tilde{h}_t: h~t=tanh(Wh(rtht1)+Uhxt+bh)\tilde{h}_t = tanh(W_h(r_t \otimes h_{t-1})+U_hx_t+b_h)

隐状态hth_t: ht=(1zt)ht1+zth~th_t = (1-z_t) \otimes h_{t-1} +z_t \otimes \tilde{h}_t

那么GRU能否应对梯度消失呢?答案是可以的,看到图中最上方那条贯穿的通路和LSTM中的细胞状态是不是很类似,而且同样也存在一个元素级加法操作,所以GRU中的隐状态与LSTM中的细胞状态一样,前后两个时刻间的梯度也会出现一个独立项,只不过是由遗忘门ftf_t变成了更新门ztz_t,只要更新门是1向量,至少可以保证htht1\frac{\partial h_t}{\partial h_{t-1}}不会完全为0,隐状态通道上的梯度可以一直传递到最前方。

四、LSTM与GRU的选择

LSTM和GRU都能缓解了RNN中梯度消失的问题,使得长距离上下文信息的捕捉变得更加容易,但是LSTM参数量大,收敛较慢,计算耗时,GRU比起LSTM它的参数量较少,计算相对较快,也减少了过拟合的风险。但是具体该用哪一个,取决于数据量和效率要求,如果数据充足,LSTM可以提供更好的性能,如果要求计算快些,可以试试GRU。

五、RNN的其他变种模型

1. 双向RNN

我们前面说的上下文信息严格来说只是前文信息,后文是还没有输入到模型中的,但是有时候句子的关键信息可能是在后文出现,所以我们希望句子既要正向输入,也要反向输入,分别计算隐状态,再进行融合。但这个模型的应用场景有限制,需要我们拥有全文语料,像实时机器翻译这种场景就不合适,因为并不知道后文。

LSTM/Untitled%204.png

2. 多层RNN

在另外一个维度堆叠参数,可以帮助网络学习到更深层的语义信息,如果作为编码器,一般是堆2~4层,作为解码器一般堆4层,如果还需要更深,则可能需要用到跳层连接或者像densenet那样的密集连接。

LSTM/Untitled%205.png

六、参考文献

  1. CS224 Lecture 7 slides & notes
  2. http://colah.github.io/posts/2015-08-Understanding-LSTMs/
  3. https://zhuanlan.zhihu.com/p/109519044
  4. https://weberna.github.io/blog/2017/11/15/LSTM-Vanishing-Gradients.html
  5. https://www.zhihu.com/question/34878706/answer/665429718

CS224N笔记(三) Lecture 6~7——深入理解循环神经网络RNN模型

NLP连载系列:

  1. NLP入门:文本特征的表示方式
  2. 命名实体分类NER初识
  3. n-gram的原理
  4. CS224N笔记(一) Lecture1~2——word2vec超详细解析
  5. CS224N笔记(二) Lecture1~2 :深入理解Glove原理
  6. CS224N笔记(三) Lecture 6~7——深入理解循环神经网络RNN模型
  7. CS224N笔记(四) Lecture 7:循环神经网络RNN的进阶——LSTM与GRU
  8. CS224N笔记(五) Lecture8 机器翻译、Seq2Seq以及Attention注意力机制

本文将从语言模型的概念出发,引出循环神经网络RNN的概念,对RNN的结构进行描述,详细推导了梯度计算过程,并解释RNN容易出现梯度消失、梯度爆炸的原因。文章的最后对RNN的应用场景进行了简单的介绍。

一、背景知识

1. 语言模型

在讲解循环神经网络RNN之前,先来回顾一下什么是统计语言模型,之前说到统计语言模型就是用以计算一个句子的概率的模型,简而言之就是判断一句话“是不是正常人说的”,常常会挖空语料中的某些位置,要求预测该位置填什么词最合适。其模型可以归结为:

p(W)=p(w1T)=p(w1,w2,...,wT)=p(w1)p(w2w1)p(w3w12)p(wTw1T1)p(W)=p(w_1^T)=p(w_1,w_2,...,w_T)=p(w_1) \cdot p(w_2|w_1) \cdot p(w_3|w_1^2) \dots p(w_T|w_1^{T-1})

上面这条公式中,W表示一个句话,它是由单词w1,w2,...,wTw_1,w_2,...,w_T按顺序排列而构成的,再次强调见到w1Tw_1^T不要认为是T幂次,它就是表示首单词为w1w_1,长度为T,末尾单词为wTw_T的一句话。

2.n-gram

上面的公式其实可以表示一切语言模型,如果我们是以统计信息来构建模型,那么就叫做统计语言模型,比如n-gram,其表述形式为:

p(wkw1k1)count(w1k)count(w1k1)p(w_k|w_1^{k-1})\approx\frac{count(w_1^k)}{count(w_1^{k-1})}

在深度学习未应用与NLP时,n-gram模型是非常流行的,但它存在以下问题

  1. 稀疏性问题:随着n增大,语料库中出现特定的连续词语组合的可能性会越小,看你从词表中找不到特定的连续词语组合。
  2. 存储问题:需要存储一个非常大的共现矩阵,且还会随着n增大而增大。
  3. n-gram通常无法捕捉深层的语义信息。

3. 固定窗口神经语言模型

为了解决这一问题,提出了固定窗口神经语言模型,它是通过将一个窗口内的词语的词向量拼接起来,送入全连接神经网络,最后通过softmax函数预测概率。这里的词向量one-hot,但其实用word2vec的词向量应该也是可以的。

RNn/Untitled.png

这一模型的好处在于:

  1. 不存在稀疏性问题
  2. 不需要存储所有的n-gram

但是它也有缺点:

  1. 固定窗口不能太小也不能太大,太小捕捉不到上下文信息,太大拼接的向量会异常大,对机器要求很高。
  2. 虽说每个词向量拼接在了一起,但是它们分别对应参数矩阵W的一条向量,参数不共享。这样不一定合理,因为它们其实都在做一件事,我们希望参数具有泛化性,对任意位置的处理是一致。
  3. 窗口大小是固定的,一旦确定不能更改。

二、基本结构

上面说了固定窗口神经语言模型的不足之处,我们希望找到了一个更强大的模型,它应当具备以下特定:

  1. 不受窗口限制,能够处理不定长的输入
  2. 对于每一个位置上的词都可以用同一套参数进行处理

循环神经网络可以满足以上要求,它的模型结构如下:

RNn/Untitled%201.png

模型的真正输入是各个词的词向量,模型中有一个隐状态hh,输出是预测词的概率y^\hat{y}。模型的参数为输入层WeW_e、隐含层WhW_h、输出层UU,注意EE不算是可学习参数,它负责从将词的one-hot向量映射到词向量,本质上是个查表的工作,相当于tensorflow中tf.embedding_lookup操作。

词向量依次送入到模型中,每个词向量ee在模型中首先和输入层WeW_e相乘,再加上另一条支路上隐状态hh与隐含层WhW_h的相乘结果,然后再加上一个偏置bb,最终的和送入非线性激活函数σ\sigma;输出的结果作为新的隐状态传递下去,参与下一轮计算,在下一轮的计算是以下一个词向量作为输入,以此不断循环,最终将隐状态hh与输出矩阵相乘,加上偏置后进行softmax计算,输出概率值。

这里每一轮计算的参数WeW_eWhW_hUU是一样的,中间的隐状态h(1)h^{(1)}h(2)h^{(2)}h(3)h^{(3)}如有需要也是可以输出,每一轮都输出一个结果,且这个循环可以一直进行下去,尽管实际操作中不会让它一直循环下去,因为会遇到别的问题——计算量巨大、梯度消失。

三、训练与优化

1. 损失函数

RNn/Untitled%202.png

在RNN的训练过程中,每一轮都会计算损失函数J(1)(θ)J^{(1)}(\theta)J(2)(θ)J^{(2)}(\theta)J(3)(θ)J^{(3)}(\theta)J(4)(θ)J^{(4)}(\theta),最后将他们相加得到最终的损失函数:

J(θ)=1Tt=1TJ(t)(θ)J(\theta) = \frac{1}{T}\sum_{t=1}^TJ^{(t)}(\theta)

每一轮的损失函数J(t)(θ)J^{(t)}(\theta)均采用交叉熵函数,即:

J(t)(θ)=CE(y(t),y^(t))=wV yw(t)logy^w(t)=log y^w(t)J^{(t)}(\theta) = CE(y^{(t)},\hat{y}^{(t)}) = -\sum_{w \isin V}\ y_w^{(t)}log\hat{y}_w^{(t)} = -log\ \hat{y}_w^{(t)}

其中y(t)y^{(t)}表示真实值,它是x(t+1)x^{(t+1)}所对应的one-hot向量,我们希望输入tt时刻前的词,能预测得到t+1t+1时刻的词。

2. 梯度计算

接下来思考怎么计算梯度进行反向传播,这是RNN中的一个难点中的难点,这里课堂上讲得不是很清晰,补充材料notes里的符号又和课件slides里的不一致,很容易让人迷惑,因此下面我会根据课件和补充材料中内容,重新组织语言进行讲解,不完全遵循课件和补充材料中的顺序和符号。

根据前面的损失函数:

J(θ)=1Tt=1TJ(t)(θ)(1)J(\theta) = \frac{1}{T}\sum_{t=1}^TJ^{(t)}(\theta) \tag{1}

很容易可以得到它关于W_h的梯度:

JWh=1Tt=1TJ(t)Wh(2)\frac{\partial J}{\partial W_h} = \frac{1}{T}\sum_{t=1}^T \frac{\partial J^{(t)}}{\partial W_h} \tag{2}

现在问题来了,那么tt时刻的梯度J(t)Wh\frac{\partial J^{(t)}}{\partial W_h}应该怎么计算?结论很简单,它等于J(t)J^{(t)}在1~tt时刻关于WhW_h的梯度之和,即:

J(t)Wh=i=1tJ(t)Whi(3)\frac{\partial J^{(t)}}{\partial W_h} = \sum_{i=1}^t \frac{\partial J^{(t)}}{\partial W_h} \bigg| _{i} \tag{3}

右边每个求和项的下标ii指的是第ii个时刻或者第ii轮,即J(t)J^{(t)}对第ii个时刻的WhW_h的梯度。这个公式是怎么来的呢?由于J(t)J^{(t)}tt时刻前每个时刻的参数矩阵Wh1Wh2...WhtW_h|_1 、 W_h|_2 、 ... 、 W_h|_t都有关,根据多元函数的链式法则,可以得到下面式子:

J(t)Wh=i=1tJ(t)WhiWhiWh=i=1tJ(t)Whi×1(4)\frac{\partial J^{(t)}}{\partial W_h} = \sum_{i=1}^t \frac{\partial J^{(t)}}{\partial W_h} \bigg| _{i} \frac{\partial W_h|_i}{\partial W_h} = \sum_{i=1}^t \frac{\partial J^{(t)}}{\partial W_h} \bigg| _{i} \times 1 \tag{4}

这条式子怎么来的?首先要明确,RNN在训练时对一段语料进行前向传播,如果语料长度为TT就会经历了TT个时刻,之后再将每个时刻tt的损失叠加起来求梯度反向传播,t=1Tt=1~T时刻内,参数矩阵是一直没有更新的,也即是同一个矩阵WhW_h。那么i=1ti=1~t时刻内,因为它们是11TT内的一个子时段,参数矩阵WhiW_h|_i肯定一直不变的,也即Wh1=Wh2=...=Wht=WhW_h|_1 = W_h|_2 = ... = W_h|_t = W_h,因此WhiWh=1\frac{\partial W_h|_i}{\partial W_h}=1,第二项可以被忽略掉。

现在问题转化为J(t)Whi\frac{\partial J^{(t)}}{\partial W_h} \bigg| _{i}要怎么计算?这个就比较复杂了,这里需要用到链式法则:

J(t)Whi=J(t)y^(t)y^(t)h(t)h(t)h(i)h(i)Whi\frac{\partial J^{(t)}}{\partial W_h} \bigg| _{i} = \frac{\partial J^{(t)}}{\partial \hat{y}^{(t)}} \frac{\partial \hat{y}^{(t)}}{\partial h^{(t)}} \frac{\partial h^{(t)}}{\partial h^{(i)}} \frac{\partial h^{(i)}}{\partial W_h|_i}

上面提到说Whi=WhW_h|_i = W_h,因此下面就都简写成WhW_h,上面的式子可以写作:

J(t)Whi=J(t)y^(t)y^(t)h(t)h(t)h(i)h(i)Wh(5)\frac{\partial J^{(t)}}{\partial W_h} \bigg| _{i} = \frac{\partial J^{(t)}}{\partial \hat{y}^{(t)}} \frac{\partial \hat{y}^{(t)}}{\partial h^{(t)}} \frac{\partial h^{(t)}}{\partial h^{(i)}} \frac{\partial h^{(i)}}{\partial W_h} \tag{5}

这个公式中第1、2、4项都很容易求得,关键是第三项h(t)h(i)\frac{\partial h^{(t)}}{\partial h^{(i)}}应该怎么求?我们还是可以用链式法则拆开它,但可以注意到它是与时刻ii有关的,可以想象当i=t1i=t-1,那就向前追溯1个时刻,当i=t2i=t-2时,要向前追溯2个时刻,以此类推,i=1i=1的话要向前追溯两个时刻,也即h(t)h(i)\frac{\partial h^{(t)}}{\partial h^{(i)}}需要向前追溯(ti)(t-i)个时刻,写成公式的话可以表示成:

h(t)h(i)=j=i+1th(j)h(j1)(6)\frac{\partial h^{(t)}}{\partial h^{(i)}} = \prod_{j=i+1}^t \frac{\partial h^{(j)}}{\partial h^{(j-1)}} \tag{6}

至此,损失JJWhW_h的梯度可以写成:

JWh=1Tt=1TJ(t)Wh=1Tt=1Ti=1tJ(t)Whi=1Tt=1Ti=1tJ(t)y^(t)y^(t)h(t)h(t)h(i)h(i)Wh=1Tt=1Ti=1t(J(t)y^(t)y^(t)h(t)(j=i+1th(j)h(j1))h(i)Wh)(7)\begin{aligned} \frac{\partial J}{\partial W_h} &= \frac{1}{T}\sum_{t=1}^T \frac{\partial J^{(t)}}{\partial W_h} \\ &= \frac{1}{T}\sum_{t=1}^T\sum_{i=1}^t \frac{\partial J^{(t)}}{\partial W_h} \bigg|_{i}\\ &=\frac{1}{T}\sum_{t=1}^T\sum_{i=1}^t \frac{\partial J^{(t)}}{\partial \hat{y}^{(t)}} \frac{\partial \hat{y}^{(t)}}{\partial h^{(t)}} \frac{\partial h^{(t)}}{\partial h^{(i)}} \frac{\partial h^{(i)}}{\partial W_h} \\ &=\frac{1}{T}\sum_{t=1}^T\sum_{i=1}^t (\frac{\partial J^{(t)}}{\partial \hat{y}^{(t)}} \frac{\partial \hat{y}^{(t)}}{\partial h^{(t)}} (\prod_{j=i+1}^t \frac{\partial h^{(j)}}{\partial h^{(j-1)}} )\frac{\partial h^{(i)}}{\partial W_h}) \end{aligned} \tag{7}

接下来其实可以继续追问h(j)h(j1)\frac{\partial h^{(j)}}{\partial h^{(j-1)}}怎么求解,我们回顾之前的图示:

RNn/Untitled%203.png

从图中我们可以得知h(j)h^{(j)}h(j1)h^{(j-1)}具有直接关系:

h(j)=σ(Whh(j1)+Wee(t)+b1)(8)h^{(j)} = \sigma(W_hh^{(j-1)}+W_ee^{(t)} + b_1) \tag{8}

则它们间的梯度也很容易求得:

h(j)h(j1)=diag(σ(Whh(j1)+Wee(t)+b1))×Wh(9)\frac{\partial h^{(j)}}{\partial h^{(j-1)}} = diag(\sigma'(W_hh^{(j-1)}+W_ee^{(t)} + b_1)) \times W_h \tag{9}

其中diag()diag(*)表示对角矩阵,对角线中的值即为*,这条式子在补充材料notes中有些许不同,但是本质上是一致的。

至此,RNN关于W_h的梯度计算就完成了,关于$$W_e$$的梯度计算也是类似,这里就不再赘述。

3. 梯度消失和梯度爆炸

可以看到(7)(7)式相当复杂,最关键的地方是两个不同时间隐状态间的梯度h(t)h(i)\frac{\partial h^{(t)}}{\partial h^{(i)}},需要从t时刻一直地追溯到i时刻,而且要进行多次这样的追溯。我们结合(6)(6)(8)(8)可将h(t)h(i)\frac{\partial h^{(t)}}{\partial h^{(i)}}写做:

h(t)h(i)=j=i+1th(j)h(j1)=j=i+1tdiag(σ(Whh(j1)+Wee(t)+b1))×Wh(10)\frac{\partial h^{(t)}}{\partial h^{(i)}} = \prod_{j=i+1}^t \frac{\partial h^{(j)}}{\partial h^{(j-1)}} = \prod_{j=i+1}^t diag(\sigma'(W_hh^{(j-1)}+W_ee^{(t)} + b_1)) \times W_h \tag{10}

接下来将结合该式子讲述了为什么会RNN特别容易梯度消失和梯度爆炸,CS224N的课件slides和补充材料notes是从两个角度来进行解释的,下面将分别对两者的思路进行讲解,在推导过程中为了保持本文符号的一致性,可能与课件材料有些出入。

课件中的解释:

为了简化问题,我们假设激活函数σ\sigma为恒等映射即σ(x)=x\sigma(x)=x,则σ=1\sigma'=1,公式(10)(10)可以改写成:

h(t)h(i)=j=i+1tI×Wh=j=i+1tWh=Whti(11)\frac{\partial h^{(t)}}{\partial h^{(i)}} = \prod_{j=i+1}^t I \times W_h = \prod_{j=i+1}^t W_h = W_h^{t-i} \tag{11}

也即是矩阵WhW_h连续自乘了(ti)(t-i)次,由于输入的词向量ee和隐状态hh一般都是保持同样的维度,因此WhW_h一定是方阵,不用担心自乘时维度对不上。假设矩阵WhW_h的特征值和特征向量分别为:

特征值:   λ1,λ2,...,λn特征向量:   q1,q2,...,qn\begin{aligned}\text{特征值:} \ \ \ \lambda_1, \lambda_2,...,\lambda_n \\ \text{特征向量:}\ \ \ q_1,q_2,...,q_n\end{aligned}

根据线性代数的知识,有:

Wh=PΛP1P是特征向量组成的矩阵,Λ是特征值组成的对角矩阵W_h = P\Lambda P^{-1},P是特征向量组成的矩阵,\Lambda是特征值组成的对角矩阵

进一步可以推出:

h(t)h(i)=Whti=PΛtiP1(12)\frac{\partial h^{(t)}}{\partial h^{(i)}}= W_h^{t-i} = P\Lambda^{t-i} P^{-1} \tag{12}

假设WhW_h的特征值全都小于1,那么一旦两个词相隔越远,或说两个时刻(ti)(t-i)相隔越长,对角矩阵Λti\Lambda^{t-i}上的元素会越乘越小,接近于0,那h(t)h(i)\frac{\partial h^{(t)}}{\partial h^{(i)}}自然也接近于零矩阵,再跟其他的矩阵或向量相乘也会存在大量的零,也就是梯度几乎都为零,这就是梯度消失的原因。反过来,如果说WhW_h的特征值全都大于1,对角矩阵Λti\Lambda^{t-i}上的元素会越乘越大,之后其他矩阵内的元素也会变得越来越大,甚至出现NaN值,这就是梯度爆炸的原因。上面在推导前是假设激活函数为恒等映射,但其实换成别的激活函数也一样会出现梯度消失或梯度爆炸,因为公式12中参数矩阵WhW_h的指数形式依然存在。

补充材料中的讲解

这次从公式(6)(6)出发,如果我们考虑矩阵的模,那么从公式(10)(10)可以得知:

h(j)h(j1)diag(σ(Whh(j1)+Wee(t)+b1))×WhβWβh(13)||\frac{\partial h^{(j)}}{\partial h^{(j-1)}}|| \le ||diag(\sigma'(W_hh^{(j-1)}+W_ee^{(t)} + b_1))|| \times ||W_h|| \le \beta_W\beta_h\tag{13}

其中的β\beta只是对diag(σ(Whh(j1)+Wee(t)+b1))||diag(\sigma'(W_hh^{(j-1)}+W_ee^{(t)} + b_1))||Wh||W_h||分别进行简写。在这之后,结合公式(10)(10),可以得到

h(t)h(i)=j=i+1th(j)h(j1)(βWβh)ti(14)||\frac{\partial h^{(t)}}{\partial h^{(i)}}|| = ||\prod_{j=i+1}^t \frac{\partial h^{(j)}}{\partial h^{(j-1)}}|| \le (\beta_W\beta_h)^{t-i} \tag{14}

如果βW\beta_Wβh\beta_h小于1,即WhW_h的模与diag(σ(Whh(j1)+Wee(t)+b1))||diag(\sigma'(W_hh^{(j-1)}+W_ee^{(t)} + b_1))||的乘积小于1,那么由于指数项的作用,h(t)h(i)||\frac{\partial h^{(t)}}{\partial h^{(i)}}||同样也会变得很小,容易出现梯度消失,相反如果它们大于1,那么h(t)h(i)||\frac{\partial h^{(t)}}{\partial h^{(i)}}||会变得相当大,容易出现梯度爆炸。

这里可以注意到两个细节,一方面,根据线性代数额知识,WhW_h的模和它的特征值有密切关系,如果像上面课件中说的那样WhW_h特征值都小于1,那么它的模肯定也很小;另一方面,另外一项βh\beta_h是和激活函数相关的,上面说激活函数不采用恒等映射同样可能出现梯度消失或梯度爆炸,此话没错,但更准确地说,梯度会不会出现问题和激活函数是会存在关系的。为了缓解梯度消失,我们可以选择ReLU激活函数,尽管它相对容易引起梯度爆炸,但是对于梯度爆炸我们好歹有梯度截断方法可以缓解,比起梯度消失更加可控。

还有最后一点需要注意的是,RNN的梯度消失主要是长距离的梯度消失,公式(12)(14)(12)、(14)的指数项(ti)(t-i)需要足够大才有明显地梯度消失效应,短距离的梯度还是正常的,而总梯度是包含了长距离和短距离的梯度,所以总梯度并不是完全为0,只是模型参数的更新方向不受长时约束,这样RNN就失去了捕捉更大范围上下文信息的能力。

四、优缺点

相比于统计语言模型以及固定窗口神经语言模型,RNN的优点在于:

  1. 可以处理不定长序列
  2. 对不同位置的词向量采用同样的参数进行计算,缩减模型参数且增强参数了泛化性
  3. 模型的大小与序列长度无关
  4. 可以更好地捕捉长时信息

但是其缺点也很明显:

  1. 很容易梯度消失,且是致命弱点,普通的RNN对此没有解决办法。梯度消失会造成两个问题:

    • 长时约束的作用减弱,模型几乎只受短时约束
    • 我们无法分辨是真的没有长距离间的两个词是真的没有联系,还是我们没能捕捉到它们间的联系
  2. 很容易梯度爆炸,可以通过梯度截断缓解

    • 模型发散,无法学习到有效信息
    • 梯度截断的实现

    RNn/Untitled%204.png

五、应用与展望

  1. 词性标注,对序列中每一个词预测其词性;类似的还有NER(name entity recognition),即命名实体识别

RNn/Untitled%205.png

  1. 文本分类,比如情感分类

RNn/Untitled%206.png

  1. 作为编码模块,可以应用到QA问答系统、机器翻译等

RNn/Untitled%207.png

  1. 可以用于文本生成,如语音识别、文本翻译、文本梗概,此时的RNN是一个条件语言模型

RNn/Untitled%208.png

六、参考文献

  1. CS224N Lecture 6 slides && notes

CS224N笔记(二) Lecture1~2 :深入理解Glove原理

NLP连载系列:

  1. NLP入门:文本特征的表示方式
  2. 命名实体分类NER初识
  3. n-gram的原理
  4. CS224N笔记(一) Lecture1~2——word2vec超详细解析
  5. CS224N笔记(二) Lecture1~2 :深入理解Glove原理
  6. CS224N笔记(三) Lecture 6~7——深入理解循环神经网络RNN模型
  7. CS224N笔记(四) Lecture 7:循环神经网络RNN的进阶——LSTM与GRU
  8. CS224N笔记(五) Lecture8 机器翻译、Seq2Seq以及Attention注意力机制

1.背景知识

word2vec 中提到,词的表征方法还可以分成统计型和预测性这两类,统计型就是计算各个词或词序列出现的频次,比如词袋BOW、词频-逆文档频率TF-IDF、n-gram等,预测型的如word2vec,将词隐式地编码成特定大小的短向量,通过词向量空间上的语义相似性预测中心词或者周边词。这两种方法的优缺点都很明显,统计型训练快,使用了语料的统计信息但很难提取出语义信息,也很难衡量词之间但相似性,预测型可以很好地捕捉词的语义信息,能够在向量空间上衡量词之间的相似性,但是它没有用到语料的统计信息。

Glove方法的提出就是要结合统计型和预测型方法的优点,既要能捕捉词之间的相似性,又要能利用上语料的统计信息。

2.基本原理

为了能用上语料的统计信息,Glove还是从共现矩阵入手,构造的方法n-gram中介绍的共现矩阵,但Glove还要求对元素进行归一化,由频次转换成概率值,为了区分下文将这种矩阵命名为共现概率矩阵,该矩阵即代表了语料统计信息。设共现矩阵为XX,则XijX_{ij}表示在单词ii的上下文出现jj的次数,Xi=kXikX_i=\sum_{k}X_{ik}表示出现在单词ii上下文的所有单词的总次数,则单词ii上下文出现单词jj的概率就可以记作

Pij=P(ji)=Xij/XiP_{ij}=P(j|i)=X_{ij}/X_i

这里需要特别特别强调一点,共现矩阵是对称的,但是共现概率矩阵可不是对称的,词i的上下文出现词j的概率和词j的上下文出现词i的频次一样,但是概率是不一样的,Xij=XjiPijPjiX_{ij}=X_{ji},P_{ij} \not = P_{ji},举个例子:

考虑语料:Tom wants a cake and Lily also wants a cake, but jack wants a pear.

我们设定窗口大小为1,考虑词“a”的上下文出现“cake”的概率P(cakea)P(cake|a)以及反过来P(acake)P(a|cake)。“a”的上下文出现了“wants”两次,"cake"两次,“pear”一次,则P(cakea)=2/5P(cake|a)=2/5,而“cake”的上下文出现了“a”两次,“and“一次,”but”一次,因此P(acake)=2/4P(a|cake)=2/4,因此P(cakea)P(acake)P(cake|a) \not= P(a|cake)

共现概率矩阵为我们提供了语料的统计信息,接下来要求词之间的相似性能够被量化,按照word2vec的思路,是将词编码成一个向量,用向量间的余弦距离(夹角)来衡量词之间的相似性。Glove也是采取了这个思路,但是将词转化为向量时采样了不一样的解决方案。

回忆word2vec中建立统计语言模型时的基本思路是:中心词和周边词间存在联系,它们可以相互预测。而Glove采取了另一种思路,一句话概括就是:共现概率矩阵上元素间的比值可以编码词的语义。

举例:

Glove/Untitled.png

Glove/Untitled%201.png

上面两个表格第一、二行展示了共现概率矩阵中部分元素,第三行是元素间比值。

下面以第一列所展示的数值为例,在上下文出现了ice的情况下,出现solid的概率为1.9×1041.9 \times 10^{-4},而上下文出现了stream的情况下,出现solid的概率为2.2×1052.2 \times 10^{-5},单单这两个概率值只能提供有限的信息,但是二者相除的结果为8.9,相差近10倍,从第三行总体情况来看8.9算是一个较大的值,这时可以说ice的上下文比起stream的上下文更可能出现solid这一个词,我们从现实情况上来看,冰可以说是固体,蒸汽不是固体,冰确实跟固体有更强的联系,既符合现实,也符合人的直觉。

ice和stream通过solid这一桥梁建立起了联系,如果用词的不是solid而是gas,那么这个比值将变得很小,说明蒸汽才和气体有较大联系,如果这个词改为water,这个比值为1.36,很接近1,改为fashion也是接近1的数值,在比值接近1时,只能作为桥梁的这个词跟ice和stream均有较强联系或是均没有联系。通过不断更改作为桥梁的词,可以计算出一系列的概率比值,这些比值使得词之间的联系能在更高的维度上建立起来,逐渐接近词的深层语义,以上就是Glove建模的出发点。

现在知道了Glove建模的出发点,但是具体要用什么模型来表达呢?一个最简单的想法,如果我们可以在线性空间建立起模型就再好不过了,便于推导也便于优化。所谓的线性空间和向量空间是一个意思,换句话说,我们希望可以通过向量间的加减乘除就能够代表共现概率间的比值。基于这一思想,我们假设共现概率PikP_{ik}PjkP_{jk}间比值能用wi,wj,wkw_i,w_j,w_k这三个词的词向量表示:

PikPjk=g(wi,wj,wk)\frac{P_{ik}}{P_{jk}} = g(w_i,w_j,w_k)

观察这个公式,左边是可以用统计算出来的已知信息,右边的词向量wi,wj,wkw_i,w_j,w_k以及函数gg的形式都是未知,和word2vec一样,词向量同样是作为模型参数,也是我们最终想得到的东西。我们暂且抛开函数g的具体形式不谈,我们可以如果要构造一个损失函数,可以根据上面式子的形式,将损失函数写作:

J= ( g(wi,wj,wk)PikPjk)J=\sum\ (\ g(w_i,w_j,w_k) - \frac{P_{ik}}{P_{jk}} )

这种式子应该很熟悉,g是模型预测,P之间的比值是真实值,也是我们已知的值,接下来用梯度下降等方法求解模型参数wi,wj,wkw_i,w_j,w_k即可。现在的问题是g的形式应该是什么样的?下面就是作者对该损失进行魔改的时间了。

说个题外话,Glove的作者原本是理论物理博士,如果接触过量子力学、微观物理那一套理论的话,应该可以深切体会到,这个领域有些推导是没有严谨地遵循数学逻辑的,很多时候需要用启发式的思维,看起来合理,但深究起来又感觉有点说不通。

作者的思路是这样的:

  1. 我们通过wkw_k这一桥梁希望发现wiw_iwjw_j间的语义关系,那么在向量空间中应该可以w_i和w_j间的差值来表示他们间的关系吧?所以g(wi,wj,wk)g(w_i, w_j, w_k)间应该存在有(wiwj)(w_i-w_j)这一项。
  2. PikPjk\frac{P_{ik}}{P_{jk}}是标量,那么gg输出的也应该是标量,而wi,wj,wkw_i,w_j,w_k都是向量,要得到标量可以通过向量间的内积,所以gg应该具有这种形式(wiwj)Twk(w_i-w_j)^Tw_k
  3. 最后是对(wiwj)Twk(w_i-w_j)^Tw_k进行指数运算exp()exp(),这样就可以得到以下推导:

PikPjk=g(wi,wj,wk)=exp((wiwj)Twk)=exp(wiTwk)exp(wjTwk)\begin{aligned}\frac{P_{ik}}{P_{jk}}=&g(w_i,w_j,w_k)\\=&exp((w_i-w_j)^Tw_k)\\ =&\frac{exp(w_i^Tw_k)}{exp(w_j^Tw_k)}\end{aligned}

这时发现分子分母可以统一形式,既写成向量内积的指数形式:

Pij=exp(wiTwj)   or   logPij=wiTwjP_{ij} = exp(w_i^Tw_j) \ \ \ or \ \ \ logP_{ij} = w_i^Tw_j

第三步套一层exp()感觉算是一种启发式推导了,就是为了推导化简,使分子分母的共现概率能够写成简单且一致的形式。此时我们的损失函数可以改写成:

J=wiTwjlogPijJ=\sum w_i^Tw_j - logP_{ij}

但现在存在一个问题,即logPij=wiTwjlogP_{ij} = w_i^Tw_j中,左右的对称性不成立,如果i和j交换,wiTwj=wjTwiw_i^Tw_j = w_j^Tw_i自然是成立,但是本文一开头也说了PijPjiP_{ij} \not = P_{ji},所以要给这条式子补个偏置项强行使等式继续成立,即:

logPij=wiTwj+bjlogP_{ij} = w_i^Tw_j+b_j

此时再看我们的损失函数,就可以继续展开

J=wiTwj+bjlogPij=wiTwj+bjlogXijXi=wiTwj+bjlogXij+logXi\begin{aligned} J&=\sum w_i^Tw_j + b_j -logP_{ij} \\ &= \sum w_i^Tw_j +b_j - log\frac{X_{ij}}{X_i} \\ &= \sum w_i^Tw_j + b_j - logX_{ij} + logX_i \end{aligned}

如果将logX_i也写记作一个偏置项b_i,则可以进一步写成:

J=wiTwj+bi+bjlogXijJ = \sum w_i^Tw_j + b_i + b_j - logX_{ij}

最后是基于词频越高权重应该越大的原则,给损失函数的每一项求和项加一个权值,写成下面的式子:

J=f(Xij)(wiTwj+bi+bjlogXij)J = \sum f(X_{ij})(w_i^Tw_j + b_i + b_j - logX_{ij})

f(Xij)f(X_{ij})即为基于词频XijX_{ij}的权值,它的表达式和函数图形如下:

f(x)={(x/xmax)0.75if  x<xmax1if  x>=xmaxf(x) = \begin{cases} (x/xmax)^{0.75} & \text{if} \ \ x<xmax \\ 1 & \text{if} \ \ x>=xmax\end{cases}

Glove/Untitled%202.png

设计成这个形式的主要目的是根据词频的大小对损失进行加权,词频越大,损失项的权值越大,代表越重要,但是词频大到一定程度时,权值也不能无限增大,权值再大不能超过1。

以上就是Glove的所有推导,Glove的构思很有想法,以共现概率的比值来表示词的意义,并将词向量运用进来,但是其模型的推导有一点随性,这也是它被人质疑的一个地方,虽说它针对word2vec中没有利用到统计信息这一缺陷进行改进,但是Glove和word2vec在实际效果并没有太大区别。

3.参考文献

  1. https://blog.csdn.net/mr_tyting/article/details/80180780
  2. http://www.fanyeong.com/2018/02/19/glove-in-detail/
  3. https://zhuanlan.zhihu.com/p/60208480
  4. Stanford cs224n Lecture 2 - Word Vectors and Word Senses (slides-marginnote)
  5. https://blog.csdn.net/coderTC/article/details/73864097

CS224N笔记(一) Lecture1~2——word2vec超详细解析

NLP连载系列:

  1. NLP入门:文本特征的表示方式
  2. 命名实体分类NER初识
  3. n-gram的原理
  4. CS224N笔记(一) Lecture1~2——word2vec超详细解析
  5. CS224N笔记(二) Lecture1~2 :深入理解Glove原理
  6. CS224N笔记(三) Lecture 6~7——深入理解循环神经网络RNN模型
  7. CS224N笔记(四) Lecture 7:循环神经网络RNN的进阶——LSTM与GRU
  8. CS224N笔记(五) Lecture8 机器翻译、Seq2Seq以及Attention注意力机制

1. 背景知识

词向量除了可以分为离散型和分布型,还可以分为计数型和预测型,计数型的如离散型中的词典模型(BOW),TF-IDF模型,以及分布型中的n-gram,他们本质上都是在计算词序列出现的频次,属于统计模型,没有可学习的参数,而word2vec则是直接建立可训练的模型进行预测,模型中的参数和词向量都是通过学习得到,词向量中隐式地编码了语料库中的语义信息。

2. 基本原理

在word2vec中,词向量的长度不再与词典的大小相关,通常是人为设定成300以内,向量中的数值也不再是离散型数值,也更不是one-hot,每个位置都是连续型变量。和n-gram原理 类似,word2vec也需要建立统计语言模型,n-gram中是用第i个词前的m个词来预测第i个词应该是什么,word2vec也类似,不过它会涉及第i+1、i+2…i+m个词,即中心词周边的2m个词wim,...,wi1,wi+1,...,wi+mw_{i-m},..., w_{i-1}, w_{i+1},...,w_{i+m}。可以根据周边词来预测中心词,也可以根据中心词来预测周边词,前者称为CBOW模型,后者称为skip-gram模型。

Untitled.png

skip-gram模型示意图,CBOW的示意图就是将途中的箭头反过来。

1. CBOW

如前面所述,CBOW用周边词来预测中心词,下面将用数学语言进行整个模型的推导。

首先明确一下word2vec中模型参数和词向量的概念,它们本质上是一回事,但是实现时却分开成了两套变量,词典中的每个词都会对应两个向量,一个姑且称为参数向量,当一个词作为模型输入用去预测别的词时,就用参数向量来表示该词,因为它是作为模型的输入,因此称为输入向量也可以,下面的符号约定中也提到一般用v来表示;另一个称为词向量,它是模型的输出,也即是我们想得到的东西,同理,也可以称之为输出向量,一般用u表示。上述套概念在skip-gram和CBOW都适用,但是因为这两个模型的输入和输出刚好是相反的,因此千万要注意别把符号弄混了。具体为什么一个词要设置两套变量?CS224N中说是这样做方便计算、方便求导,实际上最后这两套向量都可以用,甚至可以平均合并。

其次约定一些符号表示

  • 词典为VV,词典的大小为V|V|
  • 窗口大小为mm,词向量大小为nn
  • 一个词如果用它作为中心词,下标用小写字母cc表示,代表center,如果它作为周边词,则用小写字母oo表示,表示outside
  • 词用ww表示,词向量用uu或者vv表示,并且约定当词向量是作为模型输入时,用vv来表示这个词向量,如果作为模型输出,则用uu来表示;因此在skip-gram中,uu是用来表示周边词的,vv是用来表示中心词的,而CBOW则相反,uu表示中心词,而vv表示周边词;为了不弄混,还是用向量是作为模型的输入还是输出作为区分最为清晰。
  • V\mathcal{V}用来表示词典中所有词对应的输入向量组成的矩阵,用U\mathcal{U}表示输出向量对应的矩阵,那么这两个矩阵的大小均为V×n|V| \times n
  • WW依旧表示模型的参数,当然word2vec中,模型参数和词向量是一回事,因此这里的WW也就是V\mathcal{V}以及U\mathcal{U}
  • x表示词w的one-hot向量,它为列向量,长度与词典的大小|V|相同,它作为输入层的输入,从代码层面来说它才是真正的输入向量,用它与V\mathcal{V}相乘可以得到词w对应的输入向量v,tensorflow中的embedding层就是进行该操作。

最后开始推导:

Untitled%201.png

CBOW的模型示意图,x为输入词对应的one-hot列向量,WV×NW_{V \times N}就是V\mathcal{V}WV×NW'_{V \times N}就是U\mathcal{U}

算法流程:

  1. 输入为窗口中除了中心词外的所有词的one-hot向量:(xcm,...,xc1,xc+1,...,xc+m)( x_{c-m}, ..., x_{c-1}, x_{c+1}, ... , x_{c+m})
  2. 将one-hot向量与输入矩阵V\mathcal{V}做矩阵乘法,得到窗口内每个词对应的输入词向量,即vcm=Vxcmvc1=Vxc1...vc+1=Vxc+mvc+m=Vxc+mv_{c-m} = \mathcal{V}x_{c-m}, v_{c-1} = \mathcal{V}x_{c-1},...,v_{c+1} = \mathcal{V}x_{c+m},v_{c+m} = \mathcal{V}x_{c+m}
  3. 将窗口内所有的输入向量做平均池化,作为模型输入,v^=vcm+...+vc1+vc+1+...+vc+m2m\hat{v} = \frac{v_{c-m}+...+v_{c-1}+v_{c+1}+...+v_{c+m}}{2m}
  4. 计算v^\hat{v}与词典中每个词向量求点积,得到zzz=Uv^z=\mathcal{U}\hat{v}zz也为列向量,长度为词典大小V|V|
  5. zz送入softmax层进行概率归一化得到y^\hat{y}y^\hat{y}中每一个位置的数值对应词典中每一个词被预测为中心词的概率。
  6. 用模型输出的预测概率y^\hat{y}与真实概率yy计算损失,一般用交叉熵损失即可。yy是窗口内真实中心词所对应的one-hot向量,长度也为V|V|

算法解析:

算法流程第4点中求点积可以理解为求输入向量与词典中每个向量的余弦距离,也可以理解为相似度,点积的结果越大,表示两个向量越相似,从特征语义的角度来说,就是从词典中找到一个与上下文语义最相近的词语;词典中每个词都会对应一个余弦距离,余弦距离最大的那个词就是最有可能的中心词预测,但是由于余弦距离不能作为概率,因此在算法流程第5点中进行了softmax归一化。

算法流程4、5、6点可以归结为一条公式:

Loss=j=1Vyjlogyj^=yclogyc^=logyc^=logexp(ucTv^)k=1Vexp(ukTv^)\begin{aligned} Loss =& -\sum_{j=1}^V y_j log \hat{y_j} \\ =& -y_c log \hat{y_c} \\ =&-log\hat{y_c} \\ =& -log \frac{exp(u_c^T\hat{v})}{\sum_{k=1}^{|V|}exp(u_k^T\hat{v})}\end{aligned}

yyy^\hat{y}都是向量,加了下标表示向量中某一个位置上的值。公式中的第一行就是标准的交叉熵损失表达式,由于y是一个one-hot向量,只在特定的位置为1,结合CBOW模型的含义,yy向量在中心词wcw_c对应的位置ycy_c上为1,只有当j=cj=c时,求和公式的加数才不为0,因此第一行可简化为第二行,又由于ycy_c其实就是1,干脆可以不写,于是简化为第三行,最关键的地方就是如何表示yc^\hat{y_c}。算法流程第5行提到是通过对点积z进行softmax归一化而得到y^\hat{y},因此公式第四行其实就是一个求softmax概率然后求负对数,softmax概率即其中的分式,它的含义是模型预测中心词为wcw_c的概率。算法流程第5行中计算的是z=Uv^z=\mathcal{U}\hat{v},z也是一个向量,具体到某一个值如zcz_c时,就是取出矩阵U\mathcal{U}的第cc行,然后计算zc=uckv^z_c=u_c^k\hat{v}

2. skip-gram

word2vec/Untitled%202.png

和CBOW有些不同,skip-gram是用中心词预测周边词,窗口内的中心词只有一个,因此输入只有一个向量,周边词涉及多个位置,因此模型有多个输出向量,每个向量对应周边的一个位置。

算法流程:

  1. 输入为中心词,中心词对应的one-hot向量:xcx_c
  2. 将one-hot向量与输入矩阵V\mathcal{V}做矩阵乘法,得到中心词入词向量,即vc=Vxcv_{c} = \mathcal{V}x_{c}
  3. 计算vcv_c与词典中每个词向量求点积,得到zzz=Uvcz=\mathcal{U}v_czz也为列向量,长度为词典大小V|V|
  4. zz送入softmax层进行概率归一化得到y^\hat{y},并且每一个周边词都会对应一个预测概率,即y^cm,y^c1,...,y^c+1,y^c+m\hat{y}_{c-m},\hat{y}_{c-1},...,\hat{y}_{c+1},\hat{y}_{c+m},窗口有多大就得计算多少次softmax概率
  5. 用模型输出的每个预测概率y^cm,y^c1,...,y^c+1,y^c+m\hat{y}_{c-m},\hat{y}_{c-1},...,\hat{y}_{c+1},\hat{y}_{c+m}与真实概率ycm,yc1,...,yc+1,yc+my_{c-m},y_{c-1},...,y_{c+1},y_{c+m}分别计算交叉熵损失并求和,同理ycm,yc1,...,yc+1,yc+my_{c-m},y_{c-1},...,y_{c+1},y_{c+m}也都是one-hot向量。

算法解析:

skip-gram的流程与CBOW差别不大,只是输入向量变成了一个,省去了均值池化,输出向量变成了多个,每个周边词都要计算一次softmax概率并分别计算损失,最后叠加。因为同样是用交叉熵损失,因此和CBOW的公式推导没有太大区别,下面直接引用CBOW中公式推导前三行的简化结果,得到skip-gram的损失函数为:

Loss=j=m;j!=0mlogy^c+j=j=m;j!=0mlogexp(uc+jTvc)k=1Vexp(ukTvc)\begin{aligned} Loss &= -\sum_{j=-m;j!=0}^m log\hat{y}_{c+j} \\ &=-\sum_{j=-m;j!=0}^m log \frac{exp(u_{c+j}^Tv_c)}{\sum_{k=1}^{|V|}exp(u_k^Tv_c)}\end{aligned}

不要被庞杂的符号吓到,其实还是softmax概率取负对数,只不过每个周边词都要计算一次,下标有些复杂就是因为要对应每个周边词的位置,log里的分母跟CBOW模型中没什么区别,还是需要用输入向量与词典中所有词求一次点积。

上面这条公式是CS224N的notes中的写法(自己闭眼写的,跟原文有些许差别,但只是下标表示方式不同而已),在slides对于softmax概率有一个另一种写法,即:

P(oc)=exp(uoTvc)wVexp(uwTvc)P(o|c)=\frac{exp(u_o^Tv_c)}{\sum_{w\isin{V}}exp(u_w^Tv_c)}

其实整体结构是一样的,只是换了一批符号而已,这一点不需要体纠结。

需要注意在slides中的推导逻辑和上面有些许不同,它的逻辑是:skip-gram是在构建一个统计语言模型,具体来说是求一个含有未知参数的概率分布,这类问题最直接的方法就是用最大似然函数来求这个概率的分布的参数,即:

word2vec%20f7641f329a7145d8be161e35b5282da8/Untitled%203.png

它是先得出最大似然函数,然后将最大似然函数转为损失函数,转的方法为取对数简化计算,并且加负号转变任务:使似然函数最大化转成使损失函数最小化,这么一通转化后损失函数的形式和交叉熵损失一样,这也就和notes中的写法一致了。但是依然需要注意的是slides和notes的推导逻辑是不一样的,尽管在这里殊途同归了,但是最大似然函数和交叉熵损失并不一样,只是对数似然函数经常会取负对数简化计算,使得最终要优化的形式和交叉熵损失一致。

3.维度爆炸的处理

不管是CBOW还是skip-gram,最后计算softmax概率时都会涉及整个词典V,词典中的每个词的词向量都会被用到,这涉及巨大的计算量,为了解决这一点有两种做法,一个用一种很常见的操作是negative-sampling,另一个是huffman-tree霍夫曼树方法。

1. negative sampling

基本原理:negative samplingskip-gram的方法优化的目标跟前面相比有所变化,是判断给定的样本对是正样本对还是负样本对。以skip-gram为例,skip-gram用中心词预测周边词,假设半窗口大小为mm,那么在一个窗口内的正样本对(wo,wc)(w_o, w_c)2m2m对,负样本有 V2m|V|-2m对,词典中除了周边词外的所有词与中心词组成的样本对都是负样本对。所谓正样本对表示正表示在给定窗口内,wcw_c作为中心词,wow_o是它的周边词,而负则表示wow_o没有出现在wcw_c的周边。由于负样本太多,实际上对于每个窗口的训练只需要取其中一部分负样本对即可,不用考虑整个词典。

建模过程:

基于上述描述,我们要求的概率模型可以表述为P(Dwo,wc,θ)P(D|w_o,w_c,\theta)D=1D=1表示wow_owcw_c为正样本对,D=0D=0表示wow_owcw_c为负样本对,θ\theta表示概率模型的未知参数,在word2vec中它就是前面所说的参数向量和词向量。那么如何表示P(Dwo,wc,θ)P(D|w_o,w_c,\theta)?在word2vec原论文中还是用周边词的词向量和中心词的参数向量进行点积运算,但不再适用softmax函数进行归一化得到概率值,而是采用sigmoid函数直接将点积结果转化为(0,1)(0,1)间的概率值。

我们用σ\sigma表示sigmoid函数,sigmoid函数需要谨记三个关键公式:

σ(x)=11+exσ(x)=1σ(x)σ(x)=σ(x)(1σ(x))\begin{aligned} \sigma(x) &= \frac{1}{1+e^{-x}} \\ \sigma(-x)&=1-\sigma(x) \\ \sigma'(x)= &\sigma(x)(1-\sigma(x))\end{aligned}

P(Dwo,wc,θ)P(D|w_o,w_c,\theta)可以表示为:

P(D=1wo,wc,θ)=σ(uoTvc)P(D=0wo,wc,θ)=1P(D=1wo,wc,θ)=1σ(uoTvc)=σ(uoTvc)\begin{aligned} P(D=1|w_o,w_c,\theta)&=\sigma(u_o^Tv_c) \\ P(D=0|w_o,w_c,\theta)&= 1-P(D=1|w_o,w_c,\theta) \\&=1-\sigma(u_o^Tv_c) \\ &=\sigma(-u_o^Tv_c)\end{aligned}

接下来就是求概率模型的未知参数θ\theta,还是可以用极大似然法,先写出似然函数:

likehood(θ)=(wo,wc)DP(D=1wo,wc,θ)(wo,wc)D~P(D=0wo,wc,θ)likehood(\theta) = \prod_{(w_o,w_c) \isin \mathcal{D}} P(D=1|w_o,w_c,\theta) \prod_{(w_o,w_c) \isin \tilde{\mathcal{D}}} P(D=0|w_o,w_c,\theta)

其中D\mathcal{D}表示正样本对集合,D~\tilde{\mathcal{D}}表示负样本对集合,对于正样本对集合内的样本对,我们希望预测为D=1D=1的概率越大越好,对于负样本对集合内的,我们则希望预测D=0D=0的概率越大越好,这样就表示概率模型能够很好地判断给定样本对是正还是负。

有了似然函数下面就是求取参数θ\theta使似然函数取极大值,照例还是可以取负对数,转为极小值问题,并将连乘转为求和简化计算,即:

arg maxθlikehood(θ)=arg minθlog(likehood(θ))=arg minθ((wo,wc)DlogP(D=1wo,wc,θ)+(wo,wc)D~logP(D=0wo,wc,θ))=arg minθ((wo,wc)Dlog σ(uoTvc)+(wo,wc)D~log σ(uoTvc))=arg minθ((wo,wc)Dlog 11+exp(uoTvc)+(wo,wc)D~log 11+exp(uoTvc))\begin{aligned} \argmax_{\theta} likehood(\theta) &=\argmin_{\theta} -log(likehood(\theta)) \\ &=\argmin_{\theta}-(\sum_{(w_o,w_c) \isin \mathcal{D}}logP(D=1|w_o,w_c,\theta) + \sum_{(w_o,w_c) \isin \tilde{\mathcal{D}}}logP(D=0|w_o,w_c,\theta) ) \\ &=\argmin_{\theta}-(\sum_{(w_o,w_c)\isin\mathcal{D}}log\ \sigma(u_o^Tv_c) + \sum_{(w_o,w_c) \isin \tilde{\mathcal{D}}} log\ \sigma(-u_o^Tv_c) ) \\ &=\argmin_{\theta}-(\sum_{(w_o,w_c)\isin\mathcal{D}} log\ \frac{1}{1+exp(-u_o^Tv_c)} + \sum_{(w_o,w_c) \isin \tilde{\mathcal{D}}} log\ \frac{1}{1+exp(u_o^Tv_c)} )\end{aligned}

更具体一点,假设半窗口大小为mm,负样本对只采样KK对,则正样本对集合D\mathcal{D}中有2m2m个样本,负样本对集合D~\tilde{\mathcal{D}}中有KK对样本,这些样本共同组合形成一个窗口内(一个batch)的训练数据,则上式可以更具体化地写成:

arg minθ(j=m;j!=0+mlog 11+exp(ujTvc)+k=1Klog 11+exp(ukTvc))\argmin_{\theta} - (\sum_{j=-m;j!=0}^{+m} log\ \frac{1}{1+exp(-u_j^Tv_c)} + \sum_{k=1}^K log\ \frac{1}{1+exp(u_k^Tv_c)})

现在的问题在于用什么方式抽取负样本,word2vec采用的带权采样,每个词都对应一个被抽取的概率,这个概率与该词在语料库中出现的次数直接相关,即遵循:

P(w)=counter(w)uVcounter(u)P(w)=\frac{counter(w)}{\sum_{u\isin V}counter(u)}

其中,counter(w)counter(w)表示词ww在语料库中出现的频次,但是在word2vec论文中还加了个3/4幂次,这会略微降低高频词被抽取的概率,略微提高低频词被抽取的概率,即:

P(w)=[counter(w)]3/4uV[counter(u)]3/4P(w)=\frac{[counter(w)]^{3/4}}{\sum_{u\isin V}[counter(u)]^{3/4}}

在源代码中,是开辟一个片大小为MM的数组,并在数组中放入词典中的词,每个词放的个数为M×P(w)M \times P(w),在采样时随机生成一个[0,M1][0,M-1]间的整数,通过该整数从数组取出对应的词。

2. huffman tree

4. 参考文献

  1. CS224N lecture 1 notes & slides
  2. CS224N lecture 2 slides
  3. word2vec中的数学原理

n-gram的原理

NLP连载系列:

  1. NLP入门:文本特征的表示方式
  2. 命名实体分类NER初识
  3. n-gram的原理
  4. CS224N笔记(一) Lecture1~2——word2vec超详细解析
  5. CS224N笔记(二) Lecture1~2 :深入理解Glove原理
  6. CS224N笔记(三) Lecture 6~7——深入理解循环神经网络RNN模型
  7. CS224N笔记(四) Lecture 7:循环神经网络RNN的进阶——LSTM与GRU
  8. CS224N笔记(五) Lecture8 机器翻译、Seq2Seq以及Attention注意力机制

1. 共现矩阵

共现矩阵描述的是在一个固定大小的滑动窗口下,两个词同时出现的频次。假设共现矩阵用XX表示,半窗口大小为mm,则Xi,jX_{i,j}表示在语料库中,以词wiw_i为中心词时,wiw_i前后2m2m个词内wjw_j出现的次数。举例说明如下图:

共现矩阵例子/Untitled.png

假设语料库中的词典表示为VV,则共现矩阵的大小为V×V|V| \times |V|,词典的大小一般很大,因此共现矩阵也非 常巨大,这也是n-gram的一大弊端。

共现矩阵中是对称的,每一行或每一列都可以作为词向量,但是n-gram其实很少用到词向量,更多的是基于统计值预测序列的某个序列上是什么词。真用到词向量的一个例子是Glove ,在那里共现矩阵的每一行会归一化得到一个概率值P(ij)P(i|j),表示在窗口2m2m内,词wjw_j出现时,词wiw_i出现的概率。

以上的描述都是基于窗口的,还有一种方法是基于全篇文章(Word Document Maxtirx),可以理解为窗口为一整篇文章的大小,矩阵Xi,jX_{i,j}表示在第jj篇文章中,词wiw_i出现的频次,矩阵X的大小为V×M|V| \times M,其中MM为文章的篇数。

2. 数学描述

n-gram作为一种基于统计语言,应该可以用数学的语言描述。我们的目标是建立下面的模型:

p(W)=p(w1T)=p(w1,w2,...,wT)=p(w1)p(w2w1)p(w3w12)p(wTw1T1)p(W)=p(w_1^T)=p(w_1,w_2,...,w_T)=p(w_1) \cdot p(w_2|w_1) \cdot p(w_3|w_1^2) \dots p(w_T|w_1^{T-1})

模型中的基本元素是p(wkw1k1)p(w_k|w_1^{k-1}),只需要找到这个概率的表示方法即可,根据贝叶斯公式:

p(wkw1k1)=p(w1k)p(w1k1)p(w_k|w_1^{k-1})=\frac{p(w_1^k)}{p(w_1^{k-1})}

再三注意,w1kw_1^k上的k不是幂次,w1kw_1^k表示是序列w1,w2,..wkw_1,w_2,..w_k,这也解释了上面贝叶斯公式的分子为什么不是p(w1k,w1k1)p(w_1^k, w_1^{k-1}),因为w1kw_1^k已经将w1k1w_1^{k-1}包含进去了,直接简写在一起就好了。

根据大数定理,上式可以表示为为:

p(wkw1k1)count(w1k)count(w1k1)p(w_k|w_1^{k-1})\approx\frac{count(w_1^k)}{count(w_1^{k-1})}

count(w1k)count(w_1^k)表示的是语料库中,序列w1kw_1^k出现的频次,这其实就是在计算一个共现矩阵,只不过这里没有限制窗口大小,如果k很大,那么分子和分母的统计将会消耗巨大的算力。n-gram的基本思想,是它做了一个n-1阶的Markov假设,即认为一个词出现的概率只与它前面的n-1个词相关,即

p(wkw1k1)p(wkwkn+1k1)=p(wkn+1k)p(wkn+1k1)count(wkn+1k)count(wkn+1k1)p(w_k|w_1^{k-1}) \approx p(w_k|w_{k-n+1}^{k-1}) = \frac{p(w_{k-n+1}^k)}{p(w_{k-n+1}^{k-1})} \approx \frac{count(w_{k-n+1}^k)}{count(w_{k-n+1}^{k-1})}

这样在计算共现矩阵时,就只需要在大小为n的窗口内进行,不用统计一个长序列w1kw_1^k的频次,大大缩减了计算量。

至此,n-gram统计语言模型的关键概率p(wkw1k1)p(w_k|w_1^{k-1})计算完毕,基于这个概率可以对给定的序列预测下一个词最可能是什么。

3. 计算优化

即使做了Markov假设,n-gram依旧很耗费算力和存储,因为共现矩阵的大小与词典的大小有关,词典通常非常大,因为共现矩阵会非常大,推理速度也会很慢。下图给了一个共现矩阵参数量的参考值,n表示的是窗口大小,词典大小为20万(汉语词汇量的大致量级)。

n与模型参数的关系

为了进一步优化计算,在获得共现矩阵后,通常会对它进行降维,常见的方法比如PCA和SVD,用SVD降维还被讲成了故事,从多义词的角度来讲述,参考文献3、4。

此外,由于语料库中某些序列的频次为0,那么在计算概率是有可能出现分子或分母为0的情况,这不能说明这些序列的真的不存在,只能说语料库不够大没有覆盖到,所以通常是会进行平滑处理,最简单的方式是给频次+1。

4. 参考文献

  1. word2vec中的数学
  2. Stanford cs224n Lecture 2 - Word Vectors and Word Senses (slides-marginnote)
  3. https://www.cnblogs.com/sancallejon/p/4963630.html
  4. https://blog.csdn.net/qq_16633405/article/details/80577851

NLP入门:文本特征的表示方式

NLP连载系列:

  1. NLP入门:文本特征的表示方式
  2. 命名实体分类NER初识
  3. n-gram的原理
  4. CS224N笔记(一) Lecture1~2——word2vec超详细解析
  5. CS224N笔记(二) Lecture1~2 :深入理解Glove原理
  6. CS224N笔记(三) Lecture 6~7——深入理解循环神经网络RNN模型
  7. CS224N笔记(四) Lecture 7:循环神经网络RNN的进阶——LSTM与GRU
  8. CS224N笔记(五) Lecture8 机器翻译、Seq2Seq以及Attention注意力机制

一、前言

NLP区别与CV的一个重要特征之一,在于模型的输入需要预先处理,从CV转行NLP的炼丹者一开始定会对模型的输入形式感到疑惑。不像CV中模型的输入就是图像的RGB值,本来就是数值型,且像素值可以认为是连续型变量,不需要再做什么处理,最多就是做一下归一化,或者翻转、裁剪等数据增强处理。而NLP输入的是文字,非数值型变量,要如何将文字更加合理地映射成某种数值型变量,是NLP要解决的一个重要问题。

在NLP中数值型变量一般是以向量的形式展现出来,本文接下来将阐述NLP中出现过的多种词向量表示方式,并阐述词向量好坏的评判标准。

二、表示方式

文本特征的的表示方式有两大类共六种。

1、离散型表示方式(离散)

i. one-hot独热编码

将词(或字)表示成一个向量,该向量的维度是词典(或字典)的长度(该词典是通过语料库生成的),该向量中,该单词索引的位置值为1,其余的位置为0。one-hot是一种极为稀疏的表示形式。

缺点:

  1. 不同词的向量表示互相正交,无法衡量不同词之间的关系;
  2. 该编码只能反映某个词是否在句中出现,无法衡量不同词的重要程度;
  3. 使用One-Hot 对文本进行编码后得到的是高维稀疏矩阵,会浪费计算和存储资源;

ii. 词袋模型(Bag Of Word, BOW)

在词袋模型中不考虑语序和词法的信息,每个单词都是相互独立的,将词语放入一个“袋子”里,统计每个单词出现的频率。

和one-hot独热编码不同,词袋模型是对文本进行编码而不是对字、词进行编码,编码的后向量是表示整篇文档的特征,而不是单一个词。向量中每个元素表示的是某个单词在文档中的出现次数,可想而知,向量的长度等于词典的长度。在sklearn库中可以直接调用CunterVectorizer实现词袋模型。

缺点

  1. 词袋模型丢失了词的位置信息,位置信息在文本中是一个很重要信息,词的位置不一样语义会有很大的差别;
  2. 该编码方式虽然统计了词在文本中出现的次数,但是单靠这个指标无法衡量每个词的重要程度,文中大量出现的词汇比如“你”、“我”以及一些介词等对于区分文本类别意义不大。

iii. 词频-逆文档频率(TF-IDF)

这是一种词袋模型的升级版,为的是解决词袋模型无法区分常用词(如:“是”、“的”等)和专有名词(如:“自然语言处理”、“NLP ”等)对文本的重要性的问题。跟词袋模型一样,它也是对文档编码,编码后词向量的长度就是词典的长度。

  • 统计的方式主要是计算词的词频(TF)和逆向文件频率(IDF):
    • TF (Term Frequency )
      1. 某个词在当前文本中出现的频率,频率高的词语或者是重要的词
      2. TF=某单词在文章中出现的总次数文章包含的总词数TF=\frac{某单词在文章中出现的总次数}{文章包含的总词数}
    • **IDF (Inverse Document frequency )**逆文本频率。
      1. 文本频率是指:含有某个词的文本在整个语料库中所占的比例。逆文本频率是文本频率的倒数,所以分子分母看起来颠倒了过来。
      2. IDF=log(语料库的文本总数包含某词的文本数量+1)IDF = log({\frac{语料库的文本总数}{包含某词的文本数量+1}})
  • 最终词频-逆文档频率TFIDF=TFIDfTF-IDF=TF*IDf(注意“-“不是减号而是连接符)
  • 还需要提一点,在sklearn的TF-IDF实现中,对一篇文本算完tf-idf后还会进行一次欧式距离归一化,所以特征向量中的每次位置都是[0,1)之间的数值,这也省去了后续调用SVM等分类方法时对输入进行归一化的步骤。而TF在sklearn就是单词在文章中出现的总词数,不用再除以文章包含的总词数。

缺点

  1. 和词袋模型一样,依然不能反映词的位置信息。
  2. IDF 是一种试图抑制噪声的加权,本身倾向于文本中频率比较小的词,这使得IDF 的精度不高;
  3. TF-IDF 严重依赖于语料库(尤其在训练同类语料库时,往往会掩盖一些同类型的关键词;如:在进行TF-IDF 训练时,语料库中的娱乐新闻较多,则与娱乐相关的关键词的权重就会偏低 ),因此需要选取质量高的语料库进行训练;

2、分布型表示方式(连续)

前面提到的词向量表示为离散型,另一种是分布型表示,区别两种表示的最重要区别是分布型表示需要建立统计语言模型,那么什么是统计语言模型?统计语言模型就是用来计算一个句子的概率的模型,通常是基于语料库来构建,一个句子的概率可以表示为:

p(W)=p(w1T)=p(w1,w2,...,wT)=p(w1)p(w2w1)p(w3w12)p(wTw1T1)p(W)=p(w_1^T)=p(w_1,w_2,...,w_T)=p(w_1) \cdot p(w_2|w_1) \cdot p(w_3|w_1^2) \dots p(w_T|w_1^{T-1})

上面这条公式中,W表示一个句话,它是由单词w1,w2,...,wTw_1,w_2,...,w_T按顺序排列而构成的,三个括号里的符号内容是等价的,见到w1Tw_1^T不要往幂次想,它就是表示首单词为w1w_1,长度为T,末尾单词为wTw_T的一句话。

统计语言模型描述的是一段词序列的概率,通俗点说,就是计算一个句子属于正常人类所说的话的概率。那么统计语言模型和词向量有何关系,这里需要提及统计语言模型的一个重要观点:词的语意可以通过一系列句子来传达,一个中心词如果能存在与某句子中而不违和,那么其上下文所出现的词就可以用来解析中心所蕴含的意义。句子违不违和应该怎么评判?就是通过统计语言模型,如果一段词序列的概率越大P(W),说明这段话越不违和。现在问题的关键就是,如何用词向量来构造统计语言模型。

i. n-gram

n-gram 是简单地基于统计语言模型的词向量表示,词向量可以认为是共现矩阵中每一行或者每一列,但是共现矩阵是通过统计得到的,所以n-gram是完全基于词频统计的模型,没有需要学习的参数或词向量。

注意区分n-gram和词袋模型,虽然都是在计算词频,但是他们有两个区别:

  • 词袋模型就是简单地计算整篇文档中各个单词出现的频率,而n-gram计算两个词在一个窗口下出现的频率,它强调的是词与词间的关系。
  • 词袋模型目的是对一篇文章编码,而n-gram是对词编码,颗粒度有巨大差异。

同理由于TF-IDF是词袋模型的改进版,它和n-gram有一样的差异

关于n-gram的详细解释,请点击另一篇文章:

n-gram的原理

优点

  1. 相比起词袋模型和TF-IDF,n-gram考虑了句子中词的顺序;

缺点

  1. 词典的长度很大,导致词的向量长度也很大;
  2. 共现矩阵会随着词典的增大而增大
  3. 共现矩阵也是稀疏矩阵(可以使用 SVDPCA 等算法进行降维,但是计算量很大);

ii. word2vec

两种形式,CBOW利用上下文的词预测中心词,SKIP-GRAM利用中心词预测上下文的词。

关于word2vec的详细解释,请点击另一篇文章:

CS224N笔记(一) Lecture1~2——word2vec超详细解析

优点

  1. 考虑到词语的上下文,学习到了语义和语法的信息;
  2. 得到的词向量维度小,节省存储和计算资源;
  3. 通用性强,可以应用到各种NLP 任务中;

缺点

  1. 词和向量是一对一的关系,无法解决多义词的问题;
  2. word2vec是一种静态的模型,虽然通用性强,但无法针对特定的任务做动态优化;

iii. GloVe

GloVe 是斯坦福大学Jeffrey、Richard 等提供的一种词向量表示算法,GloVe 的全称是Global Vectors for Word Representation,是一个基于全局词频统计(count-based & overall staticstics)的词表征(word representation)算法。该算法综合了global matrix factorization(全局矩阵分解) 和 local context window(局部上下文窗口) 两种方法的优点。

关于Glove的详细解释,请点击另一篇文章:

CS224N笔记(二) Lecture1~2 :深入理解Glove原理

iv. ELMO

word2vec 和 glove 算法得到的词向量都是静态词向量(静态词向量会把多义词的语义进行融合,训练结束之后不会根据上下文进行改变),静态词向量无法解决多义词的问题(如:“我今天买了7斤苹果” 和 “我今天买了苹果7” 中的 苹果 就是一个多义词)。而ELMO模型进行训练的词向量可以解决多义词的问题。

该算法的精髓是:用语言模型训练神经网络,在使用word embedding 时,单词已经具备上下文信息,这个时候神经网络可以根据上下文信息对word embedding 进行调整,这样经过调整之后的word embedding 更能表达在这个上下文中的具体含义,这就解决了静态词向量无法表示多义词的问题。

三、词向量的评判标准

词向量的评判标准可以分为内部标准(intrinsic criteria or intrinsic evaluation)和外部标准(external criteria or external evaluation)。

1. 内部标准

所谓的内部标准,就是不考虑下游任务,仅从词向量本身是否能够准确地表征语义来评判词向量的好坏,比如词向量间的距离是不是能够很好地表示语义上的联系,这里举CS224N上在讲解word2vec时很爱说的一个例子:

考虑man、woman、king三个词,我们可以计算woman和man在向量空间上的差diff=wwomanwmandiff = w_{woman}-w_{man},来表示woman和man间的差异性,如果king在向量空间上加上这个差,wking+diffw_{king}+diff,那么直觉上应该得到queue的向量wqueuew_{queue},因为男人对女人、国王对女王。我们希望最终训练出来的词向量能够很好地表示这种词之间的联系,这样的词向量才是好的词向量。

在讲解glove时也有这种类似的分析:各种词从原型变换到比较级再变换到最高级,在向量空间上如果具有相同的变换方向,那么这样的词向量是好的词向量。像上述这种评价方式也叫做类比评估(analogy evaluation),它对词向量好坏的评价仅仅在向量空间倒来倒去,因此算是一种内部标准

另外一种内部标准的评价方式就要借用人手工的评判了:给定两个词,让多个人去给这两个词的相似性打分,然后同样是计算这两个词的向量距离,看看它跟人类打的分是不是比较吻合。

2.外部标准

外部标准就是通过词向量在下游任务表现的优劣来评价词向量的好坏了,常见的任务有:

  1. 命名实体识别(named entity recogniton,简称NER),判断一个词是不是某种实体的名字比如人名(PER)、组织名(ORG)、地点名(LOC)、歌名(MISC)等。在命名实体分类的两种简单方式 中介绍了CS224N中两种简单方法。
  2. 词义消歧,判断近义词、多义词
  3. 文本分类

四、参考文献

  1. https://blog.csdn.net/zwqjoy/article/details/103546648
  2. CS224N Lecture 1~2 slides

命名实体分类NER初识

NLP连载系列:

  1. NLP入门:文本特征的表示方式
  2. 命名实体分类NER初识
  3. n-gram的原理
  4. CS224N笔记(一) Lecture1~2——word2vec超详细解析
  5. CS224N笔记(二) Lecture1~2 :深入理解Glove原理
  6. CS224N笔记(三) Lecture 6~7——深入理解循环神经网络RNN模型
  7. CS224N笔记(四) Lecture 7:循环神经网络RNN的进阶——LSTM与GRU
  8. CS224N笔记(五) Lecture8 机器翻译、Seq2Seq以及Attention注意力机制

命名实体分类就是判断一个词是不是属于某个实体的名字,比如人名、地点名、组织名等,这里介绍CS224N中提到的两种简单方法,均是判断一个窗口内的中心词是不是命名实体:

  1. 将周边词的词向量做平均池化后,丢入到若干层神经网络后softmax分类或者直接softmax分类。简单但是会损失位置信息

  2. 将周边词的词向量收尾相连,丢入到若干神经网络后softmax分类,不会损失位置信息但是输入大小的窗口大小成正比,会导致输入向量长度过大:

    NER/Untitled.png

hexo+typora的图片路径问题

上一篇的博客hexo图片路径设置讲到了如何在hexo博客中插入图片,那里留下了一个坑:尽管最后的图片能够在hexo中显示出来,但是在typora中却无法正常显示,解决的方案分为四步:

  1. 安装asset-image插件

    npm install https://github.com/CodeFalling/hexo-asset-image
    
  2. 配置_config.yml中的post_asset_folder 参数为 true

  3. 打开typora的偏好设置Preferences,设置插入图片时自动将其复制到与md文件同名的文件夹下,并且在第1、3项打勾:

    image-20201205170020521

  4. 之后插入图片时,就像之前使用typora一样,直接拖入即可,图片的路径会被自动设置好,正常来说如果你的md文件名为new_article.md,那么在typora中会显示图片放置在:

    new_article/plannet.jpg
    

事实上也确实能在该路径下找到图片,此时在typora也可以看到图片正常显示,最后再运行hexo clean && hexo g && hexo s,到博客查看,正常来说首页和文章内都可以看到图片正常显示。

最后注意三点:

  1. 安装了第一步的插件后,上一篇博客hexo图片路径设置所说的相对路径法就可以同时在首页和文章中显示,在上次也提到,相对路径中只能写图片名,即

    # 此时图片路径:${博客根路径}/source/_posts/new_article/planet.jpg
    ![图片描述](planet.jpg)
    

    如果写上完整的相对路径名new_article/planet.jpg,反而无法在博客中显示,但是安装插件后就没有这个问题,写不写new_article都行,只是写上完整的相对路径名可以在typora中显示图片,在typora正常显示图片也是我们想要的。

  2. 安装了第一步的插件后,上一篇博客hexo图片路径设置的标签插件语法会失效,图片的路径会变得很奇怪,因为加插件就是自动给插入的图片补全资源路径,而标签插件语法本身就会补全路径,所以最终路径就变成两个路径拼在了一起,这样自然是无法正确找到图片。当然有了插件之后也没必要选择这种方法。

  3. md文件名及其同名文件夹如果有中文,可能会显示不正常,如果发现上述方法不成功,可以首先概率将md文件以及同名文件夹用英文命名,像这里的new_article。

hexo图片路径设置 置顶

hexo插入图片的方式有四种:绝对路径、相对路径、标签插件语法、CDN图床,下面将主要介绍前三种。

绝对路径

这种方法最简单,不需要修改任何配置,将图片放在${博客根路径}/source/images/下,然后在文章里面,用这种方式引用:

# 此时图片路径:${博客根路径}/source/images/planet.jpg
![图片描述](/images/planet.jpg)

planet

这种方法的好处是简单、博客的首页以及点击进入文章内都可以看到图片,缺点是如果文章很多,图片全都堆在images文件下会显得很凌乱,不好整理。

相对路径

这种方式需要修改${博客根路径}下hexo的配置文件_config.yml,将下面post_asset_folder属性改为true:

render_drafts: false
post_asset_folder: true # 这一行改为true
relative_link: false

之后通过治疗hexo new “new_article”,在${博客根路径}/source/_posts文件夹下同时生成一个空的new_article.md文件以及与文件同名的空文件夹new_article,当然不一定要用hexo new指令,自己在文件夹下创建md文件的同时,新建一个与md文件同名的文件夹也是可以的。

将图片放在new_article文件夹下,并在文章中用下面方式引用即可:

# 此时图片路径:${博客根路径}/source/_posts/new_article/planet.jpg
![图片描述](planet.jpg)

planet

注意是只需要写图片的名字即可,什么路径都不需要写。但是这样做在typora中无法显示图片,在下一篇博客将会讲述怎么解决这个问题。

这种方法的好处是好整理,一个md文件对应一个图片资源文件夹,缺点是文章在首页展示时是看不到图片的,只有点进去文章里面才能看到图片。

这里需要强调一下md文件名和博客文章名是可以不一样的,md文件名就是表面意思:md文件的名称,博客文章名是在md文件里面Front-matter设置的title属性,这个才是访客访问你的文章时所看到的标题。通常为了避免中文字符带来的一些路径上五花八门的bug,可以将md文件用英文命名,反正别人也看不到,而你希望别人看的文章标题,可以在md文件里的title属性中自由设置。

标签插件语法

这种方法结合上述两种方法的优点,既可以将每篇文章的图片分开存储,又可以同时支持在首页和文章内显示图片。它同样需要修改post_asset_folder参数,图片的放置也和相对路径法一样,只是在文章中引用图片时,需要用以下方式:

#此时图片路径:${博客根路径}/source/_posts/new_article/planet.jpg
{% asset_img planet.jpg 图片描述 %}

这种方式是hexo官方的解决方案,但是它仍然存在问题,即在typora中是无法解析这种写法的,所以在typora中显示的一行冷冰冰的文字,在写作时极为不便。此外,另外两种方式也是存在在typora中无法正常显示图片的问题,这将在下一篇博客中阐述如何解决。

图床

推荐一个免费的图床,免注册,免备案,想体验图床的可以先在这里尝试下:https://pic.alexhchu.com。将图片上传后会返回一个url,然后在文章中引用图床中图片的url即可,这种方法既可以统一管理图片,而且加载速度也会更快;缺点是免费的图床服务怕人跑路,稳定点的一般需要收费。

图片描述

在文章中引用的具体写法如下:

# 正常的markdown引用,括号中就是图床网站返回的url
![图片描述](https://upimage.alexhchu.com/2020/12/06/6545de7148b98.jpg) 

当然,利用github可以自建图床,配合jsdeliver加速,并用picgo作为上传工具,这一套流程下来全免费,typora上也可以设置picgo自动上传图片,可以参考这篇博客:https://www.cnblogs.com/roccoshi/p/13183890.html, 里面已经写得很详细了。

Linux网络编程 置顶

  • C++
  • 后端开发
  • TCP/IP通信
  • Socket

PC间的TCP/IP、UDP网络通信是互联网的一大关键技术,关于TCP/IP以及UDP的技术在其他文章中已经介绍过,本文将聚焦如何通过Linux的接口实现多台PC间的最简单的通信。

常用API函数

socket

int socket(int domain, int type, int protocol);
  • domain表示协议族,可选AF_INET/PF_INET,表示采用IPv4,AF_INET6/PF_INET6表示IPv6,PF_UNIX表示UNIX本地协议族
  • type表示服务类型,可选SOCK_STREAM为TCP协议,SOCK_DGRAM为UDP协议
  • protocol,一般取0即可
  • 成功返回0,失败返回-1并设置errno

bind

int bind(sockfd, const struct sockaddr* my_addr, socket_t addrlen);
  • sockfd是socket()函数的返回值
  • my_addr是服务端地址
  • addrlen是储存服务端地址的变量的长度sizeof
  • 成功返回0,失败返回-1并设置errno

listen

int listen(int sockfd, int backlog);
  • sockfd与上面一致
  • backlog最大监听队列长度,若监听队列超过backlog,服务器将不受理新的客户连接,客户端会受到ECONNREFUSED信息

accept

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd与上面一致
  • addr储存建立连接的客户端的ip地址,传入的实际参数通常是一个新建sockaddr_in结构体,并且初始化为0
  • addrlen为addr的长度sizeof
  • 成功会返回一个新的socket,它用于后续真正的信息传送与接收

connect

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
  • sockfd是在客户端创建的socket
  • serv_addr为服务端地址
  • addrlen为长度sizeof
  • 成功返回-,错误返回-1并设置errno,常见的errno是ECONNREFUSED和ETIMEOUT,其含义如下:
    • ECONNREFUSED表示端口不存在,连接被拒绝
    • ETIMEOUT表示连接超时

recv

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd为建立连接后的sock,对于服务端是accept返回的socket,对于客户端则是唯一建立的那个socket
  • buf,存放数据的缓存,事先建立,作为实参传入
  • len,期望的长度,实际接受的长度<=期望的长度
  • flag一般设置为0即可,设置为MSG_OOB为发送紧急数据(带外数据)
  • 成功返回实际读取到的数据的长度,失败返回-1并设置 errno

send

ssize_t recv(int sockfd, const void *buf, size_t len, int flags);
  • 和revc基本一致

recvfrom和sendto

recv和send是使用与TCP的接口,recvfrom和sendto是适用于UDP的接口

ssize_t recvfrom(int sockfd, void* buf, sieze_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);

ssize_t recvfrom(int sockfd, void* buf, sieze_t len, int flags, struct sockaddr* dest_addr, socklen_t* addrlen);

多了两个参数:

  • src_addr或是dest_addr,表示所要发送的地址或所要接收的地址,因为UDP不是面向连接的,所以每次传送或接受信息都要指定地址
  • addrlen长度sizeof

close

int close(int fd);
  • fd是待关闭的socket
  • 并非立刻关闭,而是将fd的引用计数减1,只有fd的引用计数为0时,才真正关闭
  • 非要立刻关闭可以用
int shutdown(int sockfd, int howto)
  • howto可选SHUT_RD关闭读的一半,SHUT_WR关闭写的一半,SHUT_RDWR同时关闭读写

hello-world示例

环境说明

一共设置了三台PC,腾讯云的一台centos 7服务器作为服务端,本地在Virtual Box开启两台ubuntu18.04虚拟机作为客户端。

端口开放设置

ubuntu18.04默认防火墙ufw一般不会开启,如果启用了,需要增添规则开放通信端口

腾讯云的centos 7服务器的端口设置需要注意两点:

  • 需要在腾讯云控制台的安全组设置中开放需要通信端口,不过一般默认是全开放,为了安全可以全关闭后只开放特定端口
  • centos 7 默认采用iptables防火墙,对于iptables防火墙的具体介绍打算在新开一个坑进行介绍,这里只做简单说明。服务端准备采用3400端口进行通信,则需要
iptables -I INPUT 2 -p tcp --dport 3400 -j ACCEPT 
service iptables save
service iptables restart

第一行指令:表示在规则链开头第二条的位置新增一个针对流入信息的规则,该规则接受3400端口的TCP应用服务。第二条指令对规则进行保存,否则重启服务或重启服务器会失效,第三条重启服务(好像不一定要重启服务)

关键代码

[]: https://github.com/zhangxixi0904/Cpp-Network.git “Linux网络编程的hello-world”

运行效果

在服务端运行

./tcp_net_server 172.0.5.171 3400 # 172.0.5.171是腾讯云的内网地址

在客户端分别运行

./tcp_net_client 服务端外网ip 3400

客户端显示

服务端显示

参考文献

  1. https://www.cnblogs.com/jfyl1573/p/6476607.html

  2. Linux高性能服务器编程 游双

  3. https://www.cnblogs.com/bazingafraser/p/8507602.html

请我喝杯咖啡吧~

支付宝
微信