Language Model and Recurrent Neural Networks (二)

本文是我去年十月份在公司的团队技术分享会里面分享过的内容,分享这个内容的初衷是我发现自己对RNN(本文均指Recurrent Neural Networks而非Recursive Neural Networks)比较陌生,想找个时间攻克一下,以便以后有此类工作需求可以快速上手。另外本文加入了CS224N关于语言模型和RNN的课堂内容。因此本文属于科普性质的文章,基本上RNN的细节会涉及到,但并非每个细节都会深入去研究。
因为篇幅较长,所以重新编辑了一下,把标题提到的两部分分开了,本文是RNN部分。


RNN浅析

1.神经网络种类

相信我们之中大部分人,接触神经网路都是从前馈神经网络开始的,在我印象中,以前大多数时候都被叫做BP神经网络或者人工神经网络,当然随着深度学习的发展,这些称呼基本上已经消失了。

Karpathy把神经网络按输入输出的数量或者映射关系归纳成几种类型:

图一 神经网络种类

One-to-One这种类型应该是最常见的,譬如Wide&Deep之类的对单个输入数据产生一种输出的网络,又或者AutoEncoder,都属于此类。One-to-many可能稍微少见一点,比较典型的应用应该是Image Captioning,以图像作为输入,接着生成若干词的文本。Many-to-one是以不定长的序列作为输入,返回固定的内容,例如文本分类、情感分析。Many-to-many分两种情况,一个是非对其的,例如机器翻译,不同目标语言翻译出来的词数和顺序都不一定跟原语言相同;另一种是对其的情形,这种就是序列标注的相关应用了,需要对每个词产生对应的tag,例如词性标注、命名实体识别、分词等等。

2.循环神经网络

因为通常它的输入和输出是固定size的,并且前馈神经网络对历史信息没有记忆,所以普通的前馈神经网络并不擅长处理序列类型的数据。RNN在这方面做了专门的改进:

图二 循环神经网络

图中等号左边是RNN的结构,右边是摊开(unroll)之后的样子。注意:这里只是逻辑上或者流程上的“展开”,而非物理上的展开,因此图中每一个A是一模一样的东西,并非每个时间步上都有一个独立的RNN

不难看出,RNN的每个time step需要以上期的状态和本期输入统一作为输入值,也即,每一期的计算都综合了当前和历史的信息,而不是固定的几个上下文信息。图中的A代表RNN的cell,我们可以先观察一下里面的结构:

图三 循环神经网络内部结构

接着我们用数学描述一下这个过程:

ht=tanh(Win[ht1,xt]+bin) h_{t} = tanh(W_{in}\cdot{[h_{t-1},x_{t}]}+b_{in})

上式表示ht1h_{t-1}xtx_t先拼接起来,再乘以权值加上偏置,这个过程其实跟前馈神经网络没什么区别。根本区别就在于,RNN的每个cell的输出都会copy一份传递到下一个时间步,而下一个时间步又重复这样的操作。当然,每一步也可以根据实际场景的需要增加一个输出层的计算

y^t=f(Woutht+bout) \hat{y}_{t} = f(W_{out}\cdot{h_{t}}+b_{out})

这里啰嗦一句,很多深度学习框架都有unit这个概念,并且RNN的相关API中都要用户传入unit的数量,那么unit是什么?据我参考各个文档得出来的结果是,unit其实就是隐藏层的维度,即上图tanh输出的维度。

3.传统RNN的缺点

模型参数的更新是依靠链式法则进行的,而越靠前的层,其梯度信息就包含越多的偏导项,如果偏导项数值较小,在多次乘积之后就可能造成梯度消失(gradient vanishing),如果较大则可能造成梯度爆炸(gradient exploding)。在梯度消失情况下,RNN因为时间步的增多导致误差信号在往早期的时间步传递中变得越来越弱;在梯度爆炸情况下,模型参数的优化过程会出现大幅度的变动。但无论哪种情况都导致相同的结果,那就是模型无法收敛。

梯度消失和梯度爆炸并不是RNN特有的问题,而是Deep Learning中很普遍的问题,也正是这两个问题导致发展早期,网络层数不能加得太深。

