ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LSTM时间序列预测实战:空气质量数据预处理与模型训练全流程

LSTM时间序列预测实战:空气质量数据预处理与模型训练全流程

简介:基于LSTM的时间序列预测Python项目,包含完整源码与全部数据,定位为期末大作业、课程设计及入门深度学习的高分参考。项目以空气污染监测数据为实例,完整覆盖时间序列预测的各个环节,包括数据读取与清洗、缺失值填充、归一化、滑动窗口构造、LSTM模型搭建、训练验证、误差评估及预测曲线绘制,代码注释细致,逻辑清晰,简单配置环境即可运行,并可迁移到金融、气象、能耗等时序场景。压缩包共129个文件,以78个py源码脚本为主体,按数据预处理、模型构建、训练评估等模块组织,配套26个csv数据文件、16个txt说明/日志文件,另有h5模型权重与checkpoint训练断点等产物,整体大小仅5.42MB,文件划分清晰,便于按模块查阅。项目自带多份观测数据,可直接用于对比实验,提升结题报告的完整性。目前已有169人浏览学习,尤其适合需要快速完成高质量大作业或从零理解LSTM时序预测流程的开发者。

1. 基于LSTM的时间序列预测:能跑通、能看懂、能交付的资源包

期末大作业最怕的不是模型训练不出来,而是训练出来之后,老师一问“你这预测的原理是什么、参数为什么这么设、数据是怎么喂进去的”,当场卡壳。这份基于LSTM的时间序列预测资源,源码和数据是配齐的,空气质量污染数据有两份原始文件加一份清洗后的新文件,checkpoint 文件夹里还有训练过程中保存的模型权重。也就是说,下载之后不需要自己重新训练就能先看到预测效果,再对照代码把数据处理、模型搭建、训练保存这一整条链路啃下来。适合正在做期末大作业、课程设计,或者第一次接触 LSTM 想拿真实数据练手的开发者——不需要懂复杂的数学推导,能跟着代码走通流程,就能在答辩时说清楚每一个环节的取舍。

2. 数据与预处理:pollution.csv 多变量序列如何变成监督学习样本

2.1 数据文件里到底有什么

先把资源里的文件过一遍。pollution.csv 和 air_pollution.csv 是原始的多变量空气质量数据,字段包含时间、污染浓度指标以及若干气象特征,属于典型的多变量时间序列。air_pollution_new.csv 是清洗后的版本,通常已经完成了缺失值处理,时间列也做了规范化,适合直接喂给模型。checkpoint 文件夹里存的是训练中按 epoch 保存的权重文件,格式是 TensorFlow 的 .index 和 .data 组合,后面加载模型时会用到。

拿到这种资源,第一步不是急着跑模型,而是先用 pandas 把数据读进来,看结构、看缺失、看时间跨度。

import pandas as pd df = pd.read_csv('pollution.csv', parse_dates=['date'], index_col='date') print(df.head()) print(df.shape) print(df.isnull().sum())

逻辑说明:parse_dates 把日期列解析成 datetime 类型,index_col 把日期设为索引,便于后续按时间切片。isnull().sum() 统计每一列的缺失数量,决定后面用删除还是填充。

参数说明:如果原始数据的时间列名不是 date,需要改成实际列名;如果读取后时间索引是乱的,加一步 df.sort_index()。这一步不做好,后面构造滑窗序列时时间顺序会错位,预测结果全偏。

2.2 缺失值处理与归一化

LSTM 对输入数据的尺度非常敏感,因为激活函数(默认 tanh 和 sigmoid)的敏感区间在 0 附近,原始污染数据动不动就是几百的数值,直接喂进去会让梯度更新不稳定。所以归一化是必须的,不是可选项。

from sklearn.preprocessing import MinMaxScaler # 按列填充缺失值,用前向填充 df.fillna(method='ffill', inplace=True) df.dropna(inplace=True) # 选择特征列,这里取全部数值列 values = df.values.astype('float32') scaler = MinMaxScaler(feature_range=(0, 1)) scaled = scaler.fit_transform(values)

