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),而不仅仅是单词本身。

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

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

2. 循环神经网络 (Recurrent Neural Networks, RNNs)
2.1 基本原理
RNN 随着时间推移维护一个隐藏状态 (Hidden State),该状态是当前输入和上一个隐藏状态的函数。

核心公式:
$$
\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)带来的价值大。

# 第 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)” 。下图表明了增长速度:

- 示例: 单层 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)\),如图所示,走到结尾时,开头的记忆已经被“乘没了”。模型只能看见最近几个词

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的缺陷:
- 用 ReLU:如果权重初始化稍大就爆炸,稍小就消失。平衡点极难寻找(像是在刀尖上跳舞)。
- 用 Tanh/Sigmoid:虽然避免了爆炸,但因为导数性质和饱和区问题,依然逃不脱梯度消失的命运,导致无法处理长序列。
4. 长短期记忆网络 (LSTMs)
LSTMs (Long Short Term Memory) 是一种特殊的隐藏单元更新形式,旨在避免普通 RNN 的一些问题(主要是梯度消失)。
4.1 结构设计
第一步: 将隐藏单元分为两个部分:
1. 隐藏状态 (Hidden State): \(h_t\)
2. 细胞状态 (Cell State): \(c_t\)

第二步: 使用特定的公式更新状态(引入“门”的概念:\(f_t\) 遗忘门、输入门、输出门)。
$$
\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\) 代表逐元素乘法)

为什么 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: 接收该初始隐藏状态,“仅”负责生成输出序列。

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。
- 效果: 整个序列的信息都能传播到隐藏状态中。
