跳至内容
序列模型:RNN 与 LSTM

序列模型:RNN 与 LSTM

文本、语音、时间序列——这些数据的特点是顺序很重要,前面的内容影响对后面内容的理解。RNN(循环神经网络)专为此而生,LSTM 解决了 RNN 的记忆退化问题。即使在 Transformer 称霸之后,LSTM 在时间序列预测、小数据集场景仍然实用。

RNN:带记忆的神经网络

普通 MLP 每次处理一个独立样本,不保留任何「历史」。RNN 在此基础上加了一个隐状态(hidden state),每个时间步的计算同时依赖当前输入和上一步的隐状态:

时间步 t:
  hₜ = tanh(Wₕ · hₜ₋₁ + Wₓ · xₜ + b)
  yₜ = Wₒ · hₜ

其中:
  xₜ  = 当前时间步的输入
  hₜ₋₁ = 上一步的隐状态(记忆)
  hₜ  = 新的隐状态
  yₜ  = 当前步的输出(可选)

把多个时间步展开,就能看出 RNN 其实是在「沿时间方向共享权重的深度网络」:

x₁ → [RNN] → h₁ → [RNN] → h₂ → [RNN] → h₃ → ... → 输出
              ↑              ↑              ↑
           同一组参数 W 在每个时间步重复使用

RNN 的问题:梯度消失

由于反向传播需要沿时间步链式乘法,当序列很长时,梯度会指数级缩小(梯度消失)或爆炸:

  • 100 步前的信息,梯度传回来时几乎为 0 → 模型完全无法学到长距离依赖
  • 梯度爆炸用**梯度裁剪(Gradient Clipping)**缓解:torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5)

LSTM:长短期记忆网络

LSTM(1997 年提出)通过引入门控机制和**细胞状态(cell state)**解决梯度消失:

LSTM 的四个核心组件:

遗忘门 fₜ = σ(Wf · [hₜ₋₁, xₜ] + bf)
  → 决定从上一个 cell state 中「忘记」多少(0=全忘,1=全保留)

输入门 iₜ = σ(Wi · [hₜ₋₁, xₜ] + bi)
候选值  c̃ₜ = tanh(Wc · [hₜ₋₁, xₜ] + bc)
  → 决定把多少「新信息」写入 cell state

更新 cell state:
  Cₜ = fₜ ⊙ Cₜ₋₁ + iₜ ⊙ c̃ₜ   (⊙ 是逐元素乘法)

输出门 oₜ = σ(Wo · [hₜ₋₁, xₜ] + bo)
隐状态  hₜ = oₜ ⊙ tanh(Cₜ)
  → 决定从 cell state 中「读出」多少作为输出

关键直觉:Cell state 是「长期记忆」的高速公路,梯度可以几乎无衰减地沿着它反传,解决了长距离依赖问题。

GRU:LSTM 的简化版

GRU(2014 年)把 LSTM 的三个门简化为两个,参数更少,训练更快,效果通常与 LSTM 相当:

import torch.nn as nn

# LSTM 和 GRU 的 API 完全一样,直接替换
lstm = nn.LSTM(input_size=10, hidden_size=64, num_layers=2,
               batch_first=True, dropout=0.3)
gru  = nn.GRU(input_size=10,  hidden_size=64, num_layers=2,
              batch_first=True, dropout=0.3)

# 双向 RNN:同时从左到右 + 从右到左处理序列
bilstm = nn.LSTM(input_size=10, hidden_size=64, num_layers=2,
                 batch_first=True, bidirectional=True)

实战一:时间序列预测(股价预测示意)

import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import Dataset, DataLoader


class TimeSeriesDataset(Dataset):
    def __init__(self, data: np.ndarray, seq_len: int = 30, pred_len: int = 1):
        """
        data:     一维时间序列
        seq_len:  用过去多少步预测
        pred_len: 预测未来多少步
        """
        self.X, self.y = [], []
        for i in range(len(data) - seq_len - pred_len + 1):
            self.X.append(data[i: i + seq_len])
            self.y.append(data[i + seq_len: i + seq_len + pred_len])
        self.X = torch.FloatTensor(self.X).unsqueeze(-1)  # [N, seq_len, 1]
        self.y = torch.FloatTensor(self.y)

    def __len__(self):  return len(self.X)
    def __getitem__(self, i): return self.X[i], self.y[i]


