五、神经网络语言模型

在 N-gram 模型中,我们用统计的方法;而下面,我们要把语言模型看做分类任务,然后训练出一个神经网络语言模型。它作为分类任务,相当于给定前 $i-1$ 个词,要确定第 $i$ 个词,它的可能性包括词汇表中的所有词,即 $\vert V\vert+1$。

那还说啥了,线性模型整起来!但是问题在就在上面了:可能性太多了。这个语言模型是确实存在的,在 90s 是很流行的,但是可以预见的是,这个模型非常垃圾,只能靠着随机梯度下降来加强性能。

1. 神经网络模型

最初的线性模型可以表示成:$z=\sum\limits_i w_ix_i+b$,其中 $x$ 是特征,$w$ 是权重。线性的模型有诸多限制,因此套一个激活函数让它不再线性,即 $y=f(z)$,当然 softmax 也算一个激活函数。有这么些激活函数:

  • Sigmoid:$y=\delta(z)=\frac{1}{1+e^{-z}}$,把 $y$ 限制在 0 到 1 之间,并且可微。
  • tanh:$y=\tanh(z)=\frac{e^z-e^{-z}}{e^z+e^{-z}}$,把 $y$ 限制在 -1 到 1 之间,且也可微,被广泛使用。
  • ReLU:$y=ReLU(z)=\max(z,0)$,更靠近线性函数。

我们对最初的线性函数 $z$,反复叠线性函数和非线性函数,增大了模型的参数数量。但是问题在于,一个是特征 $x$ 依旧没能解决,另一个是增大参数数量后运算压力会不会太大。

2. 前馈神经网络语言模型

采用非常简单的思路,先套一个 $\tanh$,再接一个线性模型,最后套一个 softmax。就长这样:

\[p(w_n|w_1,\cdots,w_{n-1})=softmax(b+\sum_j w_jA_j+W\tanh(u+\sum_j w_jT_j))\]

特征就是简单的前面的所有词,第一层的权重是 $T$,偏置为 $u$,第二层的权重是 $A,W$,偏置是 $b$。其中,第一层是隐藏层(Hiddle Layer),我们假设它的大小为 $H$。所有表示单词的向量都得先进入隐藏层,再经过外层输出。

问题在于,每个词 $w_i$ 到底应该怎么表示成数学形式?One-hot 向量太过稀疏了,我们可以压缩成更短的,比如指定其长度为 $d$。然后,需要设定一个转换矩阵 $M_{\vert V\vert\times d}$,把长度为 $\vert V\vert$ 的向量转化成 $d$ 维向量。只要获得了这个矩阵,对于任何词,都能通过 $m_i=w_i^T M$ 转化成 $d$ 维向量了,这个大家都会,不就词嵌入(word embedding)嘛,之前词义消歧的时候就学过了。这里不一样的是,矩阵 $M$ 是可以学习的参数

我们回到上述模型,现在的参数除了上述五个以外又多了一个 $M$,同时我们可以确定各个矩阵的形状:$M(V,d),T(d,H),u(H),W(V,H),A(d,V),b(V)$,因此也可以确定参数数量。

这个模型相较于旧模型的提升在于:

  • 不再需要人工特征,自动学习 $M$
  • 使用词嵌入,泛化能力更强
  • 多层网络超越了线性模型的能力

但它的局限性是:

  • 仍然是 N-gram 模型,因为一个模型只能用于计算第 $n$ 个词;
  • 不能很好区分顺序;
  • 参数很多,且不具有直接的可解释性

3. 卷积神经网络(CNN)

现在我们回到最初的问题,已知前 $t-1$ 个词,要求 $w_t$,但是我们现在舍弃马尔科夫假设,也就是看前面所有的词,同时保留词嵌入操作 $M$,希望通过卷积操作提取特征。为什么用卷积呢,因为它不用担心词数太多而参数爆炸。公式如下:

\[X^{(1)}[k,m]=f(b_k+\sum_{i=1}^d\sum_{j=1}^{sw}C^{(k)}[i,j]\cdot X^{(0)}[i,m+j-1])\]

