美文网首页
RNN语言模型

RNN语言模型

作者: 612twilight | 来源:发表于2020-03-29 15:52 被阅读0次

学习笔记

循环神经网络

循环神经网络(Recurrent Neural Networks)是另一种可以用来进行语言模型建模的网络结构,之前提到的前向神经网络语言模型是以前n-1个词作为输入来预测当前词,这种处理方式是解决不了时序问题的,而循环神经网络则可以解决时序问题。
循环神经网络引入了一个中间隐藏层h,该隐藏层的状态可以沿着时间将信息传给下一次预测。直观来说,就是将第t-1时刻的隐藏层的状态h_{t-1}作为第t时刻模型预测或者训练的一个输入,这里的时刻也可以叫做时间步。
下图是一张对RNN进行时间铺开的展示图,每个时间单元都将自身的隐藏层作为下一个时间单元的输入,这张图上面并没有画出第一个时间单元接受的隐藏层的输入,事实上,第一个单元也接受了输入,一般是一个初始化的0向量。

RNN.png
由于神经网络内部每层到底在干什么,我们无从得知,但是从直观上来说,这种RNN的结构确实将过去的信息拿来作为当前预测的参考,至于其中具体是怎么将过去信息编码,以及具体是怎么将过去信息解码,我们现在依然无从得知,但是既然事实证明,这种结构是可行的,那即使我们不知道原理,也可以去使用它解决我们的问题,科技发展会给我们答案。
上述的结构依然有局限性,就是它只能利用近期的信息去编码我们需要的格式,如果时间步的跨度过大,原先的信息会在传递中逐渐丢失。
假设现在有这样一个任务,考虑到下面这句话“I grew up in France… I speak fluent French.”,现在需要语言模型通过现有以前的文字信息预测该句话的最后一个字。通过以前文字语境可以预测出最后一个字是某种语言,但是要猜测出French,要根据之前的France语境。因为这次的有用信息与需要进行处理信息的地方之间的距离较远,这样容易导致RNN不能学习到有用的信息,最终推导的任务可能失败。

理论上RNNs是能够处理这种“长依赖”问题的。通过调参来解决这种问题。但是在实践过程中RNNs无法学习到这种特征。Hochreiter (1991) [German]Bengio, et al. (1994)深入研究过为什么RNNs没法学习到这种特征。

标准RNNs结构

所有循环神经网络都具有神经网络的重复模块链的形式。 在标准的RNN中,该重复模块将具有非常简单的结构,例如单个tanh层。标准的RNN网络如下图所示


标准RNN.png

在单个t时刻的计算过程是:
{{{h}}_t} = \tanh (W \cdot [{{{h}}_{t - 1}},{x_t}] + b)
其中[{{{h}}_{t - 1}},{x_t}]是表示这两个向量在axis=-1维度上面进行拼接。有的RNN计算公式是将h_{t-1}x_t拆开来,然后各自乘以权重矩阵,但如果在最后一维合并起来,然后将权重矩阵也在最后一维合并起来,其结果是一样的。

rnn单一时刻的计算流程.jpg

LSTM结构

LSTMs也具有这种链式结构,但是它的重复单元不同于标准RNN网络里的单元只有一个网络层,它的内部有四个网络层。LSTMs的结构如下图所示。


LSTM.png

LSTM的核心是细胞状态,用贯穿计算单元的水平线表示。这个状态区别于隐藏层的状态,它只是很少的参与信息交换,所以可以保存较远的时间步的信息。我们可以从下图看到,细胞状态在一个时间步里面只参与三次信息交互:两次接受信息,一次输出信息参与计算。这三个操作被称为门,分别称为忘记门、输入门和输出门。


cell.png

LSTM的第一步就是决定细胞状态需要丢弃哪些信息。这部分操作是通过一个称为忘记门的sigmoid单元来处理的。它通过查看h_{t-1}x_t来输出一个元素为0到1之间的向量,该向量里面的0到1表示C_{t-1}里面的信息保留多少,丢弃多少,0表示不保留,1表示都保留。后面会将输出的f_t与细胞状态C_{t-1}进行点乘(对应元素相乘),更新状态信息。

