CS224笔记之循环神经网络

RNN

如果训练普通神经网络是对函数的优化,那么训练循环网络就是对程序的优化。

简介

从多层网络出发到循环网络,我们需要利用上世纪 80 年代机器学习和统计模型早期思想的优点:在模型的不同部分共享参数。参数共享使得模型能够扩展到不同形式的样本(这里指不同长度的样本)并进行泛化。如果我们在每个时间点都有一个单独的参数,我们不但不能泛化到训练时没有见过序列长度,也不能在时间上共享不同序列长度和不同位置的统计强度。当信息的特定部分会在序列内多个位置出现时,这样的共享尤为重要。比如“I go to work in 2020.”和“In 2020,I go to work.”语义一致。假设我们要训练一个处理固定长度句子的前馈网络。传统的全连接前馈网络会给每个输入特征分配一个单独的参数,所以需要分别学习句子每个位置的所有语言规则。相比之下,循环神经网络在几个时间步内共享相同的权重,不需要分别学习句子每个位置的所有语言规则。
类似的思路,其实在一维序列上使用卷及神经网络依然使得模型可以跨时间的共享参数。

一个最简单的RNN-API

1
2
3
4
5
6
7
8
class RNN:
# ...
def step(self, x):
# update the hidden state
self.h = np.tanh(np.dot(self.W_hh, self.h) + np.dot(self.W_xh, x))
# compute the output vector
y = np.dot(self.W_hy, self.h)
return y

如果希望堆叠RNN形成更深的神经网络,形式上依然很简单。

1
2
y1 = rnn1.step(x)
y = rnn2.step(y1)

展开计算图

从定义上,RNN是一个有外部信号 $x^{(t)}$ 驱动的动态系统,其中,系统的状态 $h$ 是网络的隐层单元。当训练循环网络根据过去预测未来时,网络通常要学会使用 $h^{(t)}$ 作为截止到 t 的过去序列与任务相关方面的有损摘要。

$$h^{(t)}=f(h^{(t-1)},x^{(t)};\theta)$$

循环神经网络与BPTT算法

简单起见,将输入和隐层设置为相同长度的向量。前向计算比较简单,我们重点关注梯度的反向传播。关于各个参数计算关于损失函数的梯度是计算成本很高的操作。梯度计算涉及执行一次前向传播,接着是由右到左的反向传
播。运行时间是 O(τ),并且不能通过并行化来降低,因为前向传播图是固有循序的;每个时间步只能按顺序地计算。前向传播中的各个状态必须保存,直到它们反向传播中被再次使用,因此内存代价也是 O(τ )。应用于展开图且代价为 O(τ ) 的反向传播算法称为通过时间反向传播(back-propagation through time, BPTT)。

双向RNN

顾名思义,双向 RNN 结合时间上从序列起点开始移动的 RNN 和另一个rnn-cell上从序列末尾开始移动的 RNN。这允许输出单元 $o^{(t)}$ 能够计算同时依赖于过去和未来且对时刻 t 的输入值最敏感的表示,而不必指定 t 周围固定大小的窗口(这是前馈网络、卷积网络或具有固定大小的先行缓存器的常规RNN所必须要做的)。

双向神经网络是基于这样一种思想,即t时刻的输出可能不仅依赖于序列中先前的元素,而且还依赖于未来的元素。例如,要预测序列中丢失的单词,您需要同时查看左右上下文。双向神经网络非常简单。它们只是两个相互叠加的神经网络。然后根据两个rnn的对应的隐藏状态计算输出。

高级RNN

RNN在实践中很难捕获长期以来信息,简单的的想法是,距离远的词经过连续的矩阵乘法与非线性计算使得梯度很小,那么我们就加上shortcut-connection?就是允许遥远过去的变量直接连接到当前变量,这种连接被称为“延迟连接”。这种形式的网络结构在20世纪末被学者提出过。

