多任务LSTM空气质量预测:PyTorch实现六项污染物同时预测
2026/9/15 17:48:42 网站建设 项目流程

简介:这是一份基于Python与主流深度学习框架的多任务空气质量预测完整项目,面向深度学习初学者、环境数据分析人员及毕业设计开发者,主要解决PM2.5、PM10、二氧化硫等污染物指标的联合预测问题。压缩包共46个文件,以36个CSV数据文件、7个Python脚本和3个Markdown文档为主,总体积约5MB。其中CSV提供历史监测数据与时间特征,脚本覆盖数据预处理、特征工程、模型构建、多任务训练、评估与预测等完整流程,文档说明项目结构与运行方法。目前已有142人浏览学习,代码注释清晰,适合作为毕业设计、课程设计或相关竞赛的参考实现。项目以多任务学习为核心,包含时间序列建模、特征处理与模型评估细节,可直接运行或二次扩展,便于迁移到其他环境变量的联合预测场景。

1. 多任务框架下的空气质量预测:一次训练,同时输出六项指标

空气质量监测站每小时回传六类污染物浓度,传统做法是为每项指标单独训练一个回归模型,把同一个时间序列切片重复喂进六套网络,训练耗时翻倍不说,每个模型都只看到了数据的一个侧面。这个毕业设计项目不一样:它把 PM2.5、PM10、SO2、NO2、CO、O3 的预测任务放进同一个模型,共享底层的时序编码器,只保留独立的输出头,一次前向传播同时输出六组预测值。多任务的核心收益是参数共享——污染物之间存在物理耦合关系,PM2.5 与 PM10 同源,NO2 与 CO 都来自燃烧排放,共享特征能显著提升样本量较小的任务的精度。项目默认用 PyTorch 实现,数据预处理、模型定义、训练、评估四段代码分离,压缩包内还带了cuda_test.py环境检测脚本。适合做课程设计、毕业设计,也适合想入门多任务时间序列预测的读者照着跑通全流程。后续各章按文件结构拆开讲,重点是数据流、模型结构和训练链路三块。

2. 数据文件与预处理:stations_data 到模型输入的完整链路

2.1 压缩包目录结构与各文件分工

项目解压后是一个Graduation-Design-main主目录,代码层分为配置、数据、模型、训练、评估五条线。config.py统一管理所有可调参数,data_process.py负责从原始站点数据生成训练样本,models.py定义多任务网络结构,train.py跑训练循环,eval.py负责在测试集上计算指标。utils.py放的是数据读取和指标计算的公共函数,cuda_test.py用于训练前确认 PyTorch 能否调用 GPU。data目录下分三块:stations_data存放按站点划分的原始监测 CSV,xy存放切分好的样本,models目录保存训练过程中的权重文件。

文件/目录在本项目中的作用
config.py统一管理站点编号、序列长度、批量大小、学习率、早停轮数
data_process.py原始 CSV 清洗、缺失值处理、时间窗切分与 z-score 标准化
models.py定义共享 LSTM 编码器 + 多任务输出头的网络结构
train.py训练主循环、多任务损失计算、梯度裁剪、早停与模型保存
eval.py加载最优权重,按任务计算 MAE、RMSE、R² 并汇总输出
utils.py滑动窗口生成、站点数据读取等公共工具函数
cuda_test.py检测 CUDA 可用性及 GPU 设备信息
data/xy预处理输出的样本文件,文件名带站点和窗口参数

拿到项目后第一步别急着改网络结构,先把data_process.pyconfig.py跑通,确认xy目录能正常生成样本,再进入训练。很多多任务模型“效果不对”的根因不在模型结构,而在输入样本没有对齐到同一个时间戳上:六个目标列来自不同监测仪器,数据缺失的时段错开,切窗后某几个任务全部是 NaN,模型只能靠其余任务更新梯度。检查xy输出样本里是否存在全零或 NaN 行,是最容易被跳过的排查点。

2.2 data_process.py 的时间窗切分与标准化