忘记门.png

下一步是决定给细胞状态添加哪些新的信息。这一步又分为两个步骤,首先,利用h_{t-1}x_t通过一个称为输入门的操作来决定更新哪些信息。然后利用h_{t-1}x_t通过一个tanh层得到新的候选细胞信息,这些信息可能会被更新到细胞信息中。这两步描述如下图所示,最后会将这两步操作的信息先点乘获取那些信息需要更新进去,然后加到cell状态中。

输入门.png

将上述的忘记门和输入门的结果更新到细胞状态中,更新过程像之前介绍的,先将之前的细胞状态C_{t-1}与忘记门的输出f_t点乘,然后再与输入门计算出来的结果i_t \ast \tilde C_t相加。

更新cell状态.png

最后我们利用更新完的细胞状态输出参与到当前计算,这里需要将输入经过一个称为输出门的sigmoid层得到判断条件,然后将细胞状态经过tanh层得到一个-1~1之间值的向量,该向量与输出门得到的判断条件相乘就得到了最终该RNN单元的输出。该步骤如下图所示:

输出门.png

lstm的计算过程:这里需要注意cell状态的维度和隐藏层的维度是一样的,隐藏层的输出就是细胞状态的点乘


LSTM计算流程.jpg

最后回到语言模型上面,使用RNN进行语言模型建模,那么输入的x_t就是经过embedding的结果,并且最后对于每个h_t的输出上再接一层全连接层,输出词典数目的维度,最后再加一层softmax就可以得到下一个词输出的概率

一般可以用交叉熵损失函数的输出来计算下一个词是目标词的概率,可以从交叉熵的损失函数的计算过程来证明。只有目标词所在的y_{target}才是1,其余都是0,所以loss就是目标词的log(p_{target})
loss=-\sum_{j=0}^{J}(y_{j}log(p_{j}))

同时训练和预测的时候,需要在句子的开头和结尾加上<bos>和<eos>作为标记。
训练和预测过程:初始状态,输入<bos>,预测下一个字符,然后根据预测值和实际值的误差,进行反向传播,之后输入实际的下一个字符来预测之后的字符。强调输入实际的下一个字符是因为这是在进行语言模型建模,而不是做encoder-decoder那种生成模型,生成模型是通过前一个生成的最可能的词作为当前输入来继续生成下一个词(这是贪婪搜索,还有别的方式生成比如beam search),这有着很大不同。

那这里怎么去求取一个句子的概率呢?上面有说到在模型最外层是加了一层softmax的,这就是预测词的输出概率分布p({s_t}|{s_1},{s_2},{s_3}...{s_{t-1}}),这里可以认为是标准的语言模型概率计算方式,因为参与当前时间步计算的隐藏层h_{t-1}和细胞状态C_{t-1}记录了前面t-1个所有的词汇信息。虽然这种方式仍然有缺陷,一些新出来的模型已经进行了改进,比如双向LSTM,transformer,bert等。

Bi-LM模型

上述的都是单向语言模型,但是实际上,我们在t时刻的词的概率不只会依赖于之前的词,有时候还会和后面的词有关,比如说there is a tree,这里的is就是单数形式,依赖于后面的a tree来确定。所以就有了两个单向的LSTM(并不是Bi-LSTM)去更好的进行语言建模。

