
简介这是一份面向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_col0) # 风向是字符串先转成类别编码 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_in1, n_out1, dropnanTrue): 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 [fvar{j1}(t-{i}) for j in range(n_vars)] # 输出序列t 到 tn_out-1 的每一列 for i in range(n_out - 1, -1, -1): cols.append(df.shift(-i)) names [fvar{j1}(t{i}) for j in range(n_vars)] agg pd.concat(cols, axis1) agg.columns names if dropnan: agg.dropna(inplaceTrue) return agg这个函数的参数含义很直观n_in是回看的时间步数n_out是要预测的未来步数dropnanTrue会把因为没有足够历史或未来数据而产生的 NaN 行直接丢掉。比如 n_in1、n_out1 时一条样本就是用 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(lossmae, optimizeradam)这里有个关键参数LSTM 层没有设return_sequencesTrue。默认的 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, monitorval_loss, save_best_onlyTrue, verbose1 ) early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) history model.fit( train_X, train_y, epochs50, batch_size72, validation_data(val_X, val_y), callbacks[checkpoint, early_stop], verbose1 )checkpoint 做的是在每次 epoch 结束后比较验证集 loss只要更小就覆盖保存模型这样最后磁盘上留下的是验证集表现最好的那一个版本而不是最后一个 epoch 的结果。EarlyStopping 的patience10表示连续 10 个 epoch 验证 loss 没有下降就提前停止restore_best_weightsTrue会把模型权重回滚到最佳状态这两者配合可以省掉大量无效训练时间。这里有一个很多人忽略的事情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):]), axis1) 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, labelactual) plt.plot(inv_yhat, labelpredict) 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_sequencesTrue)) 后接 Dense(1) 直接报 Negative dimension size。原因return_sequencesTrue让 LSTM 返回每个时间步的输出形状是(batch, time_steps, units)Dense 不认这个三维形状。解决预测单值场景不要开return_sequencesTrue如果要堆叠两层 LSTM只在前面层开最后一层保持默认 False。5.3 训练 loss 徘徊不降数值一直在同一个水平震荡现象epochs 跑了一二十轮loss 曲线基本是一条横线偶尔有小幅波动。原因学习率不合适或输入数据没归一化导致梯度方向抖动。另一个常见原因是时间步长太长数据量撑不起那么深的记忆。解决先确认数据落在 0 到 1 区间然后尝试把 LSTM 单元数从 64 降到 32 或升到 128看 loss 是否有响应最后再考虑把默认学习率调低一个量级用Adam(learning_rate0.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.xAPI 位置变了。解决统一从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_out1训练一版保存 checkpoint再改滚动预测做未来 24 小时的推断这样既能拿到单步高精度又能满足业务上的中期趋势需求。如果你要套用到自己的数据需要改的地方只有特征列换成你的指标、归一化的变量范围对应调整、时间步长按你的数据粒度设置。数据是小时级就用 24 步看一天分钟级可以考虑 60 步或 1440 步看一天但要注意训练耗时。这份源码让我印象最深的不是 LSTM 本身而是它把整个链路串起来了数据读取、窗口构造、模型定义、训练保存、评估还原每一步都留了扩展位。从那以后我每接手一个时间序列项目都会先在这个框架上跑一遍基线再决定要不要上更复杂的结构比如堆叠 LSTM 或者加入注意力机制。这套套路帮我省掉了很多从头调参的冤枉时间希望也能帮到你。本文还有配套的精品资源点击获取