多任务时间序列预测的第一步是生成滑动窗口样本。假设某个站点有 30 天逐小时数据,特征列包含六项污染物浓度和三项气象特征(温度、湿度、风速),要用过去 24 小时预测未来 8 小时,data_process.py里常见做法是按固定步长切窗:

import numpy as np import pandas as pd def make_windows(df, src_cols, tgt_cols, seq_len=24, pred_len=8, step=1): X, Y = [], [] values = df[src_cols].values.astype(np.float32) targets = df[tgt_cols].values.astype(np.float32) for i in range(0, len(df) - seq_len - pred_len + 1, step): x = values[i:i + seq_len] y = targets[i + seq_len:i + seq_len + pred_len] X.append(x) Y.append(y) return np.array(X), np.array(Y)

参数说明:seq_len是回溯窗口长度,取 24 表示用最近 24 小时的观测值;pred_len是预测未来 8 小时的污染物浓度,这是多步预测配置;step控制滑动步长,数据充足时才设 1,数据量紧张时设seq_len或更大值来减少样本冗余。X的输出形状是(样本数, seq_len, 特征数)Y的形状是(样本数, pred_len, 目标数),多任务体现在最后一维同时包含多个目标列。切完窗必须做标准化,污染物数值范围差异很大,PM10 的量级通常比 O3 高一个数量级,直接用原始值训练会让损失函数被大数值任务主导,小数值任务几乎学不到梯度。

标准化建议用训练集的均值方差做 z-score 归一,校验集和测试集必须复用训练集的统计量,这一点我一般会单独存一份scaler字典到xy目录下,避免测试时发生数据泄漏。泄漏的典型表现是验证集指标明显好于线上表现,因为验证集的均值方差已经被算进特征里,模型在训练时其实“见过”验证集分布。

2.3 config.py 中的关键配置项

config.py把所有可调参数集中管理,预处理和训练共用同一份配置,改动集中在文件头部:

class Config: # 数据路径 data_dir = "data/stations_data" xy_dir = "data/xy" model_dir = "models" station_id = "1011" # 站点编号,切换数据源时只改这里 # 序列与批量 seq_len = 24 pred_len = 8 batch_size = 64 # 模型结构 hidden_size = 128 num_layers = 2 dropout = 0.2 # 训练策略 epochs = 100 lr = 1e-3 patience = 10

参数说明:station_id指定用哪个站的数据,空气质量模型常有站点迁移需求,换站点时不用改任何代码,只改编号后重新跑数据预处理;patience是早停容忍轮数,连续 10 个 epoch 验证集 loss 不刷新就停止,防止后期过拟合;hidden_sizenum_layers直接影响参数量,小数据集上 128/2 通常够用,调到 256 以上不一定带来精度提升,反而更容易过拟合。

提示:建议预处理阶段把xy_dir里的文件名带上窗口参数,例如xy_1011_seq24_pred8.npz,这样调整窗口长度后不需要备份旧样本,调参时也分得清哪份数据对应哪组参数。

3. models.py 多任务模型:共享 LSTM 编码器与独立输出头

3.1 共享编码器还是独享编码器

多任务模型的结构选择是这套代码的核心。最省事的方案是独享编码器:为每个污染物各写一个 LSTM,六套模块互不干扰,但参数量翻六倍,小数据集上很容易过拟合。项目采用的是共享编码器方案,全部任务共用一段 LSTM 隐藏状态,六个输出头从这个共享表示里各自回归出自己的目标序列。这么做的好处有两个:一是参数少,LSTM 部分只训练一套权重;二是样本利用率高,六个任务共享输入,梯度从六个方向同时更新编码器,等效于扩充了训练数据。污染物之间确实存在同源耦合,PM2.5 和 PM10 同属颗粒物,CO 与 NO2 都与燃烧排放相关,共享的隐藏状态能自动编码这些关联特征。代价是任务目标在梯度上会互相干扰,某些任务收敛快、某些任务还在震荡,如果不做损失加权,总损失会倾向拟合误差更大的任务。

3.2 多任务模型的具体结构

在 PyTorch 里,models.py大致对应这样的结构:

