ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python LSTM空气质量预测实战:从数据预处理到可视化

Python LSTM空气质量预测实战:从数据预处理到可视化 简介基于Python的LSTM空气质量可视化分析预测源码是一份面向计算机专业期末大作业、课程设计与毕业设计的完整项目适合正在准备大作业或需要项目实战演练的学生使用。压缩包共260个文件、大小约7.03MB核心代码包含15个Python脚本及对应的pyc编译文件前端部分由scss样式、JavaScript脚本与html页面构成方便界面调整与交互展示同时配有CSV格式的空气质量历史数据、SQLite数据库以及地图文件涵盖数据读取、预处理、模型训练、结果导出的完整链路。目前已有99人浏览学习。项目经导师指导后获评99分代码完整且可直接运行新手也能独立复现项目中按数据、模型和可视化划分模块内置当前空气质量、历史趋势分析和省份对比等多个页面并附有说明文档。学习者可以通过该案例掌握LSTM时间序列预测的建模思路以及如何将预测结果通过Web界面直观呈现对于进一步完成毕业设计或实战项目具有直接参考价值。1. 空气质量预测的期末大作业为什么LSTM比ARIMA和随机森林更稳学期末刷到Python基于LSTM模型对空气质量数据进行可视化分析预测源代码这种课设题目第一反应是找现成仓库但我不建议直接搬。老师要的不是一个能跑的脚本而是完整链路数据预处理、LSTM建模、预测、可视化、指标评价缺一环都扣分。空气质量数据本质是按小时采样的多变量时间序列ARIMA受限于线性假设随机森林没有时间顺序概念而LSTM靠门控结构把过去几十个小时的信息有选择地记住是用过去N小时推下一小时这类任务的默认方案。题目本身是稳的难点在做对细节归一化别泄露、序列别打乱、预测值别忘了反变换。源代码三百行左右按下面顺序写就能跑通答辩也能讲清楚。2. 数据预处理把小时级PM2.5记录切成LSTM需要的三维样本LSTM输入要求是(samples, time_steps, features)三维张量这是整个预处理的核心目标。别急着调模型先把CSV里的一维时间序列切成这个形状。这一步做扎实后面模型代码基本不用改。环境里需要装的是pandas、numpy、scikit-learn、tensorflow、matplotlibseaborn做热力图时会用到。2.1 数据集字段与读取UCI北京PM2.5数据的字段怎么用课程设计最常用的免费数据集是UCI机器学习库里的北京PM2.5小时记录文件名是PRSA_data_2010.1.1-2014.12.31.csv从2010年1月1日到2014年12月31日按小时采样一共43824行。原始数据是常见的pandas能直接读的CSV不需要登录、不需要申请权限特别适合期末大作业这种时间紧的场景。字段不多但每个字段的预处理方式不一样字段含义类型预处理注意year、month、day、hour时间标记int合并成datetime设为索引pm2.5PM2.5浓度(μg/m³)float开头24条是NaN要处理DEWP露点温度(℃)float数值型参与归一化TEMP气温(℃)float数值型参与归一化PRES气压(hPa)float数值型参与归一化cbwd组合风向object类别型做one-hotIws累计风速(m/s)float数值型参与归一化Is累计降雪时长(h)int数值型0占绝大多数Ir累计降雨时长(h)int数值型0占绝大多数读取代码import pandas as pd df pd.read_csv(PRSA_data_2010.1.1-2014.12.31.csv) # 原始12列里没有时间索引把年月日时合成一个datetime df[datetime] pd.to_datetime(df[[year, month, day, hour]]) df df.set_index(datetime).sort_index() print(df.shape) # (43824, 12) print(df.isnull().sum()) # 看每个字段缺失数量pm2.5会是24逻辑说明把year、month、day、hour四列合并成datetime并设为索引是因为时间序列必须保持时间顺序后续切训练集测试集也靠这个索引判断边界。sort_index保证小时记录按时间升序排列避免原始CSV里出现乱序导致窗口切片错位。isnull().sum()这一步必做后面所有缺失值处理都从它开始。如果不先看缺失情况就直接建模Keras会在计算损失时把NaN一起算进去loss曲线会莫名出现尖峰。2.2 缺失值与风向编码dropna、interpolate与one-hot的选择运行上面的isnull().sum()会看到pm2.5列有24个NaN正好是最早的24小时。这24行怎么处理网上教程各有说法我的结论是分开看数据中间的缺失用interpolate线性插值因为污染浓度是缓变过程插值比前向填充更符合物理直觉但开头这24个连续NaN直接dropna最简单43824行丢24行对训练几乎没有影响。# 最简单的处理开头24个pm2.5是NaN直接删除 df df.dropna() # 如果缺失出现在序列中间用插值更合适 # df[pm2.5] df[pm2.5].interpolate(methodlinear, limit_directionboth) # 风向是类别字段one-hot展开成4列 df pd.get_dummies(df, columns[cbwd], prefixcbwd) print(df.head())逻辑说明dropna删掉的是最早一天的数据代价可忽略。interpolate线性插值是把NaN前后有效值连成直线取中间点适合小段缺口limit_directionboth表示序列开头方向也能用后面的值来补不加这个参数头部NaN会原样保留。风向字段cbwd只有NE、NW、SE、cv四种取值属于类别特征不能用LabelEncoder编码成0、1、2、3因为LSTM会把数值大小当顺序理解NE0、NW1会被误读成风向有大小关系。用get_dummies展开成cbwd_NE、cbwd_NW、cbwd_SE、cbwd_cv四列互斥的0/1输入不会引入虚假的数值关系。2.3 归一化与时间窗构造MinMaxScaler和look_back的完整代码LSTM内部默认用tanh激活函数输出范围在-1到1之间输入过大或过小都会让梯度饱和训练半天不收敛。常见做法是把每个特征缩放到[0,1]sklearn的MinMaxScaler足够用。from sklearn.preprocessing import MinMaxScaler feature_cols [pm2.5, DEWP, TEMP, PRES, Iws, Is, Ir, cbwd_NE, cbwd_NW, cbwd_SE, cbwd_cv] scaler MinMaxScaler(feature_range(0, 1)) train_rows int(len(df) * 0.8) train_df df.iloc[:train_rows] test_df df.iloc[train_rows:] # 关键scaler只用训练集fit测试集只transform scaler.fit(train_df[feature_cols]) train_scaled scaler.transform(train_df[feature_cols]) test_scaled scaler.transform(test_df[feature_cols])逻辑说明fit在训练集上计算每个特征的min和maxtransform把数据映射到[0,1]。这里强调先按时间切分再fit是最容易翻车的一个顺序问题如果对整个表fit_transform测试集的取值范围会泄露进scaler后面所有指标都会虚高第5章会展开讲。训练测试比例用80/20是这类项目的常规选择时间序列不能随机打乱后切分必须按时间顺序取前80%训练、后20%测试否则模型会偷看未来数据答辩时被问到解释不清。构造监督学习样本的函数import numpy as np def create_dataset(data, look_back24, feature_idx0): 把二维序列切成(样本数, look_back, 特征数)的三维数组。 feature_idx0 表示预测目标取第0个特征也就是pm2.5。 X, y [], [] for i in range(len(data) - look_back): X.append(data[i:i look_back]) # 过去24小时全部特征 y.append(data[i look_back, feature_idx]) # 下一小时的pm2.5 return np.array(X), np.array(y) X_train, y_train create_dataset(train_scaled) X_test, y_test create_dataset(test_scaled) print(X_train.shape) # (样本数, 24, 11)11是特征数 print(y_train.shape) # (样本数,)参数说明look_back是时间步长24代表用过去24小时预测下一小时刚好覆盖一个完整日循环是这类项目的起点。feature_idx指定从多特征里取哪一列做预测目标feature_cols里pm2.5排在第0位所以传0。窗口滑动的逻辑很简单第i个样本的X是data[i:i24]对应y是data[i24]的pm2.5值。这里生成的X已经满足LSTM的输入要求不需要再reshape。提示测试集的前24小时会被消耗成第一个窗口的上下文所以X_test的实际预测范围比test_df少了24小时PPT里写测试时间段时要记得扣掉这24小时。3. 搭建LSTM预测模型网络结构、训练参数与损失曲线可视化数据已经是三维张量剩下就是定义网络。Keras的LSTM层可以直接吃(样本数, 时间步, 特征数)输入。搭建LSTM神经网络并不复杂真正的分水岭在网络结构、训练参数和回调函数的配合上。3.1 网络结构怎么定两层LSTM加Dropout为什么是默认配置用Python做LSTM时间序列预测的标准姿势是两层LSTM堆叠加Dropout。第一层return_sequencesTrue是为了给第二层输出完整序列最后一层用return_sequencesFalse只保留最后一个时间步的输出再接一个Dense层得到预测值。这种结构不是玄学一层LSTM的拟合能力有限两层可以提取更高层的时序特征Dropout防止过拟合代价是训练时间稍长。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dropout, Dense look_back 24 n_features X_train.shape[2] # 11 model Sequential([ LSTM(64, return_sequencesTrue, input_shape(look_back, n_features)), Dropout(0.2), LSTM(32, return_sequencesFalse), Dropout(0.2), Dense(1) # 输出下一小时的pm2.5 ]) model.compile(optimizeradam, lossmse, metrics[mae]) model.summary()逻辑说明64→32是信息压缩方向不是层数越多越好单元数超过数据量需要的范围会过拟合。input_shape只需要传look_back和n_featuresbatchsize在fit时传入。loss用mse是因为回归任务里大误差会被平方放大训练时模型会更在意偏离大的样本如果数据里有明显离群点也可以换mae它更抗噪。metric用mae答辩时可以直接看这两个指标判断训练是否健康。参数范围参考参数推荐区间我的习惯备注look_back12~7224覆盖一个完整日循环LSTM units32~12864/32数据量小就不要堆太多Dropout0.1~0.30.2太大了欠拟合batch_size32~12864训练速度和稳定性折中epochs上限50~200100配合EarlyStopping用3.2 训练参数与回调EarlyStopping怎么当后悔药用模型训练最怕两件事训练集loss降了但测试集一塌糊涂过拟合以及训练到一半lr太大loss震荡。EarlyStopping和ReduceLROnPlateau就是为了解决这两个问题。另一个关键参数是shuffle时间序列样本之间我一般设shuffleFalse让每个epoch按时间顺序过一遍避免模型在epoch内部随机跳变验证更稳定。from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) reduce_lr ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr1e-5 ) history model.fit( X_train, y_train, validation_split0.1, epochs100, batch_size64, shuffleFalse, # 时间序列不打乱 callbacks[early_stop, reduce_lr], verbose1 )逻辑说明validation_split0.1在Keras里取训练数组的最后10%做验证集因为数据是时间排序的这10%就是时间上最近的一批作为验证是合理的。如果想严格管控也可以手动切出验证集后用validation_data参数传入。EarlyStopping监控val_loss连续10个epoch没提升就停止restore_best_weightsTrue会把权重回滚到val_loss最低的那个epoch这是防止过拟合的后悔药避免最后几个epoch把好模型练坏。ReduceLROnPlateau在val_loss连续5个epoch不动时把学习率减半min_lr防止学习率减到0。3.3 训练过程可视化损失曲线怎么看、中文乱码怎么解训练结束后第一件事不是看指标而是画损失曲线。损失曲线能直接告诉你训练是否健康两条线同时下降并靠拢是正常收敛训练loss继续降但val_loss掉头向上是过拟合两条线震荡不降则是学习率太大或数据没归一化。import matplotlib.pyplot as plt # 先解决中文乱码SimHei没有就用系统自带黑体 plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False # 负号正常显示 plt.figure(figsize(10, 4)) plt.plot(history.history[loss], label训练集MSE, linewidth1.5) plt.plot(history.history[val_loss], label验证集MSE, linewidth1.5) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.title(LSTM训练与验证损失曲线) plt.tight_layout() plt.savefig(loss_curve.png, dpi150) plt.show()逻辑说明rcParams里设置font.sans-serif是解决中文乱码的标准做法axes.unicode_minus设为False是解决负号显示成方框的问题两行代码在整个脚本开头写一次就够。savefig把图存下来答辩PPT直接用文件不要在答辩现场现跑。损失曲线属于过程性证据能证明模型是正常训练出来的而不是随便load了一个pretrain权重老师很吃这一套。4. 预测与可视化反归一化、评价指标与三张必出图模型训练完最常翻车的其实不是训练而是出图前那几步反归一化做错整张图和所有指标都是错的。这一步值得单独说清楚。4.1 反归一化的正确写法inverse_transform为什么不能直接作用于单列常见报错是scaler.inverse_transform(y_pred)直接报shape mismatch因为scaler是基于11列特征fit的输入必须也是11列。正确的做法是构造一个和原始特征同形状的零矩阵把预测值塞进pm2.5那一列再反变换最后只取这一列出来。y_pred model.predict(X_test) y_true y_test.reshape(-1, 1) # 真实值也要reshape成列向量 feature_idx 0 # pm2.5在feature_cols中的下标 def inverse_scale_column(values, scaler, feature_idx, n_features): 把单列的归一化值还原成原始量纲。 dummy np.zeros((len(values), n_features)) dummy[:, feature_idx] values.flatten() return scaler.inverse_transform(dummy)[:, feature_idx] pred_full inverse_scale_column(y_pred, scaler, feature_idx, n_features) true_full inverse_scale_column(y_true, scaler, feature_idx, n_features) print(pred_full[:5]) # 单位是 μg/m³能看懂的实际浓度逻辑说明scaler在fit时记住的是11列各自的min和max反变换必须按同样的列数喂回去。dummy数组里其他列全填0不影响我们只取第0列结果。这也是为什么前面强调feature_cols顺序要固定顺序一变feature_idx指向的值就完全错了。反归一化之后数值才有物理意义RMSE才有单位图表坐标轴才能标μg/m³。4.2 评价指标计算RMSE、MAE、MAPE与R²哪些能进答辩PPT评价指标这一块是答辩提问的重灾区。四个指标最好都算一遍但含义要能讲清楚。from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score rmse np.sqrt(mean_squared_error(true_full, pred_full)) mae mean_absolute_error(true_full, pred_full) mape np.mean(np.abs((true_full - pred_full) / (true_full 1e-6))) * 100 r2 r2_score(true_full, pred_full) print(fRMSE: {rmse:.2f} μg/m³) print(fMAE: {mae:.2f} μg/m³) print(fMAPE: {mape:.2f}%) print(fR²: {r2:.3f})逻辑说明mape的分子分母里加1e-6是为了防止真实值是0时除零报错这是个小细节但很容易被忽略。要注意的是pm2.5在夏季晴朗天气会接近0此时MAPE会异常大所以答辩时如果被问到为什么MAPE这么高可以解释为接近0的样本放大了百分比误差必要时剔除真实值小于15μg/m³的样本再算一次。指标项目里的含义多少算合理RMSE大误差被放大体现最坏情况城市小时数据通常在30~60μg/m³MAE平均绝对误差和RMSE接近说明没有极端离群预测MAPE误差占真实值的百分比30%以下大致可接受注意接近0的样本R²模型解释了多少方差0.7以上说明趋势抓得住0.9以上很理想4.3 三张可视化图对比折线、散点拟合与误差分布数据可视化这一节是期末大作业的门面图表质量直接影响第一印象。至少出三张图预测对比折线图、散点拟合图、误差分布直方图。# 第一张真实与预测对比折线 plt.figure(figsize(12, 4)) x_axis np.arange(len(true_full)) plt.plot(x_axis, true_full, label真实PM2.5, linewidth1) plt.plot(x_axis, pred_full, labelLSTM预测, linewidth1, alpha0.8) plt.xlabel(测试集小时序号) plt.ylabel(PM2.5 (μg/m³)) plt.legend() plt.title(测试集真实值与预测值对比) plt.tight_layout() plt.savefig(pred_curve.png, dpi150) plt.show() # 第二张散点拟合图点越贴yx线越准 plt.figure(figsize(5, 5)) plt.scatter(true_full, pred_full, s8, alpha0.4) plt.plot([0, 300], [0, 300], r--, linewidth1) # yx参考线 plt.xlabel(真实PM2.5 (μg/m³)) plt.ylabel(预测PM2.5 (μg/m³)) plt.title(预测-真实散点图) plt.tight_layout() plt.savefig(scatter.png, dpi150) plt.show()逻辑说明折线图最直观能看出预测是否跟住了真实趋势。预期会看到预测曲线比真实曲线平滑且略有滞后这是LSTM单步预测的正常表现不是bug。散点图的参考线用红色虚线标出yx点群整体贴着线说明模型校准好如果点群整体在线的上方或下方说明存在系统性偏差。第三张误差分布直方图可以用plt.hist(pred_full - true_full, bins50)画看误差是否集中在0附近有没有明显的长尾长尾说明存在个别预测差得离谱的小时往往是极端污染事件。5. 期末大作业避坑指南5个让LSTM预测翻车的常见问题这一章写的是我自己踩过的坑有些光看指标根本发现不了等答辩被老师一句话问穿才反应过来。每条按现象、原因、解决三个层次写直接对标你可能的翻车现场。5.1 数据泄露scaler在整表上fit_transform测试集结果虚高现象训练还没结束val_loss已经很低测试集RMSE出奇地好图表漂亮到不真实答辩时数据一复盘就露馅。原因对全量数据做fit_transform时scaler的min和max已经看过测试集。虽然数值上只是挪了个尺度测试集取值范围信息确实混进了训练流程这在课程设计里属于数据泄露严格评审可以直接判不合格。解决把fit限制在训练集测试集只用transform也就是第2章代码里的写法。自查方法是搜索代码里fit_transform出现的位置如果它在train_test_split之前说明踩坑了。5.2 预测曲线整体滞后一拍看起来像把真值右移了现象折线图上预测值和真实值形状几乎一致但整体右移了一个小时左右峰值和谷值都慢半拍。原因单步预测模型学到的最强规律其实是下一个小时和当前小时差别不大所以它输出的是一个近似上一时刻的值。这是单步预测的结构性特征不是模型写错了也不是数据问题。解决把look_back从24加到48或72给模型更多上下文或者把预测目标改成预测未来第3小时来错开相关性最高的时段。答辩时主动解释这个现象比等老师问要体面得多。5.3 递推多步预测时误差滚雪球预测结果趋向均值现象用预测值喂回模型滚动预测未来24小时前3个小时还行第5个小时开始走平最后变成一条水平线数值趋近训练集均值。原因递归预测把上一步的误差当成下一步的输入误差累积到一定程度后模型倾向输出最安全的中间值。这是递归多步预测的通病不单是LSTM的问题。解决做直接多步预测把输出层从Dense(1)改成Dense(24)一次输出未来24个小时这样每一步都是独立输出不会滚雪球。期末项目建议直接多步改动小、答辩好解释。5.4 matplotlib中文乱码标题和坐标轴变成方块现象plt.title(真实与预测对比)出来的是四个方框负号显示成奇怪的方块。原因matplotlib默认字体不包含中文字形SimHei没安装或者没指定。系统是Windows还是Linux可用的中文字体名不一样。解决在脚本开头统一设置rcParams字体列表按优先级写多个候选plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, WenQuanYi Zen Hei] plt.rcParams[axes.unicode_minus] False如果跑代码的机器确实没有中文字体就不要逞强图上的标签改用英文比如Predicted vs Actual PM2.5代码可移植性反而更好。5.5 训练结果每次不一样随机种子没设导致没法复现现象同一份源代码上午跑RMSE是45下午跑变成41论文里没法写复现结果这几个字。原因权重初始化、Dropout、GPU算子都有随机性每次运行得到的模型不完全一样。解决在脚本最前面固定随机种子import os import random import numpy as np import tensorflow as tf os.environ[PYTHONHASHSEED] 0 random.seed(42) np.random.seed(42) tf.random.set_seed(42)说明设了随机种子依然不能保证跨GPU完全一致但至少同一台机器上可以稳定复现报告里写明固定随机种子为42是答辩的加分细节。6. 答辩加分技巧用ECharts做交互式可视化并完成三项验证matplotlib的静态图适合放PPT但答辩现场真正能撑场面的是ECharts数据可视化评委可以拖动缩放、悬停看具体数值、切换显示真实值或预测值。把Python算好的结果导成JSON再用本地HTML页面渲染半小时就能搞定。6.1 导出预测结果并用ECharts渲染交互折线图先把预测结果写成一个结构简单的JSON文件import json chart_data { actual: [round(x, 1) for x in true_full.tolist()], pred: [round(x, 1) for x in pred_full.tolist()] } with open(prediction.json, w, encodingutf-8) as f: json.dump(chart_data, f, ensure_asciiFalse)然后写一个HTML页面引入本地下载好的echarts.min.js通过fetch读取JSON后绘图!DOCTYPE html html langzh-CN head meta charsetUTF-8 titlePM2.5预测对比/title script srcecharts.min.js/script /head body div idchart stylewidth:1000px;height:450px;/div script fetch(prediction.json) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(chart)); chart.setOption({ title: { text: LSTM PM2.5预测结果展示 }, tooltip: { trigger: axis }, legend: { data: [真实值, 预测值] }, xAxis: { type: category, data: data.actual.map((_, i) i) }, yAxis: { type: value, name: μg/m³ }, dataZoom: [{ type: inside }], series: [ { name: 真实值, type: line, data: data.actual, showSymbol: false }, { name: 预测值, type: line, data: data.pred, showSymbol: false } ] }); }); /script /body /html注意浏览器直接双击打开HTML文件时fetch请求本地JSON会被同源策略拦掉。解决方式是进入项目目录后用python -m http.server 8000起一个本地静态服务再访问http://localhost:8000打开页面。这个细节容易被忽略答辩前一定要自己先开一遍。6.2 答辩前必做的三项验证第一固定随机种子后完整重跑一遍确认RMSE和图表能复现答辩时万一有老师让现场演示也不心虚。第二核对测试集的时间范围算出X_test实际覆盖的起止时间PPT里写清楚用2010到2013年数据训练2014年数据测试这类表述时间范围要扣除look_back消耗的24小时。第三把PPT里放的每个指标数字和图对上号答辩常见的连环问就是你PPT上的RMSE是哪张图算出来的现场答不上来就是你报告有水分。我自己的习惯是把data、figures、models三个目录固定下来每次跑完把loss曲线、RMSE和特征列顺序记在一个markdown文件里答辩前统一核对一遍顺便把随机种子、look_back这些关键参数写清楚。这套流程多做几次就变成肌肉记忆了希望帮到你。本文还有配套的精品资源点击获取
返回列表