环境声明

在开始本章学习之前,请确保你的开发环境满足以下要求:

环境项版本要求说明
Python3.10+建议使用 Python 3.10 或更高版本
PyTorch2.0+深度学习框架,支持 GPU 加速
NumPy1.24+数值计算基础库
Matplotlib3.7+数据可视化库
开发工具PyCharm / VS Code推荐使用带有 Jupyter 支持的 IDE
操作系统Windows / macOS / Linux全平台支持

补充:本章所有代码均经过 Python 3.12 + PyTorch 2.3 环境测试,确保可复现性。


学习目标与摘要

本章学习目标

  1. 深入理解RNN长程依赖问题的数学本质
  2. 掌握LSTM的门控机制与细胞状态原理
  3. 理解GRU的简化设计与更新门、重置门机制
  4. 能够对比分析LSTM与GRU的优缺点
  5. 了解LSTM的现代变体:Peephole、LayerNorm、Zoneout
  6. 能够使用PyTorch从零实现LSTM和GRU
  7. 掌握门控网络在时间序列预测和文本生成中的应用

文章摘要:长短期记忆网络(LSTM)和门控循环单元(GRU)是循环神经网络的重要改进版本,专门用于解决传统RNN在处理长序列时的梯度消失问题。本章将从长程依赖问题的数学推导出发,深入剖析LSTM的遗忘门、输入门、输出门三大门控机制,以及细胞状态的巧妙设计。随后介绍GRU如何通过两个门控简化LSTM结构,同时保持相近的性能。最后,我们将实现完整的LSTM和GRU代码,并探讨现代变体技术。


1. 长程依赖问题分析

1.1 什么是长程依赖问题

在序列数据处理中,长程依赖问题指的是模型难以捕捉序列中相距较远元素之间的关联关系。例如,在句子"我出生在中国,所以我的母语是____"中,预测空白处需要依赖开头的"中国"一词,但这两个词之间相隔较远。

1.2 梯度消失的数学推导

传统RNN的隐藏状态更新公式为:

h_t = tanh(W_h * h_{t-1} + W_x * x_t + b)

在反向传播过程中,损失函数L对h_t的梯度需要通过时间步传播:

∂L/∂h_t = ∂L/∂h_T * ∂h_T/∂h_{T-1} * ... * ∂h_{t+1}/∂h_t

其中每一步的雅可比矩阵为:

∂h_t/∂h_{t-1} = diag(1 - tanh^2(z_t)) * W_h

由于tanh的导数范围在(0, 1]之间,当序列较长时,多个小于1的数相乘会导致梯度指数级衰减:

||∂h_t/∂h_{t-1}|| ≤ γ,其中γ < 1

经过T步传播后:||∂L/∂h_1|| ≤ γ^T * ||∂L/∂h_T||

当T=100,γ=0.9时,γ^T ≈ 2.6×10^-5,梯度几乎消失殆尽。

1.3 梯度爆炸问题

相反,如果权重矩阵W_h的谱半径大于1,梯度会随着传播指数级增长:

||∂h_t/∂h_{t-1}|| > 1

经过T步传播后:||∂L/∂h_1|| 可能趋向无穷大

这会导致参数更新不稳定,训练过程发散。

一句话总结:长程依赖问题的本质是梯度在时间反向传播时的指数级衰减或增长,导致模型无法学习远距离的依赖关系。


2. LSTM门控机制详解

2.1 LSTM的核心思想

长短期记忆网络(Long Short-Term Memory,LSTM)由Hochreiter和Schmidhuber于1997年提出,其核心思想是引入细胞状态(Cell State)作为信息的高速通道,通过门控机制控制信息的流动。

LSTM的结构可以类比为一条传送带(细胞状态)和多个安检门(门控):

  • 传送带贯穿整个链条,信息可以在上面相对 unchanged 地流动
  • 门控决定哪些信息可以通过、哪些需要被遗忘、哪些需要更新

2.2 遗忘门(Forget Gate)

