Recurrent Neural Networks: 用隐藏状态处理序列
从隐藏状态、参数共享和时间展开出发,理解 Vanilla RNN 怎样处理序列,以及它为什么难以学习长期依赖。
普通前馈神经网络把每个输入独立处理,无法自然地表达“前面发生了什么”。但序列中的当前元素通常依赖之前的信息:理解一个词需要参考前文,判断一帧中的动作也需要观察此前的画面。
Recurrent Neural Network(RNN)的核心想法,是在处理当前输入的同时保留一个隐藏状态。这个状态随着序列向前传递,成为网络对“截至当前时刻的信息”的压缩表示。
本文根据 USC CSCI 566 Lecture 7: Recurrent Neural Networks 整理。下文用单下划线表示 vector,用双下划线表示 matrix;scalar 不加下划线。
Vanilla RNN 怎样更新状态#
Vanilla RNN 的典型状态更新是 。
其中:
- 是第 个输入;
- 是上一个时间步留下的隐藏状态;
- 将当前输入投影到隐藏空间;
- 决定过去的信息怎样影响当前状态;
- 是结合当前输入与过去信息后得到的新状态。
初始状态 通常设为零,也可以作为参数学习。如果任务要求每个时间步都产生输出,还可以计算 。
这条公式最重要的地方不是 tanh,而是 的递归关系。当前状态会继续参与下一步计算,因此信息可以沿着 向后传递。
时间展开与参数共享#
RNN 可以沿时间展开成一条计算链:
h₀ ──[x₁]──▶ h₁ ──[x₂]──▶ h₂ ──[x₃]──▶ h₃ ── … ──▶ hₜ
f_W f_W f_W f_Wtext图中的每个 都是同一个函数,并且复用同一组 、 和 。因此,序列变长并不会增加模型参数量;网络只是在不同时间步重复执行同一种状态更新。
参数共享也赋予了 RNN 一个重要假设:同一种局部规律可以出现在序列的不同位置。某个词出现在第 个位置还是第 个位置,网络都使用同一套规则处理它。
一种结构,多种序列任务#
改变在哪些时间步提供输入和读取输出,同一个 RNN 就可以表达多种任务:
| 形式 | 输入与输出 | 例子 |
|---|---|---|
| many-to-one | 整个序列产生一个输出 | 文本分类、视频动作识别 |
| many-to-many | 每个输入位置产生一个输出 | 序列标注、逐帧分类 |
| one-to-many | 一个输入逐步产生一个序列 | 图像描述、序列生成 |
| sequence-to-sequence | 先编码输入序列,再生成输出序列 | 机器翻译 |
在生成任务中,上一步生成的 还可以作为下一步输入。于是模型不仅传递隐藏状态,也会根据自己此前生成的结果继续向后生成。
RNN 怎样训练#
训练时,先沿整个序列完成前向传播,得到各时间步的损失;再把展开后的 RNN 看成一个很深、但各层共享参数的网络,沿时间反向传播梯度。这个过程叫 Backpropagation Through Time(BPTT,随时间反向传播)。
如果序列很长,保存完整计算图的成本会很高。Truncated BPTT 会继续把隐藏状态传向未来,但只在一个较短的时间窗口内反向传播。它降低了计算和显存成本,代价是窗口之外的依赖无法直接获得梯度。
为什么长期依赖很难学习#
从较晚的时间步向较早的时间步反向传播时,梯度会反复经过 和 tanh 的导数:
- 如果这些连乘的尺度持续小于 ,梯度会随时间距离迅速缩小,形成梯度消失(vanishing gradient);
- 如果尺度持续大于 ,梯度会快速增大,形成梯度爆炸(exploding gradient)。
梯度裁剪(gradient clipping)可以在梯度范数过大时将它缩小,因此主要用于控制梯度爆炸;它不能从根本上恢复已经消失的梯度。LSTM 和 GRU 后来通过更明确的信息通道与门控,改善了长期依赖的学习。
从 RNN 到 attention#
RNN 用一个固定大小的 持续压缩历史信息,同时要求时间步按顺序计算。序列很长时,早期信息既可能在状态中逐渐丢失,也很难通过梯度获得训练信号。
Attention 改变了这个信息路径:模型不必只依赖最后一个隐藏状态,而是可以在需要时直接读取不同时间步的表示。Additive Attention 的评分函数看起来很像 Vanilla RNN,因为两者都使用“两个线性投影相加,再经过 tanh”的计算模板;区别是 attention 的分数不会作为下一个位置的隐藏状态继续递归传递。
所以,理解 RNN 最重要的是抓住三点:隐藏状态把过去带到现在,同一组参数在所有时间步共享,而这种递归链既让网络能够处理可变长度序列,也带来了顺序计算和长期梯度传播的问题。