我们一个个解释。首先,$k$ 表示第 $k$ 个卷积核,$m$ 表示从第 $m$ 个词的位置往后开始做卷积运算。之前说过 $X$ 中每个词的向量长度都为 $d$,因此每个卷积的形状都是 $d\times sw$,是个一维卷积。$C$ 就是卷积核,它与 $X$ 的对应位置做点积,然后加上 $b_k$,最后经过一个激活函数。如果难以理解,想象一下:$X$ 本来是 $t-1$ 个词组成的向量,然后每个词向下伸展成了 $d$ 维向量,因此 $X^{(0)}$ 是个 $d\times t-1$ 的矩阵。然后一个形状为 $d\times sw$ 的卷积核放上去,每次做运算得到一个值,随后一直向右移动,得到 $t-1-sw$ 个值,并排放置。把每个卷积核都这样算,得到的向量一排排放好,就得到了 $X^{(1)}$。$X^{(1)}$ 等中间运算结果都是隐藏层。

假如算到最后得到了 $X^{(D)}$,它的形状是 $d\times(t-1)$。注意这里的 $d$ 不见得是最初的 $d$ 了,是由卷积核的数量决定的;而 $t-1$ 一般通过 padding 保证了长度不变。我们此时需要池化以变回 $d$ 维向量,有两种方法:第一种是最大池化,即对于每一列,都只取最大值;第二种是平均池化,顾名思义就是取平均数。最后我们还可以做一次 softmax 保证得到的是概率。

这样我们就得到了卷积神经网络(Convolutional Networks, CNN)。这个模型相较于前馈模型的提升在于:

  • 上下文不限;
  • 局部的建模更强,滑动窗口就能学习 n-gram 的特征;
  • 参数共享,更高效;
  • 结构更灵活,可以套多层卷积。

但是仍然有一定问题:每个位置仍然只能学习到自己周围的 $n$ 个词。

4. 循环神经网络(RNN)

简单来说,就是把前面所有的状态用一个隐藏状态 $s_i$ 表示。具体来说公式是这样的,假设历史词汇 $h_{1:i-1}=w_1,\cdots,w_{i-1}$,那么我们对词 $w_i$ 用一个向量 $s_{i-1}$ 来储存这个历史。假如已经确定了 $w_i$,我们就更新出 $s_i$,并预测下一个次 $w_{i+1}$,因此有:

\[s_0=0\\ s_i=\delta(m_{w_i}W_w+s_{i-1}W_s+b)\\ p(w_{i+1}^*|h_{1:i})=softmax(s_iW_p)\\\]

这里的 $W_w,W_s,W_p,b$ 都是共享的参数,$m_{w_i}$ 是当前词的 embedding。公式还是挺简单的,也容易理解。训练目标就是使得每个词的预测更加精准,也就是使每个 $p(w_i\vert h_{1:i-1})$ 越大越好。整个句子的损失值用交叉熵损失来讲就是:

\[L=-\sum_i \log p(w_i|h_{1:i-1})\]

这样我们就构建了循环神经网络(Recurrent Neural Network, RNN)。它的优点是显然的:能够记住全局的记忆。但是我们还得痛批它一下:

  • 当句子很长时,梯度从后往前累计,会反复相乘从而越来越小,导致前面的词更新很小;
  • 不仅如此,记忆容易被新输入覆盖。

5. 双向 RNN(BiRNN)

这个其实很简单,我们额外运行一个从右到左的 RNN,然后把对应词的输出对齐。这样我们在同一个词中,既有前文的记忆,又有后文的记忆,因此效果会好一点。但是注意语言模型不能用这一个模型,因为语言模型的条件是只能知道前文,不能知道后文的,这相当于作弊了。

6. LSTM

我们之前说过了 Vanilla RNN 有两个严重的问题,一个是梯度会指数衰减或放大,另一个是长期的记忆会被冲淡。因此我们引入了门控机制,引入一条加法通道来维护长期记忆,用(gates)控制信息。这一条路有两个模型,一个是本节的 LSTM,另一个是下一节的 GRU。

所谓 LSTM(Long Short-Term Memory),就是将长短记忆分开保存,然后用门控来调整它们的比重。我们先给个图,然后再解释。

诶?图片怎么不见了?

首先我们明确一下输入,我们将词 $w_t$ 变成向量形式 $m_{w_t}$,还有对外输出的短期记忆 $s_{t-1}$,以及用于保留长期记忆的细胞状态(cell state)$c_{t-1}$,这个细胞状态就是我们在 RNN 上做出的第一个改进。那么我们在一个循环内,需要获取新的输出 $s_t$,以及更新长期记忆 $c_t$。