遗忘门决定从细胞状态中丢弃哪些信息。它读取上一时刻的隐藏状态h_{t-1}和当前输入x_t,输出一个0到1之间的数值(1表示完全保留,0表示完全丢弃)。

数学公式

f_t = σ(W_f · [h_{t-1}, x_t] + b_f)

其中:

  • f_t:遗忘门的输出向量,每个元素在(0, 1)之间
  • W_f:遗忘门的权重矩阵
  • b_f:遗忘门的偏置向量
  • σ:sigmoid激活函数
  • [h_{t-1}, x_t]:表示向量拼接

直观理解:遗忘门就像一个"记忆筛选器",决定哪些旧记忆应该被遗忘。例如,在阅读"我昨天去了北京,今天去了上海"时,遗忘门会帮助模型"忘记"北京相关的信息,转而关注上海。

2.3 输入门(Input Gate)

输入门决定哪些新信息将被存储到细胞状态中。它包含两个部分:

  1. 输入门控:决定哪些值需要更新
  2. 候选细胞状态:生成新的候选值

数学公式

i_t = σ(W_i · [h_{t-1}, x_t] + b_i)        # 输入门控
C̃_t = tanh(W_C · [h_{t-1}, x_t] + b_C)     # 候选细胞状态

其中:

  • i_t:输入门的输出,控制新信息的通过程度
  • C̃_t:候选细胞状态,包含可能加入的新信息

2.4 细胞状态更新

细胞状态的更新结合了遗忘门和输入门的信息:

数学公式

C_t = f_t ⊙ C_{t-1} + i_t ⊙ C̃_t

其中:

  • C_t:当前时刻的细胞状态
  • C_{t-1}:上一时刻的细胞状态
  • ⊙:逐元素乘法(Hadamard积)

关键特性

  • 这是一个线性操作,没有激活函数,梯度可以无损传播
  • f_t ⊙ C_{t-1}:保留部分旧信息
  • i_t ⊙ C̃_t:添加部分新信息

2.5 输出门(Output Gate)

输出门决定从细胞状态中输出哪些信息到隐藏状态:

数学公式

o_t = σ(W_o · [h_{t-1}, x_t] + b_o)        # 输出门控
h_t = o_t ⊙ tanh(C_t)                      # 隐藏状态

其中:

  • o_t:输出门的输出,控制细胞状态的哪些部分输出
  • h_t:当前时刻的隐藏状态,作为下一层的输入

2.6 LSTM完整公式汇总

遗忘门:    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)

2.7 LSTM结构图示说明

                    x_t
                     |
                     v
    +-----------------------------------+
    |           LSTM Cell               |
    |                                   |
h_{t-1} ----->  +----------------+      |
    |           |   Concatenate  |      |
    |           |  [h_{t-1},x_t] |      |
    |           +--------+-------+      |
    |                    |              |
    |        +-----------+-----------+  |
    |        |           |           |  |
    |        v           v           v  |
    |    +------+    +------+    +------+|
    |    |σ     |    |σ     |    |σ     ||
    |    |W_f  |    |W_i  |    |W_o  ||
    |    +--+---+    +--+---+    +--+---+|
    |       | f_t       | i_t       | o_t|
    |       |           |           |    |
    |       v           v           |    |
    |   +------+     +------+       |    |
    |   | ⊙    |     |tanh  |       |    |
C_{t-1}--> |      |     |W_C  |       |    |
    |   +---+----+     +---+----+    |    |
    |       |              | C̃_t    |    |
    |       |              |         |    |
    |       v              v         |    |
    |   +----------------------+     |    |
    |   |  C_t = f_t*C_{t-1}   |     |    |
    |   |       + i_t*C̃_t      |     |    |
    |   +----------+-----------+     |    |
    |              | C_t              |    |
    |              v                  |    |
    |          +------+               |    |
    |          |tanh  |<--------------+    |
    |          +--+---+                    |
    |             |                        |
    |             v                        |
    |          +------+                    |
    +--------->|  ⊙   |--------------------+
               +--+---+
                  |
                  v
                 h_t

