☰
LSTM空气质量预测课设全拆解:从时间序列到可视化
2026/9/28 5:45:20 网站建设 项目流程

简介:这是一份面向计算机相关专业学生的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', inplace=True) # 对污染物浓度列做线性插值(连续缺失超过5个点的用前向填充) pollutant_cols = ['PM2.5', 'PM10', 'SO2', 'NO2', 'CO', 'O3'] for col in pollutant_cols: df[col] = df[col].interpolate(method='linear', limit=5) df[col] = df[col].fillna(method='ffill') # 去掉全部为0的异常周期(探头离线时通常输出全0) df = df[(df[pollutant_cols] != 0).any(axis=1)] return df df = load_air_quality('t_pm25.csv') print(df.head())

注意注释里的两个关键行为:limit=5限制插值跨度,防止在长时间断档时线性外推出离谱的中间值;fillna(method='ffill')是兜底策略,如果中间穿插了少量孤立空值,用前向填充比插值更稳。全0行的剔除是很多空气质量项目的隐性步骤,因为零点不代表浓度为零,而是设备离线固件归零。

3.2 滑动窗口构造:预测未来N小时的数据切片

LSTM的输入输出构造是整个模型最核心的工程问题。假设监测数据是逐小时采样的,要预测未来24小时的PM2.5变化趋势,通常做法是利用过去48小时或72小时的数据窗口作为输入。

def create_sequences(data, input_steps=48, output_steps=24): X, y = [], [] for i in range(len(data) - input_steps - output_steps): X.append(data[i:i+input_steps]) y.append(data[i+input_steps:i+input_steps+output_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_steps=48, output_steps=24) print("输入张量形状:", X.shape) # (样本数, 48, 10) print("输出张量形状:", y.shape) # (样本数, 24, 10)

输入形状(样本数, 48, 10)含义是:每个样本包含48个连续时间步,每步有10个特征维度。这里的output_steps=24代表直接预测未来24小时的PM2.5序列——注意输出也是10维的,常做做法是在模型尾部分离出需要的PM2.5列,或者让模型输出全维度。参数选择上有一个经验值:input_steps不应超过数据总时长的10%,否则大量样本会因长度不够而被丢弃,训练数据量会急剧萎缩。

3.3 MinMaxScaler归一化的正确姿势:先fit再transform

LSTM默认使用tanh激活函数,输入数值范围必须控制在(-1,1)附近才能避免梯度饱和。更隐蔽的问题是:测试集不能参与归一化参数的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或简单RNN

ARIMA对非线性气象关系和突变天气的解释能力有限,传统RNN在长序列训练中容易梯度消失。LSTM通过输入门、遗忘门、输出门结构,能够把数小时前的污染累积状态传递到当前预测中。

import torch import torch.nn as nn class AirQualityLSTM(nn.Module): def __init__(self, input_size=10, hidden_size=64, num_layers=2, output_steps=24): super(AirQualityLSTM, self).__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=0.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_size=10, hidden_size=64, num_layers=2, output_steps=24) print(model)

batch_first=True是关键参数,它让输入张量的时间维放在第二维(batch, seq_len, features),符合我们前面create_sequences返回的张量布局。如果没有这个参数,PyTorch默认的输入布局是(seq_len, batch, features),初学者最容易在这里栽跟头——训练时报错维度不匹配,但又看不出原因。

这里取最后一个时间步的输出作为全连接层的输入。hidden_size=64是课设里的常见取值,数据量较小时用32更稳,数据量大时可以提到128。num_layers=2是性价比最高的配置,三层以上的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(), lr=0.001) scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=5) # 训练循环 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:i+batch_size]).to(device) y_batch = torch.FloatTensor(y_train[i:i+batch_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_norm=1.0) optimizer.step() total_loss += loss.item() avg_loss = total_loss / (len(X_train) // batch_size) print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.6f}")

注意y_batch的切片方式:y_train[i:i+batch_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_steps=48): 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)], axis=1) )[:, -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)], axis=1))[:, -1] true_real = scaler.inverse_transform(np.concatenate( [np.zeros((24, 9)), true_series.reshape(-1, 1)], axis=1))[:, -1] plt.figure(figsize=(12, 5)) plt.plot(test_time[sample_idx:sample_idx+24], true_real, 'b-', label='真实值') plt.plot(test_time[sample_idx:sample_idx+24], 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(f"LSTM MAE: {mae:.2f} | Baseline MAE: {baseline_mae:.2f}")

注意:这里的baseline_pred是用输入窗口的均值扩展成24步预测,是时序预测任务很常用的朴素基线。如果LSTM连这个都打不过,问题基本出在数据或训练环节,回头查第3、4章的参数。

关于参数调优,还有一个便宜好用的技巧——在hidden_size=32/64/128三个值之间跑一次短训练(10个epoch),对比验证集loss。这个对比实验能在10分钟内完成,但答辩时说出来,效果能拔高一个段位。我自己在做类似课设时踩过最大的坑是过分纠结于模型结构,花大量时间在换层数上,最后数据泄露导致测试集指标虚高而不自知。从那以后我每次跑时序任务都强制走一遍时间顺序切分检查 + 基线对比验证这两步,不赶进度,但放心。这套流程对你来说,也值得在动手改代码前花十分钟走一遍,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询