简介:基于长短期记忆网络(LSTM)的空气质量数据可视化分析预测项目,是面向计算机专业学生的期末大作业完整源码,适用于课程设计、毕业设计与项目实战训练。项目经导师认可并通过,评审得分99分,代码完整可运行,基础薄弱的学习者也能按注释理解并上手。压缩包中共260个文件,大小约7.03MB,涵盖Python脚本、HTML页面、SCSS与CSS样式文件、CSV空气监测数据、SQLite数据库及说明文档等,各类文件按功能分层存放,便于定位模型训练、数据可视化与结果展示模块。项目完整演示了从数据预处理、LSTM模型构建训练,到预测结果可视化呈现的整套流程,可直接用于大作业提交、答辩展示或后续扩展学习。目前已有99人学习下载,适合需要高质量参考模板的在校学生。
1. 期末大作业选 LSTM 预测空气质量,划算在哪
期末周最磨人的不是复习,而是手里握着一份从监测站导出的空气质量表格,却不知道该拿它做什么才算“像样的结课作品”。直接做描述性统计,撑不住页面;用线性回归猜 PM2.5,答辩时老师一句“非线性呢”就会卡住。空气质量预测恰好卡在性价比最高的位置:数据是公开的逐小时监测记录,任务本质是时间序列预测,尾声再配几张图,就是一个完整的故事线。LSTM 模型对这类带周期和延迟效应的序列数据贴合度高,源代码量可控,可视化分析又能撑起报告篇幅,很多“期末大作业”选它就是看中这三点:数据不用爬、模型不是调包完事、图表能证明工作量。这篇按“数据处理 → 模型 → 评估 → 答辩”展开,每一步写到能直接抄的程度。
2. 空气质量数据预处理:从时间表到 LSTM 输入张量
LSTM 吃的是有先后顺序的序列,不是散着的表格行。期末作业里最常见的翻车点不是模型写错,而是数据没有按时间排序、时间列有重复、缺失值直接 dropna 导致序列断档。空气质量监测是按小时采样的,夜间到清晨容易出现设备离线造成的空洞,所以“读进来直接训练”大概率会在 loss 上表现得莫名其妙。
2.1 时间解析与缺失值处理:监测数据的第一个坑
先确认时间列能不能被 pandas 正确识别。常见做法是parse_dates读入后立刻 sort,再设为索引,这一步决定了后面所有滑窗切片的顺序是否正确。
import pandas as pd df = pd.read_csv("air_quality.csv", parse_dates=["time"]) df = df.sort_values("time").set_index("time") print(df.info()) print(df.isnull().sum()) df["pm2_5"] = df["pm2_5"].interpolate(method="linear") df = df.dropna(subset=["pm2_5"])interpolate(method="linear")按前后观测值做线性插补,比ffill()能保留更多浓度变化细节,适合设备短时掉线。若某一列缺失超过 3%,插值意义不大,建议直接丢列并在报告里注明。做完这两步后再检查两件事:索引是否严格递增、时间步长是否均匀。若出现重复时间戳,需要先groupby(level=0).mean()聚合成单条记录,否则滑窗时同一个小时会被重复采样。
提示:监测数据里的 PM2.5 单位通常是 μg/m³,量纲对后续归一化没有影响,但写报告时一定要标单位。
2.2 训练/测试切分与归一化:别把未来信息带进训练
时间序列不能像普通回归那样随机切分。随机打乱会破坏前后相关性,更严重的是如果用全量数据做fit_transform,测试集的极大极小值已经掺进了归一化参数,等于测试时偷看了未来信息,评估出来的 RMSE 虚低,答辩时被追问必露馅。
from sklearn.preprocessing import MinMaxScaler n_train = int(len(df) * 0.7) train_df = df.iloc[:n_train] test_df = df.iloc[n_train:] sc = MinMaxScaler(feature_range=(0, 1)) train_scaled = sc.fit_transform(train_df[["pm2_5"]]) test_scaled = sc.transform(test_df[["pm2_5"]])fit_transform只用在训练集上,transform只对测试集做同样映射。如果引入温度、风速等多个特征,就把它们的列一起传进去,LSTM 输入张量的最后一个维度对应特征数,后续模型代码无需大改。这里还有一个容易被忽略的细节:MinMaxScaler对异常值不稳健,若某天监测设备爆表记录到极高浓度,归一化后正常值会被压缩到很小区间,训练反而变难。若数据长尾明显,可以先用np.log1p对浓度做对数变换再归一化,这个改进可以在报告里单独写成一个小实验。
2.3 滑窗序列构造:lookback 是期末答辩必问参数
LSTM 的输入不是单条记录,而是一段连续历史。假设用过去 72 小时预测当前小时,那么每一条训练样本形状是(72, 1),72 就是 lookback 窗口。窗口太短学不到日周期,太长又把噪声也装进记忆,72 小时是多数空气质量预测作业的稳妥起点。
import numpy as np def create_sequences(data, lookback): X, y = [], [] for i in range(lookback, len(data)): X.append(data[i - lookback:i]) y.append(data[i]) return np.array(X, dtype=np.float32), np.array(y, dtype=np.float32) X_train, y_train = create_sequences(train_scaled, lookback=72) X_test, y_test = create_sequences(test_scaled, lookback=72)data[i - lookback:i]取的是连续切片,y是对应切片之后的那个时间点数值。循环实现简单直观,数据量在几万条以内完全够用,不需要绕道TimeseriesGenerator。X_train的形状是(样本数, 72, 1),y_train是(样本数,),这个形状直接对接 Keras 的 LSTM 输入要求。
| 参数 | 示例值 | 作用与影响 |
|---|---|---|
| lookback | 72 | 控制记忆长度,覆盖 24 小时周期并留出跨天余量 |
| features | 1 | 单特征时只喂 PM2.5,多特征时传多列 |
| 训练占比 | 70% | 顺序切分,不打乱,测试集留在时间轴末端 |
窗口与预测目标的关系值得多想一层:这里做的是“未来 1 小时”单步预测。若作业要求预测未来 24 小时浓度曲线,常见做法是用预测输出递归地接回输入,但误差会逐步累积,期末展示时主动说明这个限制,比等老师点破更显专业。
3. LSTM 模型构建与训练:从门控机制到可复现的 Keras 代码
空气质量序列本身是非线性的:早晚高峰抬升、雨天下降、前一日高浓度会延续到次日上午。RNN 理论上能处理序列,但训练时梯度在长序列上容易消失。LSTM 加入门控单元,让信息可以选择性写入记忆或遗忘,这让它成为时间序列预测作业里最容易出稳定结果的初始选择。
3.1 为什么序列建模选 LSTM:记住并遗忘的边界
LSTM 的核心是单元状态 C_t,它像一条传送带横穿整个网络。遗忘门决定上一时刻的状态保留多少,输入门决定新信息写入多少,输出门决定最终输出什么。若只记住传送带概念,或只记得“它比 RNN 强”,答辩时很容易被细节问住。
遗忘门的输入是什么数据?是当前时刻输入 x_t 与上一时刻隐状态 h_{t-1} 拼接后的向量,经过 sigmoid 输出 0 到 1 的系数。这个系数与上一时刻单元状态 C_{t-1} 逐元素相乘,完成“遗忘”。对应公式为:
f_t = σ(W_f · [h_{t-1}, x_t] + b_f)
空气质量场景里,遗忘门学到的东西往往是“今天凌晨 3 点的浓度对下午 3 点的预测没那么重要”,而前一天的相同时刻反而权重更高。这就是 LSTM 处理这种带日周期数据的直觉逻辑:它不把整个历史等权看待,而是学出一套动态权重,这一点写进报告比贴网络结构图更有说服力。
3.2 两层 LSTM 模型骨架:return_sequences 与 Dropout 的位置
Keras 是期末作业里复现成本最低的选择,原因在于它不需要像 PyTorch 那样手动管理隐状态初始化,LSTM 层的循环逻辑封装在内部,刚接触深度学习的人不容易在状态重置上出错。我更推荐先跑通 Keras,若后续想改自定义损失或做多步采样,再换 PyTorch 也不迟。
from tensorflow import keras from tensorflow.keras import layers model = keras.Sequential([ layers.Input(shape=(X_train.shape[1], X_train.shape[2])), layers.LSTM(64, return_sequences=True), layers.Dropout(0.2), layers.LSTM(32), layers.Dense(1) ]) model.compile( optimizer=keras.optimizers.Adam(learning_rate=1e-3), loss="mse", metrics=["mae"] ) model.summary()第一层 LSTM 必须设return_sequences=True,否则它只输出最后一个时间步的状态,形状变成(None, 64),第二层 LSTM 拿到的就不是序列而是单点,维度直接对不上。Input里的X_train.shape[2]是特征数,单变量为 1,多变量时会自动适配。Dropout 放在两层之间而不是输出层之后,目的是让第二层看到的是被随机置零的序列特征,这比在 Dense 后加正则更能抵抗时间序列过拟合。
提示:
model.summary()会显示每层参数量,截图放进报告可以直观展示网络规模,是期末作业常见的素材。
3.3 训练参数与回调:EarlyStopping 让 100 轮变成“够了就停”
深度学习作业最容易犯的错是把epochs固定写死,比如训练 100 轮后直接取最后一个权重。模型在第 60 轮已经收敛时,后面 40 轮只是在过拟合训练集。正确做法是用回调监控val_loss,连续若干轮不降就提前停止并恢复到最佳权重。
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop = EarlyStopping( monitor="val_loss", patience=15, restore_best_weights=True ) reduce_lr = ReduceLROnPlateau( monitor="val_loss", factor=0.5, patience=5, min_lr=1e-5 ) history = model.fit( X_train, y_train, validation_split=0.2, epochs=100, batch_size=64, callbacks=[early_stop, reduce_lr], verbose=1 )validation_split=0.2是在训练集末尾再切出 20% 作为验证集,不会穿透到测试集。patience=15意味着验证损失连续 15 轮没有更优就停;ReduceLROnPlateau在连续 5 轮平台期时把学习率减半,帮助模型跳出局部极小。训练完成后注意一件事:history里保存的是归一化空间下的 loss,不能直接写进报告说“loss 是 0.003”,要反归一化后按浓度单位计算误差。
| 超参数 | 推荐值 | 调参方向 |
|---|---|---|
| 第一层 units | 64 | 数据量小时从 32 起步 |
| 第二层 units | 32 | 两层结构比单层多层更稳 |
| dropout | 0.2 | 过拟合明显时升到 0.3 |
| batch_size | 64 | 显存小就降到 32 |
| learning_rate | 1e-3 | loss 震荡时降到 5e-4 |
关于“模型越深越好”的误解这里顺便纠正:空气质量数据集通常在几万条量级,两层 LSTM 是实践中的上限。堆到三层以上参数量变大,训练集 loss 好看,测试集反而变差,期末报告里写“两层足够了”反而体现你对模型容量有判断力。
4. 可视化分析与预测评估:把模型结果变成报告的图表
模型跑完只完成一半,期末大作业的评分标准里“可视化分析”往往占大头。可视化不是把训练曲线贴一遍,而是要让老师能从图里读出三个结论:模型收敛没有、预测准不准、失效场景在哪。下面的图和指标都围绕这三点展开。
4.1 训练/验证损失曲线:过拟合要从图上看出来
损失曲线是判断训练过程是否健康的第一个证据。把 loss 和 val_loss 画在一起,如果两者同步下降后走平,说明训练充分;如果 val_loss 在某轮之后反弹上升,而 loss 还在降,就是过拟合的典型信号,此时应提高 dropout 或提前停止。
import matplotlib.pyplot as plt plt.figure(figsize=(10, 4)) plt.plot(history.history["loss"], label="train_mse") plt.plot(history.history["val_loss"], label="val_mse") plt.yscale("log") plt.xlabel("epoch") plt.ylabel("MSE loss") plt.title("训练与验证损失曲线") plt.legend() plt.tight_layout() plt.savefig("loss_curve.png", dpi=200)plt.yscale("log")是常用技巧:loss 从 0.01 降到 0.0001 时,线性坐标下后半段会被压成一条直线,对数坐标才能看出收敛细节。图片用savefig保存而不是截图,dpi 设 200 以上,放进报告才不会被老师说“看不清”。
4.2 预测值与真实值对比:评估指标怎么算、怎么写进报告
损失函数是归一化空间的 MSE,不能直接对外汇报。测试集预测结果要反归一化回原始浓度单位,再重新计算指标。注意y_test在create_sequences后是二维数组变一维数组,反归一化前需要 reshape。
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score pred_test = model.predict(X_test, verbose=0) pred_real = sc.inverse_transform(pred_test) y_real = sc.inverse_transform(y_test.reshape(-1, 1)) mae = mean_absolute_error(y_real, pred_real) rmse = mean_squared_error(y_real, pred_real, squared=False) r2 = r2_score(y_real, pred_real) print(f"MAE: {mae:.2f} μg/m³") print(f"RMSE: {rmse:.2f} μg/m³") print(f"R2: {r2:.3f}")| 指标 | 含义 | 报告里怎么解读 |
|---|---|---|
| RMSE | 误差的均方根 | 对重污染时段的大误差更敏感,取值大于 MAE |
| MAE | 平均绝对误差 | 与“平均差多少 μg/m³”直接挂钩 |
| R² | 决定系数 | 接近 1 说明模型捕捉到了大部分波动趋势 |
RMSE 大于 MAE 是正常的,因为平方运算放大了极端误差;两者差得越多,说明模型在重污染时段的偏移越明显,这正好可以作为 4.3 节的分析入口。R² 的计算在 sklearn 新版中直接返回标量值,不推荐手写公式,手写容易出现负值时解释不清楚的情况。
4.3 误差分布与重污染时段:挖出模型失效的规律
对比图谁都会画,加一张误差分布图就能拉开差距。以真实值减预测值为横轴画直方图,观察误差是否集中在 0 附近。若直方图明显左偏,说明模型系统性低估;右侧长尾则说明偶发的高浓度漏报严重。
residuals = y_real[:, 0] - pred_real[:, 0] plt.figure(figsize=(8, 4)) plt.hist(residuals, bins=30, edgecolor="white", alpha=0.8) plt.xlabel("真实值 - 预测值 (μg/m³)") plt.ylabel("样本数") plt.title("测试集预测残差分布") plt.tight_layout() plt.savefig("residual_hist.png", dpi=200)更进一步的漏斗图是把y_real按浓度分桶,比如 0-50、50-100、100 以上,分别计算每个桶的平均绝对误差。结果大概率显示:浓度越高的桶误差越大。这不是模型 bug,而是 LSTM 在训练集里见过的高浓度样本本身就少,且重污染过程的突变性更强。把这张图放进报告并解释“模型擅长日常浓度预测,重污染事件是边界场景”,比只放一张对比图显得更有分析深度。
5. 期末答辩现场:把源代码、模型和可视化讲成一个闭环
答辩演示和写代码是两种能力。代码能跑只是底线,老师更想看到的是“你知道自己在做什么、模型哪部分最脆弱、还能怎么改”。期末大作业的评分锚点往往就在最后十分钟的追问里。
5.1 老师爱问的三个追问的应答方向
“为什么用 LSTM 而不是 ARIMA”是出现频率最高的问题。应答主线是:ARIMA 是线性模型,对 PM2.5 这种受排放、气象、地形共同影响的非线性序列拟合能力有限;LSTM 通过门控机制学习长期依赖,不需要手动指定滞后阶数。如果老师追问“你怎么证明 LSTM 比 ARIMA 好”,回应方式是“我没做对比实验,但从文献和课程讨论中做了一个选型判断”。诚实承认边界,远比编造对比结果安全。
“lookback 为什么设 72”也有固定答法:24 小时是日周期,48 小时覆盖前一天同潮汐,72 小时留出跨两天的余量;可以补充一句“我还试过 168,但训练样本变少,验证损失没有改善”。这句话说明你做过敏感性分析,是实打实的加分项。
“模型什么时候预测得最差”是压力最大的追问。直接展示 4.3 的残差桶状图,说出“高浓度样本少,模型倾向于向均值回归,所以重污染时段会低估”。老师会接着问“那怎么改”,答案不必做出来,说出思路即可:对高浓度样本加权损失、把对数变换加入预处理、或改用分位数损失。
5.2 演示前值得做的三件小事
第一,把训练入口封装成命令行参数。用argparse暴露--lookback、--epochs、--batch_size三个参数,答辩现场老师问“换一个窗口试试”时,你只需要改一个数字重新跑,而不是改代码。第二,在测试集预测图上用红框标出一次重污染事件,框出模型低估区间,并截两张局部放大图,这会让分析环节从“我画了图”变成“我发现了问题”。第三,把model.load_weights的保存路径写清楚,答辩前直接加载权重出图,不要现场训练浪费五分钟。
空气质量预测类作业的答辩节奏应该是:数据怎么清洗、窗口怎么选、网络怎么搭、误差在哪、下一步怎么改。这五步讲全,源代码本身是否完美反而不那么重要。下次再面对类似的时间序列预测小项目,先做周周期观察,再看预测残差的分布,比反复堆训练轮数更能提前发现问题。
本文还有配套的精品资源,点击获取