逻辑说明:ffill 用上一个有效值填充缺失,适合时间序列场景,比均值填充更符合数据连续性。MinMaxScaler 把每列压缩到 0~1 区间,fit_transform 先拟合再转换。注意,fit 只能用训练数据做,不能在整个数据集上 fit 完再切训练测试集,否则会有数据泄漏。

参数说明:如果序列有长时间缺失,ffill 会造成一段平坦区,LSTM 会在这一段学习到“输出等于上一个值”,严重影响预测。遇到这种数据,我一般会把连续缺失超过 5 个时间点的片段直接裁掉。feature_range 也可以试 (0.1, 0.9),让边界留一点余量,对 tanh 激活更友好。

2.3 滑窗构造:把时间序列变成监督学习样本

LSTM 不能直接吃一维时间序列,它需要的是形如 (样本数, 时间步长, 特征数) 的三维输入。滑窗(lookback)的思想是用前 N 个时间点的数据预测第 N+1 个时间点的目标值。

import numpy as np def create_dataset(data, lookback=3): X, y = [], [] for i in range(len(data) - lookback): X.append(data[i:i+lookback, :]) y.append(data[i+lookback, :]) return np.array(X), np.array(y) lookback = 3 X, y = create_dataset(scaled, lookback) print(X.shape, y.shape)

逻辑说明:循环里 data[i:i+lookback, :] 取从 i 开始的 lookback 行、全部特征列,作为输入样本;data[i+lookback, :] 取紧接着的那一行作为目标。滑动窗口每次向后移动一个时间点,所以样本总数等于 len(data) - lookback。

参数说明:lookback 取 3 表示用过去 3 个小时预测下一个小时。这个值直接决定模型能看到多长的历史信息,取太小模型学不到趋势,取太大训练样本减少、计算量增大。空气质量这种数据,我一般先用 3 作为基线,再试 5 和 10。如果你想预测的目标不是全部特征而是单一列(比如只预测 pm2.5),构造时把 y 改成 data[i+lookback, target_index] 即可。

数据切分要注意顺序问题。时间序列不能随便随机打乱,训练集和测试集必须按时间顺序切,否则模型会在测试集上“提前见过未来”。常见做法是前 80% 做训练、后 20% 做测试,切完后再分别做归一化重拟合。

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:] print(X_train.shape, X_test.shape)

逻辑说明:按索引顺序切分,保证训练集时间在前、测试集时间在后。这一步是时间序列预测和普通分类任务的本质区别,普通分类可以随机划分,时间序列不行。

参数说明:80/20 是基线比例。如果总数据量很小,可以上调到 90/10;数据量非常大时,也可以留出最后 30% 做验证,让测试结果更有说服力。切分之后如果发现测试集落在某个异常时段,预测误差会偏大,这属于正常现象,答辩时能解释清楚就行。

3. LSTM 模型构建与训练:层参数、损失函数与 checkpoint 策略

3.1 为什么选 LSTM 而不是普通神经网络

空气质量预测这种数据,当前时刻的污染浓度和过去几个小时的变化趋势强相关,比如凌晨污染积累、白天扩散,这种时间依赖性用普通全连接网络很难捕捉,因为它把每个时间点当成独立样本处理。RNN 能保留历史信息,但标准 RNN 在长序列上会有梯度消失问题,记不住太久之前的信息。LSTM 通过输入门、遗忘门、输出门三个门控结构,让信息可以选择性保留或丢弃。

在这份资源里,数据文件有多变量特征,模型要学的是“过去几小时的多个指标如何影响未来值”这种映射关系。LSTM 的记忆单元天然适合这种任务,这也是期末大作业选它而不是 ARIMA 或普通 MLP 的原因——ARIMA 对多变量支持弱,MLP 没有时间结构。