class LSTMForecaster(nn.Module):
    def __init__(self, input_size=1, hidden_size=64, num_layers=2,
                 dropout=0.2, pred_len=1):
        super().__init__()
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers,
                            batch_first=True, dropout=dropout)
        self.fc   = nn.Linear(hidden_size, pred_len)

    def forward(self, x):
        out, _ = self.lstm(x)           # [batch, seq_len, hidden]
        return self.fc(out[:, -1, :])   # 只用最后时间步的隐状态预测


# 生成示例数据:带噪声的正弦波
t    = np.linspace(0, 100, 2000)
data = np.sin(t) + np.sin(0.5*t) + 0.1 * np.random.randn(2000)

# 归一化到 [-1, 1]
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler(feature_range=(-1, 1))
data_scaled = scaler.fit_transform(data.reshape(-1, 1)).flatten()

split = int(len(data_scaled) * 0.8)
train_ds = TimeSeriesDataset(data_scaled[:split], seq_len=50)
test_ds  = TimeSeriesDataset(data_scaled[split:], seq_len=50)
train_loader = DataLoader(train_ds, batch_size=64, shuffle=True)
test_loader  = DataLoader(test_ds,  batch_size=64, shuffle=False)

device    = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model     = LSTMForecaster().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.MSELoss()

for epoch in range(50):
    model.train()
    for X_b, y_b in train_loader:
        X_b, y_b = X_b.to(device), y_b.to(device)
        optimizer.zero_grad()
        loss = criterion(model(X_b), y_b)
        loss.backward()
        # 梯度裁剪,防止 RNN/LSTM 的梯度爆炸
        nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)
        optimizer.step()

    if epoch % 10 == 9:
        model.eval()
        val_losses = []
        with torch.no_grad():
            for X_b, y_b in test_loader:
                X_b, y_b = X_b.to(device), y_b.to(device)
                val_losses.append(criterion(model(X_b), y_b).item())
        print(f"Epoch {epoch+1:3d} | Val MSE: {np.mean(val_losses):.6f}")

实战二:文本情感分类

import torch
import torch.nn as nn
from torch.nn.utils.rnn import pad_sequence, pack_padded_sequence, pad_packed_sequence


class TextLSTM(nn.Module):
    def __init__(self, vocab_size, embed_dim=128, hidden_size=256,
                 num_layers=2, num_classes=2, dropout=0.3):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
        self.lstm = nn.LSTM(embed_dim, hidden_size, num_layers,
                            batch_first=True, dropout=dropout,
                            bidirectional=True)
        # 双向 LSTM:hidden_size × 2
        self.classifier = nn.Sequential(
            nn.Linear(hidden_size * 2, 128),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(128, num_classes),
        )

    def forward(self, x, lengths):
        # x: [batch, seq_len],lengths: 各样本真实长度
        emb = self.embedding(x)    # [batch, seq_len, embed_dim]

        # packed sequence 避免 padding 参与 LSTM 计算
        packed = pack_padded_sequence(emb, lengths.cpu(),
                                      batch_first=True, enforce_sorted=False)
        out, (h, _) = self.lstm(packed)

        # 取最后一层双向的隐状态拼接
        h_last = torch.cat([h[-2], h[-1]], dim=1)  # [batch, hidden*2]
        return self.classifier(h_last)
pack_padded_sequencepad_packed_sequence 是处理变长序列的标准方法,能让 LSTM 跳过 padding 位置,结果更准确,训练更快。

RNN / LSTM vs Transformer:怎么选

维度RNN / LSTMTransformer
序列长度短中序列(< 1000 步)较好支持超长序列(Flash Attention 后)
训练速度慢(必须顺序计算)快(全并行)
长距离依赖LSTM 改善,仍有限天然全局依赖
小数据集参数少,表现稳定容易过拟合
实时流式推理天然支持(逐步更新隐状态)需要额外工程处理
可解释性隐状态可分析注意力权重可可视化

推荐:时间序列预测(传感器、金融)和小数据集序列任务优先试 LSTM / GRU;NLP 任务用预训练的 Transformer 模型(BERT、Qwen 等)远快于自己训练。

一句话小结

RNN 给网络加了「时间维度的记忆」;LSTM 用门控机制解决了梯度消失,让模型能记住几百步前的信息。两者在时间序列预测、小数据序列任务上仍是实用选择,掌握它们也是理解 Transformer「为什么要替代 RNN」的前提。

最后更新于