3. GRU:LSTM的简化版本

3.1 GRU的设计动机

门控循环单元(Gated Recurrent Unit,GRU)由Cho等人于2014年提出,旨在简化LSTM的结构,同时保持相近的性能。GRU将细胞状态和隐藏状态合并,并将三个门简化为两个门。

3.2 更新门(Update Gate)

更新门兼具LSTM中遗忘门和输入门的功能,控制前一时刻隐藏状态的信息有多少被保留到当前时刻:

数学公式

z_t = σ(W_z · [h_{t-1}, x_t] + b_z)

其中:

  • z_t:更新门的输出,范围在(0, 1)之间
  • 当z_t接近1时,保留更多旧信息
  • 当z_t接近0时,接受更多新信息

3.3 重置门(Reset Gate)

重置门决定如何将新的输入与过去的记忆相结合,控制前一时刻隐藏状态的信息有多少被用于计算候选隐藏状态:

数学公式

r_t = σ(W_r · [h_{t-1}, x_t] + b_r)

其中:

  • r_t:重置门的输出
  • 当r_t接近0时,忽略过去的隐藏状态
  • 当r_t接近1时,充分利用过去的隐藏状态

3.4 候选隐藏状态与最终隐藏状态

数学公式

h̃_t = tanh(W · [r_t ⊙ h_{t-1}, x_t] + b)     # 候选隐藏状态
h_t = (1 - z_t) ⊙ h_{t-1} + z_t ⊙ h̃_t        # 最终隐藏状态

3.5 GRU完整公式汇总

更新门:      z_t = σ(W_z · [h_{t-1}, x_t] + b_z)
重置门:      r_t = σ(W_r · [h_{t-1}, x_t] + b_r)
候选状态:    h̃_t = tanh(W · [r_t ⊙ h_{t-1}, x_t] + b)
隐藏状态:    h_t = (1 - z_t) ⊙ h_{t-1} + z_t ⊙ h̃_t

3.6 GRU结构图示说明

                    x_t
                     |
                     v
    +-----------------------------------+
    |            GRU Cell               |
    |                                   |
h_{t-1} ----->  +----------------+      |
    |           |   Concatenate  |      |
    |           |  [h_{t-1},x_t] |      |
    |           +--------+-------+      |
    |                    |              |
    |            +-------+-------+      |
    |            |               |      |
    |            v               v      |
    |        +------+        +------+   |
    |        |σ     |        |σ     |   |
    |        |W_z  |        |W_r  |   |
    |        +--+---+        +--+---+   |
    |           | z_t           | r_t   |
    |           |               |       |
    |           |               v       |
    |           |           +------+    |
    |           |           |  ⊙   |    |
    |           |      h_{t-1}-->|    |    |
    |           |           +--+---+    |
    |           |              |        |
    |           |              v        |
    |           |         +--------+    |
    |           +-------->| Concat |    |
    |                     +---+----+    |
    |                         |         |
    |                         v         |
    |                     +------+      |
    |                     |tanh  |      |
    |                     |W    |      |
    |                     +--+---+      |
    |                        | h̃_t      |
    |                        |          |
    |           +------------+          |
    |           |                       |
    |           v                       |
    |   +-----------------------+       |
    |   | h_t = (1-z_t)*h_{t-1} |       |
    |   |       + z_t*h̃_t       |       |
    |   +-----------+-----------+       |
    +---------------|-------------------+
                    |
                    v
                   h_t

4. LSTM vs GRU 对比分析

4.1 结构对比表

特性LSTMGRU
门控数量3个(遗忘门、输入门、输出门)2个(更新门、重置门)
状态数量2个(细胞状态C_t、隐藏状态h_t)1个(隐藏状态h_t)
参数量4倍隐藏层维度3倍隐藏层维度
计算复杂度较高较低
训练速度较慢较快
记忆能力更强,适合长序列稍弱,适合中等长度序列
收敛速度通常较慢通常较快

