
简介这份资源面向具备Python与机器学习基础、希望动手实践时间序列预测的开发者与数据分析学习者围绕LSTM神经网络在金融、气象、电力负荷等序列场景中的应用展开。压缩包共12个文件约108KB包含5个py脚本、2个csv数据集、1个json配置、1个md说明及txt、license等辅助文件分别承担数据处理、模型定义、训练预测与参数配置等职责。已有963人学习下载。资源提供从数据加载、归一化预处理到LSTM网络搭建、训练与预测的完整源码并附带sp500与正弦波两份示例数据便于读者直接运行验证理解输入门、遗忘门、输出门对长期依赖的捕捉机制以及超参数调整、过拟合监控和序列预测结果对比等关键环节适合作为入门到进阶的实操参考。1. 从一份销量数据说起LSTM 时间序列预测到底在解决什么去年帮一个做区域零售的朋友看数据他手里有三年多的日销量流水想预测未来 30 天各门店的出货量。一开始他用移动平均和线性回归短期还行一遇到节假日、促销叠加、连续阴雨这种组合就崩误差直接翻倍。后来换成 LSTM 时间序列预测同样的特征输入30 天滚动预测的 MAPE 从 18% 降到 7% 左右。这不是玄学是 LSTM 的门控结构天生适合处理「当前值依赖过去若干步、且依赖长度不固定」的序列。这篇要讲清楚的就是用 Python 从零搭一个 LSTM 时间序列预测的完整流程包括数据构造、模型定义、训练、滚动预测和评估。适合两类人一类是刚学完 Python 基础语法、想找一个能跑通的深度学习项目练手的新手另一类是已经用过 BP 神经网络或 ARIMA、但发现长序列效果不好想换 LSTM 的从业者。下面所有代码都可以直接复制运行数据用一份公开的日度时间序列即可不需要 GPU普通笔记本 CPU 也能在几分钟内跑完。2. 动手之前把 LSTM 预测时间序列的输入输出关系想明白2.1 为什么不是把整条序列丢进去就完事很多人第一次做 LSTM 时间序列预测会把一整条序列直接塞进网络期望它输出未来值。这样做的结果是训练 loss 降不下去或者预测出来是一条直线。原因在于 LSTM 的输入要求是三维张量形状为(样本数, 时间步长, 特征数)。整条序列只有一个样本网络没法做 mini-batch 训练梯度更新极不稳定。正确的做法是滑动窗口切分。假设用过去 30 天预测第 31 天那就把序列切成若干组第 1 到 30 天作为输入第 31 天作为标签第 2 到 31 天作为输入第 32 天作为标签依此类推。这样一条 1000 天的序列能切出 970 个样本每个样本的时间步长是 30特征数是 1如果只用单变量。这个窗口长度就是 LSTM 时间序列预测里第一个要调的参数后面会细说。2.2 单变量和多变量在代码上的差别单变量就是只用历史销量预测未来销量特征数等于 1。多变量是把温度、是否节假日、促销标记这些外部变量拼进去特征数变成 N。代码结构完全一样区别只在数据准备阶段单变量直接取一列多变量把多列按时间对齐后拼成二维数组再滑窗切成三维。我一般建议新手先从单变量跑通确认整个链路没问题再加外部变量。因为多变量会引入量纲问题温度是几十促销标记是 0/1不归一化的话 LSTM 的输入门会被大数值主导训练直接翻车。2.3 归一化别在训练集和测试集上分别做时间序列和普通回归不一样不能先划分训练测试再各自归一化。正确顺序是先按时间切分训练集和测试集然后用训练集的均值和方差去归一化测试集。如果用测试集自己的统计量等于提前泄露了未来信息评估结果会虚高。import numpy as np import pandas as pd import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset # 假设 df 有一列 value索引是日期 # 1. 按时间切分前 80% 训练后 20% 测试 split int(len(df) * 0.8) train_raw df[value].values[:split].reshape(-1, 1) test_raw df[value].values[split:].reshape(-1, 1) # 2. 用训练集统计量归一化 mean, std train_raw.mean(), train_raw.std() train_norm (train_raw - mean) / std test_norm (test_raw - mean) / std # 3. 滑动窗口切分 def make_windows(data, window_size): X, y [], [] for i in range(len(data) - window_size): X.append(data[i:iwindow_size]) y.append(data[iwindow_size]) return np.array(X), np.array(y) WINDOW 30 X_train, y_train make_windows(train_norm, WINDOW) X_test, y_test make_windows(test_norm, WINDOW) # 4. 转成 torch 张量 X_train torch.tensor(X_train, dtypetorch.float32) y_train torch.tensor(y_train, dtypetorch.float32) X_test torch.tensor(X_test, dtypetorch.float32) y_test torch.tensor(y_test, dtypetorch.float32) train_loader DataLoader(TensorDataset(X_train, y_train), batch_size32, shuffleTrue)这段代码里WINDOW是时间步长batch_size一般取 16 到 64数据量小就取小一点。shuffleTrue在训练集上可以打乱但测试集绝对不能打乱因为滚动预测依赖顺序。归一化用的mean和std要保存下来预测完反归一化时还要用。3. 搭一个能跑通的 LSTM 模型层数、隐藏单元和 Dropout 怎么定3.1 模型定义输入维度、隐藏状态和输出层PyTorch 里定义 LSTM 时间序列预测模型核心是nn.LSTM加一个全连接层。nn.LSTM的input_size等于特征数单变量就是 1hidden_size是隐藏单元数常见取值 32、64、128num_layers是堆叠的 LSTM 层数一般 1 到 2 层够用再多容易过拟合。class LSTMForecaster(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, # 输入形状 (batch, seq, feature) dropoutdropout if num_layers 1 else 0 ) self.fc nn.Linear(hidden_size, 1) def forward(self, x): # x: (batch, window, input_size) out, (h_n, c_n) self.lstm(x) # 取最后一个时间步的输出 last out[:, -1, :] return self.fc(last)batch_firstTrue很关键默认是 False输入形状会变成(seq, batch, feature)新手经常在这里踩坑。dropout只在num_layers 1时生效单层 LSTM 加 dropout 没有意义。forward里取out[:, -1, :]表示只用最后一个时间步的隐藏状态做预测这是最常见做法也可以对所有时间步做注意力池化但那是进阶玩法。3.2 训练循环损失函数、优化器和早停时间序列回归用 MSE 损失优化器用 Adam学习率从 1e-3 开始。训练轮数不用设太大配合早停即可。device torch.device(cuda if torch.cuda.is_available() else cpu) model LSTMForecaster(input_size1, hidden_size64, num_layers2).to(device) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) best_loss float(inf) patience, wait 10, 0 for epoch in range(200): model.train() total_loss 0 for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() optimizer.step() total_loss loss.item() * xb.size(0) avg_loss total_loss / len(train_loader.dataset) if avg_loss best_loss: best_loss avg_loss wait 0 torch.save(model.state_dict(), best_lstm.pth) else: wait 1 if wait patience: print(fEarly stop at epoch {epoch}) break if epoch % 20 0: print(fEpoch {epoch}, Loss {avg_loss:.6f})patience10表示验证损失连续 10 轮不下降就停。这里为了简洁只用了训练损失做早停严谨做法是从训练集里再切一段验证集。torch.save保存的是state_dict加载时先实例化模型再load_state_dict。3.3 滚动预测一次预测多步的正确姿势直接让模型输出未来 30 天是做不到的因为全连接层只输出一个值。滚动预测的做法是用测试集最后 30 天预测第 31 天然后把预测值拼到窗口末尾丢掉最旧的一天再预测下一天循环 30 次。model.load_state_dict(torch.load(best_lstm.pth)) model.eval() def rolling_forecast(model, init_window, steps): # init_window: (1, WINDOW, 1) 归一化后的张量 preds [] window init_window.clone().to(device) with torch.no_grad(): for _ in range(steps): pred model(window) # (1, 1) preds.append(pred.item()) # 把预测值拼到窗口末尾去掉最早一步 window torch.cat([window[:, 1:, :], pred.unsqueeze(1)], dim1) return np.array(preds) last_window X_test[-1:].to(device) # 测试集最后一个窗口 forecast_norm rolling_forecast(model, last_window, 30) forecast forecast_norm * std mean # 反归一化window[:, 1:, :]是丢掉最早一天pred.unsqueeze(1)是把标量变成(1,1,1)再拼到时间维。滚动预测的误差会累积步数越多越不准所以一般只做 30 到 60 步再长就要考虑多变量或换模型。4. 避坑与排查LSTM 时间序列预测最常见的 5 个翻车现场4.1 预测出来是一条水平直线现象模型输出几乎不变跟均值差不多。原因通常是归一化没做或者学习率太大导致模型直接收敛到均值解。解决先确认输入数据在 0 附近、方差为 1再把学习率降到 1e-4 试一轮。如果还是直线检查窗口长度是不是太短比如只用 3 天预测第 4 天LSTM 学不到长期依赖。4.2 训练 loss 下降但测试集一塌糊涂现象训练 MSE 很低测试集预测偏差很大。原因一般是数据泄露比如归一化时用了全量数据的均值方差或者滑动窗口切分时训练集和测试集有重叠。解决严格按时间切分归一化只用训练集统计量切窗口时在训练集末尾和测试集开头之间留出WINDOW天的间隔。4.3 报错 Expected hidden[0] size (2, 32, 64), got (2, 1, 64)现象手动初始化隐藏状态时维度对不上。原因num_layers和batch_size没对齐。解决如果要用h_0和c_0形状必须是(num_layers, batch_size, hidden_size)。更简单的做法是不手动传让 LSTM 自己初始化。4.4 多变量输入后 loss 变成 NaN现象加入温度、促销等特征后训练几个 batch 就 NaN。原因不同特征量纲差异太大梯度爆炸。解决每个特征单独做标准化或者用 MinMaxScaler 缩到 [0,1]。另外检查有没有缺失值NaN 输入会直接污染梯度。4.5 滚动预测越往后越离谱现象前 5 天还行第 20 天开始完全偏离。原因误差累积这是自回归式滚动预测的固有缺陷。解决缩短预测步数或者改成直接多步输出——让全连接层输出steps个值训练时标签就是未来steps天的序列。后者需要改模型结构和数据切分方式但能显著缓解累积误差。5. 把 MAPE 压到 10% 以内三个我反复验证过的调参习惯第一个习惯是窗口长度不要拍脑袋。我一般会跑一组对比窗口取 7、14、30、60看测试集 MAPE 的变化。多数日度数据在 14 到 30 之间有一个明显低谷再大反而变差因为太久远的信息对当前预测是噪声。这个对比实验花不了多少时间但能避免后面反复调模型结构。第二个习惯是隐藏单元数从 32 起步。很多人一上来就设 256结果训练慢还过拟合。我的经验是数据量几千条以内hidden_size32或64足够上万条再考虑 128。层数优先加在 1 到 2 层之间2 层 LSTM 加 dropout 通常比 3 层不加 dropout 更稳。第三个习惯是评估指标至少看两个。MSE 对异常值敏感MAPE 对接近零的值敏感。我一般同时看 MAPE 和 RMSE如果 MAPE 很低但 RMSE 很高说明模型在大部分点上准但少数极端点偏得厉害这时候要回去检查那些点是不是节假日或促销日考虑加外部变量。def mape(y_true, y_pred): mask y_true ! 0 return np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100 def rmse(y_true, y_pred): return np.sqrt(np.mean((y_true - y_pred) ** 2)) true_future test_raw[-30:].flatten() print(fMAPE: {mape(true_future, forecast):.2f}%) print(fRMSE: {rmse(true_future, forecast):.2f})最后说一个我自己的教训别在没跑通单变量之前就上多变量和注意力机制。我见过太多人卡在数据对齐和维度报错上最后连一个能出图的预测都没跑出来。先把单变量 LSTM 的完整链路跑通把 MAPE 压到 10% 左右再逐步加特征、换结构每一步都有对照才知道是哪个改动起了作用。希望帮到你。本文还有配套的精品资源点击获取