既然出现了问题,聪明的人们总是会想到办法去解决。对于梯度爆炸,人们提出了gradient clipping的方式去避免单步偏导过大,也即是只需要把梯度按一个阈值去截断就能比较好的解决。但梯度消失这个问题是没办法解决的,咱总归不能刻意去放大梯度信号吧?

上面提到的是一个general problem,也是从训练角度发现的问题,下面咱们提一个在序列建模中比较特有的问题,而这是则是从推断角度发现的。

假设h是一个实数,咱们建立一个不包含bias和误差项ϵ\epsilon的一阶自回归模型:ht=αht1h_{t} = \alpha\cdot{h_{t-1}},递归下去可以得到ht=i=1tαihtih_t = \sum_{i=1}^{t}{\alpha}^{i}{\cdot}h_{t-i}。不难发现,time step距离越远的项的权值是呈指数膨胀或者衰减的,在很多实际中,呈指数衰减的情况会比较多,因为距离越远权重越大不太合乎常理,如果权重α<1|\alpha| < 1,那么距离越远的项对最终结果的影响会越小,模型对历史信息的记忆能力就十分有限了。

RNN亦是如此,所以RNN不具备长期记忆能力。但最见鬼的是,实际情况并不总是时间距离越远影响越小,例如在气象数据上可能有明显的周期性特征,在文本数据中,上文很远很远的地方可能有一个对下文产生关键影响的词。传统RNN又不具备长时记忆,这就要求必须在这个基础上做一些改进。

4.LSTM & GRU

LSTM是RNN的改进版本,它是为了增加RNN的长时依赖能力,而GRU则是LSTM的简化版本,为了保证性能的同时可以减少参数并快速计算。

咱们先看看LSTM的内部结构(继续感谢Colah)

图四 LSTM的cell结构

看着很复杂?那就对了,接下来我就可以继续装13了。

与传统RNN一样,主要计算过程的input都是当期输入xtx_t和上期输出(这里讲的输出是hidden state)ht1h_{t-1}。但与传统RNN不同的是LSTM有个独立的记忆单元CtC_{t},也叫cell state,这个东西可以通过一条绿色通道通往下游,以达到长期的记忆能力。

再来,咱们看看图中的三个σ\sigma。这三个东东就是LSTM中的门限单元,分别叫输入门(input gate),遗忘门(forget gate)和输出门(output gate)。三个门作用的共同点就是控制信息量,它们都使用sigmoid函数作为激活函数,而sigmoid函数的值域是(0,1),所以这三个门可以做到对信息的屏蔽和开放。而这三个门作用上的不同点在于它们负责的地方是不一样的:输入门控制本期新增的信息量,即控制多少新信息加入;遗忘门控制历史记忆单元的信息量,即控制多少旧信息加入;输出门控制本期最终输出hidden state的输出量,即控制多少综合信息输出

怎么样,还是很绕?咱们从数学式角度看看,首先三个门的计算:
it=σ(Wi[ht1,xt]+bi)ft=σ(Wf[ht1,xt]+bf)ot=σ(Wo[ht1,xt]+bo) i_t = \sigma(W_{i}\cdot{[h_{t-1},x_{t}]}+b_{i}) \\ f_t = \sigma(W_{f}\cdot{[h_{t-1},x_{t}]}+b_{f}) \\ o_t = \sigma(W_{o}\cdot{[h_{t-1},x_{t}]}+b_{o}) \\
它们的计算方式是一样的,只是各自有各自的参数。接着,咱们看看记忆单元的更新:
Ct=ftCt1+itC~t C_{t} = f_{t}*{C_{t-1}} + i_{t}*{\tilde{C}_{t}}
其中,本期的cell state计算跟三个门类似,但激活函数不同:
C~t=tanh(Wc[ht1,xt]+bc) \tilde{C}_{t}= tanh(W_{c}\cdot{[h_{t-1},x_{t}]}+b_{c})
最后,输出并传递到下期作为输入的hidden state需要用输出门控制一下:
ht=ottanh(Ct) h_{t} = o_{t}*tanh(C_{t})
上面的*表示按位相乘。

