基于LSTM的PM2.5浓度预测:数据清洗到模型训练全流程

发布时间:2026/9/30 9:36:28
基于LSTM的PM2.5浓度预测:数据清洗到模型训练全流程
简介基于LSTM循环神经网络的PM2.5预测PDF是一份聚焦空气质量预测的学术论文面向环境科学、数据建模与机器学习方向的研究者和学生。针对PM2.5浓度变化突发、非线性且传统方法难以准确预测的问题该研究将气象与大气污染物指标作为输入先用灰色关联度分析筛选关键因子再把时间序列转为监督学习问题最终搭建多变量LSTM循环神经网络模型实现PM2.5日值浓度预测。资源包仅含1份PDF文档大小约1.1MB方便离线阅读目前已有176人学习。文档保留了期刊论文的完整结构包括摘要、关键词、引言、模型方法、仿真实验与结论等读者可系统学习从特征关联分析、数据处理到LSTM建模与验证的完整流程。文中利用北京市2010—2017年气象和污染物数据验证模型结果显示能较好预测日变化趋势对空气质量预报和环境管理具有参考价值。1. 给PM2.5浓度序列装上“记忆”LSTM预测要解决什么问题PM2.5浓度曲线不是一串孤立数字今天的峰值往往能从过去几十个小时的风速、湿度和浓度变化里找到影子。这种前后依赖关系正是循环神经网络RNN的用武之地基于LSTM循环神经网络的PM2.5预测就是用一段历史观测序列去推未来一个或多个时刻的浓度值让模型自己学习污染积累、扩散和日变化规律。如果你正在做空气质量的课程设计、毕业论文或是要给环境监测系统加一个预报模块这篇文章可以一路跟到模型跑完如果只拿普通回归模型死磕时序数据、预测曲线总是“慢半拍”这一步正好补上时间序列建模的关键环节。我会按实际项目顺序往下走数据清洗、滑窗切样本、建模训练、评估和排错每一处都给可复现的参数和代码。2. 数据准备让LSTM吃到干净、连续、有规律的PM2.5序列2.1 公开历史数据怎么组织从原始记录到标准化CSV任何LSTM项目第一步都不是写模型而是确认数据能不能被模型吃到。PM2.5预测最常用的原始数据是环境监测站点逐小时导出的记录有些来自公开空气质量平台有些来自论文附带的压缩包共同点是时间戳和要素列齐全但格式千奇百怪有的按天分表有的混着字符串。常见做法是先用pandas读进来统一列名再输出一份干净的训练CSV。一份典型的时间序列特征表如下变量名含义单位/格式是否作为特征datetime观测时间YYYY-MM-DD HH:MM:SS索引不作为特征pm25PM2.5浓度μg/m³目标变量也作为滞后特征参与建模so2 / no2 / o3气态污染物μg/m³特征temp / pres / dewp气温/气压/露点°C / hPa / °C特征wspm风速m/s特征风向这类类别变量不要直接塞进数值矩阵硬编码成{0,1,2,3}会给LSTM带来伪排序要保留就做one-hot或者第一版先删掉等模型跑通再加。这个选择对预测效果的影响很大污染过程往往带明显风向特征但模型并不天然理解类别编号的大小关系。读取和排序的代码如下import pandas as pd df pd.read_csv(pm25_raw.csv) df[datetime] pd.to_datetime(df[datetime]) df df.sort_values(datetime).reset_index(dropTrue) # 如果原始数据有非整点记录聚合到小时级 df df.set_index(datetime).resample(1h).mean().reset_index() df.to_csv(pm25_clean_stage1.csv, indexFalse)说明时间戳转成datetime是必须的排序保证后面滑窗在时间上连续resample会把不规则记录聚合到整点均值填充空位会产生NaN留到下一步清洗。2.2 清洗与时间戳处理缺失值、0值异常与乱序记录PM2.5时间序列数据有三个高频污染源缺失值、连续0值和乱序记录。缺失值多出现在凌晨站点维护和网络中断时段更隐蔽的是连续0值不少仪器在零标定或停机状态会输出0这不是真实浓度而是无效记录。乱序记录出现在合并多个分片CSV时如果直接用原始顺序切窗口会把后一天的数据接到前一天前面模型看到的“时间段”全是错位的。我一般先做一轮比例检查统计pm25为空、为0的数量占总样本比例。缺失比例低于1%直接插值超过5%按天剔除而不是硬填。import numpy as np print(缺失比例:, df[pm25].isna().mean()) print(零值比例:, (df[pm25] 0).mean()) df.loc[df[pm25] 0, pm25] np.nan # 保守起见把0先当缺失 df[pm25] df[pm25].interpolate(limit_directionboth) df[pm25] df[pm25].fillna(methodffill).bfill()说明interpolate用前后近邻做线性插值适合小时序列的短缺失ffill和bfill兜底处理序列边缘的缺失。把0当NaN要慎重我知道有些城市冬季PM2.5确实能低到接近0我的判断标准是看零值占比如果小于0.5%且没有成段出现基本可以判断是真实低值而不是停运。北方春季沙尘过程后容易出现连续0值尽量翻一下原始日志再决定是否剔除盲目插值会把一段真实过程抹平。2.3 滑窗切样本窗口长度、预测步长与训练验证划分LSTM的样本不是一行行独立记录而是一个时间片段。以过去48小时预测未来1小时为例我习惯用一个循环函数生成样本。窗口长度要匹配PM2.5的周期特性24小时周期至少需要24步才能看到要捕捉持续两到三天的污染积累过程24步不够我更常用48或72步作为窗口。def make_sequences(data, feature_cols, target_col, window48, horizon1): X, y [], [] values data[feature_cols].to_numpy(np.float32) targets data[target_col].to_numpy(np.float32) for i in range(len(data) - window - horizon 1): X.append(values[i : i window, :]) # 过去window个时刻的全部特征 y.append(targets[i window : i window horizon]) # 未来horizon个时刻的浓度 return np.array(X), np.array(y)代码逻辑X的每个样本形状是(48, 特征数)y是(horizon,)。核心规则是“未来”必须在滑窗结束之后绝不能把目标时刻的特征混进X否则模型不学规律直接读答案。第一版把horizon设为1简单可靠想要24小时预报需要改horizon或者做滚动预测最后一节细说。切好样本后按时间顺序分割前80%训练、后20%验证。不要直接用sklearn的train_test_split它默认随机拆分用在时序上会出大问题。验证集必须晚于训练集这样评估的才是“模型预测未来”的能力而不是“在序列中间回忆”。数据归一化也在这个阶段做用训练集统计量fit scaler验证集只transform。若图省事用全量数据fit后面还原浓度时数值会错位这个坑会在避坑部分单独讲。窗口越大样本数量越少。数据量只有几千小时时window不要超过总样本的5%如果切完样本不足两千batch_size降到32不要盲目加大窗口。3. 搭建LSTM模型为什么是循环神经网络以及最小可运行的PyTorch代码3.1 从RNN到LSTM长程依赖和梯度消失问题循环神经网络的核心在于“循环”这两个字它不是把整个序列一次性塞进网络而是在时间维度上共享同一组权重逐个时间步更新隐藏状态。PM2.5序列天然适合这种结构今天上午的浓度受昨晚边界层高度影响昨天的高浓度颗粒物经过积累和扩散会在两三天后留有痕迹。普通全连接网络看不到这种长程关系再宽的隐藏层也只是在拟合“拼出来的长向量”。标准的RNN在反向传播时要跨所有时间步计算梯度序列一长梯度连乘会出现指数级消失或爆炸前二三十步的信息到后面就没了。LSTM引入记忆细胞和遗忘门、输入门、输出门三套门控让信息可以在记忆细胞里长距离传递需要保留时保留需要丢弃时丢弃。这也是为什么同样是“循环神经网络”实际工程里基本默认选LSTM而不是原始RNN。这类小时级时序项目我用PyTorch而不是TensorFlow/Keras的最直接原因是动态图方便模型不收敛时可以在forward里打印中间状态一行行检查hidden state是变成0还是变成NaN。对需要反复调参的小项目这个调试优势比部署便利更值钱。3.2 最小可运行的PMLSTM模型定义PyTorch的nn.LSTM已经封装好门控逻辑手写三个门反而容易出错。我习惯把模型定义成“LSTM 全连接输出层”LSTM负责提炼序列状态全连接层把最后一个时间步的状态映射成预测值。import torch import torch.nn as nn class PMLSTM(nn.Module): def __init__(self, input_size, hidden_size, num_layers, horizon): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropout0.1 if num_layers 1 else 0, ) self.fc nn.Linear(hidden_size, horizon) def forward(self, x): out, (h_n, c_n) self.lstm(x) last out[:, -1, :] # (batch, hidden_size) return self.fc(last) # (batch, horizon)代码逻辑说明batch_firstTrue让输入和输出形状都保持(batch, seq_len, features)切片和排查都直观。nn.LSTM返回的out包含每个时间步的隐藏状态这里只取最后一个时间步因为预测目标是未来序列结尾的状态才是对“当前已发生的所有信息”的浓缩。两层LSTM之间加了dropout0.1缓解过拟合num_layers1时dropout参数会被PyTorch忽略不影响运行。实例化模型时把input_size设为7对应7个特征列这里把pm25自身作为第一个特征列参与建模模型能看到过去浓度的轨迹。hidden_size64代表每个LSTM单元输出64维的状态这是“记忆容量”不是网络层数。hidden_size太小比如16记住的细节有限太大到256在小数据集上容易过拟合到噪声。数据量上万小时、特征七八列时64到128是安全区间。3.3 三个必须对齐的维度seq_len、input_size、hidden_size新手最容易翻车的就是维度错位。seq_len是滑窗窗口长度对应X.shape[1]input_size是特征列数对应X.shape[2]hidden_size是模型定义里的超参数。模型定义只写input_size和hidden_sizeseq_len由训练数据动态决定PyTorch的LSTM允许不同批次使用不同序列长度但同一批内的seq_len必须一致否则dataloader默认的stack会报错。训练前先随机生成一个样本自检shapewith torch.no_grad(): xb torch.randn(4, 48, 7) # 4个样本窗口48特征7 yb model(xb) print(yb.shape) # 期望 (4, 1)输出如果是(4,1)说明模型对horizon1的尺寸正确。horizon改成24时重新实例化再试fc层不知道horizon是几它只认构造参数所以改horizon要重新训练不能沿用旧权重。模型参数量主要集中在LSTM层nn.LSTM(input7, hidden64)单层参数量约4×(7×64 64×64 64)18432两层再加fc层65个参数总计约3.7万。这个规模在CPU上都能轻松训练不需要纠结显存。另一个容易忽略的点LSTM内部激活是tanh和sigmoid输入数值过大会把激活压到饱和区梯度基本消失。这就是前面归一化要存在的原因。如果跳过归一化直接喂原始浓度几百μg/m³的值会让训练loss掉得极慢而且看不出模型结构有什么问题。4. 训练与评估让预测曲线贴合真实浓度的参数调整4.1 损失函数、优化器和学习率的选择回归任务首选MSE均方误差。它对大偏差的惩罚呈平方级放大训练时会让模型优先压缩污染峰值附近的误差。评估指标一般用RMSE因为单位是μg/m³可以直接和日均值75μg/m³这种标准对比。MAE可以作为辅助指标但单独拿MAE训练时模型容易趋向输出中位数峰值会被系统性低估。优化器用Adam学习率默认1e-3起步。loss曲线在第一轮就降得很低不代表模型好很有可能只是把上一时刻的观测值抄了过来训练loss持续下降但验证loss开始回升是过拟合信号下面要讲的早停就是为它准备的。4.2 训练循环里的三个“后悔药”梯度裁剪、早停与学习率衰减LSTM反向传播跨多个时间步梯度连乘之后可能瞬间爆炸。常见做法是每次backward之后做梯度裁剪限制梯度的范数不超过5.0。这是一个“后悔药”避免一次夸张的更新把整个模型状态破坏掉。第二个后悔药是权重保存只在验证loss比历史最优时保存训练崩了随时恢复。第三个是学习率衰减ReduceLROnPlateau在验证loss进入平台期时把学习率减半让模型在最优解附近继续微调而不是震荡。from torch.utils.data import TensorDataset, DataLoader X_train, y_train X[:split], y[:split] X_val, y_val X[split:], y[split:] train_loader DataLoader(TensorDataset(X_train, y_train), batch_size64, shuffleTrue) val_loader DataLoader(TensorDataset(X_val, y_val), batch_size256, shuffleFalse)shuffleTrue只用于训练集让每个epoch见到的样本顺序不同验证集不能shuffle因为评估的是一个完整连续的时段。滑窗重叠会让相邻样本高度相似打乱后存在信息重叠但验证集在时间上完全晚于训练集总体评估仍然可信。训练循环和早停一体写完optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5) criterion nn.MSELoss() best_val float(inf) wait 0 early_stop_patience 15 for epoch in range(100): model.train() for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() model.eval() with torch.no_grad(): val_pred model(X_val) val_loss criterion(val_pred, y_val).item() scheduler.step(val_loss) if val_loss best_val: best_val val_loss wait 0 torch.save(model.state_dict(), best_pm25_lstm.pth) print(fepoch {epoch}, val_mseloss {val_loss:.6f}) else: wait 1 if wait early_stop_patience: print(fearly stop at epoch {epoch}) break代码逻辑scheduler.step(val_loss)把验证loss传进去连续5个epoch没下降学习率自动减半早停逻辑连续15个epoch没有刷新best_val就break。这类小时级序列预测100个epoch很少真跑满通常40到60个epoch验证集就进入平台期。4.3 反归一化后看指标RMSE、MAE与相关系数训练时的loss是在归一化空间计算的只是一个无量纲数字。评估预测质量必须把预测值和真实值都拉回原始浓度尺度否则RMSE没有物理意义。from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score pred_norm model(X_val).detach().cpu().numpy() y_norm y_val.numpy() pred_raw scaler_y.inverse_transform(pred_norm.reshape(-1, 1)).flatten() y_raw scaler_y.inverse_transform(y_norm.reshape(-1, 1)).flatten() rmse np.sqrt(mean_squared_error(y_raw, pred_raw)) mae mean_absolute_error(y_raw, pred_raw) r2 r2_score(y_raw, pred_raw) print(fRMSE{rmse:.1f} μg/m³, MAE{mae:.1f} μg/m³, R2{r2:.3f})参数说明scaler_y是只对目标变量y fit的scaler训练时记录训练集的范围验证预测后用它还原reshape(-1, 1)是为了满足inverse_transform要求的二维输入还原后flatten回一维。RMSE受验证时段影响明显如果三个月验证集里恰好跨了一次沙尘暴RMSE会比平常高出一大截这并不代表模型变差。同参数同数据的前提下比较不同模型才有意义。正常LSTM的R2在0.6到0.8区间如果常见峰值时段R2超过0.9先怀疑有没有数据泄漏。5. LSTM预测PM2.5避坑指南五个让模型翻车的高频细节5.1 数据泄漏验证集指标虚高换一段数据立刻失灵现象训练过程里验证集loss很低自信地把模型切到另一段时间或者另一个站点预测曲线几乎完全不能用。原因最常见的是特征里夹带了未来时刻的信息比如把“当天24小时浓度均值”放进特征列做小时预测时当天均值天然包含预测目标所在时刻的信息其次是没有按时间划分数据随机抽样的train_test_split会让模型“见过”验证时间段。解决特征工程阶段逐列排查是否依赖未来数据划分数据用截断方式而不是随机抽样另外可以做一个便宜的自检把预测时间整体向前平移如果RMSE变化剧烈说明模型在强记验证时段的统计分布而不是在学规律。5.2 把序列打乱训练误差曲线正常但预测全是滞后值现象loss下降得很漂亮验证RMSE也还行但把预测和真实曲线叠在一起预测曲线总是滞后真实曲线一两小时峰值被削平。原因滑窗样本之间高度重叠随机打乱后同一时刻的样本可能同时落在训练集和验证集模型学到的是“把上一时刻的观测值搬运过来”这是一种恒等映射短期预测误差很小看起来很美实际没有任何预测能力。解决验证集严格晚于训练集用滞后相关性检验把预测序列和真实序列错位1到3小时后重新算相关系数如果错位后相关性反而更高基本可以确认模型在“抄近道”。5.3 归一化参数用错预测值永远在0-1区间现象模型训练一切正常反归一化之后数值还在0点几或者出现负浓度。原因训练前用全量数据fit了scaler切分后再拿验证集的数据也参与了scaler构建反归一化自然错位另一个常见错误是MinMaxScaler拟合的是训练集范围验证集一旦出现超出训练范围的浓度反归一化后会出现负值或大于1000的怪值。解决先切分再fit验证集和测试集只调用transform不要重新fit预测值出现负浓度时先别急着clip到0检查训练集范围是否覆盖完整或者原始数据里是否混入了异常大值。通过99.9分位数对特征截断比无脑clip更合理。5.4 loss出现NaN或训练不收敛梯度爆炸与学习率现象训练到十几或几十个epochloss突然变成NaN退回最近的checkpoint才能继续另一种是loss一直挂在0.01量级上下不降。原因LSTM反向传播经过多层多时间步梯度范数可能瞬间爆炸Adam的自适应学习率挡不住特别大的梯度另外数据里有极端离群值时标准化后虽然数值不大但这些样本产生的loss仍然很大。解决在backward之后紧跟clip_grad_norm_把梯度范数限制在5.0学习率从1e-3降到5e-4重试按特征列做99.9%分位数截断。loss不降时优先打印torch.isnan(xb).any()确认特征序列里没有混入NaN。5.5 换站点就失效训练集指标好外推能力弱现象A站点训练出来的模型在A站点验证时段表现很好直接搬到B站点预测曲线整体偏高或偏低RMSE比简单基线还差。原因模型学到的是A站点浓度分布、气象条件和浓度之间的统计关系不同站点的源排放结构、地形、仪器标定都不一样。LSTM不学物理过程它是统计模型站点差异对它来说就是两个不同的世界。解决先分别训练单站点模型作为baseline确认跨站点预测的难度部署时用B站点最近几个月的观测数据在加载好的A站点模型上继续训练几个epoch这叫轻量级迁移学习效果比从零开始训练快很多也比直接硬套好得多。6. 进阶验证用残差分析检查模型可信度再做24小时多步预测6.1 用残差和滞后校验确认模型不是“记住上一个值”模型训练完别急着交付我习惯先检查残差的自相关。残差是真实值减预测值如果残差序列在时间上强烈正相关说明模型漏掉了本该被建模的时序结构当前输出只是在“延迟复制上一时刻”。更直观的做法是把预测序列整体向右平移24小时再和真实序列算相关系数如果平移后的相关系数反而更高说明模型的有效输出是滞后项。corr0 np.corrcoef(y_raw, pred_raw)[0, 1] shifted pred_raw[:-24] target y_raw[24:] corr_shift np.corrcoef(target, shifted)[0, 1] print(f原始corr {corr0:.3f}, 预测平移24h后corr {corr_shift:.3f})平移后相关性明显更高时回头看特征当前时刻PM2.5作为特征时权重太高短窗口下模型天然走捷径。一个简单改法是加大window到72甚至120或者直接把horizon改成24强迫模型学习更长期的规律。6.2 从单步到未来24小时滚动预测与直接多步的取舍真实业务通常要求预报未来24小时浓度。直接多步做法是把模型输出维度改成horizon24损失函数一次约束24个时刻实现简单但后续输出很容易回归到均值峰值越来越平。滚动预测做法是先训练horizon1的模型预测出t1后把它拼到窗口末尾再预测t2循环24次滚动法保留了浓度动态轨迹但误差逐步累积第24小时的RMSE通常比第1小时高不少。我的实践是先做滚动预测把误差累积曲线画出来分别看第6、12、24小时的RMSE。如果第24小时RMSE超过训练集标准差的一半说明这个模型只适合短期预报不要强行撑到24小时把目标降回12小时或6小时可能比硬拟合更实用。这类项目最终交付的往往不是一个全能24小时预报器而是一个“未来6小时趋势可靠、峰值能预警”的实用模块。如果让我只给一条习惯就是每轮实验都把验证集曲线截图和RMSE一起存档包括这次改了什么、坏在哪一步。模型输出靠不靠谱最终判断依据永远是反归一化后的真实浓度曲线而不是终端里越来越低的loss。踩坑踩多了就明白LSTM的效果一半靠参数另一半靠数据边界意识。希望帮到你。本文还有配套的精品资源点击获取