放牧代码和思想
专注自然语言处理、机器学习算法
    This thing called love. Know I would've. Thrown it all away. Wouldn't hesitate.

CS224n笔记8 RNN和语言模型

目录

deepbirnn.png这次课推导RNN,介绍各种训练技巧和拓展变种。梯度消失的推导很详细,用Python演示很直观,也给出了用裁剪防止梯度爆炸的直观解释。笔记里还补充了用于机器翻译时的5项改进。cliping.pngen_decoder.png

语言模型

语言模型就是计算一个单词序列(句子)的概率($P(w_1,…,w_m)$)的模型。听上去很简单,做起来很难;听上去没什么用处,但用处非常多。比如在机器翻译中,判断译文序列中一种词序的自然程度高于另一种,判断一种用词选择优于另一种。

传统语言模型

为了简化问题,必须引入马尔科夫假设,句子的概率通常是通过待预测单词之前长度为$n$的窗口建立条件概率来预测:

$$\begin{equation}
P(w_1,…,w_m) = \prod_{i=1}^{i=m} P(w_{i} | w_1, …, w_{i-1}) \approx \prod_{i=1}^{i=m} P(w_{i} | w_{i-n}, …, w_{i-1})
\label{eqn:nat_model}
\end{equation}$$

为了估计此条件概率,常用极大似然估计,比如对于BiGram和TriGram模型,有:

$$\begin{align}
p(w_2 | w_1) &= \dfrac {count(w_1,w_2)}{count(w_1)} \\
p(w_3 | w_1, w_2) &= \dfrac {count(w_1,w_2,w_3)}{count(w_1, w_2)}
\end{align}$$

课件中写的unigrams and bigrams (conditioning on one/two previous word(s)),是错误的。

在数据量足够的情况下,n-gram中的n越大,模型效果越好。但实际上,数据量总是不如人意,这时候一些平滑方法就不可或缺。另外,这些ngram可能会占用上G的内存,在最新的研究中,一个1260亿的语料在140G内存的单机上花了2.8天才得到结果。

Bengio et al提出了第一个大规模深度学习自然语言处理模型,只不过是用前$n$个单词的词向量来做同样的事情(上文建模)而已,其网络结构如下:

bengio_03.png

公式如下:

$$\begin{equation}
\hat{y} = softmax (W^{(2)} tanh(W^{(1)}x+b^{(1)})+W^{(3)}x+b^{(3)})
\label{eqn:bengio_eqn}
\end{equation}$$

这里$W^{(3)}x+b^{(3)}$就是前$n$个单词词向量的线性运算,虽然这种模型名字里有“Neural”,但依然属于传统模型。

Recurrent Neural Networks

新的语言模型是利用RNN对序列建模,复用不同时刻的线性非线性单元及权值,理论上之前所有的单词都会影响到预测单词。

rnn.png

所需内存只与词表大小成正比,不取决于序列长度。

给定一个词向量序列: $x_1, ..., x_{t-1}, x_t, x_{t+1}, ... x_{T}$ ,在每个时间点上都有隐藏层的特征表示:

$$h_t = \sigma(W^{(hh)} h_{t-1} + W^{(hx)} x_{t})$$

其中,

$x_{t} \in \mathbb{R}^{d}$:是时间$t$时输入的单词的词向量。

$W^{hx} \in \mathbb{R}^{D_h \times d}$:用来condition输入词向量$x_t$的的权值矩阵。

$W^{hh} \in \mathbb{R}^{D_h \times D_h}$:用来condition前一个时间节点隐藏层特征表示$h_{t-1}$的权值矩阵。

$h_{t-1}  \in \mathbb{R}^{D_h}$: 前一个时间点 $t-1$ 的非线性激活函数的输出, $h_0 \in \mathbb{R}^{D_h}$ 是时间点 $t = 0$ 时的隐藏层初始状态。

$\sigma ()$: 非线性激活函数(sigmoid)

$\hat{y}_t = softmax (W^{(S)}h_t)$:在时刻$t$时输出的整个词表 $|V|$ 上的概率分布,$\hat{y}_t$是给定上文$h_{t-1}$和最近的单词 $x^{(t)}$预测的下一个单词。其中$W^{(S)} \in \mathbb{R}^{|V| \times D_h}$ , $\hat{y} \in \mathbb{R}^{|V|}$。

另外,这里的 $W^{(hh)} h_{t-1} + W^{(hx)} x_{t}$ 与拼接两个向量乘以权值矩阵的拼接是一样的。

未unroll的网络:

