尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

LSTM空气质量预测课设全拆解:从时间序列到可视化

LSTM空气质量预测课设全拆解:从时间序列到可视化 简介这是一份面向计算机相关专业学生的Python期末大作业完整源码基于长短期记忆网络模型对空气质量数据进行可视化与预测分析由导师指导并高分通过代码完整、可直接运行适合课程设计、毕业设计及期末大作业也适合需要项目实战的深度学习初学者。压缩包共260个文件大小约7.03MB主要含15个Python脚本、8个HTML分析页面、164个SCSS样式文件、23个JavaScript交互文件以及CSV空气质量数据、SQLite数据库和说明文档等文件构成覆盖前端展示与后端处理目录结构清晰便于按需修改和扩展。目前已有99人学习下载。项目完整包含数据预处理、长短期记忆模型构建、训练评估与可视化分析等环节并配有多个网页展示空气质量时序曲线与预测结果可帮助读者理解时序预测的完整流程同时为答辩演示、功能扩展和代码讲解提供扎实基础。此外资源还提供了必要的说明文档与目录导引降低上手门槛尤其适合初次接触深度学习项目的学生快速开展实战。1. 当空气质量遇上LSTM一次课设能拆出多少东西这不是一个普通的填鸭式期末项目。拿到这套Python LSTM的空气质量预测源码时我第一反应是又一个拿现成的LSTM套时间序列就完事的作业但把整个目录结构过了一遍才发现它比绝大多数课设做得要完整——不光有模型训练还有数据可视化分析、网页端报表展示、多页面HTML模板以及从二手数据到可交互图表的完整链路。对于正在为期末大作业发愁、又想在答辩时有真东西可讲的计算机相关专业学生来说这份资源的价值在于拿来即能跑、跑了能讲清、讲完能拿分。它能解决的核心问题是如何用循环神经网络处理时间序列预测并把预测结果和趋势分析以可视化手段呈现出来而不是停留在我训练出了一个loss值的层面。2. 先看清家底数据链路与页面结构的完整拆解在跑任何模型之前先把这份资源的骨架摸清。文件和目录的命名往往比读README更能说明问题——尤其是这种课设性质的源码包结构通常直接反映作者的设计思路。2.1 t_pm25.csv与pm25.csv两份数据文件的分工打开压缩包最先注意到的是两个大同小异的CSV文件t_pm25.csv和pm25.csv。很多新手拿到数据后第一反应是两个都读一遍但这里的设计意图其实很明显——用后缀t区分训练集train与原始/测试集。实际做时序预测项目时我习惯把数据集拆分逻辑直接写到数据加载阶段而不是在代码里临时切片这样更便于复现。import pandas as pd # 原始数据与训练数据分离加载 df_raw pd.read_csv(pm25.csv) # 原始监测数据 df_train pd.read_csv(t_pm25.csv) # 预处理后的训练用数据 print(原始数据形状:, df_raw.shape) print(训练数据形状:, df_train.shape) # 检查列是否一致确保两份数据对齐 assert list(df_raw.columns) list(df_train.columns), 列名不一致需要重新对齐这里做了一个显式的列一致性校验原因是课设数据在不同阶段可能被手工编辑过一旦列错位后面训练出的模型就是垃圾进垃圾出。参数上要特别注意df_raw与df_train的时间索引是否连续很多空气质量数据会因监测站点维护产生缺失时间戳这在后面构造滑窗时会直接导致样本错位。2.2 三个HTML页面的职责边界analysis.html、provinces.html、current.html三个文件是这套资源比较讨巧的地方。纯做LSTM预测的项目通常只会交付一个notebook或py脚本但这份资源把结果拆成了三个视角页面文件定位数据侧重current.html当前空气质量的实时快照最近时刻的PM2.5浓度与等级analysis.html历史趋势分析与模型预测曲线全量时序、滑动平均、LSTM预测对比provinces.html区域纬度对比展示不同监测点/城市的横向比较从模板目录里的material-dashboard系列CSS可以判断这套可视化是用现成的Admin模板改的曲线图大概率由ECharts或Chart.js驱动。对于课设来说这是一种性价比很高的做法——视觉成品有专业感且不必从零造图表轮子。如果你拿到的版本里HTML是静态的说明图表数据是预先渲染进去的如果里面出现fetch或ajax调用说明后端有实时数据注入接口。2.3 material-dashboard目录前端外观的定制空间material-dashboard.css、material-dashboard.min.css、material-dashboard-rtl.css、demo.css这组文件的用意不难理解资源作者把前端模板完整打包免得答辩演示时因缺少样式文件导致页面错乱。material-dashboard-rtl.css是阿拉伯语等从右往左读的语言版本对我们没有实际用途但它的存在说明模板是原版从外部引入的。对课设而言调整页面风格通常会动到demo.css——这是demo页面专用样式改颜色、间距、图表容器尺寸都在这里最安全。想改侧边栏或顶栏配色再去找material-dashboard.css里的对应类名。我一般建议学生只动demo.css因为主模板文件几千行改坏一个花括号就会让整个页面白屏且不容易定位。3. 数据清洗与特征构造决定LSTM上限的隐藏环节很多LSTM课设翻车的起点不在模型结构而在数据处理。Air Quality数据集是典型的多元时序数据——包含时间戳、SO₂、NO₂、CO、O₃、PM2.5等浓度字段还有气温、气压、湿度等气象要素。LSTM吃的是数值张量不是CSV原文本所以必须明确规范化和滑窗切片这两个核心流程。3.1 缺失值与异常值用插值而不是直接丢行空气质量监测数据有个显著特点传感器故障或校准维护会产生成段的连续缺失而不仅是零散空值。对LSTM来说直接dropna()会切断时间连续性滑窗采样时会丢失历史依赖关系。import pandas as pd import numpy as np def load_air_quality(file_path): df pd.read_csv(file_path) # 将时间列解析为datetime索引 df[time] pd.to_datetime(df[time]) df.set_index(time, inplaceTrue) # 对污染物浓度列做线性插值连续缺失超过5个点的用前向填充 pollutant_cols [PM2.5, PM10, SO2, NO2, CO, O3] for col in pollutant_cols: df[col] df[col].interpolate(methodlinear, limit5) df[col] df[col].fillna(methodffill) # 去掉全部为0的异常周期探头离线时通常输出全0 df df[(df[pollutant_cols] ! 0).any(axis1)] return df df load_air_quality(t_pm25.csv) print(df.head())注意注释里的两个关键行为limit5限制插值跨度防止在长时间断档时线性外推出离谱的中间值fillna(methodffill)是兜底策略如果中间穿插了少量孤立空值用前向填充比插值更稳。全0行的剔除是很多空气质量项目的隐性步骤因为零点不代表浓度为零而是设备离线固件归零。3.2 滑动窗口构造预测未来N小时的数据切片LSTM的输入输出构造是整个模型最核心的工程问题。假设监测数据是逐小时采样的要预测未来24小时的PM2.5变化趋势通常做法是利用过去48小时或72小时的数据窗口作为输入。def create_sequences(data, input_steps48, output_steps24): X, y [], [] for i in range(len(data) - input_steps - output_steps): X.append(data[i:iinput_steps]) y.append(data[iinput_steps:iinput_stepsoutput_steps]) return np.array(X), np.array(y) # 以PM2.5列为预测目标同时保留气象特征作为输入 feature_cols [PM2.5, PM10, SO2, NO2, CO, O3, TEMP, PRES, DEWP, RAIN] X, y create_sequences(df[feature_cols].values, input_steps48, output_steps24) print(输入张量形状:, X.shape) # (样本数, 48, 10) print(输出张量形状:, y.shape) # (样本数, 24, 10)输入形状(样本数, 48, 10)含义是每个样本包含48个连续时间步每步有10个特征维度。这里的output_steps24代表直接预测未来24小时的PM2.5序列——注意输出也是10维的常做做法是在模型尾部分离出需要的PM2.5列或者让模型输出全维度。参数选择上有一个经验值input_steps不应超过数据总时长的10%否则大量样本会因长度不够而被丢弃训练数据量会急剧萎缩。3.3 MinMaxScaler归一化的正确姿势先fit再transformLSTM默认使用tanh激活函数输入数值范围必须控制在-11附近才能避免梯度饱和。更隐蔽的问题是测试集不能参与归一化参数的fit否则会把未来信息泄漏进训练过程造成模型评估虚高。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) # 用训练数据拟合scaler训练与测试都调用transform scaled_features scaler.fit_transform(df[feature_cols]) # 划分训练测试集时间序列不能乱序shuffle train_size int(len(scaled_features) * 0.8) train_data scaled_features[:train_size] test_data scaled_features[train_size:] X_train, y_train create_sequences(train_data) X_test, y_test create_sequences(test_data) print(训练样本数:, len(X_train), 测试样本数:, len(X_test))这份代码里最容易被忽略的是train_size那行的顺序——时序预测必须按时间切分一旦random shuffle模型就会记住未来数据答辩时导师随机抽查测试集表现会穿帮。另外MinMaxScaler拟合后要保存到本地joblib.dump因为预测阶段新来的实时数据同样要用同一套归一化参数转换否则输入分布不一致。4. 模型搭建与训练从原理骨架到可运行代码LSTM不是黑匣子至少在这份课设里它的每一层都解释得清。模型设计的核心是三个问题用几层LSTM、每层多少隐藏单元、全连接层怎么接。针对空气质量这种周期性较强的时序数据一般会给足隐藏单元让模型记忆日周期和周周期。4.1 为什么用LSTM而不是ARIMA或简单RNNARIMA对非线性气象关系和突变天气的解释能力有限传统RNN在长序列训练中容易梯度消失。LSTM通过输入门、遗忘门、输出门结构能够把数小时前的污染累积状态传递到当前预测中。import torch import torch.nn as nn class AirQualityLSTM(nn.Module): def __init__(self, input_size10, hidden_size64, num_layers2, output_steps24): super(AirQualityLSTM, self).__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropout0.2 if num_layers 1 else 0 ) self.fc nn.Linear(hidden_size, output_steps) def forward(self, x): # x形状: (batch, 48, 10) lstm_out, _ self.lstm(x) # 取最后一个时间步的输出 last_out lstm_out[:, -1, :] # (batch, 64) output self.fc(last_out) # (batch, 24) return output model AirQualityLSTM(input_size10, hidden_size64, num_layers2, output_steps24) print(model)batch_firstTrue是关键参数它让输入张量的时间维放在第二维batch, seq_len, features符合我们前面create_sequences返回的张量布局。如果没有这个参数PyTorch默认的输入布局是seq_len, batch, features初学者最容易在这里栽跟头——训练时报错维度不匹配但又看不出原因。这里取最后一个时间步的输出作为全连接层的输入。hidden_size64是课设里的常见取值数据量较小时用32更稳数据量大时可以提到128。num_layers2是性价比最高的配置三层以上的LSTM在课设数据规模下很难有实质提升反而增加过拟合风险。4.2 损失函数与优化器MSE是时序回归的标准配置PM2.5浓度是连续值回归任务最直接的损失函数是均方误差MSE。如果希望模型更关注峰值偏差重污染时段的预测可以额外叠加一个MAPE项但课设没必要把评估指标复杂化。import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model AirQualityLSTM().to(device) criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lr0.001) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, factor0.5, patience5) # 训练循环 epochs 50 batch_size 32 for epoch in range(epochs): model.train() total_loss 0.0 # 手动切batch避免数据量小时dataloader引入额外复杂度 for i in range(0, len(X_train), batch_size): x_batch torch.FloatTensor(X_train[i:ibatch_size]).to(device) y_batch torch.FloatTensor(y_train[i:ibatch_size, :, 3]).to(device) optimizer.zero_grad() outputs model(x_batch) loss criterion(outputs, y_batch) loss.backward() # 梯度裁剪防止RNN类模型梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() avg_loss total_loss / (len(X_train) // batch_size) print(fEpoch {epoch1}/{epochs}, Loss: {avg_loss:.6f})注意y_batch的切片方式y_train[i:ibatch_size, :, 3]取的是第4列下标3对应PM2.5特征在feature_cols里的顺序。假设我们在输出维度上只关心PM2.5这样做比让模型预测全部10维收敛更快且在答辩讲解时逻辑更清晰——我预测的是未来24小时PM2.5序列。ReduceLROnPlateau在loss连续5个epoch不下降时自动减半学习率这个机制比固定学习率多扛住很多训练后期平台期。4.3 模型保存与推理保存结构还是保存参数训练完成后保存模型的方式会影响后续可视化脚本的复用便利性。PyTorch有两种保存方式torch.save(model.state_dict())只存参数torch.save(model)保存完整模型。# 保存参数字典推荐后续加载时需重新定义模型结构 torch.save(model.state_dict(), lstm_pm25.pth) # 推理流程加载新数据 → 归一化 → 滑窗切片 → 模型前向 def predict_future(model, recent_data, input_steps48): model.eval() with torch.no_grad(): # recent_data形状需为 (1, 48, 10) x torch.FloatTensor(recent_data).unsqueeze(0).to(device) pred model(x) # (1, 24) return pred.cpu().numpy().flatten() # 反归一化还原为真实PM2.5浓度 pred_pm25 predict_future(model, X_test[-1]) pred_pm25_real scaler.inverse_transform( np.concatenate([np.zeros((24, 9)), pred_pm25.reshape(-1, 1)], axis1) )[:, -1]最后一步反归一化的np.concatenate看起来绕是因为scaler是对全部10列特征fit的反变换时也要传入10列才能还原PM2.5的真实量纲。这是这类项目最常见的代码复用障碍——训练时用全特征预测时只想输出单系列维度不匹配就报错。做法是保持维度一致性或者为PM2.5单独fit一个scaler。5. 可视化与HTML报表把模型输出变成答辩能讲的图模型训练完只是工程的下半场。课设答辩时老师不看loss曲线而是看你用数据讲出了什么故事。这份资源把可视化分成了两个层面Python侧的数据图表和HTML侧的最终展示页面。5.1 用Matplotlib画预测值与真实值对比图LSTM输出需要还原到原始量纲后与测试集真实值画在同一坐标系里。这是整个项目中最直观的成果展示。import matplotlib.pyplot as plt import matplotlib.dates as mdates # 加载测试集原始时间戳 test_time df.index[train_size 48: train_size 48 len(X_test)] # 取测试集第100个样本开始的24小时预测 sample_idx 100 pred_series predict_future(model, X_test[sample_idx]) true_series y_test[sample_idx, :, 3] # 反归一化 pred_real scaler.inverse_transform(np.concatenate( [np.zeros((24, 9)), pred_series.reshape(-1, 1)], axis1))[:, -1] true_real scaler.inverse_transform(np.concatenate( [np.zeros((24, 9)), true_series.reshape(-1, 1)], axis1))[:, -1] plt.figure(figsize(12, 5)) plt.plot(test_time[sample_idx:sample_idx24], true_real, b-, label真实值) plt.plot(test_time[sample_idx:sample_idx24], pred_real, r--, label预测值) plt.legend() plt.title(LSTM 24小时PM2.5预测对比) plt.xlabel(时间); plt.ylabel(PM2.5浓度 (μg/m³)) plt.gca().xaxis.set_major_formatter(mdates.DateFormatter(%m-%d %H:%M)) plt.show()这里有个经验细节抽样画图比全量测试集画图更有说服力。全量测试集可能有上千个样本图像挤成一团看不出效果随机抽一个样本展示24小时的局部对比答辩时解释起来更清晰——这是模型在第100个时间窗口的未来24小时预测结果趋势匹配度高峰值时刻误差在可接受范围内。5.2 把ECharts图表嵌入HTML模板analysis.html中如果嵌入了ECharts的line图那么图表所需的JSON数据是预先从Python端生成的。我认为合理的工作流是训练完成后导出预测结果到JSON再供HTML加载。// 假设analysis.html中已有echarts实例 // 数据从Python导出的pred_data.json中读取 fetch(pred_data.json) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(pm25_chart)); chart.setOption({ title: { text: PM2.5历史趋势与LSTM预测 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.timestamps }, yAxis: { type: value, name: 浓度(μg/m³) }, series: [ { name: 历史真实值, type: line, data: data.history, lineStyle: { width: 1 } }, { name: 预测值, type: line, data: data.prediction, lineStyle: { type: dashed, width: 2 }, itemStyle: { color: #d32f2f } } ] }); });从Python侧导出JSON的代码不复杂这里不赘述。重点提醒export的timestamps需要是字符串格式strftime否则JavaScript的Date对象解析可能会因时区问题偏移8小时。这是跨语言数据交换最常见的日期坑没有之一。6. 避坑指南空气质量LSTM课设的五个典型翻车现场这套源码虽然能直接跑通但课设答辩时老师最爱追问的是你改过哪些参数、踩过什么坑。与其到时支支吾吾不如在复现的时候主动把这些坑趟一遍变成自己的陈述素材。6.1 现象预测结果是一条平直线原因数据预处理后目标值残差趋近于零模型认为输出均值loss最低。在空气质量数据里如果原始数据已经有平滑处理比如移动平均后存储LSTM学到的是预测平均值而不是预测波动。解决检查PM2.5序列方差。如果方差过小改数据源或用原始小时粒度数据而不是日均值也可以把预测目标改为差分序列t时刻与t-1时刻的差值模型预测增量而不是绝对浓度。6.2 现象训练集loss下降测试集loss很大原因时间序列数据中不小心让训练集和测试集交错。特别是用train_test_split默认的随机划分模式时测试集中可能混入训练时间段之后的数据模型实际上是背题。解决强制用时间顺序切分并打印训练集与测试集的时间范围核对。我每跑一个LSTM项目的前30秒就是干这件事确认train_time_end test_time_start。6.3 现象torch.load时出现module not found错误原因训练时用了nn.DataParallel包装模型保存的state_dict路径带module.前缀而加载时用的是裸模型。解决如果代码里没有DataParallel直接忽略这个坑如果有加载时做键名替换——new_state_dict {k.replace(module., ): v for k, v in state_dict.items()}。6.4 现象HTML页面图表空白但控制台无报错原因最常见的情况是div容器高度为0。ECharts初始化时依赖父容器有实际高度material-dashboard模板里默认图容器高度在demo.css中定义没有引入该文件时div高度塌缩为0。解决在demo.css中显式给图表容器加height: 400px;或在style属性中直接写死高度。这个坑在接入任何Admin模板时都会遇到不是ECharts本身的锅。6.5 现象预测未来48小时误差越来越大原因LSTM做单步预测性能尚可但在递归预测模式下用预测值作为下一步的输入特征误差会随时间步累积。这里资源的设计是预测未来24小时而不是无限外推。解决明确预测步长上限并在答辩陈述中说明模型设计目标为未来24小时预测超过该范围误差增长不符合工程预期。如果你非要预测更长时间考虑改成seq2seq结构或加入注意力机制但这已经超出期末大作业的合理范围了。7. 进阶验证与陈述技巧把资源价值转化为答辩得分点当源码能跑通、图表能展示、踩坑记录也齐了剩下的事就是让这个项目的技术深度看起来对得起99分。模型训练的收敛曲线是性价比很高的一个加分项。建议单独跑一个epoch记录loss变化并绘图——训练损失曲线呈阶梯状下降后趋于平缓证明学习率调度工作正常。这个图在答辩时可以放在PPT里比贴一堆网络结构代码更直观。还可以做一次简单的基线对比用均值预测历史48小时的平均值作为未来24小时的预测值和LSTM预测做误差对比更好的是顺便算出MAE、RMSE两个指标。当你能脱口而出LSTM的RMSE比均值预测低23%时答辩老师通常不会再纠结你为什么用LSTM这种基础问题了。from sklearn.metrics import mean_absolute_error, mean_squared_error mae mean_absolute_error(true_real, pred_real) rmse np.sqrt(mean_squared_error(true_real, pred_real)) baseline_pred np.tile(np.mean(X_test[sample_idx, :, 3]), 24) baseline_mae mean_absolute_error(true_real, baseline_pred) print(fLSTM MAE: {mae:.2f} | Baseline MAE: {baseline_mae:.2f})注意这里的baseline_pred是用输入窗口的均值扩展成24步预测是时序预测任务很常用的朴素基线。如果LSTM连这个都打不过问题基本出在数据或训练环节回头查第3、4章的参数。关于参数调优还有一个便宜好用的技巧——在hidden_size32/64/128三个值之间跑一次短训练10个epoch对比验证集loss。这个对比实验能在10分钟内完成但答辩时说出来效果能拔高一个段位。我自己在做类似课设时踩过最大的坑是过分纠结于模型结构花大量时间在换层数上最后数据泄露导致测试集指标虚高而不自知。从那以后我每次跑时序任务都强制走一遍时间顺序切分检查 基线对比验证这两步不赶进度但放心。这套流程对你来说也值得在动手改代码前花十分钟走一遍希望帮到你。本文还有配套的精品资源点击获取
返回列表