import torch import torch.nn as nn class MultiTaskAirQuality(nn.Module): def __init__(self, n_features, n_tasks, hidden_size=128, num_layers=2, pred_len=8, dropout=0.2): super().__init__() self.encoder = nn.LSTM( input_size=n_features, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0.0 ) self.heads = nn.ModuleList([ nn.Sequential( nn.Linear(hidden_size, hidden_size // 2), nn.ReLU(), nn.Dropout(dropout), nn.Linear(hidden_size // 2, pred_len) ) for _ in range(n_tasks) ]) def forward(self, x): out, _ = self.encoder(x) h_last = out[:, -1, :] return [head(h_last) for head in self.heads]

参数说明:encoder是两层 LSTM,batch_first=True表示输入形状的第一维是 batch;forward 里取out[:, -1, :]作为整条输入序列的汇总表示,这是时序预测里最直接的取法,等价于用最后一个时刻的隐藏状态来解码;n_tasks在初始化时由目标列数决定,六项污染物就传 6。每个head是两层全连接加 ReLU,输出长度为pred_len,所以返回的是一个列表,列表长度等于任务数,元素形状是(batch, pred_len)。预测阶段做多步输出时,需要把每个 head 的输出拼接成(batch, pred_len, n_tasks),再逆标准化才能和原始量纲对比。

注意 LSTM 的 dropout 参数只在num_layers > 1时生效,PyTorch 的官方文档写得很明确,单层时传 dropout 会被忽略。如果你把num_layers改成 1,别指望用这个参数做正则,否则验证集表现和预期会不一致。

3.3 多任务损失加权与梯度平衡

模型输出是六个 tensor 的列表,train.py里不能直接调用criterion(model(x), y),需要把六项损失加权求和:

criterion = nn.MSELoss() task_weights = { "PM2.5": 1.0, "PM10": 1.0, "SO2": 0.8, "NO2": 0.8, "CO": 0.6, "O3": 0.6 } def multitask_loss(preds, targets, weights): total = 0.0 tasks = list(weights.keys()) for i, task in enumerate(tasks): target = targets[:, :, i] total = total + weights[task] * criterion(preds[i], target) return total

这段代码是多任务损失最简单的做法,但如果你训练时发现某项污染物一直收敛不动,常见做法是改成动态加权:取每个任务最近若干个 epoch 的 loss 均值的倒数作为权重,拉近梯度尺度。也有人用 GradNorm 在训练中动态调整梯度幅度,不过对小数据集来说,固定权重加人工观察足够。判断量化指标是否失效的窍门是打印每个任务单独的 loss 值而不是只看总 loss——总 loss 下降不一定代表六个任务都在变好,有可能只是权重大的那个任务在主导。

4. 训练与评估:train.py 和 eval.py 的完整链路

4.1 训练循环与早停逻辑

train.py的职责是把config.py的参数、data_process.py的输出和models.py的模型串成完整训练链路。核心循环要覆盖梯度清零、反向传播、验证和早停:

from torch.utils.data import DataLoader, TensorDataset train_loader = DataLoader(train_ds, batch_size=cfg.batch_size, shuffle=True) val_loader = DataLoader(val_ds, batch_size=cfg.batch_size, shuffle=False) model = MultiTaskAirQuality(n_features, n_tasks, cfg.hidden_size, cfg.num_layers, cfg.pred_len) optimizer = torch.optim.Adam(model.parameters(), lr=cfg.lr) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode="min", patience=5 ) best_val_loss = float("inf") bad_epochs = 0 for epoch in range(cfg.epochs): model.train() for xb, yb in train_loader: optimizer.zero_grad() preds = model(xb) loss = multitask_loss(preds, yb, task_weights) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() model.eval() val_loss = evaluate(model, val_loader) scheduler.step(val_loss) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), f"{cfg.model_dir}/best_{cfg.station_id}.pt") bad_epochs = 0 else: bad_epochs += 1 if bad_epochs >= cfg.patience: break

逻辑说明:每一轮先清零梯度,forward 得到六个任务的预测,multitask_loss算出加权 MSE,反向传播后做梯度裁剪防止 LSTM 梯度爆炸;验证时用model.eval()关掉 dropout,ReduceLROnPlateau在验证 loss 连续 5 个 epoch 不下降时把学习率乘以默认系数 0.1;只有验证 loss 刷新历史最优时才保存权重,连续patience轮不刷新就终止训练。max_norm=1.0是经验值,如果日志里 loss 出现 NaN,优先检查学习率和输入数据里是否有 NaN 值,再把max_norm降到 0.5。

4.2 评估指标与多任务汇总输出

eval.py加载models/best_1011.pt之后,逐个任务计算 MAE、RMSE 和 R²:

import numpy as np def evaluate_metrics(preds, targets, names): metrics = {} for i, name in enumerate(names): p = np.array(preds[i]).ravel() t = np.array(targets[:, :, i]).ravel() mae = np.mean(np.abs(p - t)) rmse = np.sqrt(np.mean((p - t) ** 2)) ss_res = np.sum((t - p) ** 2) ss_tot = np.sum((t - np.mean(t)) ** 2) r2 = 1 - ss_res / ss_tot metrics[name] = {"MAE": round(mae, 4), "RMSE": round(rmse, 4), "R2": round(r2, 4)} return metrics

指标说明:preds[i]来自模型输出的第 i 个任务头,targets[:, :, i]是对应的真实序列。ravel()把二维的(batch, pred_len)展平,方便算全局误差。R² 小于 0 说明该任务预测结果还不如直接用历史均值,优先怀疑序列长度不够或者标准化方式出错。多任务里不同任务的量纲不同,PM10 的 RMSE 天然比 SO2 大几个量级,直接对比没有意义,要按列观察每个任务自己的误差收敛情况。

输出建议写成表格直接导出 CSV,别只在终端打印。下面是一个参考输出格式:

任务MAERMSE
PM2.58.2112.040.874
PM1015.6323.870.821
SO22.313.650.902
NO26.829.410.793
CO0.420.610.867
O311.2716.530.746

从这张总表能快速定位弱任务:O3 的 R² 最低,说明该站点臭氧的时序规律弱,可能受日照和光化学反应影响更大,此时单独调 LSTM 结构收益有限,优先检查输入特征里有没有加入相关气象变量。

5. 模型扩展与 CUDA 环境排查

5.1 cuda_test.py 与设备检测

cuda_test.py用途很直接——训练前确认 PyTorch 能拿到 GPU,避免模型在小数据集上跑了几小时才发现用的 CPU:

import torch print(torch.__version__) print(torch.cuda.is_available()) if torch.cuda.is_available(): print(torch.cuda.get_device_name(0)) else: print("CPU only")

如果输出CPU only,先看安装的 PyTorch 是不是 CPU 版。检查torch.version.cuda和显卡驱动支持的 CUDA 版本是否匹配,驱动版本过低时 PyTorch 会报CUDA driver version is insufficient。改成 GPU 训练只需在训练脚本里加两行:device = torch.device("cuda" if torch.cuda.is_available() else "cpu"),然后把模型和数据都.to(device),LSTM 和线性层的前向逻辑不用做任何修改。

5.2 多任务输出头的扩展

站点若额外提供温度、湿度、风速等气象特征,扩展一个气象输出头非常方便——models.pyheads加一项,同时把train.pytask_weights字典同步更新。实际操作时有个坑:config.py里的n_tasks改了,但data_process.py切窗时目标列顺序和task_weights的 keys 顺序必须保持一致。比如你在target_cols里先写["PM2.5", "PM10", ...],那么任务权重的字典键也必须是这个顺序,否则模型某个 head 学到的是另一种污染物的分布,而eval.py却按错位顺序去算指标。建议在data_process.py输出样本时把目标列顺序写进一个meta.json,模型初始化再从这个文件读取任务名列表,这样改顺序时所有模块自动对齐。另外,共享编码器承担了所有任务的特征提取,扩展新任务后要重跑完整训练流程,不要在旧权重上继续微调——新增的输出头是随机初始化的,直接加载旧 LSTM 权重会破坏已收敛的梯度平衡。

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

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

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

立即咨询