rnn_node.png

等效于

rnn_loop.png

损失函数

分类问题中常见的交叉熵损失函数:

$$\begin{equation}
J^{(t)}(\theta) = - \sum_{j=1}^{|V|} y_{t,j} \times log (\hat{y}_{t,j})
\label {eqn:rnn_loss}
\end{equation}$$

在大小为$T$的整个语料上的交叉熵误差为:

$$\begin{equation}
J = \dfrac{1}{T} \sum_{t=1}^{T} J^{(t)}(\theta) = - \dfrac{1}{T} \sum_{t=1}^{T} \sum_{j=1}^{|V|} y_{t,j} \times log (\hat{y}_{t,j})
\label {eqn:rnn_loss_T}
\end{equation}$$

如果以$2$为底数会得到“perplexity困惑度”,代表模型下结论时的困惑程度,越小越好:

$$\begin{equation}
Perplexity = 2^{J}
\label{eqn:perplexity}
\end{equation}$$

训练RNN很难

观察句子1:

"Jane walked into the room. John walked in too. Jane said hi to ___"

以及句子2:

"Jane walked into the room. John walked in too. It was late in the day, and everyone was walking home after a long day at work. Jane said hi to ___"

人类可以轻松地在两个空中填入“John”这个答案,但RNN却很难做对第二个。这是因为在前向传播的时候,前面的$x$反复乘上$W$,导致对后面的影响很小。

反向传播时也是如此。

整个序列的预测误差是之前每个时刻的误差之和:

$$\begin{equation}
\dfrac{\partial E}{\partial W} = \sum_{t=1}^{T}\dfrac{\partial E_t}{\partial W}
\label{eqn:bp_rnn_error}
\end{equation}$$

而每个时刻$t$的误差又是之前每个时刻的误差之和,应用链式法则:

$$\begin{equation}
\dfrac{\partial E_t}{\partial W} = \sum_{k=1}^{t} \dfrac{\partial E_t}{\partial y_t} \dfrac{\partial y_t}{\partial h_t} \dfrac{\partial h_t}{\partial h_k} \dfrac{\partial h_k}{\partial W}
\label{eqn:bp_rnn_chain}
\end{equation}$$

此处的$dh_t/dh_k$指的是在$[k, t]$时间区域上应用链式法则:

