共计 3078 个字符,预计需要花费 8 分钟才能阅读完成。
技术背景
循环神经网络 (RNN) 是处理时序数据的利器,它能通过隐藏状态记住之前的信息,这一点在传统神经网络中是无法实现的。比如我们要预测句子的下一个单词,就必须考虑之前的单词顺序,这就是 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
避坑指南
- 维度错误调试:
- 检查输入数据形状是否符合(batch, seq_len, features)
-
使用
print(x.shape)在每个关键步骤验证 -
显存不足时:
- 减小 batch_size(如从 64 降到 16)
-
使用
torch.cuda.empty_cache()释放缓存 -
实验报告必备分析:
- 训练 / 验证损失曲线对比
- 不同超参数的效果对比表格
- 测试集上的 MAE、MSE 指标
扩展思考
股票预测改造
- 数据层面:
- 加入开盘价、成交量等多维特征
-
使用 MinMaxScaler 对不同量纲特征归一化
-
模型改进:
- 在 LSTM 后添加 Attention 层聚焦关键时间点
- 使用双向 LSTM 捕捉前后依赖
进阶学习路径
- 掌握 Transformer:
- 学习 Self-Attention 机制
-
实现 Informformer 等时序专用变体
-
实战提升:
- Kaggle 上的时间序列比赛
- 复现论文《Temporal Fusion Transformers》
通过这次实验,我深刻体会到 RNN 系列模型在时序数据中的独特价值。虽然调参过程有些繁琐,但当看到预测曲线完美拟合正弦波的那一刻,所有的 debug 都值得了!建议大家多尝试不同的网络结构和超参数,直观感受它们对模型性能的影响。
