ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于LSTM的空气质量指数预测:从数据对齐到可视化全流程

基于LSTM的空气质量指数预测:从数据对齐到可视化全流程 简介这份资源是面向高校学生与Python初学者的LSTM空气质量指数预测完整项目源码可直接用于期末大作业、课程设计或毕业设计。项目以北京空气质量历史数据为基础通过长短期记忆网络实现AQI时序预测代码注释详尽新手也能读懂并快速部署运行。压缩包共约2000个文件整体29.33MB其中1989个csv为逐日空气质量与气象数据样本4个py为核心训练与预测脚本另有xml配置、md说明文档及pyc缓存文件目录结构清晰便于按数据、模型、结果分层查阅。目前已有221人学习下载属于导师认可的高分项目。读者可获得一套可复现的完整方案包括数据预处理、LSTM建模、训练调参与预测评估流程既能直接提交作业也能在此基础上替换数据集或调整网络结构用于更深入的时序预测研究。1. 从一份期末大作业说起LSTM 做空气质量指数预测到底难在哪很多同学拿到「基于 LSTM 的空气质量指数预测」这个题目时第一反应是去搜一份 lstm模型代码复制过来改改路径就跑。结果往往卡在三个地方数据里 AQI 是逐小时的但气象因子是逐日的时间粒度对不上用model.fit训练完测试集 loss 看着很低画出来的曲线却整体滞后一天老师问「你的滑动窗口取多少、为什么」答不上来。这个标题真正要解决的不是「LSTM 怎么写」而是把一份多变量时间序列从原始 CSV 走到可解释的预测曲线中间每一步都能讲清楚理由。适合谁看正在做 Python 期末大作业、课程设计的学生以及需要快速搭一个 lstm时间序列预测python 基线、再往上加特征的工程师。下面按「数据怎么对齐 → 模型怎么搭 → 训练怎么调 → 结果怎么验」的顺序走一遍代码可以直接抄参数会说明为什么这么设。2. 空气质量数据的时间对齐与滑动窗口构造2.1 先搞清楚 AQI 预测的输入输出长什么样空气质量指数预测本质是监督学习里的回归问题。给定过去 T 个时刻的多个特征PM2.5、PM10、SO2、NO2、CO、O3 六项污染物加上温度、湿度、风速、气压预测未来第 H 个时刻的 AQI。这里 T 是输入窗口长度H 是预测步长。常见做法是 T 取 24过去一天H 取 1预测下一小时也有做 H24 预测未来一整天的但那样误差会明显放大期末作业建议先做 H1 把流程跑通。原始数据通常来自公开空气质量平台字段名可能是中文也可能是pm25、PM2.5混用。第一步不是建模是把时间列转成datetime并排序因为很多导出文件是按站点分组的直接 concat 会导致时间乱序。import pandas as pd import numpy as np # 读取原始数据parse_dates 直接把时间列转成 datetime df pd.read_csv(air_quality.csv, parse_dates[time]) # 按时间升序避免多站点拼接后乱序 df df.sort_values(time).reset_index(dropTrue) # 统一列名去掉中文和特殊符号方便后续索引 df.columns [c.strip().lower().replace(., ).replace(, ).replace(, ) for c in df.columns] # 缺失值先看比例超过 20% 的列直接丢少量缺失用前向填充 missing_ratio df.isna().mean() drop_cols missing_ratio[missing_ratio 0.2].index.tolist() df df.drop(columnsdrop_cols) df df.fillna(methodffill).fillna(methodbfill) print(df.shape, df.columns.tolist())逻辑说明parse_dates保证时间列是datetime64而不是字符串否则后面重采样会报错。列名清洗是为了让df[pm25]这种写法稳定不会因为原始表头带空格而 KeyError。缺失值处理上AQI 这类连续监测数据用前向填充比均值填充更合理因为相邻小时的相关性远高于全局均值。参数说明missing_ratio 0.2这个阈值不是固定的如果某列是核心污染物比如 PM2.5即使缺失 30% 也建议保留并单独处理而不是直接丢。2.2 滑动窗口把时序转成 LSTM 能吃的三维张量LSTM 的输入形状是(样本数, 时间步, 特征数)。原始 DataFrame 是二维的必须用滑动窗口切。假设特征列有 10 个窗口 T24那么每个样本是 24×10 的矩阵标签是第 25 时刻的 AQI。def make_windows(data, target_col, window24, horizon1): data: 二维 numpy 数组形状 (n_timesteps, n_features) target_col: AQI 在特征矩阵中的列索引 window: 输入时间步长 horizon: 预测未来第几步 X, y [], [] for i in range(len(data) - window - horizon 1): X.append(data[i:i window, :]) # 过去 window 小时的全部特征 y.append(data[i window horizon - 1, target_col]) # 未来第 horizon 步的 AQI return np.array(X), np.array(y) feature_cols [pm25, pm10, so2, no2, co, o3, temp, humidity, wind, pressure] target_idx feature_cols.index(aqi) if aqi in feature_cols else 0 values df[feature_cols].values.astype(float32) X, y make_windows(values, target_idx, window24, horizon1) print(X.shape, y.shape) # 期望 (n, 24, 10) 和 (n,)逻辑说明循环里i window horizon - 1是标签位置当 horizon1 时就是窗口后紧邻的那一个点。这样切出来的 X 和 y 严格按时间顺序不能打乱后再划分训练测试集否则会造成未来信息泄漏。参数说明window24对应过去 24 小时是 AQI 预测里最常用的基线。如果数据是逐日的window 取 7 或 14 更合适。horizon 越大任务越难期末作业建议先固定为 1。2.3 归一化必须放在划分训练集之后这是最容易踩的坑。很多人先对整个数据集做 MinMax 归一化再切训练测试导致测试集的极值信息泄漏到训练过程。正确顺序是先按时间切分再用训练集的 min/max 去变换测试集。from sklearn.preprocessing import MinMaxScaler split int(len(X) * 0.8) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:] # 对特征做归一化scaler 只在训练集上 fit scaler_X MinMaxScaler() n_train, t, f X_train.shape X_train scaler_X.fit_transform(X_train.reshape(-1, f)).reshape(n_train, t, f) X_test scaler_X.transform(X_test.reshape(-1, f)).reshape(X_test.shape[0], t, f) # 标签单独归一化方便后面反变换回真实 AQI scaler_y MinMaxScaler() y_train scaler_y.fit_transform(y_train.reshape(-1, 1)).ravel() y_test scaler_y.transform(y_test.reshape(-1, 1)).ravel()逻辑说明reshape(-1, f)是把三维张量压成二维因为 sklearn 的 scaler 只接受二维输入变换完再 reshape 回去。标签单独用一个 scaler是因为 AQI 的量纲和污染物浓度不同混在一起归一化会让反变换变复杂。参数说明MinMaxScaler 默认缩放到 [0,1]如果数据里有明显离群点可以改用RobustScaler它对异常值更稳。3. 用 Keras 搭一个可解释的 LSTM 预测网络3.1 模型结构两层 LSTM 加 Dropout 就够了期末作业不需要堆很深的网络。常见做法是两层 LSTM第一层返回完整序列给第二层第二层只返回最后一个时间步的输出再接全连接层输出一个标量。Dropout 放在 LSTM 之间和全连接之前用来抑制过拟合。from tensorflow.keras import layers, models, callbacks def build_lstm(window, n_features): model models.Sequential([ # 第一层 LSTMreturn_sequencesTrue 把序列传给下一层 layers.LSTM(64, return_sequencesTrue, input_shape(window, n_features)), layers.Dropout(0.2), # 第二层 LSTM只取最后时间步 layers.LSTM(32, return_sequencesFalse), layers.Dropout(0.2), # 全连接输出单个 AQI 值 layers.Dense(16, activationrelu), layers.Dense(1) ]) model.compile(optimizeradam, lossmse, metrics[mae]) return model model build_lstm(window24, n_featureslen(feature_cols)) model.summary()逻辑说明return_sequencesTrue是两层 LSTM 堆叠的必要条件否则第二层拿不到序列维度。最后一层Dense(1)不加激活函数因为回归任务的输出范围是连续的加了 sigmoid 会把预测限制在 [0,1]反变换后虽然能还原但训练时梯度会受影响。参数说明64 和 32 是隐藏单元数数据量在几千条时够用。如果训练集超过 10 万条可以加到 128/64。Dropout 0.2 是经验值过拟合严重时提到 0.30.4。3.2 训练时的三个关键回调早停、学习率衰减、模型保存这三个回调基本是标配。早停防止过拟合学习率衰减让后期收敛更稳模型保存保证训练中断后能恢复。callbacks_list [ callbacks.EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue), callbacks.ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-6), callbacks.ModelCheckpoint(best_lstm.h5, monitorval_loss, save_best_onlyTrue) ] history model.fit( X_train, y_train, validation_split0.2, # 从训练集里再切 20% 做验证 epochs100, batch_size32, callbackscallbacks_list, verbose1 )逻辑说明validation_split0.2是在训练集内部再切一部分做验证不碰测试集。patience10表示验证 loss 连续 10 轮不下降就停restore_best_weightsTrue保证停的时候恢复最优权重而不是停在最后一轮。参数说明batch_size32是常用起点数据量小可以降到 16数据量大可以升到 64 或 128。epochs100配合早停实际可能 3050 轮就停了。3.3 预测与反变换把归一化后的值还原成真实 AQI训练完的模型输出是归一化后的值必须用scaler_y反变换才能和真实 AQI 比较。y_pred model.predict(X_test) # 反变换回真实量纲 y_pred_inv scaler_y.inverse_transform(y_pred.reshape(-1, 1)).ravel() y_test_inv scaler_y.inverse_transform(y_test.reshape(-1, 1)).ravel() from sklearn.metrics import mean_absolute_error, mean_squared_error mae mean_absolute_error(y_test_inv, y_pred_inv) rmse np.sqrt(mean_squared_error(y_test_inv, y_pred_inv)) print(fMAE{mae:.2f}, RMSE{rmse:.2f})逻辑说明reshape(-1, 1)是因为 scaler 的inverse_transform要求二维输入。MAE 和 RMSE 是最常用的两个指标MAE 反映平均绝对误差RMSE 对大误差更敏感。参数说明AQI 的 MAE 在 1020 之间算正常如果超过 30说明模型没学到东西要回去检查数据对齐和窗口构造。4. 训练不收敛、预测滞后LSTM 调参和排错清单4.1 预测曲线整体滞后一天问题出在哪这是 LSTM 做时序预测最典型的现象。模型倾向于输出「上一个时刻的值」作为预测因为这样在 MSE 下损失最小。解决办法有三个一是加入差分特征让模型学变化量而不是绝对值二是把 loss 换成对变化更敏感的指标三是检查输入窗口是否太短。# 方案一在特征里加入 AQI 的一阶差分 df[aqi_diff] df[aqi].diff().fillna(0) feature_cols.append(aqi_diff) # 重新构造窗口后训练模型会同时看到绝对值和变化趋势逻辑说明差分特征让模型能区分「AQI 稳定在 100」和「AQI 从 50 升到 100」这两种情况前者差分接近 0后者差分明显为正。这样模型不会只学会复制上一时刻的值。参数说明差分后第一行是 NaN用 0 填充。如果数据有季节性还可以加二阶差分或 24 小时前的同期值作为特征。4.2 常见报错与对应处理报错信息原因处理方式ValueError: Input 0 is incompatible输入形状和input_shape不匹配检查X_train.shape是否为(n, window, features)loss: nan学习率过大或数据有 NaN降学习率到 1e-4检查df.isna().sum()val_loss不下降过拟合或窗口太短加 Dropout增大 window加早停预测值全相同标签归一化错误检查scaler_y是否只在训练集 fit提示loss: nan出现时先不要改模型结构优先用np.isfinite(X_train).all()确认输入里没有 inf 或 nan。4.3 窗口长度和隐藏单元怎么选这两个参数没有理论最优值只能靠验证集试。常见做法是固定隐藏单元先扫 window再固定 window扫隐藏单元。下面这段代码可以直接跑一个小网格。results [] for window in [12, 24, 48]: X, y make_windows(values, target_idx, windowwindow, horizon1) # ... 划分、归一化、训练 ... # 记录验证集 MAE results.append((window, val_mae)) # 选验证 MAE 最小的 window再在测试集上报告最终指标逻辑说明window 太小模型看不到一天内的周期变化window 太大样本数减少且引入过多历史噪声。24 小时通常是最稳的起点。参数说明隐藏单元从 32 到 128 扫一遍如果 64 和 128 的验证 MAE 差距在 5% 以内选 64因为参数少、训练快、不容易过拟合。5. 把预测结果做成可视化界面和可复现的工程结构5.1 用 Matplotlib 画出真实值与预测值对比期末作业的评分点里可视化占很大比重。一张清晰的对比图比一堆数字更有说服力。import matplotlib.pyplot as plt plt.figure(figsize(12, 5)) plt.plot(y_test_inv[:200], label真实 AQI, linewidth1.5) plt.plot(y_pred_inv[:200], label预测 AQI, linewidth1.5, linestyle--) plt.xlabel(时间步) plt.ylabel(AQI) plt.title(LSTM 空气质量指数预测对比) plt.legend() plt.tight_layout() plt.savefig(prediction.png, dpi150) plt.show()逻辑说明只画前 200 个点避免曲线太密看不清。linestyle--区分预测线dpi150保证截图清晰。参数说明如果测试集很长可以按天聚合后再画或者用滑动平均平滑曲线。5.2 工程目录结构建议一份能拿高分的作业代码组织要清楚。常见做法是按功能拆成几个文件而不是全塞在一个 notebook 里。aqi_lstm/ ├── data/ │ └── air_quality.csv ├── src/ │ ├── preprocess.py # 数据清洗、窗口构造 │ ├── model.py # LSTM 定义 │ ├── train.py # 训练入口 │ └── predict.py # 预测与可视化 ├── requirements.txt └── README.md逻辑说明preprocess.py只负责把原始 CSV 变成X_train.npy、y_train.npytrain.py只负责读这些 npy 并训练。这样换数据集时只改 preprocess模型代码不动。参数说明requirements.txt里固定版本比如tensorflow2.13.0、pandas2.0.3避免老师环境里版本不一致导致跑不起来。5.3 用 Streamlit 快速加一个预测界面如果想让作业更有展示度可以用 Streamlit 包一层输入最近 24 小时的污染物浓度输出预测 AQI。import streamlit as st import numpy as np st.title(AQI 预测演示) inputs [] for col in feature_cols: v st.number_input(f输入 {col}, value0.0) inputs.append(v) if st.button(预测): arr np.array(inputs).reshape(1, 1, -1) arr np.repeat(arr, 24, axis1) # 简化复制 24 份作为窗口 arr scaler_X.transform(arr.reshape(-1, len(feature_cols))).reshape(1, 24, -1) pred model.predict(arr) st.write(f预测 AQI{scaler_y.inverse_transform(pred)[0][0]:.1f})逻辑说明这里为了演示简化了窗口构造实际使用时应该让用户输入 24 小时的历史数据。np.repeat只是让形状对得上真实场景不要这么用。参数说明Streamlit 的number_input默认值是 0.0可以改成各特征的训练集均值让演示更合理。5.4 验证模型是否真的学到了东西最后一个技巧打乱测试集的时间顺序再算一次 MAE。如果打乱后 MAE 和原来差不多说明模型根本没利用时序信息只是记住了均值。# 打乱测试集顺序重新预测 idx np.random.permutation(len(X_test)) X_shuffled X_test[idx] y_shuffled y_test_inv[idx] y_pred_shuffled scaler_y.inverse_transform(model.predict(X_shuffled).reshape(-1, 1)).ravel() mae_shuffled mean_absolute_error(y_shuffled, y_pred_shuffled) print(f原始 MAE{mae:.2f}, 打乱后 MAE{mae_shuffled:.2f})逻辑说明如果打乱后 MAE 明显变大说明模型确实依赖时间顺序时序建模是有效的。如果两者接近就要回去检查窗口构造是不是把标签泄漏进了输入。参数说明np.random.permutation每次结果不同可以固定随机种子np.random.seed(42)保证可复现。本文还有配套的精品资源点击获取
返回列表