Fork me on GitHub

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

NLP连载系列:

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

本文将介绍两种比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笔记(一):word2vec超详细解析
  5. CS224N笔记(二) Lecture1~2 :深入理解Glove原理
  6. CS224N笔记(三) Lecture 6~7:深入理解循环神经网络RNN模型
  7. CS224N笔记(四) Lecture 7:循环神经网络RNN的进阶——LSTM与GRU

本文将从语言模型的概念出发,引出循环神经网络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笔记(一):word2vec超详细解析
  5. CS224N笔记(二) Lecture1~2 :深入理解Glove原理
  6. CS224N笔记(三) Lecture 6~7:深入理解循环神经网络RNN模型
  7. CS224N笔记(四) Lecture 7:循环神经网络RNN的进阶——LSTM与GRU

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笔记(一):word2vec超详细解析

NLP连载系列:

  1. NLP入门:文本特征的表示方式
  2. n-gram的原理
  3. CS224N笔记(一):word2vec超详细解析
  4. CS224N笔记(二) Lecture1~2 :深入理解Glove原理
  5. CS224N笔记(三) Lecture 6~7:深入理解循环神经网络RNN模型
  6. CS224N笔记(四) Lecture 7:循环神经网络RNN的进阶——LSTM与GRU

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}