3.2 模型结构与参数设置

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint model = Sequential() model.add(LSTM(units=50, return_sequences=True, input_shape=(X_train.shape[1], X_train.shape[2]))) model.add(Dropout(0.2)) model.add(LSTM(units=50, return_sequences=False)) model.add(Dropout(0.2)) model.add(Dense(units=X_train.shape[2])) model.compile(optimizer='adam', loss='mse', metrics=['mae']) model.summary()

逻辑说明:第一层 LSTM 设置了 return_sequences=True,因为后面还接了一层 LSTM,需要把完整的序列输出传给下一层;最后一层 LSTM 的 return_sequences=False,只返回最后一个时间步的输出,再接全连接层映射到目标维度。两个 Dropout 层分别跟在 LSTM 层后面,随机丢弃 20% 的神经元,抑制过拟合。Dense 的输出维度等于特征数,因为当前构造的目标是预测下一时刻的全部特征值。

参数说明:units=50 是基线设置,比 32 有更强的拟合能力,又不会像 128 那样容易在少量数据上过拟合。loss 用 mse 是因为预测值是连续数值,均方误差对大误差惩罚更重,模型会更关注偏离大的点。metrics 里的 mae 用于训练过程中监控平均绝对误差,方便和后面的评估统一口径。如果你想只预测单一目标列,Dense 输出改成 1,y 也要对应改成单列。

3.3 训练、早停与模型保存

checkpoint = ModelCheckpoint( 'checkpoint/lstm_model.keras', monitor='val_loss', save_best_only=True, verbose=1 ) early_stop = EarlyStopping( monitor='val_loss', patience=10, restore_best_weights=True ) history = model.fit( X_train, y_train, validation_split=0.1, epochs=100, batch_size=32, callbacks=[checkpoint, early_stop], verbose=1 )

逻辑说明:ModelCheckpoint 在每轮 epoch 结束后检查验证集损失,如果比之前最好的结果更好,就覆盖保存模型文件。EarlyStopping 在连续 10 轮验证损失都没有下降时终止训练,并恢复到验证损失最小的那轮权重。这两个回调组合起来,既能防止训练时间过长,又能保证最后保存的是最优模型而不是最后一轮模型。

参数说明:validation_split=0.1 表示从训练数据尾部切 10% 做验证集,注意这里是在已经切分过的训练集基础上再切,不会破坏前面的时间顺序。batch_size=32 是显存和训练速度的平衡点,数据量小可以降到 16。patience=10 给模型 10 次“试探”机会,太小容易在损失局部波动时提前停止,太大会浪费时间。epochs=100 配合早停实际跑多少轮取决于数据,一般 30~50 轮就能收敛。

训练完成后,checkpoint/lstm_model.keras 里就是最优权重的模型文件。这份资源自带 checkpoint 文件夹的好处是,你可以跳过训练直接加载权重看预测效果,也可以重新训练覆盖旧权重——两种路径都能验证整个流程。

4. 预测结果还原与评估:反归一化、RMSE 与绘图对照

4.1 预测值必须反归一化

模型输出的是 0~1 区间的归一化值,直接拿来和原始浓度对比没有意义。必须用之前拟合好的 scaler 的 inverse_transform 把预测值和真实值还原到原始量纲。

y_pred = model.predict(X_test) y_pred_orig = scaler.inverse_transform(y_pred) y_test_orig = scaler.inverse_transform(y_test)

逻辑说明:scaler 在训练前用整份数据 fit 过,inverse_transform 按列还原到原始数值范围。这里要求 X_test 和 y_test 的特征排列顺序和训练数据完全一致,否则还原结果错位。

参数说明:如果前面构造数据集时只预测单一目标列,反归一化就不能直接用整列的 scaler,要单独对目标列做 scaler 的 inverse_transform,或者把预测值拼到其他特征的均值再整体还原,取目标列。这个细节容易忽略,很多同学直接对全量特征归一化后又只取单列预测,结果还原出来完全不对。

4.2 评估指标计算