GRU基本上与LSTM是类似的,只是在遗忘门这里做了简化,也不需要额外的cell state。先看看图

图五 GRU的cell结构

GRU只有两个门,分别叫更新门(update gate)和重置门(reset gate)。更新门相当于LSTM的遗忘门和输入门,GRU在这块做了简化,用一个门代替了LSTM的两个门:
zt=σ(Wz[ht1,xt]+bz)rt=σ(Wr[ht1,xt]+br) z_{t} = \sigma(W_{z}\cdot{[h_{t-1},x_{t}]}+b_{z}) \\ r_{t} = \sigma(W_{r}\cdot{[h_{t-1},x_{t}]}+b_{r}) \\
本期hidden state的计算:
h~t=tanh(Wh[rtht1,xt]+bh) \tilde{h}_{t} = \tanh(W_{h}\cdot[{r_{t} * h_{t-1}},x_{t}] + b_{h})
hidden state的更新计算:
ht=ztht1+(1zt)h~t h_{t} = z_{t} * h_{t-1} + (1-z_{t}) * \tilde{h}_{t}
这里公式是搬原论文的,Colah博客中的更新计算权值是跟上式调过来的,但是我个人认为这个没多大关系。

LSTM跟GRU到底该用哪个呢?很多人都说它们效果都差不了太多,我觉得还是要根据情况决定,LSTM毕竟参数多,表征能力自然更强,如果训练数据足够的话,可以默认使用LSTM;GRU参数少速度快,数据量不多或者对速度要求比较高的话可以优先考虑GRU。

5.bidirectional RNN & multi-layers RNN

普通RNN默认是一个顺序去处理序列的,例如我们习惯都是从左到右。但在NLP领域,我们在某个time step上只知道某个方向上的历史信息是远远不够的,要知道,我们人类理解一个词、一段话所表达的意思也不是只根据上文去理解,还要根据下文去综合考虑,于是双向RNN就应运而生了。

图六 bidirectional RNN

例如上图的输入序列 the movie was terribly exciting!,如果我们要做情感分析,只用单向RNN的话,很容易到’terribly’这个位置就产生负面的特征,因为它之前的序列’the movie was’是偏中性的,而terribly这个词本身偏负面。但如果考虑它的后文’exciting’,那么就很容易得到准确的判断。所以我们需要添加两个方向的RNN,以便综合考虑整个语境。

这里提到的RNN可以是传统RNN,也可以是LSTM或者GRU,但无论哪种,两个方向得到的hidden state都可以拼接起来作为后面layer的输入。但必须注意的是,在自然语言生成之类的场景,双向RNN是不适用的,因为我们在input的时候不知道下文是什么,所以双向RNN只适用于上下文都完整的情况,例如NER、文本分类等。

除了双向RNN之外,还有一种高级版本是多层堆叠成多层RNN,就像前馈网络一样。为什么要堆叠起来?原因也很简单,网络越大,表征能力也越强呗。

图七 bidirectional RNN

6.BPTT

RNN模型的优化方法有个特别的名字——BPTT(back-propagation through time)。听起来好像有点复杂,其实跟普通的backprop没有什么区别。

那还说啥呢?咱们可以回想一下上面提到的神经网络多种类型。RNN的特点是它并不是每个时间步都会产生output(这里指的输出层的输出结果),有可能在最后一步再产生一个output,有可能每一步产生的hidden state会用作后面层的输入,也有可能每一步都直接产生一个output。但不管哪种情况,只要我们把RNN按时间维度展开成一个流程图,我们就能发现,每一步的误差信号都包括来自自身的(spatial)以及来自下游的(temporal)。

图八 BPTT

来自自身的部分很好理解,因为跟普通前馈网络是一样的。来自下游的部分的,可能稍微复杂一点,但要理解误差传递的话,其实只需要把整个计算流程画出来,每条带权边都求偏导,再找到对应的backprop方向就okay了,不要被BPTT这个名字吓怕了,本质上其实还是chain rule而已。

参考资料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章