CNN-LSTM模型实现PM2.5浓度预测:完整项目复盘

发布时间:2026/10/21 8:57:02
CNN-LSTM模型实现PM2.5浓度预测:完整项目复盘 聊起空气质量预测北京的PM2.5浓度是很多人绕不开的话题。我去年花了两个多月时间从数据抓取到模型调参把一套基于CNN-LSTM的雾霾浓度预测完整代码跑通了效果在可接受范围之内。今天这篇就用一次真实的项目复盘的方式把完整流程、代码细节、踩过的坑全部摊开讲清楚希望能给正在做时序预测或者环境数据建模的朋友一些可以直接抄走的经验。这套代码能解决什么问题简单说给你过去一段时间的小时级PM2.5浓度以及气温、气压、风速、风向、降雨等气象数据模型能够预测未来几小时的污染浓度走势。项目适合两类人看一类是深度学习入门者想搞懂CNN和LSTM怎么配合处理时间序列另一类是环境科学或数据分析方向的人想知道这种混合模型在实际污染物预测任务里到底能跑出什么效果、有哪些坑。1. 项目设计与技术选型思路1.1 为什么是CNN-LSTM而不是单纯某个模型刚开始拿到这个题目的时候我第一反应想的是直接用LSTM做。毕竟PM2.5浓度是标准的时间序列LSTM天生就是干这个的。但实际跑下来以后发现一个问题——单用LSTM对突发性的浓度变化反应偏慢模型倾向于把上一时刻的浓度值直接平滑地带到下一时刻预测曲线整体守成有余、锐度不足。后来换了个思路把CNN加进去做前置特征提取。CNN在图像领域很强势但在时间序列上它干的是另一件事——用一维卷积核去扫描输入窗口把局部变化模式识别出来。打个比方LSTM像一个记忆力很好的人能记住前几天的污染走势但眼神不太好CNN就像一个视力极好的观察员能在短窗口里迅速发现“这阵子风速在加大、湿度在上升、浓度在下降”这类局部变换特征。两个人配合观察员先扫一遍把有价值的局部模式提炼出来记忆员再基于这些模式去推演长程规律。这套配合在物理意义上也说得通。雾霾的形成和消散不是线性过程它有积累效应也有突变节点比如冷锋过境前后的浓度骤降、早晚高峰时段的短时攀升。CNN抓的是这种短程模式特征LSTM负责的是跨小时甚至跨天的趋势记忆。两者各管一段职责边界清楚。1.2 数据集与评估基准的选择刚开始我图省事想直接爬其他平台已经清洗好的数据。后来发现单一数据源不太够用就手动合并了两部分污染物浓度数据来自某公开环境监测站点数据库的逐小时记录气象数据来自对应的地面气象观测站。时间跨度取了大约4年记录频率是每小时一条字段包括PM2.5浓度、PM10、SO₂、NO₂、CO、O₃、气温、气压、露点、风速、风向、累积降雨量。选数据集的时候有两条经验值得说第一时间跨度要足够长。至少跨过一个完整年这样才能覆盖北方冬季供暖季的高污染时段和夏季的清洁时段模型才能学到季节规律。第二数据要尽量在“同一地理尺度”上。北京的空气污染在空间上分布很不均匀如果污染物数据是城区的站点气象数据却取了郊区站点风向上的错位会影响模型学习。我最后尽量选了同一坐标系下能关联上的数据源保证时序记录能对齐。评估指标上回归任务我不建议只看一个。RMSE、MAE、R²三个指标要放一起看。RMSE对大的预测误差特别敏感能反映出模型在重污染时段是否出现离谱的偏差MAE给出的是平均绝对误差更贴近日常感知R²给出的是模型对浓度方差的解释比例。后面在评估章节我会详细展开这三者各自怎么看。1.3 代码结构怎么组织才不容易翻车代码组织这块我踩过一整个重写的坑。最开始我把数据清洗、模型构建、训练、绘图全写在一个脚本里结果换一组参数就要全局翻找调试成本极高。第二个版本我做了模块化拆分按下面的结构组织air_quality_project/ ├── data/ │ ├── raw/ # 原始数据存放 │ └── processed/ # 清洗后的数据 ├── src/ │ ├── data_preprocessing.py # 加载与清洗 │ ├── build_dataset.py # 构造滑动窗口样本 │ ├── model.py # CNN-LSTM模型定义 │ ├── train.py # 训练流程 │ └── evaluate.py # 评估与绘图 ├── models/ # 保存训练好的权重 └── results/ # 图表与指标输出这样的好处有几个换数据集时只动data_preprocessing.py调网络结构只动model.py实验记录集中在一个目录复盘容易。虽然项目初期多了些文件跳转成本但后续调参的效率提升绝对值回票价。2. 环境准备与数据预处理细节2.1 依赖库与版本选择跑这套代码建议直接用Anaconda建一个独立环境别把系统Python环境搞乱了。我用的核心依赖和版本如下Python 3.9TensorFlow 2.10Keras内置NumPy 1.21Pandas 1.4scikit-learn 1.1Matplotlib 3.5TensorFlow版本的选择有个讲究2.10及之前的版本对Windows原生支持比较友好不需要额外配WSL2.11开始Windows版本默认走DirectML有些环境反而会出现莫名其妙的算子兼容问题。如果你用的是Linux服务器那2.10到2.15问题都不大。安装建议直接一条命令装完pip install tensorflow2.10 numpy pandas scikit-learn matplotlibCPU训练能不能跑能跑就是慢。这个项目的数据量在几万条样本这个量级网络参数不算大CPU训练一个epoch大概要一两分钟。如果有NVIDIA显卡装上CUDA和cuDNN之后同一个epoch能压到十几秒整体效率提升非常明显。2.2 数据清洗与缺失值处理原始CSV数据下载下来以后第一件事不是急着灌进模型而是先做缺失值和异常值的处理。我拿到的数据里缺失值类型有两种一种是整行记录为空另一种是某个字段单独为空。前者直接删除或者用邻域均值填充后者要看字段的重要性分开处理。处理缺失值我反复用过几种方案最后稳定下来的是分字段处理PM2.5浓度缺失不超过连续3小时用前后两小时线性插值风速、风向缺失用当天同时段的滑动平均值填充气压、气温缺失优先用气象站同期记录拟合补全连续缺失超过6小时直接删除对应时间段不做插值因为这段时间的插值结果对人眼是平滑的但对模型来讲就是造假数据风向这个字段容易踩坑。原始记录里风向可能是0到360的角度值也可能是16方位字符串。如果用角度值要考虑角度数据的环形特性0度和360度其实是同一个方向直接喂给模型模型会误以为两者差异极大。我用的是把风向拆成两个分量风向正弦和风向余弦这样既保留了方向信息又消除了0/360断裂问题。2.3 归一化与滑动窗口样本构造归一化方法的选择直接影响训练效果。我对比过StandardScaler和MinMaxScaler最后选的是MinMaxScaler把数值缩放到0到1之间。原因很简单PM2.5浓度和气象要素的数值尺度差异太大温度可以在-10到40之间浮动浓度可能冲到300以上不归一化的话模型会把注意力全放在数值大的特征上。但要特别提醒一个数据泄漏的坑MinMaxScaler的fit操作只能在训练集上做然后用训练集上计算出的最小值、最大值去transform验证集和测试集。如果先对整个数据集做fit再切分相当于验证集和测试集的信息提前泄露给了模型评估指标会虚高部署到真实环境后立刻露馅。滑动窗口的设计是整个预处理的核心环节。预测任务定义为用前T小时的观测数据预测未来H小时的PM2.5浓度。我尝试过多种窗口组合最终T选了72H选了1也就是看过去三天的数据预测下一个小时。这样定义的好处是训练样本的构造比较直接模型学习的是“从长窗口提炼特征到短期输出”的映射关系。样本构造的Python代码大致如下def create_sequences(data, target_idx, lookback72): X, y [], [] for i in range(lookback, len(data)): X.append(data[i-lookback:i]) y.append(data[i, target_idx]) return np.array(X), np.array(y)这里注意data必须是经过归一化的完整二维数组target_idx是PM2.5浓度所在的列索引。这样每一个样本都是一个形状为(72, 特征数)的矩阵标签是对应时刻的浓度值。2.4 数据划分要有时间观念机器学习标准做法是随机打乱划分训练集、验证集、测试集但这个套路在时间序列上是大忌。PM2.5浓度有强自相关性今天的数据和昨天强相关如果把明天的样本混进训练集、今天的样本放进测试集模型等于直接偷看了答案。我的划分策略是按时间顺序切分前70%作为训练集中间15%作为验证集最后15%作为测试集。这样测试集永远在时间上晚于训练集模型面对的是真正意义上的“未来数据”评估结果才可信。3. 模型搭建与训练配置3.1 核心网络结构解析模型结构的确定不是一蹴而就的我至少改了四版。第一版用一层Conv1D加一层LSTM训练稳定但精度一般第二版加深了CNN部分用了两层卷积加一层池化结果发现池化会把时域信息过度压缩反而丢掉了关键的浓度峰值特征第三版保留两层卷积但去掉了池化改用步长控制序列长度效果开始好转第四版也就是最终版把CNN输出接入Dropout再接LSTM再加一层全连接就稳定定型了。最终版结构如下表所示层类型参数/说明InputInputLayer输入形状(72, 特征数)Conv1D_1一维卷积64个卷积核核大小3ReLU激活Conv1D_2一维卷积32个卷积核核大小3ReLU激活Dropout随机失活丢弃率0.2LSTM长短期记忆64个隐藏单元返回序列关闭Dropout随机失活丢弃率0.2Dense_1全连接32个神经元ReLU激活Dense_2全连接1个神经元线性激活一维卷积核大小选3代表每次扫描相邻三个时刻的特征这个设计是卷积神经网络在时间序列上的常用做法。卷积核数量我对比过32、64、128三档64在效果和训练速度的平衡点上最好。第一层卷积用64个核是为了从原始特征空间里提取足够丰富的局部模式第二层用32个核做进一步压缩提炼。关于卷积之后是否接池化层我说一下我的判断MaxPooling1D在图像任务里很常用因为它能压缩空间尺寸、保留主要特征。但在时间序列里池化会直接抹掉短窗口内的小幅波动而污染浓度的短期尖峰恰恰是预测中最需要留意的信息。所以我在CNN和LSTM之间不插池化层让卷积输出的完整时域长度全部交给LSTM处理。3.2 训练参数与回调机制训练参数的配置决定了模型是收敛还是震荡。我的配置如下model.compile( optimizerAdam(learning_rate0.001), lossmse, metrics[mae] ) early_stop EarlyStopping( monitorval_loss, patience15, restore_best_weightsTrue ) reduce_lr ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr0.00005 ) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size64, callbacks[early_stop, reduce_lr] )学习率初始设0.001这是Adam优化器最常用的起点。我试过从0.01开始结果训练前期Loss震荡得很厉害收敛速度反而更慢。回调机制里EarlyStopping的restore_best_weights参数相当关键。如果不开启训练在验证集Loss不再下降时停止但保存的权重可能是最后一个epoch的而不是验证集Loss最低的那个epoch的最终模型精度会打折扣。ReduceLROnPlateau的作用是当验证Loss连续5个epoch不下降时把学习率减半让模型在接近最优解时能够更精细地调整参数。这个机制帮助我在后期训练中明显减少了Loss的反复波动。训练轮数设100是上限实际训练中大约在第40到60轮之间触发早停。因为数据本身有周期性规律加上Dropout抑制过拟合这个收敛速度在我的预期之内。3.3 模型结构可视化与形状核对在正式训练之前还有一步容易跳过但很值得做的事情打印模型结构核对每一层的输出形状。model.summary()summary里要重点看两个地方LSTM层的输入形状是否为(None, 72, 32)其中最后一个维度是第二层卷积的输出通道数Dense层的输出形状是否为(None, 1)。形状匹配不上主要是因为卷积步长设置不对导致序列长度发生了变化下面常见问题章节会专门展开。模型训练完成后我还习惯把训练过程的Loss曲线画出来。一个健康的训练曲线应该是训练Loss和验证Loss平缓下降两者之间的差距不大。如果训练Loss持续下降但验证Loss在某个点之后掉头向上那就是过拟合的典型信号。4. 评估指标与预测结果分析4.1 RMSE、MAE、R²到底怎么用训练完成后我用测试集做了最终评估三个核心指标的结果如下RMSE约18.9 μg/m³MAE约12.4 μg/m³R²约0.87单独看一个指标意义有限三个指标放在一起才能反映出模型的全貌。RMSE对异常值敏感它把大误差通过平方放大了。重污染时段的预测如果偏离真实值很多RMSE会迅速拉高。测试集RMSE在18.9左右意味着模型在部分时段会出现接近40到50 μg/m³的单点偏差而这些时段大概率对应的是浓度剧烈变化的污染过程。MAE是更贴近日常体验的指标12.4 μg/m³的平均绝对误差对于空气质量预测任务来说属于中等偏可用的水平。如果应用场景只需要知道大致污染级别优、良、轻度、中度这个精度足够区分出主要等级但如果要精确到每小时的浓度值做健康建议误差还是偏大。R²达到0.87含义是模型解释了测试集浓度方差的大约87%。剩余13%的方差来自模型没有捕捉到的因素。PM2.5浓度受到排放源变动、二次气溶胶生成过程等复杂因素影响这部分的随机性很难用常规气象和浓度历史数据完全解释。4.2 预测曲线怎么读才有意义光看一堆数字不够直观我习惯把测试集上连续一周的真实值和预测值画到同一张图里。从曲线上能观察到的现象很有价值在浓度平缓变化的时段预测曲线和真实曲线几乎重合这说明模型对稳态污染过程的学习效果好在浓度快速上冲或下坠的转折点预测曲线往往慢半拍也就是预测滞后现象。比如某天傍晚风力突然增强真实浓度从180快速降到80模型可能还停留在150左右。这个滞后规律在几乎所有时序预测模型里都存在。原因是模型做预测时最依赖的还是最近几个小时的浓度惯性外部气象突变的信息要等下一时刻才能反映到输入里。所以当你看到预测曲线在转折点偏差大别急着否定模型先看看转折点之外平稳时段的拟合效果。我还做了一个误差分布统计发现预测误差的分布近似正态但右尾明显偏长也就是存在少数大幅低估的样本。这部分样本集中在极重污染时段比如PM2.5超过300的情况下误差能达到80以上。这说明模型在极端情形下的外推能力有限而这恰恰也是整个领域公认的难点。5. 完整预测流程与代码复用经验5.1 加载模型做单步预测模型训练好以后model.save(pm25_cnn_lstm.h5)保存整个模型。对新数据做预测时流程是加载新数据套用之前preprocessing和归一化的流程构造最后一个窗口的输入然后调用model.predict。预测代码的骨架大致如下# 假设 df 是最近的原始观测数据已经是清洗后的DataFrame # preprocess和feature_engineering函数复用训练阶段的逻辑 data_matrix feature_engineering(df) last_window data_matrix[-lookback:] last_window_scaled scaler.transform(last_window) input_batch np.expand_dims(last_window_scaled, axis0) pred_scaled model.predict(input_batch)[0, 0] pred_value scaler.inverse_transform([[pred_scaled]])[0, 0]注意一个细节scaler是在训练阶段fit在训练集上的预测阶段只能直接transform不能再fit一次否则归一化的基准已经变了预测结果没有可比性。5.2 换数据集复用的三个坑如果想把这套代码迁移到其他城市或者其他污染物预测上直接换数据大概率会翻车需要特别检查这几点。第一是特征对齐。不同城市的气象数据字段名可能不一样单位也可能不同比如风速有的是m/s有的是km/h。加载数据以后先打印列名逐项确认单位一致再进模型。第二是时间粒度。这套代码是按小时数据定制的如果新数据是日均值窗口72就完全没有意义。日均数据需要的窗口要重新实验确定一般30到60天比较合理。第三是本地气候差异。北京的污染规律受秋冬季供暖和地形影响很大搬到全年湿度偏高、污染季节性不明显的地区模型精度会显著下降。这种情况下不要指望代码原封不动跑出相同效果建议重新做数据探索必要时调整卷积核数和LSTM单元数。6. 常见问题与调试实录6.1 训练Loss居高不下训练过程中最常碰到的就是Loss下降很慢甚至不降。我先排查数据预处理检查归一化是否漏了某一列原始数据里是否存在极大的异常值这些都会让模型无所适从。数据没有问题的话下一步看学习率把初始学习率从0.001调到0.0001多数情况下能解决Loss震荡的问题。还有一种情况是输入特征包含了完全没处理过的非数值列比如风向字符串模型无法处理就直接崩溃这个在预处理阶段就要拦截。6.2 预测曲线滞后严重前面提到滞后是标配现象但滞后严重到一定程度说明模型结构有问题。如果预测值几乎就是把上一小时的真实值平移过来说明模型没有学到特征和结果之间的因果关系。我的处理办法是强化CNN部分的特征提取——增加卷积核数量或者再堆一层卷积同时检查LSTM隐藏单元数是否偏小导致记忆容量不足。另一个有效手段是缩短预测步长先把下一小时预测准再逐步尝试多步预测。6.3 维度报错和显存不足问题Keras报错中最常见的就是形状不匹配。ValueError: Input 0 of layer conv1d is incompatible with the layer——这类问题十有八九是输入样本的最后一个维度跟卷积层的输入通道数不一致。解决方法是打印X_train.shape确认形态是(样本数, 72, 特征数)再回头核对模型第一层的输入input_shape(72, 特征数)。显存不足的报错在Windows本地环境也比较常见。一个batch_size为64的模型训练会一次性加载全部训练样本如果电脑内存只有8G很可能直接卡死。处理方法把batch_size降到32或者16同时把数据切分成块不要一次性把全部序列加载到内存里。用tf.data.Dataset做按批加载可以在很大程度上缓解内存压力。6.4 过拟合问题模型参数多、训练数据相对有限过拟合基本躲不掉。我在保留Dropout的同时一开始也用了L2正则化但实测效果一般反而拉低精度。比较有效的一个操作是减小LSTM隐藏单元数从128降到64验证Loss明显改善。另一个实用的方法是在数据层面做增强把训练数据按小时粒度做小幅度时间平移相当于扩充了样本量。6.5 训练速度过慢CPU上训练这个模型做实验确实比较煎熬。我的建议是先用小数据集跑通流程比如只用一年的数据、窗口缩到24小时确认代码没问题、指标合理再切换到完整数据训练。这样单次实验循环的时间能从半小时压缩到几分钟调试效率翻倍。常见问题速查表为了方便排查我把上面提到的高频问题整理成一张表遇到问题时直接对照查现象可能原因解决方案Loss不下降特征未归一化、有异常值检查预处理改用更小学习率预测曲线滞后CNN特征提取不足增加卷积层或核数缩短预测步长维度报错输入形状与模型不匹配打印X_train.shape核对input_shape内存溢出batch过大、数据全量加载降低batch使用tf.data.Dataset按批读取验证Loss回升模型过拟合增大Dropout率、减少LSTM单元数训练慢CPU有限、数据量大先小数据集跑通再全量训练另外记录一个自己在调整过程中使用的经验调节模型结构时一次只改一个变量不要同时调整卷积层数量和LSTM单元数。否则模型表现发生变化时你根本判断不出是哪一个改动起的作用。我后期所有实验都建立在一个简单的表格上每一行记录网络结构、关键超参数、训练Loss、验证Loss、测试集RMSE和R²。想要系统性地调优这样的实验管理习惯很重要。关于多步预测的一点扩展思考上面的代码做的是单步预测也就是用过去72小时预测未来1小时。在实际应用场景里用户往往想知道未来12小时甚至24小时的浓度走势。把单步模型改造成多步预测有几个方案可以做第一种是递归预测把预测出的下一小时浓度作为输入继续预测再下一小时这种方式误差会累积预测步长越远越不靠谱第二种是直接输出多步把模型最后一层改成输出序列比如一次输出未来12小时的浓度值这种方式需要重新设计标签结构但可以有效避免误差累积第三种是seq2seq结构用编码器LSTM读完整个窗口再用解码器LSTM逐时刻生成预测序列我在后续迭代中尝试过这条路。我个人试下来多步预测的准确率随着预测步长增加下降得很快到6小时以后模型的预测结果基本只有参考价值精确度已经不适合做决策支撑了。所以建议不要一开始就追求24小时预测先把1到3小时的短时预报做扎实再逐步扩展预测时长。实际部署时的另一个问题是模型更新频率。空气质量预测模型不建议训练一次用一年污染源结构、气象规律都在变化。我的做法是每周用最新数据做一次增量训练把最近一周的真实观测加入到训练集重新走一遍训练流程更新模型权重。这个习惯也值得所有跑时序模型的朋友参考。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询