RNN概述#

为什么需要RNN?#

问题:全连接网络和CNN无法处理序列数据

  • 文本、语音、时间序列
  • 需要记忆历史信息
  • 输入长度可变

RNN的特点#

  1. 记忆能力:能够记住历史信息
  2. 参数共享:不同时间步共享参数
  3. 变长输入:可以处理不同长度的序列

应用场景#

  • 自然语言处理:文本分类、机器翻译、文本生成
  • 语音识别:语音转文本
  • 时间序列预测:股票价格、天气预测
  • 视频分析:动作识别、视频描述

基础RNN#

结构#

时间步 t-1:  h_{t-1} ──┐
                      │
时间步 t:    x_t → [RNN] → h_t → y_t
                      ↑
                      └── h_t (传递到下一时间步)

数学表示#

$$ h_t = \tanh(W_{hh} \cdot h_{t-1} + W_{xh} \cdot x_t + b_h) $$ $$ y_t = W_{hy} \cdot h_t + b_y $$

其中:

  • $h_t$ :隐藏状态(hidden state)
  • $x_t$ :输入
  • $y_t$ :输出
  • $W_{hh}, W_{xh}, W_{hy}$ :权重矩阵
  • $b_h, b_y$ :偏置

展开形式#

$$ h_0 = 0 $$ $$ h_1 = \tanh(W_{hh} \cdot h_0 + W_{xh} \cdot x_1 + b_h) $$ $$ h_2 = \tanh(W_{hh} \cdot h_1 + W_{xh} \cdot x_2 + b_h) $$ $$ \ldots $$ $$ h_T = \tanh(W_{hh} \cdot h_{T-1} + W_{xh} \cdot x_T + b_h) $$

问题:梯度消失#

问题:深层RNN中,梯度在反向传播时指数衰减。

原因

  • 每个时间步都要乘以 $W_{hh}$
  • 如果 $W_{hh}$ 的特征值 $< 1$ ,梯度会消失
  • 如果 $W_{hh}$ 的特征值 $> 1$ ,梯度会爆炸

影响

  • 无法学习长期依赖
  • 只能记住短期信息

LSTM#

概述#

LSTM(Long Short-Term Memory) 通过门控机制 解决梯度消失问题。

核心思想#

使用三个门 控制信息流:

  1. 遗忘门:决定丢弃什么信息
  2. 输入门:决定存储什么信息
  3. 输出门:决定输出什么信息

结构#

输入 x_t
  ↓
遗忘门 f_t = σ(W_f · [h_{t-1}, x_t] + b_f)
  ↓
输入门 i_t = σ(W_i · [h_{t-1}, x_t] + b_i)
候选值 C̃_t = tanh(W_C · [h_{t-1}, x_t] + b_C)
  ↓
细胞状态 C_t = f_t ⊙ C_{t-1} + i_t ⊙ C̃_t
  ↓
输出门 o_t = σ(W_o · [h_{t-1}, x_t] + b_o)
  ↓
隐藏状态 h_t = o_t ⊙ tanh(C_t)
  ↓
输出 y_t

数学公式#

$$ f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f) \quad \text{(遗忘门)} $$ $$ i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i) \quad \text{(输入门)} $$ $$ \tilde{C}_t = \tanh(W_C \cdot [h_{t-1}, x_t] + b_C) \quad \text{(候选值)} $$ $$ C_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t \quad \text{(细胞状态)} $$ $$ o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o) \quad \text{(输出门)} $$ $$ h_t = o_t \odot \tanh(C_t) \quad \text{(隐藏状态)} $$

关键机制#

1. 遗忘门#

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

$$ f_t = \sigma(\ldots) $$ $$ C_t = f_t \odot C_{t-1} + \ldots $$
  • $f_t = 1$ :完全保留
  • $f_t = 0$ :完全遗忘

2. 输入门#

决定存储什么新信息:

$$ i_t = \sigma(\ldots) $$ $$ \tilde{C}_t = \tanh(\ldots) $$ $$ C_t = \ldots + i_t \odot \tilde{C}_t $$

3. 输出门#

决定输出什么:

$$ o_t = \sigma(\ldots) $$ $$ h_t = o_t \odot \tanh(C_t) $$

优势#

  1. 长期记忆:细胞状态可以长期保存信息
  2. 选择性遗忘:遗忘门控制信息流
  3. 梯度稳定:门控机制缓解梯度消失

GRU#

概述#

GRU(Gated Recurrent Unit) 是LSTM的简化版本,只有两个门。

结构#

$$ r_t = \sigma(W_r \cdot [h_{t-1}, x_t] + b_r) \quad \text{(重置门)} $$ $$ z_t = \sigma(W_z \cdot [h_{t-1}, x_t] + b_z) \quad \text{(更新门)} $$ $$ \tilde{h}_t = \tanh(W_h \cdot [r_t \odot h_{t-1}, x_t] + b_h) \quad \text{(候选隐藏状态)} $$ $$ h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t \quad \text{(隐藏状态)} $$

与LSTM的对比#

特性LSTMGRU
门数量3个(遗忘、输入、输出)2个(重置、更新)
参数更多更少
计算较慢较快
性能通常更好相当或略差