from sklearn.metrics import mean_squared_error, mean_absolute_error import numpy as np rmse = np.sqrt(mean_squared_error(y_test_orig[:, 0], y_pred_orig[:, 0])) mae = mean_absolute_error(y_test_orig[:, 0], y_pred_orig[:, 0]) print(f'RMSE: {rmse:.2f}, MAE: {mae:.2f}')

逻辑说明:这里取了第 0 列做单指标评估,通常是主污染浓度指标。RMSE 对大误差敏感,能反映预测是否有严重偏离;MAE 更稳健,反映平均偏差水平。两个指标一起看,如果 RMSE 远大于 MAE,说明存在少数预测误差非常大的点。

参数说明:评估指标要根据任务选,如果是期末答辩,RMSE 和 MAE 两个就够。老师如果问“这个误差水平能不能接受”,要结合数据本身的量级回答——比如原始浓度平均在 100 左右,RMSE 30 就是可解释的误差水平;如果平均浓度才 20,RMSE 30 就说明模型基本没学到东西。

4.3 预测曲线与真实曲线可视化

import matplotlib.pyplot as plt plt.figure(figsize=(12, 5)) plt.plot(y_test_orig[:, 0], label='Real', linewidth=1.5) plt.plot(y_pred_orig[:, 0], label='Predicted', linewidth=1.5) plt.legend() plt.title('LSTM Air Quality Prediction') plt.xlabel('Time Steps') plt.ylabel('Pollution Concentration') plt.show()

逻辑说明:用两条曲线对比真实值和预测值,走势重合度直接反映模型效果。如果预测曲线滞后真实曲线一个时间步,说明模型学到的主要是“下一个值等于当前值”的恒等映射,这是时间序列预测最常见的问题。

参数说明:plt.figure 的 figsize 按论文要求调整,答辩展示建议 (12, 5) 以上保证清晰。如果预测的是多个特征,可以循环画多个子图,每个特征一张对比图。

绘图这一步是期末作业的加分项,比贴一堆数值指标更直观。老师看到曲线重合度高、训练损失曲线平滑下降,整个作业的完成度就立住了。

5. 常见问题与避坑:五个让期末大作业翻车的细节

5.1 预测曲线整体滞后一个时间步

现象:预测曲线形状和真实曲线几乎一样,但整体向右平移了一个时间点,RMSE 数值看着还行,但图上一眼就能看出错位。

原因:模型学到的最优策略是“当前值 ≈ 上一时刻值”。当 lookback 太小、序列本身没有明显趋势时,预测均值回归到上一个观测值,这在数学上确实是低误差策略,但没有实际预测价值。

解决:增大 lookback,让模型依赖更多历史信息;同时检查数据是否真的存在时间依赖,如果序列接近随机游走,LSTM 能做的就是滞后预测。答辩时主动说明这一点,反而显得对模型行为有深入理解。

5.2 归一化泄漏导致测试集指标虚高

现象:测试集上 RMSE 低得离谱,但换一批数据重新跑就完全不准。模型表现出“记忆测试集”的倾向。

原因:在切分训练测试集之前就对全量数据做了 scaler.fit_transform,scaler 已经看过测试集的统计信息,等于测试数据信息提前进入了训练环节。

解决:先切分,再对训练集 fit,然后用训练集的 scaler 转换测试集。也就是 scaler.fit(X_train_raw) 之后,再分别 transform 训练集和测试集。这是时间序列预测最容易踩的坑,没有之一。

5.3 每个 epoch 的损失曲线剧烈震荡

现象:训练时 loss 忽高忽低,前 10 轮不降反升,后面的曲线像锯齿一样。

原因:学习率太大导致梯度更新在最优解附近来回振荡;或者数据没有做归一化,损失曲面过于陡峭。batch_size 太小也会让梯度估计噪声增大。

解决:把默认学习率从 0.001 降到 0.0005,或者换用更稳定的优化器设置;确认归一化步骤真实生效,打印一下 scaled 数组的最大最小值应该在 0~1 之间;batch_size 提到 64 试试,很多数据集上反而比 32 收敛更稳。

5.4 checkpoint 加载后预测结果全是同一个值