但是我们直到,长期依赖导致的梯度问题与时间步数成指数关系,即使加上跨度为d的延迟连接,也不过使得指数的系数变小,所以这种方法治标不治本。因此我们希望根本上解决梯度爆炸或消失的问题就是让导数的范数接近1,因此有学者提出“线性自连接单元”。即$u^{(t)}=\alpha u^{(t-1)}+(1-\alpha)v^{(t)}$,使用这种方式累计一个滑动平均值,其中参数$\alpha$控制线性自连接的比例,来决定是记住遥远过去的信息,还是迅速忘记。这种隐藏单元被称为渗漏单元(leaky unit)。d 时间步的跳跃连接可以确保单元总能被先前的 d 个时间步值影响。使用权重接近 1 的线性自连接是确保该单元可以访问过去值的不同方式。线性自连接通过调节实值 α 更平滑灵活地调整这种效果,而不是调整整数值的跳跃长度。这个其实已经可以看到门控单元(GRU)的思路了。

这样的话,参数会提升而且计算量会指数复杂化。GRU和LSTM实际上可以看作是加上了自适应的连接到上一个时间步的highway-connection。而cell-state是为了方便进行隐层的更新,暂存新的隐层状态用的。

比如在LSTM中,所以cell-state其实是提供了一个信息快速流动的通道,注意到该通道上没有激活函数与矩阵的连乘,每个时间步只是与forget
gate的结果进行一个hadamard积,因此该通道实际上没有反向传播的梯度消失问题。

GRU是在此基础上,将输入门和和忘记门结合为更新门,使得每次cell的更新操作是类似指数加权移动平均的计算。然后去掉了输出门,即每次的cell-state就是隐状态,而对于LSTM来说,最好将cell和hidden看做两个不同的分立得东西。我们看Christopher Olah 的博客中的图也可以发现,相邻时间步的LSTM单元有两个数据的流动,而GRU只有一个。GRU的重置门有点像LSTM的输出门,不过后者是决定hidden有多少的比例去进入下一个时间步,而前者是决定上次的hidden有多少进入本次的时间步的临时hidden的计算。总而言之,LSTM的cell和GRU中的hidden近似是一个东西,注意二者的更新公式都没有矩阵乘法和激活函数。还有一个不同的是,LSTM的从cell计算hidden需要一次额外的激活函数(通常是tanh)的计算。

在cs224中介绍了一种很好理解的方式。即认为RNN的cell中维护着一个ram,每次计算是从RAM中读入内容,然后进行计算并写回RAM。而GRU是有两块RAM,一块用于临时区,每次从RAM中读出(一定比例的)内存,计算并写入临时区。然后再修改(一定比例的)RAM。

在这里插入图片描述

但是需要额外注意的是,各个门也是会有梯度流动的,就拿GRU举例子。上一时刻的hidden怎样参与到本次时间步的hidden的更新中呢?我们前面再说,通过一个和本次临时hidden的插值得到本次的真正hidden。这是我们关注的。但是不要忘了,上一个时间步的hidden state通过更新门和重置门一样参与到本次的计算中。这个通过公式很容易忽略,如果通过计算图的话就会清晰地看到上一次的hidden state会有三条梯度回传的路径。

这个想法可以自然地扩展到 2 维输入,如图像,由四个 RNN 组成,每一个沿着四个方向中的一个计算:上、下、左、右。如果 RNN 能够学习到承载长期信息,那在 2 维网格每个点 (i, j) 的输出 O i,j 就能计算一个能捕捉到大多局部信息但仍依赖于长期输入的表示。相比卷积网络,应用于图像的 RNN 计算成本通常更高,但允许同一特征图的特征之间存在长期横向的相互作用 (Visin et al., 2015; Kalchbrenneret al., 2015)。实际上,对于这样的 RNN,前向传播公式可以写成表示使用卷积的形式,计算自底向上到每一层的输入(在整合横向相互作用的特征图的循环传播之前)。

最后,回顾我们的思路,LSTM和GRU的最开始的思路都是希望解决梯度消失和梯度爆炸的问题,

可视化

参考http://karpathy.github.io/2015/05/21/rnn-effectiveness/中的实验。一个有趣的可视化是查看字符的预测分布。在下面的可视化中,图片的含义是从验证集中输入Wikipedia-RNN模型字符数据(沿着蓝色/绿色行显示),并在每个字符下显示(红色)模型为下一个字符分配的前5个猜测。猜测的颜色由它们的概率决定(因此暗红色=判断为非常可能,白色=不太可能)。请注意,在一些字符中,模型对下一个字母非常有信心。

