Skip to content

RNN & LSTM

约 2041 个字 48 行代码 12 张图片 预计阅读时间 7 分钟

1. 序列建模 (Sequence Modeling)

1.1 核心概念

在之前的学习中,我们通常假设输入输出对 \((x^{(i)}, y^{(i)})\)独立同分布 (i.i.d.) 的。
但在实际应用中,许多情况下的输入/输出对是按照特定序列 (Sequence) 给出的。我们需要利用序列信息来辅助预测。

上图的 \(x_t\) 也是向量

词性标注 (Part of Speech Tagging)

  • 任务: 给定一串单词序列,确定每个单词的词性(如名词、动词、形容词等)。
  • 特点: 一个单词的词性取决于它所处的上下文 (Context),而不仅仅是单词本身。

|500

语音转文字 (Speech to Text)

  • 任务: 给定音频信号(假设已知单词边界,并映射为固定大小的向量),确定对应的文本。
  • 特点: 上下文极其重要。

自回归预测 (Autoregressive Prediction)

  • 任务: 序列预测的一个特例,预测序列中的下一个元素。
  • 应用: 时间序列预测、语言模型 (Language Modeling)。

|500

2. 循环神经网络 (Recurrent Neural Networks, RNNs)

2.1 基本原理

RNN 随着时间推移维护一个隐藏状态 (Hidden State),该状态是当前输入和上一个隐藏状态的函数。

|350

核心公式:
$$
\begin{aligned}
h_t &= f(W_{hh}h_{t-1} + W_{hx}x_t + b_h) \
y_t &= g(W_{yh}h_t + b_y)
\end{aligned}
$$

  • \(f, g\): 激活函数 (Activation functions)
  • \(W_{hh}, W_{hx}, W_{yh}\): 权重矩阵
  • \(b_h, b_y\): 偏置项
  • \(h_t\): 时间步 \(t\) 的隐藏状态

2.2 RNN 的训练 (BPTT)

给定输入序列和目标输出 \((x_1, \dots, x_T, y_1^\star, \dots, y_T^\star)\),我们使用随时间反向传播 (Backpropagation Through Time, BPTT) 来训练 RNN

大概思路:
1. 初始化为0
2. 每读一个向量,都尝试预测下一个向量是什么
3. 预测的和实际的对比,产生Loss
4. 根据Loss进行反向传播 l.backward()

伪代码:

# 1. 准备优化器,告诉它要更新哪些参数(W和b)
# 注意:这些参数在整个循环中是“共享”的,不管读第几个字,用的都是同一套 W。
opt = Optimizer(params = (W_hh, W_hx, W_yh, b_h, b_y))

# 2. 初始化隐藏状态 (记忆)
# 在序列开始前,隐藏状态通常设为全0向量
h[0] = 0

# 3. 初始化总损失
# 用来把每一步产生的错误存起来
l = 0

# 4. 核心循环:从第1步时间走到第T步时间
for t = 1, ..., T:

    # [关键步骤 A] 更新记忆 (Hidden State Update)
    # h[t] 是当下的新记忆。
    # 它由两部分决定:
    #   1. h[t-1]: 上一秒的记忆 (过去的信息)
    #   2. x[t]:   这一秒的输入 (现在的信息)
    # 这就是“循环”的体现:上一轮的输出变成了这一轮的输入。
    h[t] = f(W_hh * h[t-1] + W_hx * x[t] + b_h)

    # [关键步骤 B] 生成输出 (Output Generation)
    # 根据当下的新记忆 h[t],计算输出 y[t] (比如预测词性,或预测下一个词)
    y[t] = g(W_yh * h[t] + b_y)

    # [关键步骤 C] 累加损失 (Accumulate Loss)
    # 看看这一步预测的 y[t] 和真实答案 y_star[t] 差多少。
    # 把这个误差加到总账 l 上。
    l += Loss(y[t], y_star[t])

# 5. 反向传播 (BPTT 的核心)
# 虽然代码只有一行,但这里发生了巨大的魔法(自动微分)。
# 计算机记住了循环中发生的每一步运算。
# 它会从时刻 T 开始,把梯度一步步往前传回时刻 1。
# 这就是为什么叫 "Through Time" (穿越时间)。
l.backward()