4.2 参数量对比

假设输入维度为input_size,隐藏层维度为hidden_size:

LSTM参数量

总参数量 = 4 × (input_size + hidden_size) × hidden_size + 4 × hidden_size

GRU参数量

总参数量 = 3 × (input_size + hidden_size) × hidden_size + 3 × hidden_size

GRU的参数量约为LSTM的75%,在资源受限场景下更具优势。

4.3 性能对比

根据大量实验研究:

场景推荐选择原因
长序列(>100步)LSTM细胞状态提供更强的长程记忆能力
短序列(<50步)GRU参数量少,训练更快,性能相当
资源受限环境GRU计算量和内存占用更少
需要精细控制信息流LSTM三个门提供更细粒度的控制
快速原型验证GRU收敛更快,调试更简单

4.4 选择建议

在实际应用中,建议遵循以下原则:

  1. 从GRU开始:由于GRU更简单、训练更快,建议先尝试GRU
  2. 监控验证集性能:如果GRU在长序列上表现不佳,切换到LSTM
  3. 考虑计算资源:在移动端或嵌入式设备上,优先使用GRU
  4. 尝试双向结构:无论是LSTM还是GRU,双向版本通常能显著提升性能

5. LSTM的变体与优化

5.1 Peephole LSTM

标准LSTM中,门控只依赖前一时刻的隐藏状态h_{t-1}和当前输入x_t。Peephole LSTM允许门控"窥视"细胞状态C_{t-1}:

f_t = σ(W_f · [C_{t-1}, h_{t-1}, x_t] + b_f)
i_t = σ(W_i · [C_{t-1}, h_{t-1}, x_t] + b_i)
o_t = σ(W_o · [C_t, h_{t-1}, x_t] + b_o)

优势:门控可以直接感知细胞状态,做出更精确的控制决策。

5.2 Layer Normalization LSTM

Layer Normalization(层归一化)可以稳定LSTM的训练过程,加速收敛:

# 在门控计算后添加LayerNorm
f_t = σ(LayerNorm(W_f · [h_{t-1}, x_t] + b_f))
i_t = σ(LayerNorm(W_i · [h_{t-1}, x_t] + b_i))
C̃_t = tanh(LayerNorm(W_C · [h_{t-1}, x_t] + b_C))
o_t = σ(LayerNorm(W_o · [h_{t-1}, x_t] + b_o))

优势

  • 减少对初始化的敏感性
  • 允许使用更大的学习率
  • 在循环神经网络中效果尤为显著

5.3 Zoneout

Zoneout是一种针对循环网络的正则化技术,以一定概率保持隐藏状态不变:

# 训练时
if random() < p:
    h_t = h_{t-1}  # 保持前一状态不变
else:
    h_t = o_t ⊙ tanh(C_t)  # 正常计算

# 测试时(推理时)
h_t = p * h_{t-1} + (1 - p) * (o_t ⊙ tanh(C_t))

优势

  • 防止过拟合
  • 保持梯度流动的稳定性
  • 不需要修改网络结构

5.4 双向LSTM/GRU

双向结构同时考虑过去和未来的上下文信息:

正向LSTM:  h_t_forward = LSTM(x_t, h_{t-1})
反向LSTM:  h_t_backward = LSTM(x_t, h_{t+1})
最终输出:  h_t = [h_t_forward; h_t_backward]  # 拼接

应用场景

  • 命名实体识别(NER)
  • 情感分析
  • 语音识别
  • 机器翻译的编码器

6. LSTM/GRU PyTorch完整实现

6.1 使用PyTorch内置LSTM

import torch
import torch.nn as nn

# 定义LSTM模型
class LSTMModel(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, num_classes):
        super(LSTMModel, self).__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,  # 输入格式为(batch, seq, feature)
            dropout=0.2 if num_layers > 1 else 0,  # 多层时添加dropout
            bidirectional=False  # 单向LSTM
        )
        
        # 全连接层
        self.fc = nn.Linear(hidden_size, num_classes)
    
    def forward(self, x):
        # 初始化隐藏状态和细胞状态
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
        c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
        
        # 前向传播
        out, _ = self.lstm(x, (h0, c0))  # out: (batch, seq, hidden)
        
        # 取最后一个时间步的输出
        out = out[:, -1, :]
        
        # 全连接层
        out = self.fc(out)
        return out

