简介这是一份基于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存放按站点划分的原始监测 CSVxy存放切分好的样本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.py和config.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_len24, pred_len8, step1): 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_size和num_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_size128, num_layers2, pred_len8, dropout0.2): super().__init__() self.encoder nn.LSTM( input_sizen_features, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout 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是两层 LSTMbatch_firstTrue表示输入形状的第一维是 batchforward 里取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_sizecfg.batch_size, shuffleTrue) val_loader DataLoader(val_ds, batch_sizecfg.batch_size, shuffleFalse) model MultiTaskAirQuality(n_features, n_tasks, cfg.hidden_size, cfg.num_layers, cfg.pred_len) optimizer torch.optim.Adam(model.parameters(), lrcfg.lr) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, patience5 ) 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_norm1.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()关掉 dropoutReduceLROnPlateau在验证 loss 连续 5 个 epoch 不下降时把学习率乘以默认系数 0.1只有验证 loss 刷新历史最优时才保存权重连续patience轮不刷新就终止训练。max_norm1.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别只在终端打印。下面是一个参考输出格式任务MAERMSER²PM2.58.2112.040.874PM1015.6323.870.821SO22.313.650.902NO26.829.410.793CO0.420.610.867O311.2716.530.746从这张总表能快速定位弱任务O3 的 R² 最低说明该站点臭氧的时序规律弱可能受日照和光化学反应影响更大此时单独调 LSTM 结构收益有限优先检查输入特征里有没有加入相关气象变量。5. 模型扩展与 CUDA 环境排查5.1 cuda_test.py 与设备检测cuda_test.py用途很直接——训练前确认 PyTorch 能拿到 GPU避免模型在小数据集上跑了几小时才发现用的 CPUimport 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.py里heads加一项同时把train.py的task_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 权重会破坏已收敛的梯度平衡。本文还有配套的精品资源点击获取