# 6. 更新参数
# 根据算出来的梯度,修改权重 W 和 b,让模型下次预测得更准。
opt.step()

注意点:
1. 普通神经网络(例如CNN,MLP)是一次性塞进整个图片和向量,没有顺序的说法,但RNN必须按顺序处理,因为第 t 步的计算依赖于第 t-1 步的结果 (h[t-1])。
2. 在 RNN 里,虽然物理上只有一层网络(只有一套 W 参数),但我们在时间上把它展开 成了 T 层。当计算 l.backward() 时,梯度需要从 \(t=T\) 传给 \(h[T]\),再传给 \(h[T-1]\),... 一直传回 \(h[1]\)

2.3 堆叠 RNN (Stacking RNNs)

就像普通神经网络一样,RNN 也可以堆叠。
* 方法: 将一层的隐藏单元输出作为下一层的输入。
* 注意: 实际上,"非常深" (Very deep) 的 RNN 往往不如其他架构(如深层 CNN 或 Transformer)带来的价值大。

|375

# 第 1 层处理原始输入
h1[t] = rnn_layer_1(input=x[t], hidden=h1[t-1])
# 第 2 层把"第 1 层的隐藏状态"当作输入
h2[t] = rnn_layer_2(input=h1[t], hidden=h2[t-1])
# 输出层使用最顶层的状态
y[t] = output_layer(h2[t])

3. 训练挑战:梯度与激活问题

训练 RNN 的挑战类似于训练深层 MLP 网络,因为展开后的 RNN 本质上是一个非常深的网络。

3.1 梯度爆炸 (Exploding Activations/Gradients)

  • 原因: 如果权重或激活值缩放不当,隐藏层激活值(及其梯度)会随着序列长度无界增长

Info

为了理解这一点,你需要把 RNN 在时间轴上展开(Unroll):

  • \(t=1\) 时刻的隐藏状态 \(h_1\) 会乘以权重 \(W_{hh}\) 传给 \(t=2\)
  • \(t=2\) 时刻的结果又乘以 \(W_{hh}\) 传给 \(t=3\)
  • ……
  • 到了 \(t=200\),相当于原始信号被 \(W_{hh}\) 连续乘了 200 次(近似理解为 \(W_{hh}^{200}\))。

直观类比:复利效应(或麦克风啸叫)
如果权重的“放大倍数”哪怕只比 1 大一点点(比如 1.1),经过 200 次乘法后:
$$ 1.1^{200} \approx 189,905,276 $$
数值会变得极其巨大。这就是所谓的 “无界增长 (grow unboundedly)” 。下图表明了增长速度:

|375

  • 示例: 单层 RNN, ReLU 激活,初始化 \(W_{hh} \sim \mathcal{N}(0, 3/n)\)。ReLU 在 \(x>0\) 的时候有 \(y=x\),意味着它有【没有上限可以一直增长】的条件
    * ReLU 的“正确”初始化应为 \(\sigma^2 = 2/n\),从而保证信号传到下一层时,方差保持不变。一旦改成 \(3/n\) 就会不断变多 \(\rightarrow\) 爆炸

3.2 梯度消失 (Vanishing Activations/Gradients)

如果权重太小,来自输入的信息会随着时间迅速衰减。模型无法捕捉我们希望序列模型处理的“长距离依赖 (long range dependencies)”。

  • 示例: 单层 RNN, ReLU 激活,初始化 \(W_{hh} \sim \mathcal{N}(0, 1.5/n)\),如图所示,走到结尾时,开头的记忆已经被“乘没了”。模型只能看见最近几个词

|375

3.3 替代激活函数 (Alternative Activations)

使用有界激活函数(如 Sigmoid 或 Tanh)能否解决梯度消失?

  • Sigmoid: \(\text{sigmoid}(x) = \frac{1}{1+e^{-x}}\)
  • Tanh: \(\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}\)

结论