# 模型参数
input_size = 28      # 输入特征维度
hidden_size = 128    # 隐藏层维度
num_layers = 2       # LSTM层数
num_classes = 10     # 输出类别数

# 创建模型
model = LSTMModel(input_size, hidden_size, num_layers, num_classes)
print(model)

6.2 使用PyTorch内置GRU

import torch
import torch.nn as nn

# 定义GRU模型
class GRUModel(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, num_classes):
        super(GRUModel, self).__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=0.2 if num_layers > 1 else 0,
            bidirectional=False
        )
        
        # 全连接层
        self.fc = nn.Linear(hidden_size, num_classes)
    
    def forward(self, x):
        # 初始化隐藏状态
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
        
        # 前向传播
        out, _ = self.gru(x, h0)  # out: (batch, seq, hidden)
        
        # 取最后一个时间步的输出
        out = out[:, -1, :]
        
        # 全连接层
        out = self.fc(out)
        return out

# 创建模型
model = GRUModel(input_size, hidden_size, num_layers, num_classes)
print(model)

6.3 从零实现LSTM(NumPy版本)

import numpy as np

class LSTMCell:
    """LSTM单元的手动实现"""
    
    def __init__(self, input_size, hidden_size):
        self.input_size = input_size
        self.hidden_size = hidden_size
        
        # 初始化权重(使用Xavier初始化)
        concat_size = input_size + hidden_size
        
        # 遗忘门权重
        self.W_f = np.random.randn(hidden_size, concat_size) * np.sqrt(2.0 / concat_size)
        self.b_f = np.zeros((hidden_size, 1))
        
        # 输入门权重
        self.W_i = np.random.randn(hidden_size, concat_size) * np.sqrt(2.0 / concat_size)
        self.b_i = np.zeros((hidden_size, 1))
        
        # 候选细胞状态权重
        self.W_C = np.random.randn(hidden_size, concat_size) * np.sqrt(2.0 / concat_size)
        self.b_C = np.zeros((hidden_size, 1))
        
        # 输出门权重
        self.W_o = np.random.randn(hidden_size, concat_size) * np.sqrt(2.0 / concat_size)
        self.b_o = np.zeros((hidden_size, 1))
    
    def sigmoid(self, x):
        return 1 / (1 + np.exp(-np.clip(x, -500, 500)))
    
    def tanh(self, x):
        return np.tanh(x)
    
    def forward(self, x_t, h_prev, C_prev):
        """
        前向传播
        x_t: 当前输入 (input_size, 1)
        h_prev: 前一隐藏状态 (hidden_size, 1)
        C_prev: 前一细胞状态 (hidden_size, 1)
        """
        # 拼接输入和前一隐藏状态
        concat = np.vstack((h_prev, x_t))
        
        # 遗忘门
        f_t = self.sigmoid(np.dot(self.W_f, concat) + self.b_f)
        
        # 输入门
        i_t = self.sigmoid(np.dot(self.W_i, concat) + self.b_i)
        
        # 候选细胞状态
        C_tilde = self.tanh(np.dot(self.W_C, concat) + self.b_C)
        
        # 更新细胞状态
        C_t = f_t * C_prev + i_t * C_tilde
        
        # 输出门
        o_t = self.sigmoid(np.dot(self.W_o, concat) + self.b_o)
        
        # 更新隐藏状态
        h_t = o_t * self.tanh(C_t)
        
        return h_t, C_t

# 测试LSTM单元
input_size = 10
hidden_size = 20
lstm_cell = LSTMCell(input_size, hidden_size)

# 随机输入
x_t = np.random.randn(input_size, 1)
h_prev = np.zeros((hidden_size, 1))
C_prev = np.zeros((hidden_size, 1))