给定N个token的序列(t_1,t_2...t_N),前向语言模型的表示方法为:
{{p}}({t_1},{t_2},...{t_N}) = \prod\limits_{k = 1}^N {p({t_k}|} {t_1},{t_2},...,{t_{k - 1}})
同样的,后向语言模型的表示方法为:
{\rm{p}}({t_1},{t_2},...{t_N}) = \prod\limits_{k = 1}^N {p({t_k}|} {t_{k + 1}},{t_{k + 2}},...,{t_N})
前向模型和后向模型都是用同样的方式去预测下一个词,只是方向有点不通,另外ELMo不光是两个反方向的的LSTM模型叠加,还可以是多层的两个反方向LSTM叠加,因此会有多个细胞状态和隐藏层,但是其原来和单层的是一样的,只是上层的LSTM接受的输入是下层的隐藏层(也可以加上residual connect),而其中两个不同方向的LSTM模型则是互不干扰的,他们的联系就只有输入的token的embedding是共用的,以及最后的全连接加softmax是通用的。如果计算loss的时候,就将前后向的两个loss相加(个人理解,相加比较简单方便),训练的时候比如预测词token_k,输入token_k左边的词汇,经过正向LSTM得到一个预测值和loss,同时输入token_k右边的词汇,经过反向LSTM也得到了一个预测值和loss,loss相加就可以反向传播了。
最后的极大似然估计则为:
\sum\limits_{k = 1}^N {(\log p({t_k}|{t_1},{t_2},...,{t_{k - 1}},{\theta _x},{{\vec \theta }_{LSTM}},{\theta _s})} + \log p({t_k}|{t_{k + 1}},{t_{k + 2}},...,{t_N},{\theta _x},{{\mathord{\buildrel{\lower3pt\hbox{$\scriptscriptstyle\leftarrow$}} \over \theta } }_{LSTM}},{\theta _s}))

其中token 的embedding表示的参数\theta_x以及softmax 层(全连接加softmax转成字典的向量维度)参数\theta_s前后向是通用的,LSTM 的参数按照方向取不同值。

从语言模型的角度来说,上面的已经说完了。但是从语言模型中获取动态embedding还需要再多做一步。
对于tokent_k,在L层的ELMo模型中,总共会得到2L+1个特征。其中2L个特征属于每个隐藏层的状态,双向就是2L个,还有一个初始化的token的encoding的特征。通常获取动态embedding会有两种方式,一种直接用最上层的状态作为embedding的值。另一种是将这2L+1个特征线性组合起来。以下是对于token_k,其2L+1个特征为:
{R_{\rm{k}}} = \{ x_k^{LM},\vec h_{k,j}^{LM},\mathord{\buildrel{\lower3pt\hbox{$\scriptscriptstyle\leftarrow$}} \over h} _{k,j}^{LM}|j = 1,2...L\} = \{ h_{k,j}^{LM}|j = 0,1,...L\}
其中h_{k,0}^{LM}就是bi-lstm进行embedding的特征
h_{k,j}^{LM} = [\vec h_{k,j}^{LM};\mathord{\buildrel{\lower3pt\hbox{$\scriptscriptstyle\leftarrow$}} \over h} _{k,j}^{LM}]\\ h_{k,0}^{LM} = x_k^{LM}
最终作者是通过如下方法组合各层特征:
ELM{\rm{o}}_{{k}}^{task} = E({R_k};{\theta ^{task}}) = {\gamma ^{task}}\sum\limits_{j = 0}^L {s_j^{task}h_{k,j}^{LM}}
其中s_j^{task}是一个softmax-normalized weights,原论文中对此没有详细说这是怎么构造的。但是凭字面意思,这个是一个独立的weight,然后这个s^{task}的维度是L+1,同时满足和为1(因为softmax-normalized嘛),同时既然是weight,那么就应该是可以学习的,所以大家可以想象存在一个前置变量e,其维度也是L+1,然后我们将该变量进行softmax变化得到s^{task},此时s_j^{task}中的第j个元素就是
s_j^{task} = \frac{{{e_j}}}{{\sum\limits_{i = 0}^L {e{}_{_i}} }}
最后我们将这个新得到的向量作为各层间的权重矩阵。而{\gamma ^{task}}是根据下游任务所设置的scale参数,这个值对下游任务来说还是很重要的。
原文这里真的是一句话带过,以下是原文

sjtask.png

参考:
Understanding LSTM Networks
tensorflow 笔记8:RNN、Lstm源码,训练代码输入输出,维度分析
ELMo
[论文笔记]ELMo

相关文章

网友评论

      本文标题:RNN语言模型

      本文链接:https://www.haomeiwen.com/subject/derluhtx.html