BJTU深度学习实验五:循环神经网络(RNN)从原理到实战避坑指南

1次阅读
没有评论

共计 3078 个字符,预计需要花费 8 分钟才能阅读完成。

image.webp

技术背景

循环神经网络 (RNN) 是处理时序数据的利器,它能通过隐藏状态记住之前的信息,这一点在传统神经网络中是无法实现的。比如我们要预测句子的下一个单词,就必须考虑之前的单词顺序,这就是 RNN 的用武之地。

BJTU 深度学习实验五:循环神经网络 (RNN) 从原理到实战避坑指南

不过基础的 RNN 有个大问题:梯度消失。简单来说,当序列很长时,反向传播时梯度会越来越小,导致网络无法学习到长期依赖。这就像你记不住很久以前的事情一样。数学上可以表示为:

$$ \frac{\partial L}{\partial W} = \sum_{t=1}^{T} \frac{\partial L}{\partial h_T} \frac{\partial h_T}{\partial h_t} \frac{\partial h_t}{\partial W} $$

其中 $\frac{\partial h_T}{\partial h_t}$ 会随着 T - t 增大而指数级变小。

为了解决这个问题,LSTM 和 GRU 应运而生。它们通过门控机制选择性地记住或忘记信息,就像人脑会选择重要的事情记住一样。LSTM 有三个门 (输入门、遗忘门、输出门) 和一个记忆单元,而 GRU 简化成两个门(重置门、更新门),计算量更小但效果相近。

实验环境配置

建议使用以下环境配置,避免版本不兼容问题:

# environment.yml
name: rnn_lab
channels:
  - pytorch
  - conda-forge
dependencies:
  - python=3.8
  - pytorch=1.12.1
  - torchvision
  - cudatoolkit=11.3
  - matplotlib
  - pandas
  - jupyter

安装完成后可以用以下命令检查:

import torch
print(torch.__version__)  # 应输出 1.12+
print(torch.cuda.is_available())  # 检查 GPU 是否可用

核心代码实现

数据预处理

实验数据通常是时间序列,我们需要将其处理成 (sample, sequence_length, features) 的形式:

def create_dataset(data, look_back=10):
    X, y = [], []
    for i in range(len(data)-look_back):
        X.append(data[i:(i+look_back)])
        y.append(data[i+look_back])
    return torch.FloatTensor(X), torch.FloatTensor(y)

# 示例:正弦波数据
import numpy as np
data = np.sin(np.arange(0, 100, 0.1))
X, y = create_dataset(data)
print(X.shape, y.shape)  # 如(990, 10, 1), (990, 1)

模型构建

以下是带注释的 LSTM 实现:

import torch.nn as nn

class LSTMModel(nn.Module):
    def __init__(self, input_size=1, hidden_size=64, output_size=1):
        super().__init__()
        self.lstm = nn.LSTM(
            input_size=input_size,
            hidden_size=hidden_size,
            batch_first=True  # 输入格式为(batch, seq, feature)
        )
        self.linear = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        # LSTM 返回:(output, (h_n, c_n))
        out, _ = self.lstm(x)  # out 形状: (batch, seq_len, hidden_size)
        # 只取最后一个时间步
        out = self.linear(out[:, -1, :])
        return out

训练循环

关键技巧包括梯度裁剪和学习率调度:

model = LSTMModel()
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min')

for epoch in range(100):
    model.train()
    optimizer.zero_grad()
    outputs = model(X_train)
    loss = criterion(outputs, y_train)
    loss.backward()
    # 梯度裁剪防止爆炸
    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
    optimizer.step()

    # 验证集评估
    model.eval()
    with torch.no_grad():
        val_loss = criterion(model(X_val), y_val)
    scheduler.step(val_loss)

    if epoch % 10 == 0:
        print(f'Epoch {epoch}, Loss: {loss.item():.4f}, Val Loss: {val_loss.item():.4f}')

实验优化技巧

学习率调整

对比实验表明:
– Adam 优化器初始 lr=0.001 效果较好
– 配合 ReduceLROnPlateau 调度器,当验证损失不再下降时自动减小 lr

隐藏层大小

在正弦波预测任务中测试:
– hidden_size=16:欠拟合,训练损失 >0.1
– hidden_size=64:最佳,训练损失≈0.01
– hidden_size=256:过拟合,验证损失上升

早停法实现

best_loss = float('inf')
patience = 5
counter = 0

for epoch in range(100):
    # ... 训练代码...

    if val_loss < best_loss:
        best_loss = val_loss
        counter = 0
        torch.save(model.state_dict(), 'best_model.pth')
    else:
        counter += 1
        if counter >= patience:
            print(f'Early stopping at epoch {epoch}')
            break

避坑指南

  1. 维度错误调试:
  2. 检查输入数据形状是否符合(batch, seq_len, features)
  3. 使用 print(x.shape) 在每个关键步骤验证

  4. 显存不足时:

  5. 减小 batch_size(如从 64 降到 16)
  6. 使用 torch.cuda.empty_cache() 释放缓存

  7. 实验报告必备分析:

  8. 训练 / 验证损失曲线对比
  9. 不同超参数的效果对比表格
  10. 测试集上的 MAE、MSE 指标

扩展思考

股票预测改造

  1. 数据层面:
  2. 加入开盘价、成交量等多维特征
  3. 使用 MinMaxScaler 对不同量纲特征归一化

  4. 模型改进:

  5. 在 LSTM 后添加 Attention 层聚焦关键时间点
  6. 使用双向 LSTM 捕捉前后依赖

进阶学习路径

  1. 掌握 Transformer:
  2. 学习 Self-Attention 机制
  3. 实现 Informformer 等时序专用变体

  4. 实战提升:

  5. Kaggle 上的时间序列比赛
  6. 复现论文《Temporal Fusion Transformers》

通过这次实验,我深刻体会到 RNN 系列模型在时序数据中的独特价值。虽然调参过程有些繁琐,但当看到预测曲线完美拟合正弦波的那一刻,所有的 debug 都值得了!建议大家多尝试不同的网络结构和超参数,直观感受它们对模型性能的影响。

正文完
 0
评论(没有评论)