# 前向传播
h_t, C_t = lstm_cell.forward(x_t, h_prev, C_prev)
print(f"隐藏状态形状: {h_t.shape}")
print(f"细胞状态形状: {C_t.shape}")

6.4 从零实现GRU(NumPy版本)

import numpy as np

class GRUCell:
    """GRU单元的手动实现"""
    
    def __init__(self, input_size, hidden_size):
        self.input_size = input_size
        self.hidden_size = hidden_size
        
        # 初始化权重
        concat_size = input_size + hidden_size
        
        # 更新门权重
        self.W_z = np.random.randn(hidden_size, concat_size) * np.sqrt(2.0 / concat_size)
        self.b_z = np.zeros((hidden_size, 1))
        
        # 重置门权重
        self.W_r = np.random.randn(hidden_size, concat_size) * np.sqrt(2.0 / concat_size)
        self.b_r = np.zeros((hidden_size, 1))
        
        # 候选隐藏状态权重
        self.W = np.random.randn(hidden_size, concat_size) * np.sqrt(2.0 / concat_size)
        self.b = np.zeros((hidden_size, 1))
    
    def sigmoid(self, x):
        return 1 / (1 + np.exp(-np.clip(x, -500, 500)))
    
    def tanh(self, x):
        return np.tanh(x)
    
    def forward(self, x_t, h_prev):
        """
        前向传播
        x_t: 当前输入 (input_size, 1)
        h_prev: 前一隐藏状态 (hidden_size, 1)
        """
        # 拼接输入和前一隐藏状态
        concat = np.vstack((h_prev, x_t))
        
        # 更新门
        z_t = self.sigmoid(np.dot(self.W_z, concat) + self.b_z)
        
        # 重置门
        r_t = self.sigmoid(np.dot(self.W_r, concat) + self.b_r)
        
        # 候选隐藏状态(使用重置门)
        concat_reset = np.vstack((r_t * h_prev, x_t))
        h_tilde = self.tanh(np.dot(self.W, concat_reset) + self.b)
        
        # 更新隐藏状态
        h_t = (1 - z_t) * h_prev + z_t * h_tilde
        
        return h_t

# 测试GRU单元
input_size = 10
hidden_size = 20
gru_cell = GRUCell(input_size, hidden_size)

# 随机输入
x_t = np.random.randn(input_size, 1)
h_prev = np.zeros((hidden_size, 1))

# 前向传播
h_t = gru_cell.forward(x_t, h_prev)
print(f"隐藏状态形状: {h_t.shape}")

6.5 时间序列预测实战

import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt

# 生成正弦波数据用于测试
def generate_sine_wave(seq_length, num_samples):
    X = []
    y = []
    for _ in range(num_samples):
        # 随机相位和频率
        phase = np.random.uniform(0, 2 * np.pi)
        freq = np.random.uniform(0.1, 0.5)
        
        # 生成序列
        seq = np.sin(2 * np.pi * freq * np.arange(seq_length) + phase)
        
        # 输入是前seq_length-1个值,输出是最后一个值
        X.append(seq[:-1])
        y.append(seq[-1])
    
    return np.array(X), np.array(y)

# 生成数据
seq_length = 50
num_samples = 1000
X, y = generate_sine_wave(seq_length, num_samples)

# 转换为PyTorch张量
X_tensor = torch.FloatTensor(X).unsqueeze(-1)  # (num_samples, seq_length-1, 1)
y_tensor = torch.FloatTensor(y).unsqueeze(-1)  # (num_samples, 1)

# 划分训练集和测试集
train_size = int(0.8 * num_samples)
X_train, X_test = X_tensor[:train_size], X_tensor[train_size:]
y_train, y_test = y_tensor[:train_size], y_tensor[train_size:]