现象:加载保存的模型后,预测输出几乎恒定,所有预测值都接近某个固定数。

原因:加载模型时输入形状和保存时不匹配,或者模型是从头重新定义的,但没有 rebuild input shape。另一种常见情况是,加载的权重来自训练初期,模型还没收敛。

解决:在预测前用 model.build(input_shape) 重建模型输入形状,或者直接 model.predict 前打印 model.summary() 检查输入维度。再确认加载的文件是 save_best_only 保存的最优权重,而不是最后一次保存的高 loss 权重。

5.5 每次运行结果都不一样,无法复现

现象:同一个文件、同一个模型,两次运行预测结果有差异,连 loss 曲线都不同。

原因:神经网络初始化随机、训练数据顺序随机、Dropout 随机丢弃,这些随机性没有固定种子,每次运行都会产生新的随机序列。

解决:在代码开头固定随机种子,TensorFlow 和 NumPy 都要设置。

import random import numpy as np import tensorflow as tf random.seed(42) np.random.seed(42) tf.random.set_seed(42)

逻辑说明:固定三个库的随机种子,保证训练过程的随机数序列一致。这样即使有 Dropout,随机丢弃的模式也是可复现的。参数说明:种子值选什么都可以,只要全代码一致。如果用了 GPU,还需要在代码开头设置环境变量 TF_DETERMINISTIC_OPS=1,否则不同硬件设备上的结果仍然有细微差异。

6. 进阶用法:多步预测与超参数验证的小技巧

前面的流程默认是“用过去 3 小时预测未来 1 小时”的单步预测,这在学校作业里通常够用了。但如果想让项目再往上走一个台阶,可以试试多步预测——用过去 N 步预测未来 M 步,这更贴近真实场景,也在答辩时更有讨论空间。常见做法有两种:递归预测和直接预测。递归预测是把模型的单步输出拼回输入窗口,再预测下一步,代码改动小,但误差会逐步累积,预测越远越偏。直接预测是把 Dense 层的输出维度改成 M,一次输出未来 M 个时间点,训练时 y 的构造需要对应改成“从 i+lookback 到 i+lookback+M-1”的区间,误差不累积,但训练样本构造更复杂。

我做这类作业的习惯是,先跑通单步预测,确认数据和模型链路都没问题,再尝试递归多步。原因很简单:如果单步预测的滞后问题都没解决,多步预测只会把误差放大得更严重。

超参数验证方面,有一个成本很低的顺序。先固定 lookback=3、units=50,跑一遍拿到基线 RMSE。然后只改 lookback 试 5 和 10,记录指标变化。再固定表现最好的 lookback,把 units 改成 64 和 128,看模型容量增加带来的收益是否值得训练时间的增加。最后微调 dropout 从 0.2 到 0.3。每次只改一个参数,否则两个参数同时变动,你根本不知道是哪一个起了作用。

# 递归多步预测示例代码 def recursive_predict(model, last_window, steps=5): predictions = [] current_input = last_window.reshape(1, last_window.shape[0], last_window.shape[1]) for _ in range(steps): next_val = model.predict(current_input, verbose=0) predictions.append(next_val[0]) current_input = np.concatenate([current_input[:, 1:, :], next_val[:, np.newaxis, :]], axis=1) return np.array(predictions)

逻辑说明:每次预测出下一个时间点的所有特征,然后把它拼接到输入窗口末尾,同时丢掉窗口最前面的一个时间点,窗口长度保持不变。concat 的 axis=1 是时间步维度,表示向后滑动一个时间步。参数说明:steps 决定预测多远,建议先从 5 步开始,超过 10 步递归误差会明显累积。

这套资源的 checkpoint 文件对调试特别有用。训练一次之后,后面每次调整评估代码,都可以直接加载保存好的权重,不用重新训练几十轮。从那以后我每次做这类多变量序列预测,都会强制自己在写模型之前先把数据检查和归一化泄漏排查一遍,这两个地方出问题,后面所有环节都会跟着翻车。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进