用Keras LSTM做PM2.5时间序列预测:数据预处理与训练避坑指南

发布时间:2026/10/2 8:34:21
用Keras LSTM做PM2.5时间序列预测:数据预处理与训练避坑指南 简介面向深度学习入门者的Keras LSTM实战资源以空气污染相关时间序列数据为例完整演示从数据预处理、LSTM模型构建、编译训练到评估预测的整个流程。压缩包体积仅861KB共含5个文件两个Python脚本分别负责数据生成与模型训练测试两个CSV文件提供原始及处理后的实验数据一个HDF5文件保存训练好的最佳模型权重结构精简、便于按需取用。目前已有1702人学习下载适合希望借助实际案例掌握LSTM在时间序列预测中应用方法的初学者。通过运行脚本并对照CSV数据可直观理解Keras中Sequential模型、LSTM层、Dense层以及compile、fit、evaluate等核心API的用法同时了解数据标准化、序列填充、训练集划分等预处理要点还能直接加载已有HDF5权重进行预测验证或基于脚本调整超参数开展调参实验从而更系统地掌握LSTM的实战流程。1. 用 Keras 跑 LSTM 实战这份资源把时间序列预测的完整链路一次给齐很多人以为 LSTM 实战最大的门槛是模型结构真正拿数据跑过一遍的人会发现数据处理才是决定成败的那一环。这个压缩包里正好是这么一套完整的实战组合MakeData.py 负责把 raw.csv 和 pollution.csv 处理成 LSTM 能吃的序列样本TrainandTest.py 负责构建和训练 Keras LSTM 模型best_model.hdf5 是已经训练好的权重文件。它解决的问题非常具体——用逐小时的气象数据预测 PM2.5 浓度这是标准的多变量时间序列回归任务。适合两类人一类是刚接触 Keras、想在一个真实数据集上完整跑通流程的新手另一类是已经在用 LSTM 做设备寿命预测、能耗预测想回来对照检查自己数据处理细节的从业者。整个包最有价值的不是模型——LSTM 结构在网上到处都是——而是 MakeData.py 里那套时间序列处理逻辑它直接决定了模型能不能收敛、能不能泛化。2. MakeData.py 数据管线从 raw.csv 到 LSTM 可训练样本的完整转换2.1 先分清两个 CSVraw.csv 与 pollution.csv 差异拿到手先别急着跑代码花两分钟看清楚 raw.csv 和 pollution.csv 的关系。raw.csv 是原始采集数据字段是按小时记录的时间戳年、月、日、时和气象观测值PM2.5 浓度、露点温度 DEWP、气温 TEMP、气压 PRES、风向 cbwd、风速 Iws、积雪时间 Is、降雨时间 Ir。这类野外采集数据最常见的毛病就是缺值和异常值——PM2.5 传感器偶尔掉线某几行会出现 NaN或者干脆用 -1 这种占位符填充。pollution.csv 就是在这个基础上整理出来的规范版本。对比项raw.csvpollution.csv数据状态原始采集可能含 NaN、-1 异常值已清洗对齐可直接读取特征字段时间戳 8 个气象维度与 raw 字段一致格式统一在包中的作用数据加工的源头参考MakeData.py 的实际输入典型处理位置MakeData.py 的清洗阶段MakeData.py 的特征工程阶段这里要强调一个容易忽略的点MakeData.py 的清洗逻辑并不复杂但删掉这步直接训练loss 会在一个奇怪的位置卡住。原因后面第 4 章细说先记住结论——凡是序列数据里有 -1 这种占位符必须先替换成 NaN 再填充否则模型会用“负值缺数据”这种假模式去拟合。2.2 MakeData.py 核心逻辑滑动窗口切分与归一化接下来是 MakeData.py 里最核心的一段也是整个包最值得抄的部分。它的作用是把一列连续的时间序列变成监督学习能用的样本对过去 N 个时刻的特征作为 X当前时刻的 PM2.5 作为 y。# MakeData.py 核心逻辑清洗、one-hot、归一化、滑动窗口 import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler def make_dataset(scaled, look_back24): X, y [], [] for i in range(look_back, len(scaled)): X.append(scaled[i - look_back:i, :-1]) # 前 24 小时的特征不含 PM2.5 y.append(scaled[i, -1]) # 当前小时的 PM2.5最后一列 return np.array(X), np.array(y) df pd.read_csv(pollution.csv) df df.replace(-1, np.nan).ffill().dropna() # 异常值转缺失前向填充后丢弃开头不完整段 df pd.get_dummies(df, columns[cbwd]) # 风向类别 one-hot feature_cols [DEWP, TEMP, PRES, Iws, Is, Ir] feature_cols [c for c in df.columns if c.startswith(cbwd_)] feature_cols.append(pm2.5) # PM2.5 挪到最后一列 values df[feature_cols].astype(float).values scaler MinMaxScaler(feature_range(0, 1)) scaled scaler.fit_transform(values) # 全量归一化 X, y make_dataset(scaled, look_back24) split int(len(X) * 0.8) # 按时间顺序切 80% 训练 X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:] print(X_train shape:, X_train.shape) # (样本数, 24, 特征数) print(y_train shape:, y_train.shape) # (样本数, 1)这段代码包含四个关键决定每个都直接影响后续训练效果。第一look_back24。这是给 24 小时窗口PM2.5 浓度有明显的日周期24 正好覆盖一个完整昼夜循环模型能从窗口里看到“前一天同时段”的规律。如果你换数据这个值要按你的业务周期重新定——股票日线用 5 或 20工业传感器用设备一个操作循环的采样点数。窗口太小学不到周期太大特征维度不变但每个样本信息变长训练更慢。第二MinMaxScaler(feature_range(0,1))。LSTM 用的是 tanh 和 sigmoid 类激活函数对输入量纲非常敏感。气温可以到 40 度气压在 1000 上下风速可能有几十这些原始数值直接进网络会让梯度更新被大数值特征主导。归一化到 0~1 区间是 LSTM 的默认安全做法。第三scaled scaler.fit_transform(values)是对全量数据一次性求统计量。这里有个血泪经验不要先把数据切成训练和测试再分别对两段做 fit_transform。那样测试集用自己的统计量归一化等于把未来信息泄露给模型验证结果会虚高上线后真实预测立刻翻车。第四split int(len(X) * 0.8)按时间顺序切分而不是随机抽 80%。时间序列样本之间有前后依赖随机切分会把一部分“未来的样本”混进训练集让模型偷看到答案。切分完记得验证一下时间对齐别后面画预测曲线时发现横轴对不上。2.3 换成你自己的数据时MakeData.py 这三处必须改把这份代码搬到自己项目时大多数人只改个文件名就跑然后模型不收敛。至少以下三处要按你的数据重设。第一处是异常值处理。我这里用replace(-1, np.nan).ffill().dropna()适合传感器偶发掉线的场景。如果你的数据缺失成片比如连续几天没采集前向填充会把假值一直带下去这时要改成插值或直接丢弃这几段。判断标准很简单看一眼缺失比例超过 5% 就别用单纯 ffill。第二处是归一化时机。如果你的数据是流式的比如设备在线监测每来一批新数据分布都可能漂移那么应该只在训练数据上 fit后续每批验证数据单独 transform保持统计口径一致。离线数据集用全量 fit 没问题但上线部署时一定要记住用训练时保存的 scaler而不是重新 fit。第三处是切分比例。0.8 是通用值数据量小时容易过拟合我一般会降到 0.7 并配合早停。数据量大又想严谨可以用时间序列交叉验证把数据按时间折成五段每轮用前四段训练、最后一段验证。这类数据预处理代码值得多花半小时打磨后面训练阶段反而省心。3. TrainandTest.py 拆解Keras LSTM 的模型结构、编译与训练参数跑这个包之前先把环境确认一遍。Keras LSTM 实战最常见的问题是版本错位建议直接用 Keras 2.x TensorFlow 2.x 的组合不会互相踩。如果环境里只装了 TensorFlow 没有独立 Keras就把代码里的from keras.models import Sequential改成from tensorflow.keras.models import Sequential其余层导入同理。装好之后能顺利 import说明环境没问题。注意环境确认后先跑一次model.summary()确认输出形状和预期一致再开始训练能拦下 90% 的维度报错。3.1 LSTM 层参数input_shape、units、return_sequences 怎么设TrainandTest.py 里模型结构不复杂单层 LSTM 加一个全连接输出适合回归任务。先把核心代码贴出来。# TrainandTest.py 核心模型结构 from keras.models import Sequential from keras.layers import LSTM, Dense, Dropout n_timesteps X_train.shape[1] # 时间步 look_back 24 n_features X_train.shape[2] # 特征维度 one-hot 后的特征数 model Sequential() model.add(LSTM(50, activationtanh, input_shape(n_timesteps, n_features))) model.add(Dropout(0.2)) model.add(Dense(1)) model.compile(lossmse, optimizeradam, metrics[mae]) model.summary()三个参数在这里的含义要讲清楚。第一个是units50即 LSTM 隐状态维度。50 是这个数据量级的均衡选择样本量在两万条量级50 个单元足够拟合非线性关系又不至于过拟合。第二个是input_shape(n_timesteps, n_features)第一个数是时间步长对应 MakeData.py 里的 look_back第二个数是特征数注意这里不是 7因为风向 one-hot 后加了列。第三个是return_sequences这里没写默认 False意思是输出最后一个时间步的隐状态形状 (batch, 50)。如果你后面再接一层 LSTM必须显式设成 True否则网络结构直接报错。Dropout(0.2)放在 LSTM 输出后面随机丢弃 20% 的连接是防止时间序列模型过拟合最省事的手段。0.2 是常用起点val_loss 和训练 loss 差距大就加到 0.3不用一上来就上 0.5容易欠拟合。3.2 编译参数回归用 mse别照抄分类的 categorical_crossentropy摘要里给的示例是 categorical_crossentropy 加 softmax那是多分类的写法。这个包的 PM2.5 预测是回归任务输出层只有一个神经元激活函数默认线性所以 loss 必须用 mse 或 mae。任务类型输出层神经元输出激活loss常用 metrics数值回归本包1线性mse / maemae二元分类1sigmoidbinary_crossentropyaccuracy多分类Nsoftmaxcategorical_crossentropyaccuracy回归用 mse 的原因在于它给大误差更高惩罚PM2.5 这种场景里峰值预报错了比普通时刻错了更严重。如果你更关心误差绝对量可以换成 mae它的梯度更稳定不容易被个别大值样本带偏。optimizer 直接用 adam它对学习率不敏感是 LSTM 实战最省心的选择如果发现 loss 在训练后期抖动再把 adam 的默认学习率降到 0.0001 试试。3.3 fit 与 CallbackModelCheckpoint 保存 best_model.hdf5 的完整配置训练部分是本文件的重点主要看回调函数怎么配合。# TrainandTest.py 训练与回调配置 from keras.callbacks import EarlyStopping, ModelCheckpoint checkpoint ModelCheckpoint(best_model.hdf5, monitorval_loss, verbose1, save_best_onlyTrue, modemin) early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) history model.fit(X_train, y_train, batch_size72, epochs50, validation_data(X_test, y_test), callbacks[checkpoint, early_stop], shuffleFalse)batch_size72意味着每次梯度更新用 72 条样本。数据是逐小时的72 条相当于三天的量选这个值能让每个 batch 里覆盖到完整的日周期变化梯度更新方向更稳定。如果 batch 太小比如 8loss 曲线会剧烈震荡太大比如 512则训练慢且容易陷进局部最优。epochs50配合 EarlyStopping 其实跑不满。patience10 表示 val_loss 连续 10 轮没有改善就停restore_best_weights 会在停止时把权重回滚到历史最优。这套组合解决了时间序列任务里最常见的“后段过拟合”问题LSTM 在前几轮通常快速收敛后面就开始在训练集上死记硬背val_loss 反弹没有早停会白白保存一个次优模型。shuffleFalse是这里最容易被忽略的一行。默认情况下 Keras 的 fit 会每轮打乱训练样本对常规分类没问题但时间序列样本之间存在严格顺序依赖打乱会让模型丢掉连续上下文。验证集固定用 validation_data 传 X_test、y_test而不是 validation_split0.2原因在于 validation_split 是从训练集尾部切一块出来而时序数据的尾部恰好是最新一段它的分布跟测试集重叠度太高这样调出来的“最优模型”在真实预测时会打折扣。最后说下保存逻辑。ModelCheckpoint 的 save_best_onlyTrue 会在每个 epoch 结束时比较 val_loss只有创历史新低才覆盖写入 best_model.hdf5。这样你在测试集上评估的就是训练全程最强的那个权重而不是最后一轮的权重——最后一轮通常已经开始过拟合了。这也是这个包里 best_model.hdf5 比你自己训练完手动 save 出来的模型更可靠的原因。4. LSTM 实战避坑指南五个高频错误与排查记录这章是拿真金白银的跑批换来的每一条都对应一个“模型就是不出活”的现场。我按排查优先级排序前两条先看数据处理后三条看训练与保存照着这个顺序检查能省半天。4.1 shuffle 关没关直接决定验证集有没有意义现象训练 loss 还挺顺但验证 loss 忽高忽低而且模型在测试集上对晚高峰类的突变完全反应不过来。原因fit 里没写 shuffleFalseKeras 默认对训练样本做随机打乱。LSTM 本来的优势是捕捉时间依赖样本顺序一乱输入窗口里的 24 个时间步变成随机组合模型只能学到一些统计共性学不到“昨天的 PM2.5 影响今天”这种递推关系。解决训练时显式指定 shuffleFalse。如果确实想让每个 batch 更多样可以只在训练集内部按时间顺序打包再在每个 batch 内做局部乱序但这个操作比较高端新手不要动直接关掉最稳。4.2 反归一化写错预测图压扁在 0~1现象predict 完画图预测曲线整体压扁在 0~1 之间跟真实 PM2.5 的几百微克水平对不上。原因训练前做了 MinMaxScaler模型输出的是归一化后的值。画图前直接拿 predict 结果去对真实值等于拿 0.3 这种数字当 300 微克用。解决加载模型预测后必须用训练时保存的 scaler 做 inverse_transform。注意个小细节scaler 是在整个 feature 矩阵上 fit 的逆变换时要恢复完整的特征形状再取 PM2.5 那一列不要只对预测列单独 inverse_transform否则数值错位。正确写法见第 5 章。4.3 loss 不降怎么办激活函数与学习率现象训练 loss 从第一轮开始就纹丝不动或者呈锯齿状剧烈震荡evaluate 指标怎么都不发生变化。原因最常见的是把 LSTM 的激活函数改成了 relu。relu 在时间步过长时容易造成梯度爆炸尤其输入的特征没归一化时更严重。少部分情况是学习率太大adam 默认的 0.001 对某些数据集偏激进。解决回归任务直接用 LSTM 默认的 tanh别自己换。如果必须用 relu加一个梯度裁剪Adam(clipvalue1.0)。检查学习率就用默认 0.001 跑 5 轮看 loss 有没有往下走没走就降到 0.0001。这个参数调起来有点像玄学但规律很明确先保证能下降再追求下降快。4.4 val_loss 先降后升过拟合在时序任务里更隐蔽现象前 15 轮训练 loss 和 val_loss 都在降看起来很好从第 16 轮起 val_loss 开始反弹训练 loss 还在降像两条线分道扬镳。原因LSTM 参数多时间序列又存在样本自相关性模型学完真实模式后开始记训练集细节。没有 EarlyStopping 的话它会一直记下去最后 test loss 比轮次中途的模型差一大截。解决按 3.3 的方式配置 EarlyStoppingpatience 设在 8~12 之间。另外确认 Dropout 生效模型结构里没加 Dropout 的话先加 0.2。还有一种做法是减少 units从 50 降到 32参数量直接减半过拟合通常能压下去。4.5 加载 hdf5 报错先检查版本和特征列顺序现象训练时一切正常但 load_model(best_model.hdf5) 时报错常见的是找不到自定义层或权重形状不匹配。原因训练时用的 Keras 版本和加载时的版本不一致。最典型的是 keras 3 和 keras 2 的序列化格式不同或者训练机用了 GPU 版 TensorFlow加载机只有 CPU 版。权重形状不匹配则多半是 one-hot 特征列顺序变了——重新读 csv 时 get_dummies 生成的列顺序可能不一样。解决训练环境和部署环境保持同一套版本。遇到 shape 报错看提示里 (None, 24, 10) 变成了 (None, 24, 9)多半就是风向列没对齐或漏了某列回到 MakeData.py 检查 feature_cols 的生成逻辑。5. 用 best_model.hdf5 做验证与调参滚动预测、多步预测和网格搜索5.1 加载与评估反归一化时注意特征对齐# 加载 best_model.hdf5单步预测 反归一化 评估 from keras.models import load_model from sklearn.metrics import mean_squared_error, mean_absolute_error import numpy as np model load_model(best_model.hdf5) pred model.predict(X_test, verbose0) dummy np.zeros((len(pred), 10)) dummy[:, :9] X_test[:, -1, :9] # 最后时间步真实特征作骨架 dummy[:, 9] pred[:, 0] # 预测 PM2.5 填入最后一列 pred_pm25 scaler.inverse_transform(dummy)[:, 9] y_true_vals np.concatenate([X_test[:, -1, :9], y_test.reshape(-1, 1)], axis1) y_true scaler.inverse_transform(y_true_vals)[:, 9] rmse np.sqrt(mean_squared_error(y_true, pred_pm25)) mae mean_absolute_error(y_true, pred_pm25) print(fRMSE: {rmse:.2f}, MAE: {mae:.2f})代码背后有个容易出错的地方scaler 是在完整特征矩阵上 fit 的逆变换必须凑够 10 列。dummy[:, :9] X_test[:, -1, :9]的意思是拿测试样本最后一个时间步的真实特征做骨架把预测的 PM2.5 填到第 10 列再 inverse_transform 后取最后一列。这样还原出的预测值和真实值才处在同一尺度上。RMSE 在这个数据集上跑进 25 左右算正常超过 40 基本说明模型没学到趋势回去检查特征列顺序和 look_back。5.2 多步预测与网格搜索单步预测只能看下一小时业务上通常要预测未来 6~24 小时。最常见的做法是滚动预测把预测值拼到序列末尾再滑动窗口预测下一步代价是误差随时间累积窗口越长偏差越大。另一种是直接多输出把 Dense(1) 改成 Dense(24)y 对应未来 24 小时的值但训练标签之间互相关联复杂度上了一个台阶。设备寿命预测、回声消除这类序列任务换过来也一样先想清楚要几步再决定用哪条路。网格搜索给一个骨架抄的时候注意先粗后细# 网格搜索骨架交叉验证 units 和 look_back for units in [32, 50, 64]: for look_back in [12, 24, 48]: X, y make_dataset(scaled, look_back) split int(len(X) * 0.8) X_train, X_test, y_train, y_test X[:split], X[split:], y[:split], y[split:] model Sequential() model.add(LSTM(units, input_shape(look_back, X.shape[2]))) model.add(Dropout(0.2)) model.add(Dense(1)) model.compile(lossmse, optimizeradam) history model.fit(X_train, y_train, validation_data(X_test, y_test), epochs20, batch_size72, verbose0) val_mse min(history.history[val_loss]) print(funits{units}, look_back{look_back}, val_mse{val_mse:.4f})网格搜索的耗时是组合数乘以单次训练时长我一般先固定 look_back24 扫 units找到最优区间后再固定 units 扫 look_back控制在十几次以内。我自己第一次调这个包时val_loss 死活下不去查了整晚才发现是归一化时把未来统计量泄露进了训练集。从那以后凡是新接手的时序项目我都强制走一遍三件事确认切分是按时间、确认归一化是整个序列一次完成、确认 fit 里 shuffle 是 False。这份资源里的 MakeData.py 和 TrainandTest.py 已经把这三件事写进代码了照抄再用自己的数据替换掉特征列就行希望帮到你。本文还有配套的精品资源点击获取