简介:这是一份面向Python开发者和AI学习者的LSTM时间序列分析预测源码包,覆盖数据加载、归一化、滑窗切分、LSTM模型构建、训练、预测与评估的完整流程,适合希望用深度学习解决股票价格、气象、设备维护等时序预测问题的初中级开发者。压缩包共126个文件,以75个Python脚本为主体,提供可直接运行的训练与预测逻辑;26个CSV文件包含多组时序数据集,便于验证模型效果;15个说明文本补充代码与数据说明;h5和checkpoint文件保存了预训练模型权重,方便加载测试。整体约5.42MB,轻量实用。目前已有5095人学习浏览。通过阅读源码,读者可以掌握Keras中LSTM层的参数设置、多步预测的数据构造方式,以及MSE、MAE、R²等指标的评估技巧,是一份兼顾理论与实践、可二次修改的优质参考。
1. 基于LSTM的时间序列分析预测:一份能跑通空气污染预测全流程的Python源码
做时间序列预测的人,最怕的不是模型不收敛,而是拿到一份数据后不知道从哪一步开始:要不要差分、窗口取多长、归一化用全局还是滑动、LSTM输入维度到底怎么 reshape。这份基于LSTM的时间序列分析预测源码包,用北京的空气污染数据集把完整流程走了一遍——从 polluction.csv 的原始字段清洗、多变量序列构造,到 Keras 里 LSTM 网络的定义、训练、checkpoint 保存,再到反归一化还原真实量纲并计算 MSE、MAE、R2。它不只是一个模型文件,而是一套可以照抄的脚手架。适合两类人:刚入门深度学习、想把 LSTM 用在序列数据上的 Python 开发者;以及手里有历史指标数据、想快速验证 LSTM 预测效果的数据分析从业者。
2. 数据预处理先行:pollution.csv 里藏着的多变量序列构造逻辑
2.1 原始数据字段与时间粒度
这份资源里的 pollution.csv 关键字段大体上包括 year、month、day、hour、pollution、dew、temp、press、wnd_dir、wnd_spd、snow、rain 这几列,其中 pollution 是目标变量,dew、temp、press 分别是露点、温度和气压,wnd_dir 是风向字符串,wnd_spd 是风速,snow 和 rain 是降雪和降雨量。数据的时间粒度是小时级,意味着一个样本代表某一小时该监测站点记录的状态。
这种字段组合在空气质量预测场景里很典型,pollution 同时受气象条件、风向风速和降水影响,适合做多变量预测。源码里把 wnd_dir 这种类别字符串做了编码处理,常见做法是先用 Pandas 的 Categorical 类型转换成整数码,再接后续数值流程。
import pandas as pd df = pd.read_csv('pollution.csv', index_col=0) # 风向是字符串,先转成类别编码 df['wnd_dir'] = pd.Categorical(df['wnd_dir']).codes # 确认缺失值数量 print(df.isnull().sum())这里用pd.Categorical(...).codes而不是手写字典映射,好处是风向类别变化时不需要维护映射表,缺点也很明显:类别之间会被模型当作有大小关系,好在 LSTM 对这点不敏感。检查缺失值是预处理里不能省的一步,后续 fillna 策略依赖这一步的统计结果。
读进来之后,第一件事就是统一时间索引并按时间顺序排序。很多人在这一步翻车:数据文件里的时间顺序可能是乱的,如果直接切训练集和测试集,未来信息会混进训练集,模型评估结果虚高。我会先确认索引是否单调递增。
df.index = pd.to_datetime(df.index) df = df.sort_index()这步做完,后续所有窗口切分都建立在严格的时间序上,不会出现倒序样本。
2.2 归一化为什么必须做,以及用哪种方式
LSTM 内部是梯度下降驱动,输入特征的数值量级差异过大会让梯度更新不稳定,所以归一化是标准动作。这份源码里用 MinMaxScaler 把数据压缩到 0 到 1 区间,属于时间序列预测的常规选择。
from sklearn.preprocessing import MinMaxScaler values = df.values.astype('float32') scaler = MinMaxScaler(feature_range=(0, 1)) scaled = scaler.fit_transform(values)注意这里 fit_transform 用的是全量数据,包括测试集。这个做法在这个源码包的场景里是能用的,因为数据是同一段连续采集的记录,量纲范围基本稳定。但如果你想做严格的时间序列评估,应该在训练集上 fit,再用同一个 scaler 去 transform 测试集,避免数据泄漏。关于这个坑,第五章还会详细说。
2.3 多变量序列转监督学习的窗口构造
原始数据是一行一个时刻,LSTM 需要的是“过去的 n 个时刻预测未来的 m 个时刻”这种监督学习格式。源码里的series_to_supervised函数做的事情就是滑动窗口重组。
def series_to_supervised(data, n_in=1, n_out=1, dropnan=True): n_vars = 1 if type(data) is list else data.shape[1] df = pd.DataFrame(data) cols, names = [], [] # 输入序列:t-n_in 到 t-1 的每一列 for i in range(n_in, 0, -1): cols.append(df.shift(i)) names += [f'var{j+1}(t-{i})' for j in range(n_vars)] # 输出序列:t 到 t+n_out-1 的每一列 for i in range(n_out - 1, -1, -1): cols.append(df.shift(-i)) names += [f'var{j+1}(t+{i})' for j in range(n_vars)] agg = pd.concat(cols, axis=1) agg.columns = names if dropnan: agg.dropna(inplace=True) return agg这个函数的参数含义很直观:n_in是回看的时间步数,n_out是要预测的未来步数,dropnan=True会把因为没有足够历史或未来数据而产生的 NaN 行直接丢掉。比如 n_in=1、n_out=1 时,一条样本就是用 t-1 时刻的所有特征预测 t 时刻的所有特征。
但这里有个细节:函数生成的是“所有变量都在做预测”的监督格式。预测污染浓度只需要 polluction 在 t 时刻的值作为 y,所以切分 X 和 y 时,要定位 polluction 列在重组后数据里的位置。一般做法是先统计列名里的 var1 对应哪一列,再切片。实际使用时,我通常把缩放后的数据先转成 DataFrame,准确找到列序号再拆分,避免手工数错列。
2.4 时间步长的选择:从 1 小时到回头看 24 小时
源码里把输入输出维度切分出来后,会 reshape 成(样本数, 时间步长, 特征数)。时间步长这个参数直接决定 LSTM 能“回看”多长的历史。设 1,模型只看上一个时刻;设 24,模型能看到过去一天的气象和污染变化,容易学到昼夜周期。
经验上说,空气污染预测设 24 小时起步是合理的。时间步长不是越大越好,序列拉长后训练成本上升,而且距离当前时刻太远的历史信息对预测的贡献有限。有一个可行思路是先用 24 步跑一版,观察 validation loss,再改成 12 和 48 做对比。源码里这个值通常定义在加载数据之后、划分训练测试集之前,改起来成本最低。
n_hours = 24 # 切成 supervised 格式 reframed = series_to_supervised(scaled, n_hours, 1)把n_hours提到函数外部作为配置项,是我比较推荐的习惯,后续做多组实验只需要改这一处,不用动核心逻辑。
3. Keras 建模:LSTM 层数、Dropout 与优化器配置的搭配逻辑
3.1 单层 LSTM 加 Dense 是最稳妥的起步结构
这份源码的模型结构很克制:一个 LSTM 层接一个 Dropout 层,再接一个输出单值的 Dense 层。对于单变量或者多变量预测单个目标值,这个结构在大多数场景下已经够用。LSTM 层的输出维度设为 64,意味着这层用 64 个记忆单元去学习序列中的时序依赖;Dense(1) 输出的是一个连续值,对应预测时刻的污染浓度。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model = Sequential() model.add(LSTM(64, input_shape=(train_X.shape[1], train_X.shape[2]))) model.add(Dropout(0.2)) model.add(Dense(1)) model.compile(loss='mae', optimizer='adam')这里有个关键参数:LSTM 层没有设return_sequences=True。默认的 False 表示只返回最后一个时间步的输出,正好可以直接接 Dense 层做回归。如果这里改成 True,输出形状会变成三维,接 Dense 之前需要手动展平或改用 Flatten 层,很多人第一次写这个结构就从这儿开始报维度错误。
loss 用的是 MAE 而不是 MSE。MAE 对离群点不那么敏感,空气污染数据里偶尔会有仪器异常导致的极端高值,用 MAE 能让训练过程更稳。如果你更关心大误差被惩罚,可以换成 MSE,看任务目标而定。
3.2 把训练集和验证集按时间切分,而不是随机切分
时间序列的样本之间有强关联,随机打乱会让模型看到未来数据,验证集就失去了“检测泛化能力”的意义。源码里正确的做法是:把构造好的监督数据按行号顺序,前面大部分作为训练集,后面小部分作为测试集。验证集则常从训练集尾部再切一段出来。
n_train_hours = 365 * 24 train = reframed.values[:n_train_hours, :] test = reframed.values[n_train_hours:, :]切分后要分别取特征和标签列。多变量预测单输出时,标签是重组后数据里的 polluction 列,特征要排除掉它。更严格来说,如果要避免测试时用到当前时刻的其他变量值,特征应该只保留 t-n_hours 到 t-1 的列,源码里通常直接按列范围截取,效果上差异不大。
一个容易踩的点:train_X.shape[2]在模型定义时是动态取的,说明特征维度来自实际数据形状,改特征列数时不用同步改模型定义,这算是一个省事的设计。
3.3 训练参数:epochs、batch_size 与 checkpoint 保存
模型训练时,batch_size 决定了每个批次放进多少条样本。空气污染数据量通常是几千到几万条小时记录,batch_size 设在 72 附近是常见选择,对应一天 24 小时乘 3 天,训练时收敛平稳。epochs 设在 50 上下,配合 EarlyStopping 可以防止无意义的长跑。
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping checkpoint = ModelCheckpoint( 'best_model.h5', monitor='val_loss', save_best_only=True, verbose=1 ) early_stop = EarlyStopping( monitor='val_loss', patience=10, restore_best_weights=True ) history = model.fit( train_X, train_y, epochs=50, batch_size=72, validation_data=(val_X, val_y), callbacks=[checkpoint, early_stop], verbose=1 )checkpoint 做的是在每次 epoch 结束后比较验证集 loss,只要更小就覆盖保存模型,这样最后磁盘上留下的是验证集表现最好的那一个版本,而不是最后一个 epoch 的结果。EarlyStopping 的patience=10表示连续 10 个 epoch 验证 loss 没有下降就提前停止,restore_best_weights=True会把模型权重回滚到最佳状态,这两者配合可以省掉大量无效训练时间。
这里有一个很多人忽略的事情:fit 返回的 history 对象里记录了每个 epoch 的 loss 和 val_loss,训练完先画一条 loss 曲线,能快速判断模型是欠拟合、过拟合还是学习率不合适。源码包没有强制你这么做,但我建议复现时加上,这是成本最低的诊断手段。
提示:如果你用的是 TensorFlow 2.x 较新版本,
model.fit里validation_data的元组顺序不能反,第一个元素是验证特征,第二个是验证标签。
4. 预测与评估:从反归一化到 MSE、MAE、R2 的真实含义
4.1 预测结果必须先反归一化再算指标
模型输出的预测值是在 0 到 1 区间内的,直接和原始量纲的真实值做对比算出来的误差毫无意义。正确的流程是:用之前 fit 好的 scaler 对预测结果做 inverse_transform,还原成真实的污染浓度单位,再和同一量纲下的真实值比较。
import numpy as np # 模型预测,输入测试集特征 yhat = model.predict(test_X) # 由于训练时是多变量归一化,这里需要把预测值放回原始矩阵形状再反归一化 test_X_flat = test_X.reshape((test_X.shape[0], test_X.shape[1] * test_X.shape[2])) inv_yhat = np.concatenate((yhat, test_X_flat[:, -(test_X.shape[2]-1):]), axis=1) inv_yhat = scaler.inverse_transform(inv_yhat)[:, 0]这段代码的目的:LSTM 输出只有一列预测值,而 MinMaxScaler 是在所有特征上训练的,反归一化时需要补齐原来的特征维度,把预测值放在第一列,其余列用测试集的真实特征值填充,再整体 inverse_transform,最后取第一列。这个过程看着绕,实际上是多变量归一化带来的必要步骤,少做一步指标都会虚低或虚高。
4.2 三个指标各看什么:MSE 放大误差、MAE 看平均偏差、R2 看拟合度
评估代码一般会同时算出 MSE、MAE 和 R2,分别从不同角度描述模型表现。
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score mse = mean_squared_error(inv_y, inv_yhat) mae = mean_absolute_error(inv_y, inv_yhat) print('Test MSE: %.3f' % mse) print('Test MAE: %.3f' % mae) print('Test R2: %.3f' % r2_score(inv_y, inv_yhat))MSE 是均方误差,对大误差的惩罚重,适合用来排查是否存在某些时刻预测严重偏离;MAE 是平均绝对误差,直观反映平均偏移量,空气污染浓度单位是微克每立方米,MAE 直接告诉你平均差了大约多少;R2 接近 1 说明模型解释了大部分方差,接近 0 说明模型不比直接用均值预测好到哪去。实际工程里我更常参考 MAE 和 R2 的组合:MAE 控制在合理范围内,同时 R2 别掉到 0.8 以下,这个模型的可用性就算过关。
如果复现时发现 R2 是负数,不必惊讶,这通常意味着模型连均值水平都没达到,先回去查特征列选对没有、归一化有没有泄漏。
4.3 可视化对比:一条预测曲线能看出比十个指标更多的问题
指标只能给数字,曲线能暴露指标藏起来的问题。把真实测试值和预测值画在同一张图上,重点关注三个位置:波峰波谷的贴合度、相位是否滞后、是否存在系统性低估或高估。
import matplotlib.pyplot as plt plt.figure(figsize=(12, 5)) plt.plot(inv_y, label='actual') plt.plot(inv_yhat, label='predict') plt.legend() plt.show()LSTM 做空气污染预测常见的画面是:总体趋势跟得住,但峰值经常被削平,因为训练时 MAE 不鼓励模型冒尖。如果峰值偏差严重,可以在数据里对高浓度样本加权,或者改用对数变换压缩高值范围。这个观察靠指标很难量化,但在曲线上一眼就能看出来。
提示:如果你的数据集很大,画图前先抽样或只画最近几百条,否则所有点挤在一起什么都看不清。
5. 复现避坑:归一化泄漏、维度对齐与训练不收敛的七个常见问题
5.1 测试集的信息混进了训练流程
现象:模型在验证集上 R2 高达 0.98,测试集却只有 0.6,两者差距悬殊。
原因:归一化时用全量数据 fit 了 MinMaxScaler,测试集的数值范围泄漏给了训练过程。模型在训练时已经“见过”测试集统计量,验证分数虚高。
解决:先切分数据,再单独在训练集上 fit scaler,用这个 scaler 去 transform 测试集。如果一定要用全量归一化,就在切换训练集和测试集之前把 scaler fit 提前,并且心里清楚分数会乐观一些,只用于对比实验。
5.2 LSTM 层返回三维输出,Dense 层报维度错误
现象:model.add(LSTM(64, return_sequences=True)) 后接 Dense(1) 直接报 Negative dimension size。
原因:return_sequences=True让 LSTM 返回每个时间步的输出,形状是(batch, time_steps, units),Dense 不认这个三维形状。
解决:预测单值场景不要开return_sequences=True;如果要堆叠两层 LSTM,只在前面层开,最后一层保持默认 False。
5.3 训练 loss 徘徊不降,数值一直在同一个水平震荡
现象:epochs 跑了一二十轮,loss 曲线基本是一条横线,偶尔有小幅波动。
原因:学习率不合适,或输入数据没归一化导致梯度方向抖动。另一个常见原因是时间步长太长,数据量撑不起那么深的记忆。
解决:先确认数据落在 0 到 1 区间;然后尝试把 LSTM 单元数从 64 降到 32 或升到 128,看 loss 是否有响应;最后再考虑把默认学习率调低一个量级,用Adam(learning_rate=0.001)换成 0.0001 跑几轮对比。
5.4 验证集 loss 在某个 epoch 后开始反弹
现象:训练 loss 持续下降,val_loss 先降后升,两条曲线出现“开口”。
原因:过拟合,模型记住了训练集的噪声模式。
解决:增大 Dropout 比例到 0.3 甚至 0.5,或者减少 LSTM 单元数。检查一下训练集和验证集的时间范围是否重叠,时间序列切分重叠同样会造成这种假象。
5.5 反归一化后预测值全部挤在一个很窄的区间
现象:预测曲线几乎是一条平线,预测值都在真实值均值附近小幅抖动。
原因:模型收敛到了“预测均值”这个局部最优解,这在回归任务里很常见。LSTM 没有学到足够的周期特征,或者时间步长取得太短。
解决:增大n_hours,让模型能看到跨天的周期信息;同时检查训练集里目标变量的方差,如果本身波动就小,模型倾向输出均值是正常行为。
5.6 复现时 Keras 版本 API 对不上
现象:from keras.layers import LSTM报错,或者model.add(LSTM(..., input_shape=(...)))提示参数类型不对。
原因:老代码基于 Keras 独立包或 TF 1.x 写法,当前环境是 TensorFlow 2.x,API 位置变了。
解决:统一从tensorflow.keras导入模型和层组件,这是兼容 TF 2.x 的标准路径。单元数、dropout 这些参数在两个版本里含义一致,不用改。
5.7 同样的代码,两次训练结果差异很大
现象:同一份数据跑两遍,loss 和预测曲线差别明显。
原因:权重初始化是随机的,训练集和验证集切分如果用了 Pandas 的随机抽样而没有固定种子,样本组成也会变。
解决:在所有涉及随机操作的入口设置固定种子,包括 NumPy、Python random 以及 TensorFlow 的随机种子。
import random import numpy as np import tensorflow as tf random.seed(42) np.random.seed(42) tf.random.set_seed(42)种子固定之后,结果才具备可复现性。做实验对比超参数时,这一步不做等于让随机运气参与评分。
6. 再往前走一步:把单步预测改成多步滚动预测的改法
源码包默认是单步预测,即用过去 24 小时预测下一个小时的污染浓度。实际业务里更多场景是要预测未来 6 小时或 24 小时的趋势。改法不复杂,核心是构造训练数据时把n_out从 1 调大,模型输出层对应调整,预测阶段用滚动方式不断把预测值拼回输入窗口。
n_out = 6 reframed = series_to_supervised(scaled, n_hours, n_out) # 特征列不变,标签列改为未来 6 小时的目标值 # model.add(Dense(n_out)) # 输出未来 6 个时间点的值这里有个取舍:直接让模型一次输出 6 个值,训练简单,但预测质量通常会随时间步拉长而衰减,模型在短期步长上更准。另一种做法是单步预测加滚动:每次预测一个值,把它归一到当前窗口末尾,丢弃窗口最前面的旧值,再继续预测下一个小时。这种模式更接近模型训练时的数据分布,误差会累积,但趋势方向一般不会跑偏。
我一般会先用n_out=1训练一版,保存 checkpoint,再改滚动预测做未来 24 小时的推断,这样既能拿到单步高精度,又能满足业务上的中期趋势需求。如果你要套用到自己的数据,需要改的地方只有:特征列换成你的指标、归一化的变量范围对应调整、时间步长按你的数据粒度设置。数据是小时级就用 24 步看一天,分钟级可以考虑 60 步或 1440 步看一天,但要注意训练耗时。
这份源码让我印象最深的不是 LSTM 本身,而是它把整个链路串起来了:数据读取、窗口构造、模型定义、训练保存、评估还原,每一步都留了扩展位。从那以后我每接手一个时间序列项目,都会先在这个框架上跑一遍基线,再决定要不要上更复杂的结构,比如堆叠 LSTM 或者加入注意力机制。这套套路帮我省掉了很多从头调参的冤枉时间,希望也能帮到你。
本文还有配套的精品资源,点击获取