简介:基于Python与LSTM神经网络的时间序列预测完整项目,面向有一定Python基础、希望掌握深度学习时序建模的开发者与学生。项目基于TensorFlow框架与Keras接口构建,覆盖数据清洗、特征提取、模型搭建与预测输出全流程,可迁移到空气质量、天气、股票等时序场景中学习。压缩包内含129个文件,以Python脚本(78个py)与CSV数据集(26个csv)为核心,另有checkpoint模型权重、TensorBoard日志(index/meta/data)及H5格式模型文件,能直接查看训练过程与复用模型,txt与md文件则提供说明文档。包体仅5.42MB,轻量易下载。目前已有7271人学习浏览,适合作为入门到进阶LSTM时序预测的参考资料。资源包含多个污染数据集(如pollution.csv、air_pollution_new.csv)与对应处理脚本,可据此复现数据清洗、特征工程、模型训练与预测的完整链路,是动手实践神经网络时序建模的实用工具包。
1. Python 基于 LSTM 神经网络的时间序列预测:这个 .zip 里到底装着什么
拿到一批带时间戳的数据(网站流量、机房温度、设备读数),想用 Python 预测下一小时或明天的值,很多人绕不开 LSTM。这个标题里的 .zip 大概率是一份打包好的 Python 工程:数据预处理、LSTM 模型定义、训练脚本、预测脚本都堆在一个压缩包里。我打开过的类似压缩包,翻车最狠的不是模型结构,而是数据形状和反归一化。LSTM 本身不是黑匣子,只是它要求“用过去 N 步预测下一步”这种监督结构,很多人第一步就理解偏了。这篇文章讲清楚完整路径:为什么 LSTM 适合时间序列预测,怎么把数据切成样本,怎么用 Keras 或 PyTorch 训练,以及 5 个高频踩坑点。适合手里有一维或多维序列数据、想跑通并评估一个可落地的预测模型的从业者。
2. 先看懂三个门再调参:LSTM 凭什么能做时间序列预测
2.1 从 RNN 梯度消失说起:LSTM 的细胞状态和三个门
普通 RNN 在每个时间步用同一个权重矩阵更新隐藏状态。理论上它能利用任意长度的历史,但反向传播时梯度要沿时间方向不断连乘,只要序列超过若干步,梯度就指数级缩小,长距离信息根本传不回来。这就是常说的长时间依赖问题。
LSTM 通过引入一条额外的细胞状态链来缓解。细胞状态像一条传送带,贯穿整个序列,内容由三个门控制。遗忘门决定上一时刻细胞状态中多少信息保留;输入门决定当前候选值有多少写入细胞状态;输出门决定细胞状态中哪些信息输出到隐藏状态。整体上,网络可以学会“长期记住某些模式,在需要时再释放出来”,这是普通 RNN 很难做到的。
| 门 | 核心作用 | 控制公式的输入 |
|---|---|---|
| 遗忘门 | 丢弃上一时刻的哪些旧信息 | h_{t-1} 与 x_t 拼接,激活后得到 0~1 权重 |
| 输入门 | 决定当前信息写入细胞状态的比例 | 同上,独立一组权重 |
| 输出门 | 决定从细胞状态输出到隐藏状态的内容 | 同样基于 h_{t-1} 和 x_t 计算 |
所以 LSTM 适合有明显长期依赖、但又不是纯粹固定周期的序列。如果你的数据是随机游走或强平稳噪声,LSTM 不一定比简单的均值或“预测值等于上一时刻值”的基线更好。这也是很多人跑完项目发现“LSTM 不如 Excel”的原因。选型时先看序列里有没有能被历史解释的结构,而不是盲目套神经网络。
2.2 输入形状 (batch, time, features) 是 LSTM 的第一个硬门槛
LSTM 和 Dense 网络最大的差异在输入形状。Dense 吃二维矩阵(样本数,特征数),LSTM 吃三维张量(样本数,时间步,特征数)。对时间序列来说,时间步就是“用过去多少个点”,特征数就是“每个点上有几路信号”。
举两个例子:
- 单变量序列一共 1000 个点,用 lookback=20,得到的 X 形状是 (981, 20, 1)。981 是能切出的窗口数量,20 是时间步,1 是特征数。
- 多变量序列有 3 个特征,同样 lookback=20,X 形状是 (981, 20, 3)。每个时间步上同时输入 3 个数值,比如温度、湿度、风速。
隐藏层 units 决定了每个时间步输出向量的维度,也决定了记忆容量。units=64 就是每个时间步压缩成 64 维向量。这里没有统一标准,太小学不到关系,太大容易过拟合,训练也慢。实战里我从 32 起步,验证集 loss 不再下降就翻倍到 64,最多到 128 左右。
经常有人把一维序列直接传给 model.fit,报错长得像 expected ndim=3, found ndim=2。解决办法就是把序列 reshape 成 (-1, 1),然后再切片。
2.3 最小可运行模型:用 Keras 定义 LSTM 网络
这里先给一个能直接跑通的最小模型,用随机数据验证网络结构没问题,再谈真实预处理。
import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense # 模拟一组监督样本:X 是历史窗口,y 是预测目标 X = np.random.rand(200, 10, 1) # (样本数, 时间步, 特征数) y = np.random.rand(200, 1) model = Sequential() # input_shape 只写时间步和特征数,不写 batch model.add(LSTM(units=64, input_shape=(10, 1), return_sequences=False)) model.add(Dense(1)) model.compile(optimizer='adam', loss='mse', metrics=['mae']) model.summary()逻辑说明:代码里的 X 已经是三维,200 个样本,每个样本 10 个历史点,每个点 1 个特征。LSTM 层会按时间步展开,逐步更新隐藏状态。
参数说明:
- units=64:LSTM 内部记忆向量的维度,可以理解为“用 64 个数来描述当前上下文”。
- input_shape=(10, 1):时间步 10,特征 1。这里不写样本数,是因为 Keras 会自动把第一维当作 batch。
- return_sequences=False:只返回最后一个时间步的隐藏状态,正好用来接 Dense 做单步预测。如果后面要继续叠 LSTM 层,前面层的 return_sequences 必须设成 True。
- Dense(1):输出一个标量,就是下一步的预测值。
- loss='mse':回归任务最常用的损失,时间序列预测基本都从 MSE 开始。
如果你第一轮只跑这个结构,就能确认数据流是通的。真实数据必须经过滑动窗口切分和归一化,下一章处理这件事。
3. 把序列切成 LSTM 能吃的样本:滑动窗口、MinMaxScaler 与切分边界
3.1 时间序列要做成监督学习:用过去 lookback 步预测未来一步
LSTM 不会自己理解“时间轴”,它只认“特征矩阵 + 标签”。所以必须先把一维序列转换成监督学习样本:每个样本的 X 是过去 lookback 个点,y 是窗口后紧邻的那个点。这个操作叫滑动窗口,也叫 look_back 或 sequence_length。
单变量时很简单,就是不断复制历史上的一小段。多变量时,窗口里同时包含多个通道,相当于每个时间步上放一组特征。这个转换看似机械,却决定了模型能学到什么。如果 lookback 太短,模型看不到足够上下文;如果太长,会把无关噪声也塞进每个样本,训练成本变高,预测也不一定更准。
3.2 可直接复用的数据预处理代码:滑动窗口 + 归一化 + 按时间切分
下面这份代码我在多数单变量项目里直接改改就能用,关键是训练测试切分和归一化的顺序不能乱。
import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler df = pd.read_csv("series.csv") values = df["value"].values.reshape(-1, 1) # 先按时间顺序切出训练集和测试集,再分别缩放 train_size = int(len(values) * 0.8) train_raw, test_raw = values[:train_size], values[train_size:] scaler = MinMaxScaler(feature_range=(0, 1)) train_scaled = scaler.fit_transform(train_raw) # 只 fit 训练集 test_scaled = scaler.transform(test_raw) # 复用同一个 scaler def make_windows(data, lookback): X, y = [], [] for i in range(lookback, len(data)): X.append(data[i-lookback:i]) y.append(data[i]) return np.array(X), np.array(y) lookback = 20 X_train, y_train = make_windows(train_scaled, lookback) X_test, y_test = make_windows(test_scaled, lookback) # LSTM 要求输入形状 (样本数, 时间步, 特征数) X_train = X_train.reshape((X_train.shape[0], lookback, 1)) X_test = X_test.reshape((X_test.shape[0], lookback, 1)) print(X_train.shape, X_test.shape)逻辑说明:我先按时间顺序把原始序列切成训练段和测试段,再在每段上独立构造窗口。这样训练集的任何一个样本都不会包含测试集的数据点。
不这样做的后果很常见:有人先对整个序列 fit MinMaxScaler,再用整个序列做滑动窗口,最后随机切分训练和测试。问题是测试集的 min 和 max 已经提前进入了 scaler,测试指标会虚高;随机切分还会把时间顺序打乱,模型会看到未来的部分信息。
参数说明:
- lookback=20:如果数据按天记录,相当于用过去 20 天预测下一天。如果序列有周周期,至少让 lookback 覆盖 7;有月周期就覆盖 30。我一般会在 5、10、20、40 里各跑一遍,用验证集 loss 选。
- MinMaxScaler(feature_range=(0,1)):适合有明确上下界的量,比如温度、百分比。如果数据里有极端离群点,MinMaxScaler 会被异常拉低正常区间,这时改用 RobustScaler 更稳。
- train_size=0.8:常见切分比例。数据只有几百个点时,可以降到 0.7,或者用 walk-forward 滚动验证代替单一切分。
- 最后一个 reshape:把二维窗口变成三维 (样本, 时间步, 特征)。这一步漏了很多人的报错。
3.3 lookback、train_size、scaler:三个必调参数的经验值
| 参数 | 建议起点 | 出现什么信号时怎么改 |
|---|---|---|
| lookback | 序列周期长度,或 20 | 训练 loss 高欠拟合就加大;验证 loss 高过拟合就减小 |
| train_size | 0.8 | 数据量少于 500 个点建议 0.7,并配合 walk-forward |
| scaler | MinMaxScaler | 有极端峰值时换 RobustScaler,无界序列用 StandardScaler |
这三个参数里,lookback 对结果的影响往往比网络层数还大。因为它直接决定了输入信息量。我见过一个项目,lookback 从 10 调到 30,验证集 MAPE 从 8% 掉到 5%,而把神经网络从一层加到三层只提升了 0.2 个百分点。所以调参顺序应该是:先定 lookback,再定网络规模,最后动训练参数。
4. 训练模型:Keras 和 PyTorch 两套可复现的 LSTM 代码
4.1 用 Keras 训练单变量 LSTM:完整训练脚本与 EarlyStopping
预处理做好后,训练部分其实很直白。下面接上一章的 X_train、y_train,两层 LSTM 加一个 Dropout。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping model = Sequential() # 第一层 LSTM 返回完整时间步,让第二层看到序列细节 model.add(LSTM(units=64, return_sequences=True, input_shape=(lookback, 1))) # 第二层只返回最后时刻,交给 Dense 回归 model.add(LSTM(units=32, return_sequences=False)) model.add(Dropout(0.2)) model.add(Dense(1)) model.compile(optimizer='adam', loss='mse', metrics=['mae']) early_stop = EarlyStopping( monitor='val_loss', patience=10, restore_best_weights=True ) history = model.fit( X_train, y_train, validation_split=0.1, epochs=50, batch_size=32, callbacks=[early_stop], verbose=1 )逻辑说明:第一层 return_sequences=True,输出整个时间步序列;第二层 return_sequences=False,只取最后一个时间步的隐藏状态,再接一个全连接层输出预测值。加 Dropout 是为了减少过拟合,尤其当训练样本只有几千个时。
参数说明:
- units=64/32:两层分别设置隐藏维度。经验上从 (64, 32) 这种塔式结构开始,不要第一层就堆 128。
- Dropout(0.2):随机丢弃 20% 的神经元输出。太小没效果,太大会让模型欠拟合。
- validation_split=0.1:从训练样本尾部切 10% 做验证。因为样本是按时间顺序生成的,这个操作仍然保持时间顺序。
- patience=10:验证 loss 连续 10 轮不降就停,并恢复最优权重。这是防止把训练时间浪费在过拟合阶段的关键。
- 如果数据量大,batch_size 可以提到 64;如果训练 loss 震荡厉害,降到 16。
预测和反归一化:
import numpy as np pred = model.predict(X_test) pred_real = scaler.inverse_transform(pred) y_test_real = scaler.inverse_transform(y_test.reshape(-1, 1)) rmse = np.sqrt(np.mean((pred_real - y_test_real) ** 2)) print("RMSE:", rmse)逻辑说明:模型输出的是归一化空间的数值,必须先 inverse_transform 才能和原始量级对比。y_test 原本是 (样本数,) 的一维数组,reshape(-1, 1) 是为了让 MinMaxScaler 能按列处理。
4.2 用 PyTorch 重写同一个模型:nn.LSTM 与手动训练循环
如果你更习惯 PyTorch,等价代码也简单,只是训练循环需要手动三步。
import torch import torch.nn as nn class LSTMRegressor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True ) self.output = nn.Linear(hidden_size, 1) def forward(self, x): out, _ = self.lstm(x) # out: (batch, seq_len, hidden_size) last = out[:, -1, :] # 取最后一个时间步 return self.output(last) model = LSTMRegressor(input_size=1, hidden_size=64, num_layers=2) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.MSELoss() X_train_t = torch.tensor(X_train, dtype=torch.float32) y_train_t = torch.tensor(y_train, dtype=torch.float32).unsqueeze(1) model.train() for epoch in range(50): optimizer.zero_grad() pred = model(X_train_t) loss = criterion(pred, y_train_t) loss.backward() optimizer.step() if epoch % 10 == 0: print(f"epoch {epoch}, loss {loss.item():.4f}")逻辑说明:nn.LSTM 默认输入顺序是 (seq_len, batch, feature),batch_first=True 可以改成更符合直觉的 (batch, seq_len, feature)。forward 里 out 保存了每个时间步的输出,out[:, -1, :] 取最后一个时间步。训练循环里三个动作都要手动调用:optimizer.zero_grad() 清空梯度,loss.backward() 反传,optimizer.step() 更新权重。
参数说明:hidden_size=64 对应 Keras 里的 units,num_layers=2 对应两层 LSTM。lr=1e-3 是 Adam 的常见起点,loss 震荡就降到 1e-4;loss 下降缓慢且稳定时可以适当调大到 3e-3。如果数据量很大,建议把训练逻辑封装成 mini-batch 而不是一次性喂全量。
4.3 四个训练参数:batch_size、epochs、learning rate、loss 函数的调参方向
| 参数 | 经验起点 | 出现什么信号时怎么改 |
|---|---|---|
| batch_size | 32 | 训练 loss 震荡大就降 16;显存吃紧也降 |
| epochs | 50 | 配合 EarlyStopping,不要硬等 1000 |
| learning_rate | 1e-3 | 发散则降 1e-4;收敛太慢可先 1e-2 再衰减 |
| loss | mse | 数据有大量异常峰时换 mae 或 huber |
我调参的习惯是:先固定随机种子,再每次只动一个变量,记录验证集 loss。否则 LSTM 初始化带来的随机波动比参数变化还大,你会误以为某个参数有效。
5. 避坑指南:LSTM 时间序列预测的 5 个高频翻车点
5.1 训练损失一路下降,预测曲线却是一条水平直线
现象:训练 loss 很漂亮,画出预测值是近似常数,或者贴着训练窗口的最后一个值平移。
原因:这是 LSTM 时间序列预测最容易踩的坑。模型学到的是“复制最近观测值”而不是“学习趋势”,尤其是数据接近随机游走时。因为最小化 MSE 的最简单策略就是输出窗口最后一个值,这个值往往比任何复杂规律都接近未来。
解决:先把最朴素的基线跑出来,也就是用 x_{t} 预测 x_{t+1},计算 RMSE。如果 LSTM 只比这个基线好不到 5%,说明序列本身没有足够的可学习结构,或者 lookback 没取到有效上下文。其次检查预测值是否和最后一个窗口的末值差不多,如果是,试着加大 lookback、减少正则化,让模型有能力捕捉缓慢变化。
5.2 测试集 RMSE 低得离谱,上线后预测失灵
现象:离线测试集指标非常好,但一到真实场景就崩。
原因:数据泄漏。常见泄漏点有两个:一是 scaler 在整个序列上 fit,测试集的统计信息提前参与了归一化;二是滑动窗口切完样本后,训练集和测试集没有按时间边界切干净,导致测试样本里包含了训练时间段的数据。
解决:严格按时间顺序先切 raw 序列,再分别对 train_raw 和 test_raw 做窗口化;scaler 只用 train_raw fit。窗口化只能在每个区间内部进行,不能把整个序列切成样本后再随机分训练测试。
5.3 预测曲线形状对,但数值整体偏移一个量级
现象:预测和真实曲线的走势几乎重合,但整体偏高或偏低,有时还差一个常数倍。
原因:反归一化用错了 scaler,或者反归一化时输入 shape 不对。举例:你为了预测某个特征,分别对 X 和 y 做了两个 scaler,但预测完成后用 X 的 scaler 去还原 y,量级自然对不上。另一个常见情况是 y_test 是一维数组,直接塞给 inverse_transform,MinMaxScaler 会把整段当成一个样本处理,导致结果错乱。
解决:先打印 scaler.data_min_ 和 scaler.data_max_ 确认范围;反归一化前把预测值和真实标签都 reshape 成 (样本数, 1)。多特征场景里,明确每个 scaler 对应哪一列,别混用。
5.4 验证集和测试集同样优秀,但样本边界被污染
现象:验证 loss 和测试 loss 都很好,但你发现测试集第一个样本的前 lookback 个点,和训练集最后一个样本的后几个点完全一样。
原因:切分发生在窗口构造之后,而不是原始时间序列上。假设原始数据有 1000 个点,先构造窗口得到 981 个样本,再按样本序号切,测试部分前几个样本仍然会引用训练段末尾的数据点。虽然标签没有完全泄漏,但这种边界污染会让模型在“半见过”的数据上评估,指标偏高。
解决:按时间点先切序列,再分段造窗口。如果测试段开头不够 lookback 个点,可以用上一段末尾的历史点来补窗口,但标签必须落在测试段内。严格做法是写一个按时间索引切分的函数,保证每个样本的所有 X 点都早于它的 y 点。
5.5 多步预测误差随着步数变大而滚雪球
现象:单步预测精度很高,但你用“预测值递归地作为下一次输入”预测未来 7 天,误差一天比一天大,最后接近发散。
原因:递归多步预测的本质是用模型自己的输出当输入,误差会被逐步放大。单步模型训练时输入都是真实历史数据,没有见过带误差的输入分布,所以一旦进入递归,就会进入模型不熟悉的区域。
解决:如果需求是多步预测,不要用递归。常见的做法是直接多输出:把模型最后一个 Dense 改成 Dense(horizon),一次输出未来多个点。这样训练标签就是未来 horizon 个连续值,模型在训练时就在学习“一个历史窗口对应多个未来时刻”的映射。代价是输出之间可能不够连贯,但整体误差通常比递归小。
6. 从单步到多步:直接多输出 LSTM 与模型验证的技巧
如果业务要的是“未来一周的走势”,单步模型递归预测会像滚雪球。换成直接多输出结构,训练数据和模型都只需要小幅改动。构造多步标签的窗口函数长这样:
def make_multistep_windows(data, lookback, horizon): X, y = [], [] for i in range(lookback, len(data) - horizon + 1): X.append(data[i-lookback:i]) y.append(data[i:i+horizon]) # 连续未来 horizon 个点 return np.array(X), np.array(y)模型最后一层改成 Dense(horizon),其余结构不变。这里的 y 从单步标量变成长度为 horizon 的向量,每个训练样本同时监督未来多个时刻,模型就不会只学会“复制上一个点”。
验证阶段我强烈建议做 walk-forward:拿原始数据最后一段做测试,每次只用测试段之前的数据训练,预测未来 horizon 步,然后把窗口向前移动一步,重复多次,最后对多次预测结果求平均 RMSE。这和单次切分相比更接近真实上线场景,也能看出模型在不同时间段的稳定性。
我现在的习惯是任何序列预测项目都先跑一个“前值预测”基线,再上 LSTM。如果 LSTM 只比基线好不到 5%,我会重新检查数据有没有足够规律,或者考虑换更简单的模型。模型文件、scaler 和目标列名称也要一起保存,否则几个月后你根本想不起当时用的窗口大小和归一化范围。这些血泪经验踩过一遍就长记性了。希望帮到你。
本文还有配套的精品资源,点击获取