不能完全解决。
为了防止激活/梯度消失,需要较大的权重,但这会使激活进入“饱和 (saturating)”区域。在饱和区,梯度非常小,这仍然会导致梯度消失。一旦神经元进入饱和区,梯度就传不回去了,反向传播链条断裂,梯度依然 消失

由此体现了传统RNN的缺陷:

  1. 用 ReLU:如果权重初始化稍大就爆炸,稍小就消失。平衡点极难寻找(像是在刀尖上跳舞)。
  2. 用 Tanh/Sigmoid:虽然避免了爆炸,但因为导数性质和饱和区问题,依然逃不脱梯度消失的命运,导致无法处理长序列。

4. 长短期记忆网络 (LSTMs)

LSTMs (Long Short Term Memory) 是一种特殊的隐藏单元更新形式,旨在避免普通 RNN 的一些问题(主要是梯度消失)。

4.1 结构设计

第一步: 将隐藏单元分为两个部分:
1. 隐藏状态 (Hidden State): \(h_t\)
2. 细胞状态 (Cell State): \(c_t\)

|500

第二步: 使用特定的公式更新状态(引入“门”的概念:\(f_t\) 遗忘门、输入门、输出门)。

\[ \begin{bmatrix} i_t \\ f_t \\ g_t \\ o_t \end{bmatrix} = \begin{pmatrix} \text{sigmoid} \\ \text{sigmoid} \\ \text{tanh} \\ \text{sigmoid} \end{pmatrix} (W_{hh}h_{t-1} + W_{hx}x_t + b_h) \]

$$
\begin{aligned}
c_t &= c_{t-1} \circ f_t + i_t \circ g_t \
h_t &= \tanh(c_t) \circ o_t
\end{aligned}
$$
(注: \(\circ\) 代表逐元素乘法)

|325

为什么 LSTM 有效?

关键在于这一行公式:
$$ c_t = c_{t-1} \circ f_t + i_t \circ g_t $$

  • 我们通过 \(f_t\) (范围在 \([0, 1]\)) 来缩放 \(c_{t-1}\),然后加上新项。
  • 重要的是,如果 \(f_t\) 的 Sigmoid 激活处于饱和区(即 \(f_t \approx 1\)),\(c_{t-1}\) 的信息将几乎原封不动地传递下去。
  • 这意味着对于更宽范围的权重,LSTM 不会遭受梯度消失的问题(梯度可以像在“传送带”上一样回传)。
  • \(i_t \in [0,1], g_t \in [-1,1]\). 整个式子相当于衰减了原先的\(c_t\)基础上,加上了一个有界项\([-1,1]\),权重由 \(f_t\)\(i_t\) 决定

slide里的推荐阅读: The Unreasonable Effectiveness of Recurrent Neural Networks:还没看,挖坑中

5. 超越“简单”序列模型

5.1 序列到序列模型 (Sequence-to-sequence / Seq2Seq)

  • 任务: 在机器翻译中,英文句子 "Translating language is difficult" (4个词) 翻译成西班牙语 "Traducir un idioma es difícil" (5个词)。输入和输出的长度不一样,甚至词序都不一样,简单的“对齐”模型做不到。
  • 架构: 将两个 RNN 串联。
    1. Encoder: 处理输入序列,生成最终的隐藏状态(此阶段没有损失函数)。
    2. Decoder: 接收该初始隐藏状态,“仅”负责生成输出序列。

|475

English ("Translating language is difficult") \(\to\) Encoder \(\to\) Hidden State \(\to\) Decoder \(\to\) Spanish ("Traducir un idioma es difícil").

5.2 双向 RNN (Bidirectional RNNs)

  • 传统 RNN 的局限: 当你读到句子的第 3 个词时,你完全不知道第 4、5、6 个词是什么。这对自回归任务(比如股市预测、天气预报、打字联想)是合理的,因为你确实不能穿越时空看到未来。但对于翻译、情感分析、语音识别等任务,我们通常是拿到了整段录音或整句文本。这时候强行假装看不见后面的词,反而会丢失信息。
  • 解决方案: 堆叠一个前向 (Forward) 运行的 RNN 和一个后向 (Backward) 运行的 RNN。
  • 效果: 整个序列的信息都能传播到隐藏状态中。

|346

Comments