$$\begin{equation}
\dfrac{\partial h_t}{\partial h_k} = \prod_{j=k+1}^{t}\dfrac{\partial h_j}{\partial h_{j-1}} = \prod_{j=k+1}^{t}W^T \times diag [f'(j_{j-1})]
\label{eqn:bp_rnn_k}
\end{equation}$$

由于$h \in \mathbb{R}^{D_n}$,每个 $\partial h_j/\partial h_{j-1}$ 就是$h$的雅克比矩阵(导数矩阵):

$$\begin{equation}
\dfrac{\partial h_j}{\partial h_{j-1}} = {[\dfrac{\partial h_{j}}{\partial h_{j-1,1}} ...  \dfrac{\partial h_{j}}{\partial h_{j-1,D_n}}]} =
\begin{bmatrix}
\dfrac{\partial h_{j,1}}{\partial h_{j-1,1}} & . & . & . & \dfrac{\partial h_{j,1}}{\partial h_{j-1,D_n}} \\
. & . & & & . \\
. & & . & & . \\
. & & & . & . \\
\dfrac{\partial h_{j,D_n}}{\partial h_{j-1,1}} & . & . & . & \dfrac{\partial h_{j,D_n}}{\partial h_{j-1,D_n}} \\
\end{bmatrix}
\label{eqn:bp_rnn_jaocb}
\end{equation}$$

将这几个式子写在一起,得到:

$$\begin{equation}
\dfrac{\partial E}{\partial W} = \sum_{t=1}^{T}\sum_{k=1}^{t} \dfrac{\partial E_t}{\partial y_t} \dfrac{\partial y_t}{\partial h_t} (\prod_{j=k+1}^{t}\dfrac{\partial h_j}{\partial h_{j-1}}) \dfrac{\partial h_k}{\partial W}
\end{equation}$$

这个式子最中间的部分最值得关注,因为它是一个连乘的形式,长度为时间区域的长度。记$\beta_W$ 和 $\beta_h$分别为矩阵和向量的范数(L2),则上述雅克比矩阵的范数满足:

$$\begin {equation}
\parallel \dfrac{\partial h_j}{\partial h_{j-1}} \parallel \leq \parallel W^T\parallel  \parallel diag [f'(h_{j-1})]\parallel \leq \beta_W \beta_h
\label{eqn:bp_rnn_k_norm}
\end {equation}$$

由于使用了sigmoid激活函数,所以$f'(h_{j-1})$的矩阵范数最大只能为1,所以链式法则利用上式$t-k$次后得到一个更松弛的上界:

$$\begin {equation}
\parallel \dfrac{\partial h_t}{\partial h_k} \parallel = \parallel \prod_{j=k+1}^{t} \dfrac{\partial h_j}{\partial h_{j-1}}\parallel \leq (\beta_W \beta_h)^{t-k}
\label{eqn:bp_rnn_k_norm_total}
\end {equation}$$

指数项$(\beta_W \beta_h)^{t-k}$在$\beta_W \beta_h$显著地大于或小于1的时候,经过足够多的$t-k$次乘法之后就会趋近于0或无穷大。小于1更常见,会导致很长时间之前的词语无法影响对当前词语的预测。

而大于1时,浮点数运算会产生溢出(NaN),一般可以很快发现。这叫做梯度爆炸。小于1,或者下溢出并不产生异常,难以发现,但会显著降低模型对较远单词的记忆效果,这叫做梯度消失。

矩阵范数可参考:1.4 向量和矩阵的范数.pdf

梯度消失实例

有个IPython Notebook专门演示梯度消失,对于如下数据:

1.png

学习非线性的决策边界:

boundry.png

用经典的三层网络结构,得到蓝色的第一层梯度的长度和绿色的第二层梯度的长度,可视化:

sigmoid激活函数下:

2.png

ReLU激活函数下:

3.png

在这个例子的反向传播中,相邻两层梯度是近乎减半地减小。

防止梯度爆炸

一种暴力的方法是,当梯度的长度大于某个阈值的时候,将其缩放到某个阈值。虽然在数学上非常丑陋,但实践效果挺好。

其直观解释是,在一个只有一个隐藏节点的网络中,损失函数和权值w偏置b构成error surface,其中有一堵墙:

cliping.png

每次迭代梯度本来是正常的,一次一小步,但遇到这堵墙之后突然梯度爆炸到非常大,可能指向一个莫名其妙的地方(实线长箭头)。但缩放之后,能够把这种误导控制在可接受的范围内(虚线短箭头)。

但这种trick无法推广到梯度消失,因为你不想设置一个最低值硬性规定之前的单词都相同重要地影响当前单词。

减缓梯度消失

与其随机初始化参数矩阵,不如初始化为单位矩阵。这样初始效果就是上下文向量和词向量的平均。然后用ReLU激活函数。这样可以在step多了之后,依然使得模型可训练。

hankcs.com 2017-06-22 下午8.04.04.png

困惑度结果

hankcs.com 2017-06-22 下午8.07.09.png

相较于NGram,RNN的困惑度要小一些。

问题:softmax太大了

词表太大的话,softmax很费力。一个技巧是,先预测词语的分类(比如按词频分),然后在分类中预测词语。分类越多,困惑度越小,但速度越慢。所以存在一个平衡点:

hankcs.com 2017-06-22 下午8.12.05.png

最后的实现技巧

记录每个$t$的误差不要丢,反向传播的时候将其累加起来。

序列模型的应用

可以把每个词分类到NER、实体级别的情感分析(饭菜味道不错,但环境不太卫生)、意见表达。

其中,意见挖掘任务就是将每个词语归类为:

DSE:直接主观描述(明确表达观点等)

ESE:间接主观描述(间接地表达情感等)

语料标注采用经典的BIO标注:

hankcs.com 2017-06-22 下午8.23.51.png

实现这个任务的朴素网络结构就是一个裸的RNN:

hankcs.com 2017-06-22 下午8.29.08.png

但是这个网络无法利用当前词语的下文辅助分类决策,解决方法是使用一些更复杂的RNN变种。

Bidirectional RNNs

birnn.png

$$\begin{equation}
\overrightarrow{h}_t = f(\overrightarrow{W} x_t + \overrightarrow{V} \overrightarrow{h}_{t-1} + \overrightarrow{b})
\end{equation}$$

$$\begin{equation}
\overleftarrow{h}_t = f(\overleftarrow{W} x_t + \overleftarrow{V} \overleftarrow{h}_{t+1} + \overleftarrow{b})
\end{equation}$$

$$\begin{equation}
\hat{y}_t = g(U h_t + c) = g(U [\overrightarrow{h}_t; \overleftarrow{h}_t] + c)
\end{equation}$$

这里箭头表示从左到右或从右到左前向传播,对于每个时刻$t$的预测,都需要来自双向的特征向量,拼接后进行分类。箭头虽然不同,但参数还是同一套参数(有些地方是两套参数[1] G. Lample, M. Ballesteros, S. Subramanian, K. Kawakami, and C. Dyer, “Neural Architectures for Named Entity Recognition.,” HLT-NAACL, 2016.)。

Deep Bidirectional RNNs

理解了上图之后,再加几个层,每个时刻不但接受上个时刻的特征向量,还接受来自下层的特征表示:

deepbirnn.png

$$\begin{equation}
\overrightarrow{h}_t^{(i)} = f(\overrightarrow{W}^{(i)} h_t^{(i-1)} + \overrightarrow{V}^{(i)} \overrightarrow{h}_{t-1}^{(i)} + \overrightarrow{b}^{(i)})
\end{equation}$$

$$\begin{equation}
\overleftarrow{h}_t^{(i)} = f(\overleftarrow{W}^{(i)} h_t^{(i-1)} + \overleftarrow{V}^{(i)} \overleftarrow{h}_{t+1}^{(i)} + \overleftarrow{b}^{(i)})
\end{equation}$$

$$\begin{equation}
\hat{y}_t = g(U h_t + c) = g(U [\overrightarrow{h}_t^{(L)}; \overleftarrow{h}_t^{(L)}] + c)
\end{equation}$$

评测

评测方法是标准的F1(因为标签样本不均衡),在不同规模的语料上试验不同层数的影响:

hankcs.com 2017-06-22 下午8.51.29.png

可见层数不是越多越好。

应用:RNN机器翻译模型

传统机器翻译模型在不同的阶段用到大量不同的机器学习算法,这里讨论用RNN统一整个流水线。

比如将3个单词的德语翻译为2个单词的英语,用到如下RNN:

rnn_translate.png

其中:

$$\begin{equation}
h_t = \phi (h_{t-1}, x_t) = f (W^{(hh)} h_{t-1} + W^{(hx)} x_t)
\end{equation}$$

$$\begin{equation}
h_t = \phi (h_{t-1}) = f (W^{(hh)} h_{t-1})
\end{equation}$$

$$\begin{equation}y_t = softmax (W^{(S)}h_t)\end{equation}$$

用来处理输入的叫encoder,生成输出的叫decoder。训练时使用交叉熵损失函数,最大化对数似然:

$$\begin{equation}\max_{\theta} \dfrac {1}{N} \sum_{n=1}^{N} \log p_{\theta} (y^{(n)}|x^{(n)})\end{equation}$$

但预测准确率不理想,有如下拓展方法:

1、encoder和decoder使用不同的权值矩阵。也就是上述两个$W^{(hh)}$不再相同。

2、decoder中的隐藏层的输入来自3个方面:

  • 前一个时刻的隐藏层

  • encoder的最后一个隐藏层($c = h_T$)

  • 前一个预测结果 $\hat{y}_{t-1}$

    这样导致decoder函数变为:

    $$\begin{equation}
    h_{t} = \phi (h_{t-1}, c, y_{t-1})
    \end{equation}$$

  • en_decoder.png

3、使用深度RNN

4、使用 bi-directional encoder

5、不再用$\textit{A B C} \to \textit{X Y}$作为训练实例,而是逆转原文词序:$\textit{C B A} \to \textit{X Y}$。因为A更可能翻译为X,而梯度消失导致A无法影响输出,倒过来A离输出近一些。

回顾

  • RNN是最好的DeepNLP模型之一

  • 因为梯度消失和梯度爆炸,训练很难

  • 可以用很多技巧来训练

  • 下次课将介绍更强大的RNN拓展:LSTM和GRU

知识共享许可协议 知识共享署名-非商业性使用-相同方式共享码农场 » CS224n笔记8 RNN和语言模型

评论 5

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #2

    感谢分享知识

    littlehann5年前 (2019-05-16)回复
  2. #1

    谢谢博主!写的很好!就是有一个小问题就是,最后一张图的 c 这个向量我查阅了以下原paper,原话是 is a summary c of the whole input sentence. 是不是这里你写的有误呢

    林青城7年前 (2017-08-24)回复
    • LSTM最后一个时刻的hidden state就是summary of the whole input sentence

      hankcs6年前 (2017-11-23)回复
    • 注意是summary 不是sum. 就是最后一个节点的输出.

      chao6年前 (2017-12-26)回复

我的作品

HanLP自然语言处理包《自然语言处理入门》