另外,第一行表示模型的输入,输入的字符序列的颜色是基于在RNN的隐藏表示中随机选择的神经元的激发而上色的。可以把它想象成绿色表示非常兴奋,蓝色表示不怎么兴奋(对于那些熟悉LSTMs细节的人来说,这些值是隐藏状态向量中[-1,1]之间的值,即门控和tanh’d LSTM细胞状态)。直观地说,这是在RNN读取输入序列时,可视化“大脑”中某个神经元的放电速率。不同的神经元可能在寻找不同的模式;下面我们来看看作者发现的并认为有趣或可解释的4个不同的例子:

例子1在这里插入图片描述

图中突出显示的神经元似乎对url非常兴奋,并在url之外关闭。LSTM很可能使用这个神经元来记住它是否在URL中。

例子2

在这里插入图片描述当RNN在两个中括号标中时,这里突出显示的神经元会变得非常兴奋。有趣的是,神经元在看到字符“[”后不能马上打开,它必须等待第二个“[”,然后激活。计算模型中是否出现过一个“[”的任务可能是用不同的神经元完成的。

例子3

在这里插入图片描述在这里,我们看到神经元的值在整个双中括号环境中呈线性增强。换句话说,它的激活为RNN提供了一个跨越[[]]范围的时间对齐的坐标系。RNN可以根据[[]]作用域的早/晚程度,使用这些信息使不同的字符出现的可能性或多或少。

例子4

在这里插入图片描述这是另一个具有非常局部行为的神经元:它相对安静,但在“www”序列的第一个“w”之后就会急剧关闭。RNN可能正在使用这个神经元来计算它在“www”序列中的位置,以便知道它是否应该输出另一个“w”,或者是否应该进入URL相关状态。

例子5

下面这个例子不对输出进行可视化,而是只可视化文本,通过激活一个隐层神经元来着色。我们可以看到,大约5%的细胞已经学会了非常有趣和可解释的算法:

在这里插入图片描述
在这里插入图片描述

RNN的延伸

在最近几年有这么几个RNN的研究方向是有前景的。

  • Neural Turing Machine
  • Attention Interfaces
  • Adaptive Computation Time
  • Neural Programmers

单独来看,这些技术都是 rnn 的有效扩展,但真正令人吃惊的是它们可以组合在一起,而且似乎只是更广阔空间中的点。此外,它们都依赖于相同的基本技巧来工作,即注意力机制。

后记

训练技巧

  1. 使用LSTM或者GRU
  2. 循环状态中涉及到矩阵的反复乘法,因此涉及到这种操作的矩阵初始化为正交矩阵很有帮助。
  3. 其他的参数初始化为很小的值,一旦值过大,就会导致激活后的导数很小,这就容易使得神经元“死亡”。
  4. 对于forget gate的bias设置为1,或者其他大小合适的正数。这意味着我们期望默认的初始状态,模型能更倾向于记住东西(这么一想,似乎叫做don‘t forget gate更合适一点)。
  5. 更适合自适应学习率的算法,比如Adam,Adadelta等。
  6. 防止梯度爆炸,对梯度进行剪裁(1,或者5都是合理的值)。
  7. 在垂直方向上进行dropout。所谓垂直,指的是,如果是多层的RNN,那么在不同层之间加上dropout,而不是在同一层的不同时间步上进行dropout。在水平方向上进行dropout也不是不合理,但是方式要更精巧一些,比如以固定的掩码的方式等等。
  8. curriculum learning,是机器学习中的一个训练策略,简单说就是先学习简单训练数据,再学习复杂训练数据。
  9. 可以使用预训练好的词向量用于具体NLP任务中,如果数据足够,可以继续训练词向量,这样可以使得词向量的表示更贴合当前任务,比如good和bad在单纯的word2vec中位置可能很近,但是用于情感分析的任务中并继续训练词向量可以将good和bad分得更开。

Softmax的替代方案

hierachical softmax的问题在于树结构的计算形式无法利用GPU的计算能力。

容易忽略的点

  1. 为什么正则项不考虑bias?bias项的目的是帮助预测向类分布的方向倾斜,从而起到非均匀先验的作用。添加正则化会阻止bias的这种功能。
  2. 验证梯度时为什么使用对称的逼近方式?因为误差更小。
    在这里插入图片描述

参考:
LSTM推导

本站总访问量