简介:一份基于Python的LSTM模型时间序列预测实战代码包,面向具备一定Python基础、希望系统入门深度学习和时间序列预测的开发者。代码覆盖股票市场、销售趋势、天气预测等常见场景,从时间序列数据理解、滑窗构造监督学习样本与标准化,到单变量/多变量LSTM模型搭建、训练、验证和预测均有实现。压缩包共15个文件,包含6个Python脚本(模型训练、输入测试等)、2个Markdown说明文档、2个CSV示例数据集、4张结果图片,整体约391KB;其中脚本完成训练与测试,文档提供代码说明,CSV为可直接运行的示例数据,图片展示预测结果,目录结构清晰便于对照学习。已有39799人学习。通过这份资源,读者可掌握基于Keras/TensorFlow的LSTM建模核心步骤,理解输入门、遗忘门、输出门和细胞状态如何控制信息流动,并能在本地快速复现完整预测流程;文件内还涉及epoch设置、验证集监控、结果反标准化等实用细节。LSTM通过门机制有效缓解传统RNN的梯度消失/爆炸问题,尤其适合长期依赖的时间序列数据,是入门该领域的高质量参考。 自从开始整理时间序列预测相关的代码和踩坑记录,后台问得最多的一个题目就是:LSTM模型到底怎么用Python实现。这个问题看起来简单,但真动起手来,数据怎么切、窗口怎么滑、序列怎么归一化、Loss不降怎么办,每一步都有隐藏的坑。这篇博文我就用一套完整可跑的Python代码,把LSTM做时间序列预测的全流程讲清楚,同时把我在实际项目里踩过的坑和排查思路也一起写出来。适合刚配好Python环境、想正经跑通一个深度学习时序项目的读者,也适合已经跑通过代码但换数据就翻车的人作为排查参考。
1. 为什么时间序列预测绕不开LSTM:先看懂它和传统方法的差异
1.1 传统时序方法卡在哪里
大多数人接触时间序列预测,第一个想到的往往是ARIMA。ARIMA的思路是从历史数据里提取自相关结构,通过差分把非平稳序列变成平稳序列,再用自回归和移动平均去拟合。在一段线性趋势加周期性波动比较规律的序列上,ARIMA确实能给出不错的预测。
问题在于,真实业务里的序列很少这么“乖”。比如网站访问量,遇到促销、节假日、突发新闻,曲线会突然拉出一个尖峰;再比如销量预测,促销活动前后几天的数据模式跟平时完全不一样。这些非线性关系,ARIMA这种线性模型天生拟合不了。强行用,预测误差会迅速放大,而且越往后预测越离谱。
还有人会用机器学习方法做:手工构造滞后特征、滚动均值、滚动标准差,再喂给XGBoost或随机森林。这个方法在业务上经常表现稳定,但缺点是特征工程成本很高。滞后阶数少,抓不住长期依赖;滞后阶数多,特征矩阵膨胀,而且像“30天前这个时间点有个相似的波动”这种跨周期相关性,手工特征很难完整表达。
1.2 LSTM的记忆机制与适用边界
LSTM的全称是长短期记忆网络,属于循环神经网络(RNN)的一种变体。它解决的核心问题是:怎么让模型在读取很长一段序列时,仍然能记住早期的重要信息。RNN的原始结构在这件事上很吃力,因为梯度在反向传播过程中会逐渐消失,导致前面的信息传不到后面。
LSTM的做法是引入门控机制。你可以把它理解成每个人心里都有一个“动态记账本”:遗忘门决定哪些旧账目可以擦掉,输入门决定新信息值不值得记下来,输出门决定这次汇报要拿出多少账目来讲。这样一来,模型就不再是机械地把每个时间步的信息叠加,而是学会了“该记的记、该忘的忘”。这也是为什么LSTM在流量预测、能耗预测、金融序列等非线性、长依赖场景里,往往比传统统计模型表现更好。
需要泼一盆冷水的是,LSTM不是万能的。如果你的序列本身只有几十个点、模式非常简单,它跟移动平均或线性回归的差距并不大,却要付出高得多的训练成本。更合适的场景是:数据量足够、存在复杂时间依赖、人工特征工程成本太高。理解这个边界,比学会调用模型代码更重要,它能帮你在面对一个新问题时,先判断该不该上LSTM。
2. 数据预处理:滑动窗口和归一化,这两步决定模型上限
2.1 演示数据从哪来:可复现的合成序列与CSV替换
为了让代码开箱即用,我用NumPy生成了一条带趋势、季节性和噪声的演示序列。你可以直接运行下面的代码,得到一条比较“像真实业务”的时间序列:
import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.preprocessing import MinMaxScaler np.random.seed(42) t = np.arange(0, 600, 1) data = 10 + 0.03 * t + 5 * np.sin(2 * np.pi * t / 50) + np.random.normal(0, 0.5, len(t)) df = pd.DataFrame({"date": t, "value": data}) plt.figure(figsize=(12, 4)) plt.plot(df["date"], df["value"]) plt.title("Demo Time Series") plt.show()如果你有自己的数据,替换起来也很简单:
# df = pd.read_csv("your_data.csv", parse_dates=["date"]) # data = df["value"].values.astype(float)唯一的要求是value是一维数值序列,时间字段暂时用不上。真正做项目时时间字段很重要,但在模型输入阶段,LSTM读取的只是一串按顺序排列的数值,时间戳先放一边。
2.2 滑动窗口:把时间切片变成监督学习样本
时间序列预测和普通回归任务最大的不同是:样本之间不能随机打乱。你不能像图像分类那样把一条序列里的点随机重排,否则就彻底破坏了时间依赖。标准做法是用滑动窗口把连续的look_back个点作为输入,预测下一个时间点的值。
def create_dataset(dataset, look_back=10): X, y = [], [] for i in range(len(dataset) - look_back - 1): X.append(dataset[i:(i + look_back), 0]) y.append(dataset[i + look_back, 0]) return np.array(X), np.array(y)这段代码的意思是:对第i个样本,输入是第i到i+look_back-1共10个连续点,要预测的目标是第i+look_back这个点。你可以把滑动窗口想象成一台“切片机”,每往前挪一步切出一片新的样本。这里最容易被忽略的是目标值的对齐:有人会把目标错写成dataset[i:i+look_back, 0],那就变成预测“当前序列本身”了,等于把答案喂给了模型。
2.3 归一化里的两个隐蔽坑
LSTM默认使用的激活函数对输入范围比较敏感,数值过大时容易让梯度计算不稳定。所以模型输入前要先归一化,把数据压缩到0到1之间。用MinMaxScaler就可以:
# 先按时间顺序划分训练段和测试段,防止归一化参数泄露未来信息 train_size_raw = int(len(data) * 0.8) train_data_raw = data[:train_size_raw] test_data_raw = data[train_size_raw:] scaler = MinMaxScaler(feature_range=(0, 1)) scaler.fit(train_data_raw.reshape(-1, 1)) # 用训练段的参数变换全量数据,保持窗口切分连续性 scaled_data = scaler.transform(data.reshape(-1, 1)) X, y = create_dataset(scaled_data, look_back=10) train_size = int(len(X) * 0.8) X_train, X_test = X[:train_size], X[train_size:] y_train, y_test = y[:train_size], y[train_size:] # 转换为LSTM需要的三维输入:(样本数, 时间步数, 特征数) X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], 1)) X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], 1))很多人在这里会犯一个隐蔽错误:对全量数据调用fit_transform,然后再切分训练集和测试集。这样做的话,scaler已经“看过”测试集的取值范围,测试阶段的归一化参数里带着未来信息,最后评估出来的指标会虚高,上线后实际效果对不上。
另一个坑是reshape维度。LSTM的输入必须是三维的:(样本数, 时间步数, 特征数)。我们这里每个时间步只有一个特征,所以最后那个维度是1。如果你有多变量输入,比如同时预测“销量+客单价”,这个维度就是特征数量。忘记reshape是LSTM代码里最常见的报错来源之一,报错信息通常是ValueError: Input 0 of layer "lstm" is incompatible with the layer。
3. 模型搭建与超参数选择:核心代码逐行拆解
3.1 框架选择:Keras还是PyTorch
做LSTM时序预测,框架无非Keras和PyTorch二选一。我的建议是:如果你主要目标是把模型快速用起来、跑通基线、验证想法,用Keras。它的SequentialAPI写起来非常直观,模型结构、训练、回调一套流程下来代码量很少,排错也容易。
如果你在做研究,需要自定义复杂的模型结构、控制每个batch的训练细节,那PyTorch更合适。它可以让你在手写循环的过程中更清楚每步在干什么,但也意味着你需要写更多样板代码。
本文的代码基于Keras。环境方面,在VSCode里配好Python解释器后,执行一次pip install tensorflow就行。只跑CPU版本也不影响本示例,这个数据量级几十秒就能训完。
3.2 核心模型代码与每个参数的含义
直接看模型部分:
import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout tf.random.set_seed(42) model = Sequential([ LSTM(64, return_sequences=True, input_shape=(look_back, 1)), Dropout(0.2), LSTM(64, return_sequences=False), Dropout(0.2), Dense(1) ]) model.compile(optimizer="adam", loss="mse") model.summary()逐层说清楚:
- 第一层
LSTM(64, return_sequences=True):64表示这一层有64个记忆单元,return_sequences=True表示把每个时间步的输出都传给下一层。因为后面还要再接一层LSTM,所以这里必须返回完整的时间步序列。 Dropout(0.2):训练时随机丢弃20%的神经元连接,防止模型死记硬背训练集。- 第二层
LSTM(64, return_sequences=False):这一层只返回最后一个时间步的输出,因为我们最终要预测的是下一个时刻的值,不需要把整个时间轴都保留。 Dense(1):全连接层,输出一个标量,也就是预测值。loss="mse":回归任务最常用的损失函数,均方误差。它对大误差的惩罚比较大,能让模型学会把整体偏差压小。
这两个Dropout层是容易被新手砍掉的,但实际项目中加了它,验证集Loss往往明显更稳。
3.3 look_back、units、batch_size到底怎么设
超参数没有“万能答案”,但有相对靠谱的起点。以日数据为例:
| 超参数 | 常用范围 | 经验原则 |
|---|---|---|
| look_back | 7~30 | 匹配业务周期,太小抓不住依赖,太大引入噪声 |
| units | 32~128 | 数据量小用32~64,数据量大可试128 |
| batch_size | 16~64 | 过小Loss抖动明显,过大收敛慢 |
| learning_rate | 1e-4~1e-2 | 通常从1e-3开始,配合衰减策略 |
look_back的选择逻辑是最有意思的。很多教程默认设10或30,但换到真实数据时,你应该先问自己:序列里最明显的周期性是多长?如果是周维度数据,周期是7,那look_back至少得覆盖7;如果是月度数据,那30以上的窗口才有意义。窗口太小,模型看不到一个完整的周期;窗口太大,模型要消化的噪声也更多,训练时间成倍增加。我的习惯是先做一组简单实验:分别试7、14、30,看验证集Loss差异,再决定最终值。
4. 训练调参实录:Loss不降、过拟合、曲线抖动怎么治
4.1 Loss不降时的排查清单
我见过最典型的“跑不通”场景是:model.fit跑了几十个epoch,Loss纹丝不动,甚至变成NaN。这时候不要急着改模型结构,按顺序检查下面几项:
- 输入数据的shape和取值范围是否正确:打印
X_train.shape,确认是(样本数, look_back, 1);检查X_train.min()和X_train.max()是否落在合理区间。 - 归一化是否真的做了:有些数据集本身数值很大,直接喂给LSTM会让梯度爆炸,Loss直接变成NaN。
- 学习率是否过大:默认Adam的学习率是1e-3,但某些数据分布下1e-2就会不收敛。把学习率调到1e-4试一次,看Loss有没有缓慢下降。
look_back是否太小或太大:窗口太小,模型输入信息不够;窗口太大,整个训练集特征维度高,收敛变慢,也可能导致Loss看起来“卡住”。
排查思路是先确认数据没问题,再动模型参数。很多人一上来就改网络层数,结果折腾半天发现是归一化的问题,白白浪费时间。
4.2 EarlyStopping与学习率衰减的组合用法
训练曲线最典型的翻车现场是:训练Loss一直在降,但验证Loss先降后升,这就是过拟合的信号。对付它,最直接的办法是提前停止,再配合学习率自动衰减。
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop = EarlyStopping( monitor="val_loss", patience=15, restore_best_weights=True, verbose=1 ) reduce_lr = ReduceLROnPlateau( monitor="val_loss", factor=0.5, patience=5, min_lr=1e-5, verbose=1 ) history = model.fit( X_train, y_train, validation_split=0.1, epochs=120, batch_size=32, callbacks=[early_stop, reduce_lr], verbose=1 )EarlyStopping的patience=15表示验证Loss连续15个epoch没有变好就停止训练;restore_best_weights=True这个参数特别关键,它会把模型权重恢复到你监控指标最优的那个时刻,而不是最后一步。ReduceLROnPlateau的factor=0.5表示验证Loss连续5个epoch不降时,学习率砍半。两个回调配合使用,基本不用手动盯训练过程,模型会在合适的地方停下来。
4.3 一个常被忽略的随机种子问题
LSTM模型初始化的随机权重对训练结果影响很大。同一个代码,不固定随机种子,跑两次的测试集RMSE可能差10%以上。这会导致一个很尴尬的问题:你调了一个参数,发现效果变好,但可能只是随机波动,不是参数真的起作用。
所以在模型构建之前,一定要固定随机种子:
np.random.seed(42) tf.random.set_seed(42)两层种子都要设,NumPy负责数据生成部分的随机,TensorFlow负责模型权重初始化和训练阶段的随机。实际项目里,我还会把训练日期、数据版本、当时的种子一起记录在模型文件名里,这样任何一个结果都能追溯。
5. 评估与多步预测:反归一化之后才算真正落地
5.1 预测结果反归一化与常见报错
模型输出的是归一化后的数值,要评估真实误差,必须把它们还原成原来的量纲。
from sklearn.metrics import mean_absolute_error, mean_squared_error train_pred = model.predict(X_train) test_pred = model.predict(X_test) train_pred = scaler.inverse_transform(train_pred.reshape(-1, 1)) test_pred = scaler.inverse_transform(test_pred.reshape(-1, 1)) y_train_raw = scaler.inverse_transform(y_train.reshape(-1, 1)) y_test_raw = scaler.inverse_transform(y_test.reshape(-1, 1)) rmse = np.sqrt(mean_squared_error(y_test_raw, test_pred)) mae = mean_absolute_error(y_test_raw, test_pred) print(f"RMSE: {rmse:.4f}, MAE: {mae:.4f}")这里最容易翻车的是inverse_transform时报错:ValueError: non-broadcastable output operand。原因是MinMaxScaler在fit时输入的是(n, 1)的二维形状,inverse_transform时也必须传入二维数组。模型输出的test_pred是一维的,所以必须.reshape(-1, 1),否则维度对不上。
5.2 评估指标:RMSE、MAE、MAPE怎么选
评估指标有很多,但不能随手拿一个就用。三者各有侧重:
| 指标 | 公式 | 适用场景 |
|---|---|---|
| RMSE | sqrt(mean((y-yhat)^2)) | 对大误差敏感,适合惩罚极端偏差 |
| MAE | mean(abs(y-yhat)) | 直观反映平均偏离,对异常值不敏感 |
| MAPE | mean(abs((y-yhat)/y))*100 | 无量纲,适合横向对比,但实际值接近0时失真 |
RMSE和MAE的量纲跟原始数据一致,业务同事容易理解。MAPE的好处是可以实现“百分之多少的误差”这种表述,但遇到分母为0的数据就会爆炸。我在供应链预测项目里一般优先看RMSE和MAE两个值,再用MAPE向业务解释相对误差,但会提前排查序列里有没有0值。
5.3 单步预测曲线别太乐观,多步预测才接近业务
最后把训练和测试曲线画出来,直观判断模型有没有学到数据规律:
plt.figure(figsize=(12, 5)) train_idx = range(len(y_train_raw)) test_idx = range(len(y_train_raw), len(y_train_raw) + len(y_test_raw)) plt.plot(train_idx, y_train_raw, label="train actual", linewidth=1.2) plt.plot(train_idx, train_pred, label="train pred", linewidth=1.2) plt.plot(test_idx, y_test_raw, label="test actual", linewidth=1.5) plt.plot(test_idx, test_pred, label="test pred", linewidth=1.5) plt.axvline(x=len(y_train_raw) - 0.5, color="gray", linestyle="--", linewidth=1) plt.legend() plt.title("LSTM Time Series Prediction") plt.show()这里必须提醒一句:上面这种预测方式是单步滚动预测,测试集每一个点的预测都使用了它之前的真实值作为输入。所以曲线贴合度高是正常的,别被“看起来完美”误导。真实业务里,你要预测的是未来7天、未来30天的值,不可能提前拿到未来的真实观察值。这时需要换成多步预测思路:
- 递归多步预测:把预测出来的值当成下一步的输入,循环预测。代码简单,但误差会一步步累积,预测步数越多越不可靠。
- 直接多步预测:把输出层改成N个神经元,一次预测未来的N个点,等于把问题从“回归一个点”变成“回归N个点”,训练起来略复杂,但能避免误差累积。
- Seq2Seq结构:用Encoder-Decoder结构,Encoder读取历史窗口,Decoder逐步生成未来多个预测值,适合较长预测周期的场景。
我的建议是先踏踏实实跑通单步预测,把数据预处理、评估、反归一化这套流程理解到位,再决定是否升级到多步。多步预测的难度主要在数据构造和评估方式上,基础不牢很容易得到“看起来能预测、实际一上线就崩”的结果。
文章写到这儿,我再分享一个自己的习惯。每次跑完模型,我不会只盯着测试集RMSE,而是把预测结果写回DataFrame、保留日期列,再把误差大的样本单独拉出来看,看是不是节假日、异常流量、数据缺失导致的。时间序列预测的模型结构其实只是项目的一部分,更多功夫在数据和业务理解上。LSTM不是银弹,但当你把流程跑顺、把数据洗干净、把评估方式定清楚之后,它会成为你工具箱里非常趁手的一件工具。
本文还有配套的精品资源,点击获取