顺序模型学习日记

目录

  1. 顺序模型概述
  2. [RNN 基础](#RNN 基础)
  3. LSTM
  4. GRU
  5. 模型对比
  6. 面试常见问题

顺序模型概述

什么是顺序模型?

定义: 处理序列数据的神经网络,能记住历史信息

特点:

  • 顺序敏感:输入的顺序影响输出
  • 状态记忆:通过隐藏状态传递历史信息
  • 时序依赖:当前输出依赖于之前的输入

应用场景

领域 输入 输出 应用
NLP 文本序列 词向量/标签 翻译、情感分析
语音 音频序列 文本/标签 语音识别
视频 帧序列 动作标签 视频分类
时间序列 历史数据 预测值 股价、天气预测
推荐 用户行为 下一个物品 个性化推荐

顺序数据的特点

复制代码
序列: x₁, x₂, x₃, ..., xₜ

特点:
1. 长度可变
2. 时序关系:xₜ 依赖于 x₁, ..., xₜ₋₁
3. 长期依赖:xₜ 可能依赖于很早的 x₁

RNN 基础

RNN 结构

基本思想: 通过隐藏状态传递历史信息

复制代码
x₁ ──┐
     ├─ h₁ ──┬─ y₁
x₂ ──┤      │
     ├─ h₂ ──┼─ y₂
x₃ ──┤      │
     ├─ h₃ ──┼─ y₃
... ──┤      │
     ├─ hₜ ──┴─ yₜ
xₜ ──┘

hₜ: 当前隐藏状态(包含历史信息)

RNN 公式

复制代码
hₜ = tanh(Wₕₕ hₜ₋₁ + Wₓₕ xₜ + bₕ)
yₜ = Wₕy hₜ + bᵧ

其中:
  hₜ: 当前隐藏状态
  hₜ₋₁: 上一时刻的隐藏状态
  xₜ: 当前输入
  yₜ: 当前输出
  Wₕₕ, Wₓₕ, Wₕy: 权重矩阵
  bₕ, bᵧ: 偏置

RNN 计算过程

示例:处理单词序列 "I love deep learning"

复制代码
时刻 1: x₁ = "I"
  h₁ = tanh(Wₕₕ × 0 + Wₓₕ × I)  # 初始 h₀ = 0
  y₁ = Wₕy × h₁

时刻 2: x₂ = "love"
  h₂ = tanh(Wₕₕ × h₁ + Wₓₕ × love)  # h₂ 依赖 h₁
  y₂ = Wₕy × h₂

时刻 3: x₃ = "deep"
  h₃ = tanh(Wₕₕ × h₂ + Wₓₕ × deep)  # h₃ 依赖 h₂
  y₃ = Wₕy × h₃

...

隐藏状态 hₜ 携带了之前所有输入的信息

RNN 的问题

梯度消失/梯度爆炸

复制代码
梯度传播:
  ∂Loss/∂h₁ = ∂Loss/∂hₜ × ∂hₜ/∂hₜ₋₁ × ... × ∂h₂/∂h₁
                         ↑
                    连乘多个梯度

如果 |∂hₜ/∂hₜ₋₁| < 1:
  梯度逐渐消失,无法学到长期依赖

如果 |∂hₜ/∂hₜ₋₁| > 1:
  梯度爆炸,训练不稳定

无法捕捉长期依赖

复制代码
例子: "我出生在法国... (长文本) ... 所以我说流利的法语"
                       ↑
                  需要记住"法国"

RNN: 难以记住这么久之前的信息

LSTM

LSTM 结构

核心思想: 引入门、遗忘、输出三个门控机制

复制代码
细胞状态 (Cell State) ──┬─ 遗忘门 ── 保留/遗忘信息
                        ├─ 输入门  ── 写入新信息
                        └─ 输出门  ── 输出信息

LSTM 组件

1. 遗忘门 (Forget Gate)

作用: 决定从细胞状态中丢弃什么

复制代码
fₜ = σ(W_f · [hₜ₋₁, xₜ] + b_f)

其中:
  fₜ: 遗忘门 (0-1)
  σ: sigmoid 函数
  [hₜ₋₁, xₜ]: 拼接

例子:

复制代码
输入: "我出生在法国,搬到了中国,现在说流利的___"
                         ↑
                  遗忘门决定忘记"法国"
2. 输入门 (Input Gate)

作用: 决定向细胞状态写入什么新信息

复制代码
iₜ = σ(W_i · [hₜ₋₁, xₜ] + b_i)     # 输入门
C̃ₜ = tanh(W_C · [hₜ₋₁, xₜ] + b_C) # 候选细胞状态

更新细胞状态:

复制代码
Cₜ = fₜ ⊙ Cₜ₋₁ + iₜ ⊙ C̃ₜ

其中:
  ⊙: 逐元素相乘
  Cₜ: 当前细胞状态
  Cₜ₋₁: 上一时刻细胞状态

例子:

复制代码
输入: "我出生在法国,搬到了中国,现在说流利的___"
                                     ↑
                          输入门决定写入"中文"
3. 输出门 (Output Gate)

作用: 决定从细胞状态输出什么

复制代码
oₜ = σ(W_o · [hₜ₋₁, xₜ] + b_o)  # 输出门
hₜ = oₜ ⊙ tanh(Cₜ)              # 隐藏状态

完整 LSTM 公式:

复制代码
fₜ = σ(W_f · [hₜ₋₁, xₜ] + b_f)           # 遗忘门
iₜ = σ(W_i · [hₜ₋₁, xₜ] + b_i)           # 输入门
oₜ = σ(W_o · [hₜ₋₁, xₜ] + b_o)           # 输出门
C̃ₜ = tanh(W_C · [hₜ₋₁, xₜ] + b_C)        # 候选状态
Cₜ = fₜ ⊙ Cₜ₋₁ + iₜ ⊙ C̃ₜ                 # 更新细胞状态
hₜ = oₜ ⊙ tanh(Cₜ)                       # 隐藏状态

LSTM 可视化

复制代码
                ┌─────────────────────────────────────┐
                │                                     │
   Cₜ₋₁ ────→[+]←───────┐                   ┌─────────┤
                │        │                   │         │
           遗忘门 (fₜ)    │               输出门 (oₜ)   │
                ↓        │                   ↓         │
              [σ]       │                [σ] ───[⊙]─────┤
                │        │                   │         │
                │        └────────→ [tanh] ──┘         │
                │                           ↑         ↓
                │                        [⊙]      hₜ ───→
                │                           ↑         │
                │        输入门 (iₜ)        │         │
                │           ↓                │         │
                └──[⊙] ←─ [σ] ──→ [⊙] ───────┘         │
                    ↑          ↑                     │
                C̃ₜ ←─ [tanh]  │                     │
                    ↑          │                     │
                    └── [⊙] ───┘                     │
                         ↑                          │
          [hₜ₋₁, xₜ] ───┘                          │
                                                    │
                ┌───────────────────────────────────┘
                │
                ↓
              Cₜ (细胞状态)

GRU

GRU 结构

核心思想: 简化 LSTM,合并门控机制

主要变化:

  1. 合并细胞状态和隐藏状态
  2. 合并遗忘门和输入门为更新门

GRU 公式

复制代码
zₜ = σ(W_z · [hₜ₋₁, xₜ] + b_z)    # 更新门
rₜ = σ(W_r · [hₜ₋₁, xₜ] + b_r)    # 重置门
h̃ₜ = tanh(W_h · [rₜ ⊙ hₜ₋₁, xₜ] + b_h)  # 候选隐藏状态
hₜ = (1 - zₜ) ⊙ hₜ₋₁ + zₜ ⊙ h̃ₜ   # 最终隐藏状态

其中:
  zₜ: 更新门 (0-1)
  rₜ: 重置门 (0-1)
  h̃ₜ: 候选隐藏状态
  hₜ: 最终隐藏状态

GRU 工作原理

更新门 (zₜ):

复制代码
zₜ 决定保留多少旧信息,写入多少新信息

如果 zₜ = 0:
  hₜ = hₜ₋₁ (完全保留旧信息)

如果 zₜ = 1:
  hₜ = h̃ₜ (完全写入新信息)

重置门 (rₜ):

复制代码
rₜ 决定如何组合旧信息和新信息

如果 rₜ = 0:
  忽略 hₜ₋₁,只用 xₜ

如果 rₜ = 1:
  完全考虑 hₜ₋₁

模型对比

RNN vs LSTM vs GRU

维度 RNN LSTM GRU
门控机制 3个门 (遗忘/输入/输出) 2个门 (更新/重置)
细胞状态 无(合并到 hₜ)
参数量
计算复杂度
长期依赖
训练速度
内存占用

架构对比图

RNN:

复制代码
hₜ₋₁ ──┐
       ├─ tanh ── hₜ ── yₜ
xₜ  ───┘

LSTM:

复制代码
Cₜ₋₁ ──┐
       ├─ [遗忘门] ──┬─ Cₜ ── [输出门] ── hₜ ── yₜ
xₜ  ───┤              ↑
       └─ [输入门] ───┘
       hₜ₋₁

GRU:

复制代码
hₜ₋₁ ──┐
       ├─ [更新门] ── hₜ ── yₜ
xₜ  ───┤
       └─ [重置门]

什么时候用哪个模型?

场景 推荐模型 原因
短序列 (<10) RNN 简单高效
长序列 (>20) LSTM 长期依赖强
计算受限 GRU 参数少
需要解释性 LSTM 门控机制清晰
实时应用 GRU 速度快

参数量计算

假设:

  • 输入维度: d
  • 隐藏维度: h

RNN:

复制代码
Wₕₕ: h × h
Wₓₕ: d × h
Wₕy: h × output_dim
参数: h² + dh + h × output_dim

LSTM:

复制代码
W_f: (d + h) × h
W_i: (d + h) × h
W_o: (d + h) × h
W_C: (d + h) × h
参数: 4 × (d + h) × h

GRU:

复制代码
W_z: (d + h) × h
W_r: (d + h) × h
W_h: (d + h) × h
参数: 3 × (d + h) × h

结论: LSTM 参数量是 GRU 的 4/3 倍


双向 RNN (Bi-RNN)

基本思想

单向 RNN: 只能看过去

复制代码
x₁ → x₂ → x₃ → ... → xₜ

双向 RNN: 同时看过去和未来

复制代码
前向: x₁ → x₂ → x₃ → ... → xₜ
后向: x₁ ← x₂ ← x₃ ← ... ← xₜ
      ↓      ↓      ↓       ↓
    h₁    →  h₂  →  h₃  →  hₜ
      ↑      ↑      ↑       ↑
    ĥ₁  ←   ĥ₂  ←  ĥ₃  ←   ĥₜ

最终: hₜ = [hₜ, ĥₜ] (拼接)

适用场景

场景 单向 双向
文本生成
翻译
命名实体识别
语音识别
实时推荐

公式

复制代码
前向: hₜ = f(xₜ, hₜ₋₁)
后向: ĥₜ = f(xₜ, ĥₜ₊₁)
最终: hₜ = [hₜ, ĥₜ]  # 拼接

其中 f 可以是 RNN/LSTM/GRU

面试常见问题

Q1: RNN 的主要问题是什么?

A:

  1. 梯度消失/梯度爆炸: 长序列中梯度传播困难
  2. 无法捕捉长期依赖: 信息随时间丢失
  3. 无法并行: 必须按顺序计算

解决方法:

  • LSTM/GRU: 门控机制
  • 梯度裁剪: 解决梯度爆炸
  • 残差连接: 缓解梯度消失

Q2: LSTM 如何解决梯度消失?

A:

复制代码
LSTM 引入细胞状态 Cₜ:

Cₜ = fₜ ⊙ Cₜ₋₁ + iₜ ⊙ C̃ₜ
     ↑
  通过加法而非乘法传播

梯度传播:
  ∂Loss/∂Cₜ₋₁ = ∂Loss/∂Cₜ × fₜ

如果 fₜ = 1:
  ∂Loss/∂Cₜ₋₁ = ∂Loss/∂Cₜ (无衰减)

LSTM 可以通过遗忘门控制信息流

Q3: LSTM 和 GRU 的区别是什么?

A:

维度 LSTM GRU
细胞状态 有 (Cₜ)
门数量 3 2
参数量 多 (4×) 中 (3×)
复杂度
性能 略好 接近

结论:

  • 数据充足 → LSTM
  • 计算受限 → GRU

Q4: 为什么使用 tanh 和 sigmoid?

A:

tanh:

复制代码
范围: (-1, 1)
优点: 0 中心化,梯度不消失

用于:
  - RNN/LSTM/GRU 的隐藏状态计算
  - LSTM 的候选状态 C̃ₜ

sigmoid:

复制代码
范围: (0, 1)
优点: 概率解释,门控机制

用于:
  - LSTM/GRU 的门 (0=关, 1=开)

Q5: 为什么 RNN 无法并行计算?

A:

复制代码
RNN 计算依赖前一时刻的隐藏状态:

hₜ = f(xₜ, hₜ₋₁)  # hₜ 依赖 hₜ₋₁
hₜ₋₁ = f(xₜ₋₁, hₜ₋₂)  # hₜ₋₁ 依赖 hₜ₋₂

必须串行计算,无法并行

对比 Transformer (Self-Attention):
  所有位置同时计算

Q6: 如何初始化 RNN 的隐藏状态?

A:

复制代码
1. 零初始化: h₀ = 0
   - 简单,常用

2. 随机初始化: h₀ ~ N(0, 0.01)
   - 有时收敛更快

3. 学习初始化: 将 h₀ 作为可学习参数
   - 效果通常更好

代码:
  h₀ = torch.zeros(num_layers, batch_size, hidden_dim)
  # 或
  h₀ = nn.Parameter(torch.randn(num_layers, batch_size, hidden_dim))

Q7: 什么是序列到序列模型 (Seq2Seq)?

A:

复制代码
架构: Encoder-Decoder

Encoder:
  输入序列 → RNN → 上下文向量

Decoder:
  上下文向量 → RNN → 输出序列

应用:
  - 机器翻译
  - 文本摘要
  - 问答系统

例子:
  输入: "I love deep learning"
  Encoder: → 上下文向量 (语义表示)
  Decoder: → "我爱深度学习"

Q8: Teacher Forcing 是什么?

A:

复制代码
训练 Decoder 时,使用真实标签作为输入

标准方式:
  yₜ = Decoder(yₜ₋₁)  # 用预测值

Teacher Forcing:
  yₜ = Decoder(yₜ₋₁_真实)  # 用真实值

优点:
  - 训练更稳定
  - 收敛更快

缺点:
  - 训练推理不一致 (Exposure Bias)
  - 可能过度依赖真实标签

解决:
  - Scheduled Sampling: 混合使用预测值和真实值

代码实现

PyTorch RNN 实现

python 复制代码
import torch
import torch.nn as nn


class SimpleRNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super().__init__()
        
        self.hidden_size = hidden_size
        
        # RNN 层
        self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
        
        # 输出层
        self.fc = nn.Linear(hidden_size, output_size)
    
    def forward(self, x):
        # x: (batch_size, seq_len, input_size)
        
        # RNN 前向传播
        # output: (batch_size, seq_len, hidden_size)
        # hidden: (1, batch_size, hidden_size)
        output, hidden = self.rnn(x)
        
        # 取最后一个时间步的输出
        last_output = output[:, -1, :]
        
        # 输出层
        logits = self.fc(last_output)
        
        return logits


if __name__ == '__main__':
    # 参数
    batch_size = 32
    seq_len = 10
    input_size = 16
    hidden_size = 64
    output_size = 10
    
    # 模型
    model = SimpleRNN(input_size, hidden_size, output_size)
    
    # 输入
    x = torch.randn(batch_size, seq_len, input_size)
    
    # 前向传播
    output = model(x)
    
    print(f'Output shape: {output.shape}')  # (32, 10)

PyTorch LSTM 实现

python 复制代码
class LSTMModel(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers=2, dropout=0.1):
        super().__init__()
        
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        
        # LSTM 层
        self.lstm = nn.LSTM(
            input_size=input_size,
            hidden_size=hidden_size,
            num_layers=num_layers,
            batch_first=True,
            dropout=dropout if num_layers > 1 else 0
        )
        
        # 输出层
        self.fc = nn.Linear(hidden_size, output_size)
    
    def forward(self, x, h=None, c=None):
        # x: (batch_size, seq_len, input_size)
        
        # LSTM 前向传播
        if h is None:
            output, (h_n, c_n) = self.lstm(x)
        else:
            output, (h_n, c_n) = self.lstm(x, (h, c))
        
        # 取最后一个时间步
        last_output = output[:, -1, :]
        
        # 输出层
        logits = self.fc(last_output)
        
        return logits, h_n, c_n


if __name__ == '__main__':
    # 参数
    batch_size = 32
    seq_len = 20
    input_size = 16
    hidden_size = 64
    num_layers = 2
    output_size = 10
    
    # 模型
    model = LSTMModel(input_size, hidden_size, num_layers)
    
    # 输入
    x = torch.randn(batch_size, seq_len, input_size)
    
    # 前向传播
    output, h, c = model(x)
    
    print(f'Output shape: {output.shape}')  # (32, 10)
    print(f'Hidden state shape: {h.shape}')  # (2, 32, 64)
    print(f'Cell state shape: {c.shape}')    # (2, 32, 64)

PyTorch GRU 实现

python 复制代码
class GRUModel(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers=2, dropout=0.1):
        super().__init__()
        
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        
        # GRU 层
        self.gru = nn.GRU(
            input_size=input_size,
            hidden_size=hidden_size,
            num_layers=num_layers,
            batch_first=True,
            dropout=dropout if num_layers > 1 else 0
        )
        
        # 输出层
        self.fc = nn.Linear(hidden_size, output_size)
    
    def forward(self, x, h=None):
        # x: (batch_size, seq_len, input_size)
        
        # GRU 前向传播
        if h is None:
            output, h_n = self.gru(x)
        else:
            output, h_n = self.gru(x, h)
        
        # 取最后一个时间步
        last_output = output[:, -1, :]
        
        # 输出层
        logits = self.fc(last_output)
        
        return logits, h_n


if __name__ == '__main__':
    # 参数
    batch_size = 32
    seq_len = 20
    input_size = 16
    hidden_size = 64
    num_layers = 2
    output_size = 10
    
    # 模型
    model = GRUModel(input_size, hidden_size, num_layers)
    
    # 输入
    x = torch.randn(batch_size, seq_len, input_size)
    
    # 前向传播
    output, h = model(x)
    
    print(f'Output shape: {output.shape}')  # (32, 10)
    print(f'Hidden state shape: {h.shape}')  # (2, 32, 64)

双向 LSTM 实现

python 复制代码
class BiLSTM(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers=2):
        super().__init__()
        
        self.hidden_size = hidden_size
        
        # 双向 LSTM
        self.lstm = nn.LSTM(
            input_size=input_size,
            hidden_size=hidden_size,
            num_layers=num_layers,
            batch_first=True,
            bidirectional=True
        )
        
        # 输出层 (双向隐藏状态维度是 2 * hidden_size)
        self.fc = nn.Linear(hidden_size * 2, output_size)
    
    def forward(self, x):
        # x: (batch_size, seq_len, input_size)
        
        # 双向 LSTM 前向传播
        # output: (batch_size, seq_len, hidden_size * 2)
        output, (h_n, c_n) = self.lstm(x)
        
        # 取最后一个时间步
        last_output = output[:, -1, :]
        
        # 输出层
        logits = self.fc(last_output)
        
        return logits


if __name__ == '__main__':
    # 参数
    batch_size = 32
    seq_len = 20
    input_size = 16
    hidden_size = 64
    num_layers = 2
    output_size = 10
    
    # 模型
    model = BiLSTM(input_size, hidden_size, num_layers)
    
    # 输入
    x = torch.randn(batch_size, seq_len, input_size)
    
    # 前向传播
    output = model(x)
    
    print(f'Output shape: {output.shape}')  # (32, 10)

快速检查清单

理解顺序模型,你应该能回答:

  • 解释 RNN、LSTM、GRU 的区别
  • 说明 LSTM 的三个门的作用
  • 知道如何解决梯度消失
  • 计算 LSTM 的参数量
  • 理解双向 RNN 的原理
  • 了解 Teacher Forcing
  • 能从零实现简单的 RNN/LSTM/GRU

参考资料


相关推荐
_日拱一卒1 小时前
LeetCode:199二叉树的右视图
算法·leetcode·职场和发展
The Chosen One9851 小时前
分享对dp题目的理解-不断更新ing
笔记·算法·深度优先·动态规划·dp
有时间要学习1 小时前
【无标题】
算法
re林檎1 小时前
算法札记——5.15
算法
鱼子星_1 小时前
【数据结构与算法】OJ题目详解(一)-单链表:从易到难的面试OJ题目
c语言·数据结构·算法·链表·面试·职场和发展
人道领域1 小时前
【LeetCode刷题日记】递归与回溯实战 257.二叉树的所有路径——一篇文章彻底搞懂回溯
开发语言·python·算法·leetcode
ulias2121 小时前
leetcode热题 - 7
数据结构·算法·leetcode
吃好睡好便好1 小时前
在Matlab中用sphere( )函数绘制球面图
开发语言·前端·javascript·学习·算法·matlab·信息可视化
图码1 小时前
矩阵中的“对角线强迫症”:如何优雅地判断Toeplitz矩阵?
数据结构·c++·线性代数·算法·青少年编程·矩阵