
简介面向需要完成期末大作业、课程设计或希望掌握时间序列预测实战的学生这份基于LSTM的空气质量可视化分析源码提供了完整可运行的Python项目。项目以PM2.5等监测数据为对象融合数据清洗、模型训练与多页面可视化展示配有前端看板与SQLite存储能直接复现结果也便于在此基础上做参数调整与扩展。包内共260个文件约7.03MB以15个Python源码脚本和编译后的pyc文件为核心搭配HTML、SCSS、CSS、JS等前端资源以及CSV数据与SQLite数据库目录结构清晰适合作为结构完整的参考项目。目前已有99人学习下载。该项目是经导师指导的高分设计项目代码评审为99分对正在做毕业设计或期末作业的计算机专业学生具有较强的参考价值。1. 从“期末大作业”到能上答辩的LSTM空气质量预测每到学期末后台都会冒出一批搜索“空气质量 LSTM 预测源代码”的人光看标题就知道是课程设计或者期末大作业。这个任务几乎可以称得上“时间序列预测 深度学习入门”的标准标本数据好拿、问题直观、可视化展示点多做完还能顺手解释清一整套技术链路。完整做下来这个项目要完成四件事读取历史空气质量与气象数据做清洗和归一化切出带时间步长的样本把数据喂进 LSTM 模型训练最后把预测结果和真实值画出来最好还能做成网页图表。从评分角度看代码能不能跑通只占一半另一半看你是否讲得清“为什么用 LSTM、哪些参数影响结果、结果怎么验证”。照着本文思路做拿到一套能答辩、能二次开发的完整落地方案问题不大。2. 为什么用LSTM预测空气质量选型逻辑与时序数据准备2.1 空气质量数据为什么是 LSTM 的“主场”先想清楚一件事预测目标是什么。常见做法是预测未来 124 小时的 PM2.5 浓度、AQI 指数偶尔加上臭氧或 NO₂。这类数据天然是时间序列当前时刻的空气质量和几小时之前的气象条件强相关冷空气过境、早晚高峰、降水过程都会在时序上留下明显规律。传统回归模型把每个时刻当独立的样本丢掉了“先后顺序”处理不了这种时间依赖。LSTM长短期记忆网络属于循环神经网络的一个变体核心贡献是引入了三个门结构——输入门、遗忘门、输出门让网络有能力决定哪些历史信息要保留、哪些要丢弃。普通 RNN 面临梯度消失问题学到第 20 个时间步时早就忘了第 5 步发生了什么而 LSTM 能做到把两三天前的污染过程“记牢”。这正是空气质量数据的刚需一次重污染过程往往有 2448 小时的持续时间模型必须抓住这种中期记忆。另一个常被提到的方案是 ARIMA。ARIMA 在平稳序列上表现不错但对非线性和多维特征的处理明显吃力。你可以给 LSTM 喂的绝不只一个污染物浓度还能把温度、湿度、风速、气压都接进去。ARIMA 想加入外生变量需要退化成 ARIMAX实际建模效果很多场景下不如 LSTM 灵活。所以课程设计选 LSTM既是因为它适合这个数据也是因为它比传统时序模型的“故事”更完整——论文、答辩PPT里都好展开。2.2 拿到 csv 之后先做这几件事清洗、滑窗、归一化空气质量数据最常见的问题是缺失值。监测站停电、设备维护、网络传输故障都会导致某几个小时没有记录。一般在项目里我用两种策略结合数量少的连续缺失用前后线性插值超过连续 6 小时的大段缺失直接剔除所在日期。如果放任不管LSTM 在训练时会把“空洞”当成正常波动预测曲线的形状会明显变形。处理完缺失还有异常值问题。有时候监测站会记录出 PM2.5 浓度 999 μg/m³ 这种极端数值这基本是传感器故障不是真实污染过程。我一般用滚动窗口的 z-score 来做异常检测超过 3 个标准差且维持超过 2 个时刻的值视为异常替换成窗口内中位数。这一步不做模型会被极少数样本带偏LSTM 的损失曲线在训练后期会出现毫无规律的尖峰。import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler df pd.read_csv(air_quality.csv, parse_dates[time]) df df.set_index(time).sort_index() # 1. 缺失值处理小于等于6小时的连续缺失线性插值 df df.interpolate(methodlinear, limit6, limit_directionboth) # 2. 异常值处理滚动窗口 z-score for col in [pm25, pm10, no2]: mean df[col].rolling(24, min_periods1).mean() std df[col].rolling(24, min_periods1).std() z (df[col] - mean).abs() / std df.loc[z 3, col] np.nan df[col] df[col].interpolate(methodlinear, limit6) # 3. 归一化所有特征缩放到 0~1避免 PM2.5 和温度量纲差异过大 feature_cols [pm25, pm10, no2, temp, humidity, wind_speed] scaler MinMaxScaler() scaled_data scaler.fit_transform(df[feature_cols])这段代码有三个关键点要说明。interpolate的limit6是控制允许连续填充的最大缺失点数超过这个数就保留 NaN后面训练时统一丢弃。做异常值检测时我特意用rolling(24, min_periods1)而不是全局均值和标准差因为空气质量有明显日周期早晚高峰的 PM2.5 本来就比午间高用全局统计会把正常的早晚高峰误判成异常。最后所有特征统一走 MinMaxScaler这是 LSTM 训练的老规矩特征量纲不一致时梯度更新会被大数值特征主导模型收敛速度和稳定性都会出问题。2.3 把时序转成监督学习样本look_back 窗口怎么选LSTM 不能直接吃一维序列它期望的输入形状是(样本数, 时间步长, 特征数)。时间步长就是常说的look_back表示“用过去几个时刻预测未来”。这个参数是整篇作业里第一个需要你拍板的关键参数。look_back 太小模型看不到完整的污染积累过程太大样本数量骤减训练效率下降还容易把噪声也学进来。挑选方式我遵循一条经验规则先做自相关分析计算 PM2.5 序列的自相关系数找到系数开始衰减到 0 附近的滞后阶数以此为基准选择 look_back。大气污染物的自相关通常能维持 1224 小时所以中等规模数据集上我一般取 12 或 24。如果数据集记录间隔是 1 小时look_back24 就是“用过去一天预测下个小时”物理含义清晰答辩时也说得出口。import torch from torch.utils.data import Dataset, DataLoader def create_sequences(data, look_back24, forecast_horizon1): X, y [], [] for i in range(len(data) - look_back - forecast_horizon): X.append(data[i:i look_back]) y.append(data[i look_back:i look_back forecast_horizon, 0]) # 只预测 pm25 return np.array(X), np.array(y) look_back 24 X, y create_sequences(scaled_data, look_backlook_back, forecast_horizon1) print(fX shape: {X.shape}, y shape: {y.shape}) # 输出示例X shape: (N, 24, 6)代表 N 个样本每个样本含24个历史时刻、6个特征这里X的三维形状是理解整个 LSTM 工程的关键。data[i:i look_back]抽取连续 24 行每行 6 列特征这就是一个完整样本y只取下一时刻的 PM2.5 数值第 0 列因此是二维的。数据集总量不变但监督学习样本数是总时长 - 24 - 1。操作上要注意最后几十个样本的划分必须严格按时间先后不能像图像分类那样乱序洗牌后训练——时间序列一旦泄露未来信息验证集分数会虚高答辩现场一追问就露馅。这个隐患我在第 5 章会单独说排错方法。3. 搭建LSTM预测模型网络结构、关键参数与训练脚本3.1 用 PyTorch 搭一个不寒碜的两层 LSTM模型结构的选择直接对应最终分数。只用一层 LSTM 加一个全连接层属于“能跑但没深度”的水平画网络结构图时也撑不起一页 PPT。常见的成熟做法是两层 LSTM 堆叠中间加 Dropout再接全连接层输出预测值。两层结构可以理解为第一层捕捉短期波动模式第二层把第一层的隐状态序列再抽象一层抽取更长周期的依赖。再加一层会让参数量暴涨训练时间翻倍对几百兆的空气质量数据集来说收益有限。import torch.nn as nn class AirLSTM(nn.Module): def __init__(self, input_size6, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout ) self.regressor nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, 1) ) def forward(self, x): # x: (batch, seq_len, input_size) lstm_out, _ self.lstm(x) # 取最后一个时间步的隐状态 last_hidden lstm_out[:, -1, :] return self.regressor(last_hidden) model AirLSTM(input_size6, hidden_size64, num_layers2, dropout0.2) print(model)注意batch_firstTrue这个参数。PyTorch 的 LSTM 默认输入是(seq_len, batch, input_size)初学者最容易在这里翻车少写这个参数的话数据维度对不上训练直接报错。lstm_out[:, -1, :]取最后一个时间步的输出作为整条序列的压缩表示这是常见的做法有些实现会用nn.LSTM返回的(h_n, c_n)里的最后一层隐状态效果上差别不大但取最后一个时间步的输出理解起来更直观画前向传播图也方便。两个 LSTM 层之间由 PyTorch 自动衔接第二层接收第一层每个时间步的输出dropout0.2只在层间生效。如果要写进论文或实验报告建议把hidden_size当作一个可调的实验变量分别跑 32、64、128 三种配置对比验证集 RMSE。课程设计的评分往往看“你是否做了对比实验”哪怕只多这组数据报告的含金量就不一样。3.2 训练时真正决定成败的五个参数模型结构定了真正影响结果的反而是训练配置。我见过太多人固定写死epochs50, batch_size32跑完就提交训练曲线根本不收敛。下面这段是我实际项目中会用到的训练脚本核心部分每个参数都有它的存在理由。import torch.optim as optim from torch.utils.data import TensorDataset, DataLoader X_tensor torch.tensor(X, dtypetorch.float32) y_tensor torch.tensor(y, dtypetorch.float32) # 严格按时间顺序切分前80%训练后20%验证不做随机打乱 split_idx int(len(X_tensor) * 0.8) train_dataset TensorDataset(X_tensor[:split_idx], y_tensor[:split_idx]) val_dataset TensorDataset(X_tensor[split_idx:], y_tensor[split_idx:]) train_loader DataLoader(train_dataset, batch_size64, shuffleFalse) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse) optimizer optim.Adam(model.parameters(), lr0.001) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, factor0.5, patience3) criterion nn.MSELoss() best_val_loss float(inf) for epoch in range(50): model.train() train_loss 0.0 for x_batch, y_batch in train_loader: optimizer.zero_grad() pred model(x_batch).squeeze() loss criterion(pred, y_batch) loss.backward() optimizer.step() train_loss loss.item() * len(x_batch) model.eval() val_loss 0.0 with torch.no_grad(): for x_batch, y_batch in val_loader: pred model(x_batch).squeeze() val_loss criterion(pred, y_batch).item() * len(x_batch) val_loss / len(val_dataset) scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_lstm.pt) if epoch % 5 0: print(fepoch {epoch:02d} | train_loss {train_loss/len(train_dataset):.4f} | val_loss {val_loss:.4f})这里最关键的是shuffleFalse。时间序列训练样本之间不是独立同分布的随机打乱等同于让模型从“未来”偷看“过去”验证集评估结果虚高。这在课程设计答辩中是最容易被老师一句话问到哑口的点。ReduceLROnPlateau用验证损失作为监控指标连续 3 轮没有改善就把学习率减半防止 loss 卡在局部极小值附近震荡。best_val_loss机制保留验证集上表现最好的那一次权重避免最后一轮过拟合后保存了坏模型。训练完直接用model.load_state_dict(torch.load(best_lstm.pt))恢复最优权重再去测试集上评估。学习率初始值我一般从 0.001 起步。如果训练曲线前 10 轮几乎不变先看数据归一化有没有做规范再考虑调大学习率到 0.003如果 loss 直接炸成 NaN那就是学习率太大降到 0.0005 重新跑。batch_size 在空气质量这种中小规模数据上选 32 或 64 都可以太大的 batch比如 256在单机上反而会拉长单轮时间对最终精度帮助甚微。3.3 反归一化预测完之后最容易漏的一步训练、验证都在 01 的归一化空间里进行但最后要展示和计算误差必须把预测结果还原成真实的 μg/m³ 数值。很多人只在训练前做了标准化预测完忘了反向处理画出来的曲线和真实值在数值上完全对不上误差动不动几百还以为模型崩了。# 取 scaler 中 pm25 列对应的最小值、最大值 pm25_min scaler.data_min_[0] pm25_max scaler.data_max_[0] def inverse_scale(arr): # 还原到原始 PM2.5 量纲 return arr * (pm25_max - pm25_min) pm25_min pred_original inverse_scale(pred.cpu().numpy()) y_original inverse_scale(y_tensor[split_idx:].numpy())注意一个细节scaler.data_min_和scaler.data_max_是一维数组顺序对应fit_transform时的特征列顺序。空气质量数据的第一个特征列是 PM2.5所以取[0]。如果你把特征列顺序换过这里必须同步换不然还原出来的数值完全错位。多特征预测时每个特征都要单独反归一化但预测目标一般只有一个所以重点处理目标列即可。4. 用可视化把预测结果讲清楚从 matplotlib 到网页图表4.1 四张图让老师三分钟看懂你的模型可视化不是给机器看的是给人看的。课程设计的评分场景里评审老师不会去读你的训练日志他看的就是图。我通常强制自己产四张图训练损失曲线、验证集预测 vs 真实值对比、残差分布直方图、PM2.5 与各气象特征的相关性热力图。这四张图分别回答“模型收敛了吗”“预测得准不准”“误差是什么形态”“输入特征有没有道理”。import matplotlib.pyplot as plt # 第一张训练/验证损失曲线 plt.figure(figsize(10, 4)) plt.plot(train_losses, labeltrain_loss) plt.plot(val_losses, labelval_loss) plt.xlabel(epoch) plt.ylabel(MSE) plt.legend() plt.grid(alpha0.3) plt.savefig(loss_curve.png, dpi150) # 第二张验证集最近200个点的预测对比 plt.figure(figsize(12, 4)) plt.plot(y_original[-200:], labeltrue, linewidth1.5) plt.plot(pred_original[-200:], labelpred, linewidth1.5, alpha0.8) plt.xlabel(hour) plt.ylabel(PM2.5 (μg/m³)) plt.legend() plt.grid(alpha0.3) plt.savefig(pred_vs_true.png, dpi150)loss 曲线必须呈现整体下降、验证损失和训练损失差距不大的形态。如果验证损失在某个 epoch 后掉头上升而训练损失继续下降就是过拟合要把 dropout 调大或加权重衰减。预测对比曲线注意别画全量几千个点密密麻麻根本看不出细节截取最近 200 个点反而信息密度最高。理想形态是两条线趋势同步、峰值位置不偏移太多。残差分布是验证模型有没有系统性偏差的窗口残差应该围绕 0 对称分布如果残差整体为正说明模型系统性地低估了 PM2.5需要检查是不是训练数据里高浓度样本比例不足。相关性热力图用seaborn.heatmap(df.corr())直接画能直观展示 PM2.5 与湿度、风速的关系。冬季静稳天气下 PM2.5 和风速往往显著负相关这张图放在报告“特征分析”章节特别加分。4.2 把结果做成网页图表ECharts 可视化方案的接法如果作业要求允许展示一个简单的前端页面用 ECharts 做可视化大屏是最常见的选择。Python 后端处理好数据后用 Flask 返回 JSON 格式的预测结果前端 ECharts 负责渲染曲线图。这样整个项目的技术栈更完整从数据处理到模型推理再到前端展示全链路跑通答辩时的演示效果比贴 matplotlib 图高出一个档次。from flask import Flask, jsonify import json app Flask(__name__) app.route(/api/prediction) def prediction(): # pred_original 和对应时间戳在模型推理时已经算好 timestamps [ts.strftime(%Y-%m-%d %H:%M) for ts in df.index[split_idx:]] payload { times: timestamps[-200:], true: y_original[-200:].tolist(), pred: pred_original[-200:].tolist() } return jsonify(payload) if __name__ __main__: app.run(host0.0.0.0, port5000)前端页面只需要一个 div 容器和一段 ECharts 配置。用fetch(/api/prediction)拉取接口数据配置series里的两条 line 分别对应真实值和预测值。ECharts 默认的 tooltip 联动、dataZoom 缩放功能零成本交付比 matplotlib 交互体验好太多。!DOCTYPE html html head meta charsetutf-8 title空气质量 LSTM 预测/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body div idchart stylewidth: 100%; height: 500px;/div script fetch(/api/prediction) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(chart)); chart.setOption({ tooltip: { trigger: axis }, legend: { data: [真实值, 预测值] }, xAxis: { type: category, data: data.times }, yAxis: { type: value, name: PM2.5 (μg/m³) }, dataZoom: [{ type: inside }], series: [ { name: 真实值, type: line, data: data.true, smooth: true }, { name: 预测值, type: line, data: data.pred, smooth: true } ] }); }); /script /body /html这里要注意 ECharts 的 CDN 地址用的是公共资源加载方式如果答辩现场的机器没联网页面会白屏。保险做法是提前把echarts.min.js文件下载到本地用相对路径引用。另外接口返回的数组要控制长度一次传几千个点浏览器渲染没问题但会卡顿建议只传最近 200500 个点既流畅又突出重点。5. LSTM空气质量预测的常见问题与排查记录5.1 训练了 30 轮loss 纹丝不动现象训练损失从第一轮开始就稳定在 0.08 左右之后几十轮几乎一条水平线。验证损失也差不多但预测曲线基本是一条平直线。原因排查顺序先看归一化是否生效。如果原始数据里有 NaN 没清干净传给模型后 loss 计算会出问题。再看学习率0.001 对这个小模型通常合适但如果你的输入特征维度特别多比如接了 20 个气象特征梯度更新可能过慢把学习率调到 0.005 再试。最后检查代码里是否忘记调用optimizer.zero_grad()梯度会跨 batch 累积导致参数更新路径混乱loss 表现为忽高忽低的噪声。解决前两种情况按上述操作处理即可。第三种情况在循环开头加optimizer.zero_grad()。这个坑每届都有人踩属于“看着代码没问题但就是训不动”的典型。5.2 预测曲线和真实值形态一致但整体滞后一拍现象验证集上的预测曲线形状、峰值位置都和真实值高度一致但时间上错后一个采样间隔。看起来像是“模型用今天的 PM2.5 预测出了昨天的值”。原因这是单步预测任务的经典滞后现象。模型学到的最优策略是“最近一小时的值就是下一小时的值”尤其在 PM2.5 这种自相关性极强的序列上上一时刻数值对下一时刻的贡献太大模型懒得去学气象特征的变化规律。解决常见做法有两个方向。一是在损失函数上做文章把预测值和真实值的差分误差也加入 loss惩罚“滞后”行为二是改变输入结构把风速、湿度等外生特征在时间维度上做对比削弱 PM2.5 自回归项的支配地位。对课程设计来说最简单有效的是采用多步预测策略一次输出未来 3 小时而不是 1 小时滞后现象会显著缓解因为模型必须学会推断趋势而不是抄近路。5.3 训练损失很低测试集一测误差大得离谱现象训练集和验证集上的 RMSE 都很漂亮但把模型部署到新数据或测试集时间段后预测曲线直接飞掉。原因最常见的是数据划分出了问题。有人图省事随机抽样 20% 做验证集这在时序任务上是致命的——模型在训练时见过验证集时刻邻近的上下文信息评估结果虚高。另外还有可能测试集的时间段和你训练集的时间段存在显著分布差异比如训练数据在夏季测试数据在冬季模型没见过污染高发场景预测值被拉到历史均值附近。解决严格按时间顺序划分前 80% 时间段训练中间 10% 验证最后 10% 测试。考试前要补一句如果确实需要用不同季节的数据做测试至少把温度、湿度特征显式传入模型让模型有机会学到季节性差异而不是靠序列自回归硬扛。5.4 每次训练结果都不一样答辩完还在纠结用哪个模型现象同一次训练脚本重复跑两次的验证集 RMSE 差 3 个点。答辩时老师问“你这个模型效果到底是多少”你说不清楚。原因PyTorch 默认情况下权重初始化是随机的Dropout 层的随机屏蔽机制也带来不确定性。没有固定随机种子每次实验都等于从不同的起点出发。解决训练脚本开头固定三处随机种子——Python 内置随机模块、NumPy、PyTorch。import random import numpy as np import torch seed 42 random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)固定之后同一台机器上重复运行结果应该完全一致。如果仍然不一致检查 DataLoader 有没有开num_workers0多进程加载数据时会引入不确定顺序建议单机训练保持num_workers0或者至少固定generator。这个细节做到位实验报告里所有数据都是可复现的遇到追问也不心虚。6. 让预测结果更可信验证方法与三种进阶改进6.1 给模型配一组量化指标绘图能说服眼睛但论文和答辩需要数字。三个指标最常被问到RMSE、MAE、R²。RMSE 对高浓度误差敏感适合暴露极端预测失误MAE 更稳健R² 衡量模型相对“用均值做预测”的提升幅度。计算时全部在反归一化后的原始量纲上做归一化空间里的误差数字没有物理意义。from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score rmse np.sqrt(mean_squared_error(y_original, pred_original)) mae mean_absolute_error(y_original, pred_original) r2 r2_score(y_original, pred_original) print(fRMSE: {rmse:.2f} μg/m³ | MAE: {mae:.2f} μg/m³ | R²: {r2:.3f})对 PM2.5 小时数据来说RMSE 在 1525 μg/m³ 属于相当不错3040 是合理区间超过 50 就要回去检查数据清洗。R² 在 0.8 以上可以放心写进报告0.60.8 是常见水平低于 0.5 说明输入特征和模型结构需要大改。6.2 三种还能继续往下做的改进方向第一个方向是多步预测。把forecast_horizon从 1 改成 6 或 24输出维度设为对应步数模型从“只看下一小时”进化成“看未来一天”。代价是训练难度上升误差会随时间步长累积。第二个方向是引入注意力机制在 LSTM 层后加一个 attention 模块让模型自己学会关注过去 24 小时中“哪几个时刻最关键”。第三个方向是把周期性时间特征小时、星期几、月份作为附加特征拼进输入模型能更显式地学到早晚高峰和工作日效应。6.3 一个值得养成的收尾习惯把数据清洗、模型训练、评估、可视化拆成四个独立脚本中间用 CSV 或 JSON 传递结果。第一次做也许觉得麻烦但后期调参数时你会庆幸自己没把代码堆在一个 notebook 里。我自己的习惯是每调一次参数就记录下验证集 RMSE 和当时的模型结构攒够十几组数据后再统一对比。这样哪怕某个实验没有保留最佳权重也能从记录里复现当时的完整条件。这套流程跑通一遍并不难真正花时间的是理解每个参数背后的物理直觉和数学动机。做得好的同学会把 20 页的实验报告压缩成 3 页图表干货而那些跑通后急着提交的人往往会在答辩时被“为什么选这个 look_back”这种问题问住。数据质量哪怕差一点模型结构保守一点只要每个决策都能给出理由这个期末大作业的分数一定不会差。希望帮到你。本文还有配套的精品资源点击获取