
简介基于深度学习的多任务空气质量预测模型设计与实现压缩包是一套面向环境科学与深度学习交叉领域学习者的完整项目资源。项目以Python为开发语言围绕PM2.5、PM10、二氧化硫、氮氧化物等多指标预测任务构建了从数据清洗、特征工程到模型训练、验证与预测的端到端流程适合需要实战多任务学习与时间序列预测的读者。压缩包共46个文件包含36个CSV格式的空气质量数据集、7个Python脚本覆盖数据预处理、模型定义、训练、评估与预测等环节以及3个Markdown说明文档整体大小约5.08MB目录结构清晰便于按模块阅读和扩展。通过该资源读者可以了解多任务深度学习模型如何共享特征并同时输出多个污染物浓度预测掌握CNN、RNN或Transformer等架构在空气质量时序数据上的应用并复用现成的脚本完成自己的实验。已有142人学习对于正在准备毕业设计或想开展环境智能监测项目的研究者具有直接参考价值。1. 基于深度学习的多任务空气质量预测模型从六套单模型到一套共享模型现实中的空气质量预报不是预测“明天空气好不好”这一句话而是要同时输出PM2.5、PM10、NO2、SO2、CO和O3六项污染物浓度每一项都有独立的考核标准和健康指引。早期做法是为每个污染物单独训练一个深度学习时序模型六套特征工程、六套超参数、六个推理服务部署和运维成本成倍增加而且臭氧这类强非线性任务在样本不足时很容易过拟合。基于深度学习的多任务空气质量预测模型把多个污染物目标放进同一个共享网络让化学耦合和气象扩散规律在底层特征复用中被模型隐式学习。这篇内容从物理依据、模型设计、损失函数到AQI发布链路给出一套在PyTorch上可复现的方案。2. 共享特征与损失加权多任务空气质量预测模型的三个设计点2.1 污染物之间的物理耦合为什么能让多任务受益把六项污染物放进同一个模型训练很多人第一反应是任务之间互相拖累。实际在空气质量场景里多任务学习的收益恰恰来自污染物之间真实存在的化学反应和气象耦合。PM2.5在静稳天气和高湿条件下更容易累积NO2在逆温和低风速时浓度上升臭氧则依赖NO2在强光照下的光化学反应生成。同一套大气扩散条件影响着所有污染物的稀释与累积过程。这意味着六项指标不是六个相互独立的回归问题而是共享同一组气象驱动因子的多输出回归问题。污染物主要生成机制主导气象因子与PM2.5的相关性PM2.5燃烧排放、气粒转化静稳、高湿基准PM10扬尘、沙尘输送强风、地表干燥中高NO2机动车与工业燃烧逆温、低风速高O3NOxVOCs光化学反应高温、强辐射负相关SO2燃煤、工业过程高湿、弱扩散中CO不完全燃烧低风速高把六项污染物放在一起训练实质是对“气象条件相似则污染物趋势相似”这一先验做参数复用。单独建模时每个模型都要重新学习温度、湿度、风速对各自目标的差异化影响共享模型则可以在底层把这些规律表达为一组气象特征向六个任务的多条映射路径每个任务头只需要学习从共享表征分叉出去的小部分参数。这种结构在单站点小时级数据上尤其重要一个站点一年只有8760个样本六项浓度任务合并后共享层的有效训练样本量大约等于任务数乘以序列长度小样本污染物的过拟合风险明显下降。2.2 硬参数共享与软参数共享空气质量任务选哪种多任务学习架构首先分成两大类。硬参数共享是底层放一个公共网络多个任务共享这部分参数软参数共享是每个任务各自持有一整套网络通过损失函数里的正则项约束任务间权重保持接近。空气质量预测的数据来自同一组监测站点、同一个时间窗口任务间的关联有明确的物理基础我一般选硬参数共享。硬共享结构下六个任务的参数总量大约是一个单任务模型的一倍半而不是六倍训练和推理开销都可控。若干扰任务差异性很大比如把站点A的极端污染数据和站点B的正常数据合并训练软共享更能避免任务间相互压制但调参复杂度和训练耗时都会显著上升。动手做的时候很多人把多任务和多步预测混为一谈。用同一个模型输出未来24个时刻的浓度这是多步预测关注的是误差随时间步累积同时输出六项污染物浓度这才是多任务关注的是任务间关系。实际项目里两者经常组合但要在模型设计上分开看待。共享编码器先解决“过去24小时的气象和浓度序列如何压缩成表征”的问题预测头和任务头则分别承担时序展开与污染物映射这两类不同的职责。2.3 共享层、任务头与损失加权多任务模型的三个设计点确定了硬共享架构后真正影响效果的是三个设计点的取舍这也是我每次搭建多任务空气质量预测模型时最先确定的三件事。第一是共享层的容量。共享层决定了特征提取的表达上限容量太低任务头拿到的特征区分度不够容量太高共享层会把表达能力集中在数值量级最大的那个任务上其他任务反而退化。我一般让共享层参数占整个模型的六到七成任务头占三到四成。隐藏单元数可以比单任务单模型小单任务LSTM需要256单元的时候共享TCN用128单元配合更深的层数效果往往更稳。第二是任务头的复杂度。每个任务头只需要把共享表征映射到单个污染物的浓度区间一到两层全连接就够。六项浓度在数值范围上比较接近六个任务头可以共用一个结构。一旦加入AQI等级这样的离散分类目标分类头与回归头的最后一层必须分开不要让分类任务的反向传播直接污染回归特征。第三是损失加权。六项浓度直接相加不是不行但量级差异会在模型里变成隐式权重。有些站点NO2浓度高有些站点O3数值波动大直接相加会让梯度被数值大的任务牵引。固定权重需要人工反复标定换一个城市就要重新来一轮。通用做法是不确定性加权把每个任务的可学习噪声项作为权重的一部分让模型在训练中自适应决定每个任务的梯度比例公式为L Σ_i (1 / (2σ_i²)) · MSE_i (1/2) · log σ_i²其中σ_i是任务i的可学习噪声参数训练中与网络参数一起更新。公式里后半部分的log正则项保证某个任务不会因为噪声估计过大而彻底失去梯度也不会有任务权重归零。经过10个epoch左右的震荡后权重会稳定在一个合理区间。加权方式适用场景主要问题直接MSE相加各浓度量级接近且标准化充分大数值任务主导梯度固定权重明确知道PM2.5等任务优先级跨城市迁移需要重新手调不确定性加权任务间噪声水平差异大训练初期loss波动剧烈GradNorm任务梯度方向冲突明显多一次梯度计算训练时间增加3. 用PyTorch搭建多任务空气质量预测模型数据窗口、共享编码器与训练循环3.1 数据准备切分、标准化与序列窗口构造气象和污染物数据通常以小时为粒度。动手之前先把时间序列的切分顺序理清楚否则后面所有结果都不可信。标准化的正确做法是先按时间切分训练集、验证集和测试集再在训练集上计算均值和方差验证集和测试集只做同样的变换。如果先在全量数据上统计再切分验证集和测试集的数值范围就通过均值方差泄漏进了训练过程。时间序列预测里这是个高频错误而且很难从指标上发现因为短期预测在这种泄漏下往往更好误导性很强。import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler df pd.read_csv(station_hourly.csv, parse_dates[timestamp]) df df.sort_values(timestamp).reset_index(dropTrue) feature_cols [temp, rh, wind_speed, wind_sin, wind_cos, pm25, pm10, no2, so2, co, o3] target_cols [pm25, pm10, no2, so2, co, o3] n len(df) train_df df.iloc[: int(n * 0.8)] val_df df.iloc[int(n * 0.8) : int(n * 0.9)] test_df df.iloc[int(n * 0.9) :] # 关键只用训练集fit验证/测试集只transform scaler_x StandardScaler().fit(train_df[feature_cols]) scaler_y StandardScaler().fit(train_df[target_cols])这里的逻辑是所有统计量来自训练集保证验证集和测试集零信息泄漏。feature_cols中同时包含气象特征与滞后浓度特征这是标准的自回归外生输入结构wind_sin和wind_cos是风向处理后的分量稍后说明。窗口构造函数把连续时间序列切成监督学习样本。每个样本输入过去24小时的特征序列输出未来24小时的六项浓度序列def make_sequences(df, feature_cols, target_cols, scaler_x, scaler_y, in_steps24, out_steps24): x scaler_x.transform(df[feature_cols].values) y scaler_y.transform(df[target_cols].values) X, Y [], [] for i in range(len(df) - in_steps - out_steps 1): X.append(x[i : i in_steps]) Y.append(y[i in_steps : i in_steps out_steps]) return np.array(X), np.array(Y) X_train, Y_train make_sequences(train_df, feature_cols, target_cols, scaler_x, scaler_y) X_val, Y_val make_sequences(val_df, feature_cols, target_cols, scaler_x, scaler_y) X_test, Y_test make_sequences(test_df, feature_cols, target_cols, scaler_x, scaler_y)in_steps选24小时是因为污染物浓度有明确日周期少于24就没有覆盖完整的昼夜循环。out_steps取24对应未来一天的中期预报如果目标改成48小时预报in_steps建议同步拉长到48让模型看到与输出长度相当的历史信息否则预测曲线会出现明显的相位滞后。Y的形状是(num_samples, out_steps, 6)六个任务共享同一个时间输出维度。3.2 共享编码器用TCN还是Transformer/LSTM对小时级空气质量数据时序主干的选择直接影响训练速度和预测误差。LSTM的优点是序列建模通用性强但逐点递归的结构让输入长度超过48步后计算开销明显增大反向传播路径也长。TCN用因果膨胀卷积实现时间依赖训练时并行度高没有梯度沿时间步逐点衰减的问题通过指数膨胀的卷积核可以覆盖很长的感受野。Transformer在空气质量时序预测上也有人用自注意力机制能捕捉长距离依赖但数据需求量较大。单站点一小时粒度数据一年只有8760个样本输入窗口几十个小时Transformer相对TCN的优势发挥不出来却要承担更多的参数和训练不稳定风险。我一般优先选TCN只有拿到多站点多年数据、输入窗口达到96个时刻以上时才把Transformer加入对比。import torch import torch.nn as nn class AirQualityMultiTask(nn.Module): def __init__(self, n_features, hidden_size128, kernel_size3, out_steps24, n_tasks6, dropout0.2): super().__init__() self.channel_conv nn.Conv1d(n_features, hidden_size, kernel_size1) self.tcn_layers nn.ModuleList([ nn.Conv1d(hidden_size, hidden_size, kernel_sizekernel_size, dilation2 ** i, padding(kernel_size - 1) * 2 ** i) for i in range(3) ]) self.norm nn.LayerNorm(hidden_size) self.dropout nn.Dropout(dropout) self.gelu nn.GELU() self.pool nn.AdaptiveAvgPool1d(1) self.heads nn.ModuleList([ nn.Sequential( nn.Linear(hidden_size, hidden_size // 2), nn.GELU(), nn.Linear(hidden_size // 2, out_steps), ) for _ in range(n_tasks) ]) def forward(self, x): # x: (batch, in_steps, n_features) x x.transpose(1, 2) # (B, F, T) x self.channel_conv(x) # (B, H, T) for conv in self.tcn_layers: res x x self.gelu(conv(x)) x self.dropout(x) x x[..., : res.size(2)] res # 残差连接长度裁剪保证可加 x x.transpose(1, 2) # (B, T, H) x self.norm(x) # 对每个时间步的特征维做归一化 x x.transpose(1, 2) # (B, H, T) x self.pool(x).squeeze(-1) # (B, H) return [head(x) for head in self.heads] # 6个 (B, out_steps)模型结构上channel_conv先把输入特征投影到隐藏维度降低后续卷积感受野计算的负担。三个TCN卷积层的膨胀系数分别为1、2、4配合3的卷积核大小输入长度在24以上时感受野可以完整覆盖。LayerNorm放在池化之前对每个时间步的隐藏特征做归一化稳定多任务头输入的数据分布。池化用AdaptiveAvgPool1d压缩时间维度比只取最后一个时间步能保留更多全序列信息。六个任务头各自独立输出未来24小时浓度序列这一步没有直接输出单一数值是因为长期预报需要看到浓度演变曲线而不是一个平均结果。3.3 不确定性加权损失六个任务的损失怎么合并多任务损失的关键是让六个任务的梯度方向合理合并。固定权重的手调成本高换站点就要重新标定。我在实际项目里多数情况直接用不确定性加权这是一种噪声自适应加权六个任务各自维护一个可学习噪声参数log_var训练中自动寻找平衡点。import torch.nn.functional as F class UncWeightedLoss(nn.Module): def __init__(self, n_tasks6): super().__init__() self.log_vars nn.Parameter(torch.zeros(n_tasks)) def forward(self, preds, targets): total 0.0 for i, (p, t) in enumerate(zip(preds, targets)): mse F.mse_loss(p, t) lv self.log_vars[i] total 0.5 * torch.exp(-lv) * mse 0.5 * lv return total公式背后的机制是如果任务i的MSE一直偏大log_var会在训练中变大exp(-log_var)随之变小任务权重下降防止这个任务主导整个模型。反过来一个已经收敛的任务MSE很小log_var倾向于变小权重保持在较高水平但log正则项约束了权重上限任务不会因为噪声估计过小而失控。训练前5到10个epoch这个权重会明显震荡这是正常现象。如果某个log_var持续增大且没有收敛迹象优先检查对应污染物标签的数据质量而不是急着改损失函数。3.4 训练循环、梯度裁剪与早停多任务训练循环里最容易被忽略的是梯度裁剪。六个任务的梯度叠加后范数很容易超过单任务模型的水平训练后期出现指标震荡直接把之前调好的学习率全部打乱。clip_grad_norm_设为1.0能有效控制这一步。优化器用AdamW配合1e-3初始学习率先在10个epoch内保持之后验证集指标不下降时把学习率乘0.5。def train_one_epoch(model, loader, optimizer, loss_fn, device): model.train() total_loss 0.0 for x_batch, y_batch in loader: x_batch x_batch.to(device) y_batch y_batch.to(device) # (B, out_steps, 6) preds model(x_batch) # 6个 (B, out_steps) y_list [y_batch[..., i] for i in range(6)] loss loss_fn(preds, y_list) optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() total_loss loss.item() return total_loss / len(loader)y_batch按最后一个维度拆成六列对应六项污染物的浓度序列。损失函数接收的preds和targets都是列表结构长度都是6。梯度裁剪放在backward之后、step之前裁剪的是整个模型的全部参数不只是共享层。早停判断以验证集六任务平均MAE为准连续12个epoch不下降就恢复到最佳模型参数。评估时六个任务的MAE和RMSE分开输出不要只看一个总指标。臭氧的误差如果集中在午后峰值时段单独看全天平均MAE发现不了需要按小时分组计算误差分布这种细致的错误定位在动辄几十轮训练的长周期项目里能省大量时间。参数建议值说明in_steps / out_steps24 / 2424小时中期预报长预报同步加长hidden_size128共享TCN隐藏宽度TCN层数3膨胀系数1、2、4初始学习率1e-3 AdamW10 epochs后按需衰减clip_grad_norm1.0防止多任务梯度爆炸早停patience12基于验证集六任务平均MAEbatch_size64单站点样本量约8000条64够稳4. 多任务空气质量预测模型落地特征对齐、负迁移排查与AQI换算4.1 风向必须拆成sin和cos风速特征保留滞后风向是环形变量直接拿0到360的数值喂给模型会引入巨大误差。359度与1度在数值上差了358模型会把东北风和正北风当两个极端方向处理而实际上它们几乎同一个方向。常见的处理是拆成风向正弦与余弦两个特征合成风向向量。风速与污染扩散的关系不是同一时刻对应的上游输送对本地浓度的影响通常滞后2到6小时特征窗口后部多保留几小时的风场信息比单纯增加模型深度更有效。df[wind_sin] np.sin(np.deg2rad(df[wind_dir])) df[wind_cos] np.cos(np.deg2rad(df[wind_dir])) # 风速和风向分量输入滞后2小时的版本 df[wind_speed_lag2] df[wind_speed].shift(2) df[wind_sin_lag2] df[wind_sin].shift(2) df[wind_cos_lag2] df[wind_cos].shift(2)风向分量与风速本可以合并成u、v风矢量但在多任务共享特征里保留分量形式更直观任务头可以各自学习不同污染物对风场的差异化响应。shift产生的NaN直接丢弃对应行避免让模型学习缺失模式。4.2 负迁移判断与任务分组多任务不是任务越多越好。六项污染物浓度同时预测通常不会互相干扰但一旦把AQI等级这类离散分类任务加入同一个硬共享模型负迁移很容易出现。判断方法是训练一个单任务基线对比多任务与单任务在同一个污染物目标上的MAE如果多任务反而差超过5%说明该任务与其他任务发生了冲突。处理方式有两种。把冲突任务从共享网络中移出单独建模损失一定共享信息但换来稳定性或者降低冲突任务的初始权重先让共享层在其他任务上收敛训练中期再逐步提高该任务权重。空气质量项目里回归类浓度任务之间的冲突远小于回归与分类的冲突AQI等级预测用单独的分类头会更省事。损失权重调度本身也是可选手段前文实现中不确定性加权已经能覆盖大部分冲突情况。4.3 输出浓度再换算AQI而不是直接回归AQIAQI是一个分段线性函数每个污染物的浓度区间对应不同的AQI分指数IAQI分段边界是硬阈值。直接用AQI作为回归目标模型在分段点附近很难学因为梯度在边界处不连续边界两侧相同的数值差会产生截然不同的损失。正确做法是让模型先输出六项污染物的浓度值也就是多任务模型本身的输出口径在推理阶段再按公式换算成AQI。def concentration_to_aqi(c, pollutantpm25): if pollutant pm25: bp [(0, 35, 0, 50), (35, 75, 50, 100), (75, 115, 100, 150), (115, 150, 150, 200), (150, 250, 200, 300), (250, 350, 300, 400), (350, 500, 400, 500)] # 其他污染物按各自标准填入对应的浓度分段 for c_lo, c_hi, aqi_lo, aqi_hi in bp: if c_lo c c_hi: return (aqi_hi - aqi_lo) / (c_hi - c_lo) * (c - c_lo) aqi_lo return 500最终发布时取六项IAQI的最大值作为当日AQI对应污染物作为首要污染物。部署环节还有一处细节值得留心推理服务加载模型后标准化参数必须使用训练时保存的scaler而不是在服务里基于历史数据重新计算。污染物分布逐年变化重新计算的均值和方差会让输入分布偏移导致预测浓度整体偏大或偏小这种错误很隐蔽排查起来要花不少时间。把scaler和模型权重打包存成同一个版本文件是成本最低的防错手段。本文还有配套的精品资源点击获取