选择建议#

  • LSTM:需要更强的长期记忆能力
  • GRU:计算资源有限,需要更快训练

双向RNN#

原理#

同时使用前向后向 信息:

前向: x₁ → x₂ → ... → x_T
后向: x_T → x_{T-1} → ... → x₁

结构#

前向RNN: h₁^f → h₂^f → ... → h_T^f
后向RNN: h_T^b ← h_{T-1}^b ← ... ← h₁^b
         ↓
     拼接: h_t = [h_t^f, h_t^b]

应用#

  • 文本分类:利用上下文信息
  • 命名实体识别:识别实体边界
  • 机器翻译:理解完整句子

代码实现#

基础RNN#

import torch
import torch.nn as nn

class SimpleRNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(SimpleRNN, self).__init__()
        self.hidden_size = hidden_size
        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, seq_len, input_size]
        out, h_n = self.rnn(x)
        # out: [batch, seq_len, hidden_size]
        # h_n: [1, batch, hidden_size]
        
        # 使用最后一个时间步的输出
        out = self.fc(out[:, -1, :])
        return out

# 使用示例
model = SimpleRNN(input_size=10, hidden_size=64, output_size=1)
x = torch.randn(32, 20, 10)  # [batch=32, seq_len=20, input_size=10]
output = model(x)  # [32, 1]

LSTM#

class LSTMModel(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size):
        super(LSTMModel, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        
        self.lstm = nn.LSTM(
            input_size, 
            hidden_size, 
            num_layers, 
            batch_first=True,
            dropout=0.2 if num_layers > 1 else 0
        )
        self.fc = nn.Linear(hidden_size, output_size)
    
    def forward(self, x):
        # x: [batch, seq_len, input_size]
        out, (h_n, c_n) = self.lstm(x)
        # h_n: [num_layers, batch, hidden_size]
        # c_n: [num_layers, batch, hidden_size]
        
        # 使用最后一层的最后一个时间步
        out = self.fc(h_n[-1])
        return out

# 使用示例
model = LSTMModel(input_size=10, hidden_size=128, num_layers=2, output_size=1)
x = torch.randn(32, 20, 10)
output = model(x)  # [32, 1]

GRU#

class GRUModel(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size):
        super(GRUModel, self).__init__()
        self.gru = nn.GRU(
            input_size,
            hidden_size,
            num_layers,
            batch_first=True,
            dropout=0.2 if num_layers > 1 else 0
        )
        self.fc = nn.Linear(hidden_size, output_size)
    
    def forward(self, x):
        out, h_n = self.gru(x)
        out = self.fc(h_n[-1])
        return out

双向RNN#

class BidirectionalRNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(BidirectionalRNN, self).__init__()
        self.rnn = nn.LSTM(
            input_size,
            hidden_size,
            batch_first=True,
            bidirectional=True  # 双向
        )
        self.fc = nn.Linear(hidden_size * 2, output_size)  # 双向需要*2
    
    def forward(self, x):
        out, (h_n, c_n) = self.rnn(x)
        # h_n: [2, batch, hidden_size] (前向+后向)
        # 拼接前向和后向的最后一个隐藏状态
        h_forward = h_n[0]  # 前向
        h_backward = h_n[1]  # 后向
        h_concat = torch.cat([h_forward, h_backward], dim=1)
        out = self.fc(h_concat)
        return out

文本分类示例#

class TextClassifier(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_size, num_classes):
        super(TextClassifier, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim)
        self.lstm = nn.LSTM(embed_dim, hidden_size, batch_first=True, bidirectional=True)
        self.fc = nn.Linear(hidden_size * 2, num_classes)
        self.dropout = nn.Dropout(0.5)
    
    def forward(self, x):
        # x: [batch, seq_len] (token indices)
        x = self.embedding(x)  # [batch, seq_len, embed_dim]
        out, (h_n, c_n) = self.lstm(x)
        # 使用最后一个时间步的输出
        out = torch.cat([h_n[-2], h_n[-1]], dim=1)  # 双向拼接
        out = self.dropout(out)
        out = self.fc(out)
        return out

序列到序列(Seq2Seq)#

class Seq2Seq(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(Seq2Seq, self).__init__()
        self.encoder = nn.LSTM(input_size, hidden_size, batch_first=True)
        self.decoder = nn.LSTM(input_size, hidden_size, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)
    
    def forward(self, x_enc, x_dec):
        # 编码器
        _, (h_enc, c_enc) = self.encoder(x_enc)
        
        # 解码器(使用编码器的隐藏状态)
        out_dec, _ = self.decoder(x_dec, (h_enc, c_enc))
        
        # 输出
        out = self.fc(out_dec)
        return out

总结#

  1. RNN:处理序列数据,具有记忆能力
  2. 梯度消失:基础RNN的主要问题
  3. LSTM:通过门控机制解决梯度消失,适合长期依赖
  4. GRU:LSTM的简化版本,计算更快
  5. 双向RNN:利用前后文信息

关键要点

  • RNN适合处理序列数据
  • LSTM/GRU解决长期依赖问题
  • 双向RNN利用完整上下文
  • 注意梯度消失和爆炸问题