
简介这是一份面向深度学习与空气质量建模场景的PDF论文资料围绕LSTM循环神经网络在PM2.5预测中的应用展开适合机器学习、数据建模、环境科学等方向的学习者阅读。针对PM2.5浓度变化非线性强、影响因素多、传统方法难以准确预测的问题资料提出以灰色关联度分析方法筛选气象和空气污染物指标对数据进行平滑处理后把时间序列问题转换为监督学习问题进而搭建多变量LSTM模型实现对PM2.5日值浓度的准确预测。内容中系统介绍了LSTM原理、灰色关联度分析、多变量分析等关键知识点并利用北京市2010—2017年气象与污染物数据完成仿真验证有助于读者掌握从数据预处理、特征关联分析到循环神经网络建模的完整研究思路。资源包内含1个PDF文件大小约1.1MB已有176人学习浏览是开展相关论文研读、课程设计或入门实践的良好参考资料。1. 说到PM2.5预测为什么我劝你先试试LSTM再折腾ARIMA手上有小时级的PM2.5监测数据想预测未来几小时的浓度很多人的第一反应是ARIMA或线性回归。但真处理过重污染过程的人都知道PM2.5序列在冬季静稳天气下会出现连续十几小时的高值平台进入下一轮冷空气时又骤降这种强非线性、长依赖的形态ARIMA的差分和自回归项很难抓住。LSTM循环神经网络靠三个门控结构把“昨天的浓度走势”和“当前的气象变化”一起记在细胞状态里天然适合做这种中等时间尺度的序列预测。这篇笔记不聊论文推导只讲怎么用LSTM把PM2.5预测跑通从数据构造、网络搭建到训练调参最后把我踩过的坑一条条列出来。适合手里有至少一个月的小时级浓度数据、会用Python和Keras的环保或数据从业者照着做能少走两个星期的弯路。2. 造数据比调模型更决定上限PM2.5时序数据的清洗与滑窗构造2.1 从ARIMA到LSTMPM2.5序列的哪些特征决定了选型PM2.5浓度序列和股票、温度这类时间序列最大的不同在于它同时受排放源、气象扩散条件、二次生成过程的共同影响。拿冬季典型日来看早晨和傍晚有排放高峰但浓度不一定会准时涨因为边界层高度和风速的变化会覆盖掉源强的规律。这样的序列是非平稳的均值、方差都在滑移它也是非线性的前一天的高浓度能否在第二天持续取决于当时的相对湿度有没有触发二次气溶胶增长。ARIMA要求序列先做差分变成平稳差分会丢掉“高浓度平台是否正在发育”这种长程信息。RNN循环神经网络从概念上更适合因为它把过去的隐藏状态传进当前时间步的计算。但普通RNN在时间步超过10到20的时候反向传播的梯度会连乘衰减导致“记不住前几天”的效应。LSTM神经网络在隐藏状态之外增加了一条细胞状态传送带用输入门、遗忘门、输出门控制信息的写入和丢弃。对PM2.5预测来说遗忘门可以学会“冷空气过境后的旧浓度该放下”输入门可以决定“当前静稳天气下的累积趋势该记住多少”。这就是选型背后的根本原因——不是因为它比ARIMA炫而是因为它能显式建模长期依赖。2.2 小时级数据清洗缺失值、异常值和时间戳对齐拿到原始监测站点的CSV最常见的格式是两列时间戳和浓度值。第一步是先把时间戳转成datetime并设为索引然后重采样到小时避免有的站点上传频率是分钟级导致时间轴不对齐。我一般用pandas的resample(H)做聚合取每小时平均。缺失值先看连续缺失的跨度短于3个小时的用线性插值长于24个小时的干脆截断掉因为长时间插值会把浓度曲线磨平后面模型学到的全是假信号。import pandas as pd import numpy as np # 读取原始数据假设只有两列time, pm25 df pd.read_csv(obs_pm25.csv, parse_dates[time]) df.set_index(time, inplaceTrue) # 重采样为小时均值同时确保连续小时索引 df df.resample(H).mean() # 连续缺失不超过3小时用线性插值 df[pm25] df[pm25].interpolate(limit3, limit_areainside) # 超过24小时的缺失段直接置为NaN并整段删除前后避免插值污染 group_nan df[pm25].isna().astype(int).groupby( df[pm25].notna().cumsum() ).sum() long_nan_idx group_nan[group_nan 24].index df.loc[df[pm25].notna().cumsum().isin(long_nan_idx), pm25] np.nan df df.dropna(subset[pm25]) # 异常值用滚动中位数加3倍MAD剔除 rolling_med df[pm25].rolling(window24, centerTrue).median() mad (df[pm25] - rolling_med).abs().rolling(window24, centerTrue).median() df[pm25] df[pm25].mask( (df[pm25] - rolling_med).abs() 3 * 1.4826 * mad 50, np.nan ) df[pm25] df[pm25].interpolate(limit3)这段代码的逻辑是先做时间对齐和小时聚合再用interpolate(limit3)只填小缺口。检测长缺失段时我用了一个小技巧——用notna().cumsum()给连续非空段编号再统计每个编号里的NaN数量。异常值剔除用的是滚动中位数加MAD比固定3σ稳健因为PM2.5本身就可能出现300以上的真实高值不能用全局均值和方差去套。参数1.4826是把MAD换算成标准差的系数后面加50是为了防止重污染日里滚动中位数滞后把真实峰值误杀。2.3 滑窗构造样本滞后窗口与预测步长怎么匹配模型吃的不是单条记录而是“过去24小时 → 未来1小时”这样的样本对。构造滑窗时最重要的决定是滞后窗口长度和预测步长。我的默认起点是lookback24预测horizon1也就是用过去一天预测下一个小时。如果要做未来6小时预测可以先以horizon1训练出一个基准模型再评估滚动预测效果而不是一开始就构造多步标签。def make_sequences(data, lookback24, horizon1): X, y [], [] for i in range(lookback, len(data) - horizon 1): X.append(data[i - lookback:i]) y.append(data[i horizon - 1]) return np.array(X), np.array(y) # 归一化必须在切分之后只对训练集fit验证集/测试集用同一套参数 from sklearn.preprocessing import StandardScaler scaler StandardScaler() train_data scaler.fit_transform(train_raw.reshape(-1, 1)) test_data scaler.transform(test_raw.reshape(-1, 1)) X_train, y_train make_sequences(train_data.flatten(), lookback24) X_test, y_test make_sequences(test_data.flatten(), lookback24)这里要特别强调两个容易翻车的点。第一make_sequences返回的X是二维[样本数, 时间步]后面送进LSTM前还要reshape成[样本数, 时间步, 特征数]特征数在单变量场景是1。第二归一化必须在时间序列切分之后只对训练集fit_transform验证集和测试集用同一个transform。如果你先对全序列做了标准化再切分验证集的最小值、最大值已经泄漏进训练集模型在验证集上的RMSE会虚低线下指标一片大好一上线就现原形。这个坑在时序预测里太常见了我写它的时候都是血泪。3. 搭一个能跑的LSTM网络结构、输入形状与四个必调参数3.1 一个能跑的最小LSTMSequential搭法用Keras搭一个能跑的LSTM模型不需要一上来就上双向或深层。单变量PM2.5预测我的起始配置是两层LSTM加一个Dense输出其中第一层返回完整的序列给第二层第二层只返回最后一步。这个结构比起一层LSTM能多学一层非线性变换但训练量不会爆炸。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam model Sequential([ LSTM(units64, return_sequencesTrue, input_shape(lookback, 1), namelstm_1), Dropout(0.2), LSTM(units32, return_sequencesFalse, namelstm_2), Dropout(0.2), Dense(units1, nameoutput) ]) model.compile( optimizerAdam(learning_rate0.001), lossmae, metrics[mse] )这里input_shape是两个整数(lookback, 1)别写成包含batch的三维。第一层return_sequencesTrue很关键因为第二层LSTM需要完整的序列输入如果这里写成False第二层收到的就只有一个最后一次的隐藏状态序列信息被压扁了我见过好几个人在这翻车。Dropout(0.2)加在两层之间的遗留dropout只对输入层起作用如果要用循环dropout需要在LSTM的recurrent_dropout参数里单独指定。我一般先把recurrent_dropout留在0因为它会强制模型用CPU实现某些算子训练慢很多等模型收敛了再开不迟。3.2 关键参数units、dropout、batch_size、时间步这里列一个参数对照表标注我的习惯值和建议范围方便你直接抄作业。参数习惯值搜索范围影响units第一层6432128隐藏状态维度。太小学不进去太大在小时级数据上容易过拟合units第二层321664压缩信息负责把两层学到的特征投射到输出Dropout0.20.10.4输入层随机丢弃缓解过拟合recurrent_dropout000.3对循环连接做dropout能防过拟合但训练慢batch_size6432256影响梯度稳定性。小时级数据通常不用太大显存不是瓶颈时64够用learning_rate0.0010.00050.003Adam的默认值对标准化后的序列基本靠谱不收敛再降lookback241272输入窗口。太长不会简单变好可能引入无关噪声关于lookback有个常见误解不是把过去144小时全塞进去模型就一定能记得更远。LSTM的长期记忆是靠门学出来的如果输入窗口里大部分时间都是平静期关键的高浓度起涨点反而被淹没。我习惯先用24小时跑出基线再试着增加到48、72小时看验证集MAE是否真的下降。有一次我加到168小时验证集反而变差了原因是夜间浓度低值重复出现模型学会了用平均窗内的低值来压低输出把真正要预测的高值平滑掉了。3.3 输入输出形状与层之间的匹配关系形状不匹配是刚入门的人报错最多的地方。LSTM层期望三维输入(batch, timesteps, features)。在单变量场景下make_sequences得到的二维X需要扩维成X_train.reshape(-1, lookback, 1)。输出形状取决于最后一层的配置return_sequencesFalse时LSTM输出的形状是(batch, units)可以直接接一个Dense(1)如果return_sequencesTrue再接Dense那Dense会作用在每一个时间步上输出(batch, timesteps, 1)这通常不是你要的结果除非在做seq2seq。# 训练前强制检查形状 print(X_train shape:, X_train.shape) # (样本数, 24) print(y_train shape:, y_train.shape) # (样本数,) # 给LSTM需要的第三维 X_train X_train.reshape(X_train.shape[0], lookback, 1) X_test X_test.reshape(X_test.shape[0], lookback, 1) model.build(input_shape(None, lookback, 1)) model.summary()model.summary()会列出每一层输出的shape这是排查形状问题最快的方法。我在模型build之后再打印能确认None, 24, 1正常送进第一层LSTM。这里的None代表batch维度Keras在训练时填充实际值。如果你的输入特征不止PM2.5比如把温度、湿度拼进来那第三维就是特征数input_shape(lookback, n_features)后面我会专门讲多变量输入。这一阶段出现ValueError: Input 0 of layer lstm is incompatible with the layer十有八九是特征维没对齐先把sumary()和实际X.shape对一遍比翻报错日志快得多。4. 训练与评估损失函数、早停和验证集上真实的RMSE4.1 损失函数与评估指标MAE还是MSEPM2.5预测评估里有个现实矛盾环境监测用MBE、R²业务上关心的是“中度污染过程是否提前6小时报出”。如果损失函数定为MSE模型会把全部注意力放在减小少数大误差样本比如重污染峰值上结果平时预测很准一遇到污染过程就被拉偏。如果定为MAE模型更关注中位数附近的误差对峰值的惩罚不足但整体偏差更小人工读曲线也更舒服。我的做法是训练损失用MAE同时用RMSE做人工复核指标因为RMSE单位更直观而且能放大那些“预测曲线整体偏移”的系统性错误。from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop EarlyStopping( monitorval_loss, patience15, restore_best_weightsTrue, verbose1 ) reduce_lr ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr0.00001, verbose1 ) model.compile( optimizerAdam(learning_rate0.001), lossmae, metrics[mse, mae] )在第3章定义的模型编译里我把指标加上了mse。这里说明一下训练日志的读法loss是MAEmse是中间指标val_loss是验证集MAE。我一般不看训练集loss因为模型在训练集上很容易到0.1以下但那只是记住样本。真正决定能不能上线的是val_loss有没有持续下降。如果训练集loss一路暴跌验证集loss在第5个epoch就开始反弹那就是过拟合早停会在patience15后停止并恢复最好的权重。4.2 早停与学习率衰减防止在验证集上过拟合早停的patience不是越大越好。PM2.5数据量通常只有几千到几万条模型容量一大验证集loss会出现“下降-回升-再下降”的假象。我试过patience30结果模型在某个局部最优附近震荡了30个epoch最后恢复的权重还是第12轮的白白浪费两小时。现在的习惯是patience15配restore_best_weightsTrue让Keras在训练结束时把权重回滚到验证集loss最小的位置。注意restore_best_weights默认是False不设的话结束后用的是最后一轮的权重很多人的“早停失效”问题其实出在这里。学习率衰减也要盯着val_loss用。ReduceLROnPlateau在验证集连续5个epoch不降时把学习率乘0.5最低降到1e-5。这个组合比固定学习率跑到底稳定得多。还有一点Keras的verbose参数在实际训练时最好设成1让我能看到每个epoch的loss和val_loss变化及时判断是否发散到NaN。4.3 训练过程可视化与预测结果反标准化训练结束后预测值还在标准化空间里要还原成真正的浓度再做评估和画图。这一步如果做错后面所有的分析都没意义。import matplotlib.pyplot as plt from sklearn.metrics import mean_absolute_error, mean_squared_error # 在测试集上预测并回退到原始量纲 y_pred_scaled model.predict(X_test) y_pred scaler.inverse_transform(y_pred_scaled) y_true scaler.inverse_transform(y_test.reshape(-1, 1)) rmse np.sqrt(mean_squared_error(y_true, y_pred)) mae mean_absolute_error(y_true, y_pred) print(fTest RMSE: {rmse:.2f} μg/m³, MAE: {mae:.2f} μg/m³) # 画前72小时的对比能直观看到滞后或系统性偏差 plt.figure(figsize(12, 5)) plt.plot(y_true[:72], labelobserved, linewidth2) plt.plot(y_pred[:72], labelpredicted, linewidth2) plt.legend() plt.title(PM2.5 LSTM Forecast vs Observed (first 72h)) plt.tight_layout() plt.savefig(lstm_forecast.png, dpi120)这里有个反直觉的细节scaler.inverse_transform需要输入的形状是(样本数, 1)而model.predict返回的y_pred_scaled形状是(样本数, 1)可以直接用但y_test是从make_sequences里取出来的是一维形状(样本数,)所以要先reshape(-1, 1)否则报错。画图时选前72小时差不多是3天能看到模型在早晚峰值上的跟随程度。如果预测曲线比实测晚1到2小时说明时间窗口里的滞后信息不够或者模型学会了一个简单的“拿上一小时的值当预测”的懒办法——这种情况下RMSE可能看着不错但污染过程的方向性变化完全没抓到。5. PM2.5预测避坑五个常见坑的现象、原因与排查5.1 归一化泄漏验证集虚低上线就翻车现象训练时验证集RMSE是每立方米15微克自己心里很美结果拿去预测新数据前三天的RMSE就要35以上。原因在切分训练集和测试集之前就做了全局归一化测试集的均值、方差已经混进训练集的标准化参数里相当于模型提前“见过”了测试集的统计特征。这是时序预测的第一大坑。解决只对训练集部分fit_transform测试集用同一个scaler.transform。如果数据本身有季节漂移比如要训练全年的模型那么最好每个月滚动更新scaler而不是用一整年的均值。更稳妥的做法还有用sklearn.preprocessing.StandardScaler单独拟合训练段验证段每次从后面取30天我上个月就是这样修正了一版模型才敢给别人看数字。5.2 预测值退化成序列均值污染过程完全消失现象预测曲线几乎是一条平线偶尔有小幅波动RMSE在平稳段还行但一到重污染时段预测值卡在150左右实测都冲250了。原因第一训练数据里正常天占绝大多数模型发现“输出平均值”能最小化MAE的期望损失就选择了这个安全策略第二损失函数设置不当MSE对峰值和大值惩罚过重反而把模型吓回去了。解决先检查标签分布如果重度污染占比低于5%就要考虑加权损失或对重污染样本过采样。我常用的办法是把序列做一阶差分后作为预测目标让模型学“变化量”而不是“绝对值”预测时再把前一个真实值加回去。这样即使模型对绝对值有偏差对趋势方向的捕捉也会好很多。5.3 训练集随机打乱时间依赖被破坏现象验证集loss很低但查看预测曲线时发现第一天的预测用到了第100天的信息时间线完全错乱。原因写数据加载时手滑用了train_test_split默认的shuffleTrue默认行为是随机洗牌时间序列的全部依赖关系都被打断了。这种模型在训练时记的是样本序号而不是时间结构。解决要么用np.split按时间顺序切分要么用train_test_split时强制shuffleFalse。我自己的习惯是把时间索引的最后30%作为测试集前70%训练验证集从训练集的最后20%里再切一次保证验证集和测试集都严格在训练集之后。这是时间序列预测和图像分类最不一样的地方。5.4 时间步长设得过大模型越训练越慢loss不降现象把lookback从24改成168后训练时间涨了三四倍但loss几乎不下降。原因LSTM在长时序上的梯度传播仍然存在衰减虽然比RNN强但时间步过长时信息还是会在长路径里丢失同时输入维度过大非线性映射更难学。解决不要盲目堆窗口。先画自相关系数图看PM2.5序列的偏自相关在哪里截断——小时序列一般24小时和168小时各有周期性但真正有预测增量的是过去12到48小时。如果确实想用更长的记忆可以用Conv1D先把时序降维再接LSTM让卷积层做局部特征提取LSTM负责长程依赖。这样lookback可以到168训练速度还比纯LSTM快。5.5 预测结果比实测滞后23小时方向趋势对但相位不对现象重污染过程来时预测曲线总是比实测晚两三个小时才抬头等实际到峰值了预测值还在爬坡整个过程被右移。原因模型学成了“用上一时刻的值来预测下一时刻”这是时序预测最常见的退化解。尤其在小时级数据里1阶自相关系数极高模型发现把t-1的值原样搬到t就能得到不错误差。解决从特征层面对抗滞后——不只是给模型过去的PM2.5还要给它过去的气象因子风速、风向、湿度以及滞后1小时、6小时、24小时的浓度差分项。输入特征的多元化会让模型必须依赖多因子的组合来预测而不是单纯地把序列平移。另外一个有效做法是训练时把horizon从1改成3直接预测3小时后的浓度这样模型无法直接抄上一时刻的值必须真正学到演变规律。6. 进阶技巧多变量输入、多步预测和上线前的盲测6.1 把气象因子接进LSTM输入单靠历史浓度做预测本质是拿过去的自己解释未来的自己。实际情况里偏东风湿度上升风速转弱这些才是重污染过程的关键触发器。多变量输入的做法很简单把PM2.5和气象因子分别归一化后用np.column_stack拼装再构造滑窗。注意每个因子用各自的scaler不要让风速和湿度共享PM2.5的标准化参数。我曾经直接把所有列丢给一个StandardScaler结果湿度因为方差小被压缩到0.1附近LSTM几乎读不到它的变化白白丢了一个重要特征。6.2 预测未来6小时的多步输出直接预测与滚动预测多步预测有两种常见路线。直接预测是让输出层有6个神经元一次输出未来6小时的浓度滚动预测是先用horizon1的模型预测下一小时把预测值拼到输入序列末尾再预测再下一小时循环6次。直接预测收敛快但会丢失“预测的t2建立在t1之上”的递推关系滚动预测更接近实际使用但误差会累积尤其模型本身有滞后时滚到第6步已经是“预测的预测的预测”。我的折衷方案是训练一个horizon6的直接多输出模型作为基线同时用horizon1的模型做滚动对照最后把两者对验证集的RMSE画在一张图里谁小用谁。这个对比每次都能让我重新校准对模型的信任。6.3 模型上线前必须做的盲测最后一步也是我以前经常跳过导致被领导追着问的一步留出最近30天完全不参与训练、不参与验证。做法是把数据按时间拉回一条线前70%训练中间20%验证最后30%盲测。验证集上的MAE再好看都不算数只有盲测集上第一天的预测结果与真实值对比才能证明模型对“没见过”的事件有泛化能力。我现在每次训练完都会重新检查时间顺序确认没有哪行数据因为乱序误入训练集。上一版模型就是因为验证集一个偶然的泄漏盲测翻了车现在养成这个习惯后心里踏实不少。希望这些篇幅能帮你少踩几个坑也祝你跑出的第一个LSTM预测曲线能在污染过程来临时提前抬头而不是跟在实测身后补作业。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。