# 定义模型
class TimeSeriesLSTM(nn.Module):
    def __init__(self, input_size=1, hidden_size=50, num_layers=2):
        super(TimeSeriesLSTM, self).__init__()
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, 
                           batch_first=True, dropout=0.2)
        self.fc = nn.Linear(hidden_size, 1)
    
    def forward(self, x):
        out, _ = self.lstm(x)
        out = self.fc(out[:, -1, :])
        return out

# 训练模型
model = TimeSeriesLSTM()
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

# 训练循环
num_epochs = 100
for epoch in range(num_epochs):
    model.train()
    optimizer.zero_grad()
    
    outputs = model(X_train)
    loss = criterion(outputs, y_train)
    
    loss.backward()
    optimizer.step()
    
    if (epoch + 1) % 20 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.6f}')

# 测试模型
model.eval()
with torch.no_grad():
    predictions = model(X_test)
    test_loss = criterion(predictions, y_test)
    print(f'\n测试集损失: {test_loss.item():.6f}')

7. 避坑小贴士

7.1 初始化问题

常见错误:使用默认的随机初始化导致训练不稳定。

解决方案

# 使用Xavier初始化
for name, param in model.named_parameters():
    if 'weight' in name:
        nn.init.xavier_uniform_(param)
    elif 'bias' in name:
        nn.init.zeros_(param)

7.2 梯度裁剪

常见错误:长序列训练时出现梯度爆炸。

解决方案

# 在反向传播后添加梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5)

7.3 隐藏状态初始化

常见错误:忘记在每个batch开始时重置隐藏状态。

解决方案

# 如果不重置,状态会跨batch保留(除非需要stateful训练)
def init_hidden(self, batch_size):
    return (torch.zeros(self.num_layers, batch_size, self.hidden_size),
            torch.zeros(self.num_layers, batch_size, self.hidden_size))

7.4 序列填充与packing

常见错误:处理变长序列时直接填充,导致计算浪费。

解决方案

# 使用pack_padded_sequence处理变长序列
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence

packed = pack_padded_sequence(embedded, lengths, batch_first=True, enforce_sorted=False)
output, _ = self.lstm(packed)
output, _ = pad_packed_sequence(output, batch_first=True)

7.5 学习率选择

常见错误:使用过大的学习率导致训练发散。

建议

  • LSTM/GRU通常需要较小的学习率(0.001或更小)
  • 使用学习率衰减策略
  • 考虑使用学习率预热

8. 本章小结和知识点回顾

核心概念回顾

  1. 长程依赖问题:传统RNN在处理长序列时,由于梯度消失/爆炸,无法有效学习远距离依赖关系。

  2. LSTM核心机制

    • 细胞状态:信息的高速通道,梯度可以无损传播
    • 遗忘门:决定丢弃哪些旧信息
    • 输入门:决定添加哪些新信息
    • 输出门:决定输出哪些信息
  3. GRU简化设计

    • 合并细胞状态和隐藏状态
    • 使用更新门和重置门替代三个门
    • 参数量减少25%,计算效率更高
  4. LSTM vs GRU选择

    • 长序列、需要精细控制:选择LSTM
    • 资源受限、快速训练:选择GRU
  5. 现代变体技术

    • Peephole LSTM:门控窥视细胞状态
    • LayerNorm LSTM:加速训练收敛
    • Zoneout:有效的正则化方法

数学公式速查

LSTM

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)

GRU

z_t = σ(W_z · [h_{t-1}, x_t] + b_z)
r_t = σ(W_r · [h_{t-1}, x_t] + b_r)
h̃_t = tanh(W · [r_t ⊙ h_{t-1}, x_t] + b)
h_t = (1 - z_t) ⊙ h_{t-1} + z_t ⊙ h̃_t

一句话总结

LSTM和GRU通过引入门控机制,为神经网络提供了选择性记忆的能力,就像给模型配备了一个智能笔记本,能够自动决定记录什么、遗忘什么、输出什么,从而有效解决了长程依赖问题。


如果本文对你有帮助,欢迎点赞、收藏、评论交流!更多深度学习内容,请关注《深度学习精通》系列专栏。

更多推荐