然后我们看看它的整体思路。整体思路是这样的:首先对词向量和短期记忆进行运算,得到了不受长期记忆影响的,反映当前词比重的值。然后,我们有三个门:$i$ 是 input gate,表示有多少要写入长期记忆;$f$ 是 forget gate,表示有多少要被擦除;$o$ 是 output gate,表示要输出多少内容;以及一个候选记忆 $g$,我不知道为啥有个这玩意。反正就是,我们的输入被兵分四路,有三个去影响了长期记忆,还有一个等着被输出。为什么是等着呢,因为它在等待长期记忆来支配它,长期记忆被更新后,就对输出进行调整,然后就得到了 $s_t$。

思路了解了,下面就是公式。首先我们来看经过门控后的输出:

\[i_t=\sigma(m_{w_t}U_i+s_{t-1}W_i+b_i)\\ f_t=\sigma(m_{w_t}U_f+s_{t-1}W_f+b_f)\\ o_t=\sigma(m_{w_t}U_o+s_{t-1}W_o+b_o)\\ g_t=\tanh(m_{w_t}U_g+s_{t-1}W_g+b_g)\]

可以看到他们四个简直如出一辙,都是一个线性模型,加上一个激活函数,没有任何记忆难度吧。$\sigma$ 的值域是 0 到 1,保证了三个门的输出都是表示比重,控制了每维的信息流量。接着我们调整 cell state:

\[c_t=f_t\odot c_{t-1}+i_t\odot g_t\]

$f_t$ 就是应该遗忘的比重,这个值越小,就说明长期的记忆越该被遗忘。然后加上应当写入的内容,就得到了新的长期记忆。请注意 $c_t$ 的更新是通过加法实现的,这是梯度能沿时间传播而不消失的关键。最后就只要更新输出了:

\[s_t=o_t\odot\tanh(c_t)\]

把长期记忆经过一个激活函数,然后与输出门做点乘,轻松得到输出。

7. GRU

GRU(Gated Recurrent Units)就是在 LSTM 的基础上做了小小的优化:删去了 cell state,把三个门变成了两个门,即 reset gate 和 update gate。依旧先上图:

诶?图片怎么不见了?

GRU 为啥能做这样的优化,关键就是它让一些功能合并了,比如之前的输出是先不看长期记忆获取 output gate,再让长期记忆去调整;现在我们直接让 $s_t$ 保留长期记忆,先利用当前输出去获取更新后的记忆,再对这个记忆和曾经的记忆进行插值获得输出。同时还把 $g_t$ 和 $i_t$ 合并了,直接变成 update gate。我们直接看公式吧。

首先是让输入的 $m_{w_t}$ 和 $s_{t-1}$ 经过两道门,跟 LSTM 一模一样:

\[r_t=\sigma(m_{w_t}U_r+s_{t-1}W_r+b_r)\\ z_t=\sigma(m_{w_t}U_z+s_{t-1}W_z+b_z)\]

前者是 reset 门,决定多大程度忘掉过去;后者是 update 门,它是对旧的隐状态和新的隐状态进行插值的比重,比重越大说明新状态的比重越高。然后我们获取新状态:

\[\tilde{s_t}=\tanh(m_{w_t}U+(r_t\odot s_{t-1})W+b)\]

有点复杂,其实就是两步,第一步我们让记忆被遗忘一下,就是 $r_t\odot s_{t-1}$。第二步,让被遗忘的记忆与输入词进行一波线性运算,得到了新状态。最后我们线性插值得到输出:

\[s_t=(1-z_t)\odot s_{t-1}+z_t\odot\tilde{s_t}\]

就这么简单。

最后我们在此比较一下三种 RNN 的区别。首先是参数量,最少的肯定是 RNN,因为它没有门控;其次 GRU 比 LSTM 的门要少,所以 GRU 的参数又比 LSTM 少。其次比较一下 GRU 和 LSTM,首先 GRU 的训练肯定更快,但是合并了长短记忆的代价就是对长期记忆的捕捉能力变弱了(没有 cell state),以及没法控制记忆的暴露(没有 output gate),这是不可避免的。

Leave a comment