ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python+LSTM空气监测预测系统:从数据清洗到注意力机制实战

2026/9/23 11:20:40 拓冰建站 浏览量
Python+LSTM空气监测预测系统:从数据清洗到注意力机制实战 简介这份资源是面向计算机相关专业在校学生、教师及企业员工的空气质量监测与预测系统完整项目源码采用Python、Django、pandas、numpy与LSTM技术栈实现从pm25.csv读取数据经pandas完成数据清洗处理后借助LSTM神经网络进行空气质量预测适合作为课程设计、毕业设计、作业或项目初期立项演示的参考方案。压缩包共260个文件约6.99MB以164个scss样式文件、23个js脚本、15个py源码、8个html页面及csv数据文件为主另含sqlite3数据库与md说明文档前端页面涵盖当前数据、分析与省份展示等模块目录结构清晰便于按模块检索学习。目前已有131人学习下载。项目代码均经过测试运行成功答辩评审平均分达96分读者可据此掌握数据读取、预处理、模型搭建与预测的完整流程也可在现有代码基础上修改扩展实现其他功能。1. 空气监测预测系统从传感器数据到LSTM预测的完整落地路径很多做环境监测的团队都遇到过这样的场景手头攒了几个月甚至几年的PM2.5、PM10、SO2、NO2、CO、O3六项污染物的小时级数据领导或甲方要求“做个预测”但真正动手时才发现——数据有缺失、时间戳不连续、特征之间量纲差异巨大、模型训完在测试集上看着还行一到真实场景就翻车。这套基于PythonLSTM的空气监测及预测系统解决的正是从原始监测数据到可用预测模型的全链路问题。它适合有Python基础、想做时间序列预测但缺乏完整工程经验的开发者也适合需要快速搭建空气质量预警原型的环保信息化从业者。核心思路不复杂用Pandas做数据清洗与特征工程用PyTorch或Keras搭建LSTM网络用滑动窗口构造监督学习样本最终输出未来1至24小时的污染物浓度预测值。下面按实际落地顺序拆开讲。2. 数据管道搭建从原始CSV到LSTM可用的三维张量2.1 空气监测数据的典型脏乱差与清洗策略空气监测站导出的原始数据通常长这样时间列格式不统一有的带秒有的不带、六项污染物浓度列夹杂空值和负值、气象参数温度、湿度、风速、气压与污染物混在一张表里。直接丢给LSTM训练损失曲线会像心电图一样乱跳。我一般按以下顺序处理第一步统一时间索引。把时间列解析为datetime类型并设为DataFrame的index然后用asfreq(H)强制对齐到小时频率。这一步会把缺失的时间点显式暴露出来。第二步处理缺失值。空气监测数据的缺失分两种短时缺失1-3小时和长时缺失超过6小时。短时缺失用线性插值长时缺失直接标记为NaN并在后续构造样本时跳过。不要用均值填充长段缺失那会引入虚假的平稳性。第三步处理负值和异常值。监测设备在低浓度时段可能输出负值统一截断为0。对于超过物理合理上限的值比如PM2.5超过1000用前后各3个点的中位数替换。import pandas as pd import numpy as np def clean_air_quality_data(filepath): 清洗空气监测原始数据 参数: filepath: 原始CSV路径需包含时间列和六项污染物列 返回: 清洗后的DataFrame时间索引为小时频率 df pd.read_csv(filepath, parse_dates[timestamp]) df df.set_index(timestamp).sort_index() # 强制小时频率暴露缺失时间点 df df.asfreq(H) # 污染物列名 pollutants [PM2.5, PM10, SO2, NO2, CO, O3] for col in pollutants: # 负值截断 df[col] df[col].clip(lower0) # 短时缺失线性插值limit3表示最多连续插3个点 df[col] df[col].interpolate(methodlinear, limit3) # 长时缺失保持NaN后续构造样本时跳过 # 异常值处理超过物理上限的用滚动中位数替换 for col in pollutants: upper df[col].quantile(0.999) * 2 mask df[col] upper if mask.any(): rolling_med df[col].rolling(window7, centerTrue, min_periods1).median() df.loc[mask, col] rolling_med[mask] return df这段代码的关键参数是limit3和quantile(0.999)*2。limit3控制插值范围超过3小时的连续缺失不插值避免制造虚假数据。quantile(0.999)*2作为异常阈值比固定阈值更适应不同季节的浓度波动。清洗完的数据仍然保留NaN这是有意为之——LSTM训练时通过样本构造逻辑自动跳过含NaN的窗口。2.2 滑动窗口构造监督学习样本LSTM需要三维输入(样本数, 时间步长, 特征数)。空气监测预测的常见做法是用过去24小时的数据预测未来1小时或未来24小时。这里以预测未来1小时为例时间步长设为24特征包括六项污染物加温度、湿度、风速、气压共10维。构造样本时有两个容易翻车的点一是窗口跨越了NaN区域二是训练集和测试集划分时发生了数据泄漏。我的处理方式是先按时间顺序切分训练/测试集比如前80%训练后20%测试再各自独立构造滑动窗口绝不在切分前做全局归一化。def create_sequences(data, target_col, seq_length24, pred_length1): 构造LSTM监督学习样本 参数: data: 清洗后的DataFrame含污染物和气象列 target_col: 预测目标列名如PM2.5 seq_length: 输入时间步长默认24小时 pred_length: 预测步长默认1小时 返回: X: (样本数, seq_length, 特征数) y: (样本数, pred_length) feature_cols [PM2.5, PM10, SO2, NO2, CO, O3, temperature, humidity, wind_speed, pressure] values data[feature_cols].values targets data[target_col].values X, y [], [] for i in range(len(values) - seq_length - pred_length 1): window values[i:i seq_length] target targets[i seq_length:i seq_length pred_length] # 跳过含NaN的窗口 if np.isnan(window).any() or np.isnan(target).any(): continue X.append(window) y.append(target) return np.array(X), np.array(y)seq_length24对应24小时输入窗口这是空气污染物日变化周期的完整覆盖。pred_length1表示单步预测如果要预测未来24小时把pred_length改为24同时y的形状变为(样本数, 24)。注意np.isnan检查放在append之前含NaN的窗口直接丢弃而不是填充这是保证模型不学到虚假模式的关键。2.3 归一化为什么MinMaxScaler比StandardScaler更适合空气监测数据空气污染物浓度分布通常右偏PM2.5在重污染时段可能飙到正常值的10倍以上。StandardScaler假设近似正态分布遇到极端值会把大部分正常样本压缩到均值附近LSTM反而学不到正常波动模式。MinMaxScaler把数据缩放到[0,1]区间虽然对极端值敏感但配合前面的异常值处理实际效果更稳定。归一化必须只在训练集上fit然后transform测试集。我见过太多人把全量数据一起fit测试集信息泄漏导致评估指标虚高上线后直接翻车。from sklearn.preprocessing import MinMaxScaler def normalize_data(train_df, test_df, feature_cols): 训练集fit测试集transform避免数据泄漏 返回归一化后的数组和scaler对象用于反归一化 scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_df[feature_cols]) test_scaled scaler.transform(test_df[feature_cols]) return train_scaled, test_scaled, scalerfeature_range(0,1)是默认值显式写出来是为了提醒自己如果后续要换激活函数为tanh输入范围匹配[-1,1]可能更好但ReLU系列用[0,1]足够。scaler对象必须保存预测结果要反归一化回真实浓度才能计算MAE、RMSE这些指标。3. LSTM模型搭建与训练PyTorch实现的关键参数与训练技巧3.1 网络结构设计几层LSTM、多少隐藏单元、要不要Dropout空气监测预测的LSTM结构不需要太深。我的经验是单层LSTM加一个全连接输出层隐藏单元数在64到128之间就能覆盖大部分城市空气质量预测场景。层数超过2层后梯度消失问题加重训练时间翻倍但精度提升有限。如果数据量特别大超过5年小时级数据可以尝试2层LSTM第一层返回完整序列第二层只取最后时间步的输出。Dropout加在LSTM层和全连接层之间比例0.2到0.3。注意PyTorch的LSTM自带dropout参数但它只在多层LSTM时生效单层LSTM的dropout要手动加在输出上。import torch import torch.nn as nn class AirQualityLSTM(nn.Module): def __init__(self, input_size10, hidden_size64, num_layers1, output_size1, dropout0.2): 参数: input_size: 特征维度六项污染物四项气象10 hidden_size: LSTM隐藏单元数 num_layers: LSTM层数 output_size: 预测步长单步预测为1 dropout: 丢弃率 super(AirQualityLSTM, self).__init__() self.hidden_size hidden_size self.num_layers num_layers self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, # 输入形状(batch, seq, feature) dropoutdropout if num_layers 1 else 0 ) self.dropout nn.Dropout(dropout) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): # x: (batch, seq_length, input_size) h0 torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) c0 torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) out, _ self.lstm(x, (h0, c0)) # 取最后一个时间步的输出 out out[:, -1, :] out self.dropout(out) out self.fc(out) return outbatch_firstTrue让输入维度顺序为(batch, seq, feature)和前面构造的样本形状一致。h0和c0初始化为零这是标准做法如果数据有明显的长期依赖可以考虑用可学习的初始状态但空气监测场景下零初始化足够。out[:, -1, :]取最后一个时间步因为LSTM的最后一个隐藏状态聚合了整个24小时窗口的信息。3.2 训练循环损失函数、优化器、学习率调度空气监测预测用MSELoss或HuberLoss。MSELoss对异常值敏感如果数据里还有没清理干净的尖峰梯度会被带偏。HuberLoss在误差小于delta时等价于MSE大于delta时等价于MAE更鲁棒。我一般先用MSELoss跑一轮看损失曲线如果震荡严重就换HuberLoss。优化器用Adam学习率1e-3起步。配合ReduceLROnPlateau调度器验证损失连续5个epoch不下降就把学习率乘以0.5。早停策略也加上验证损失连续10个epoch不改善就停止训练保留验证损失最低的模型参数。from torch.utils.data import DataLoader, TensorDataset from torch.optim.lr_scheduler import ReduceLROnPlateau def train_model(model, X_train, y_train, X_val, y_val, epochs100, batch_size64, lr1e-3, patience10): 训练LSTM模型 参数: patience: 早停耐心值验证损失不改善的epoch数 device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) train_dataset TensorDataset( torch.FloatTensor(X_train), torch.FloatTensor(y_train)) val_dataset TensorDataset( torch.FloatTensor(X_val), torch.FloatTensor(y_val)) train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue) val_loader DataLoader(val_dataset, batch_sizebatch_size, shuffleFalse) criterion nn.HuberLoss(delta1.0) optimizer torch.optim.Adam(model.parameters(), lrlr) scheduler ReduceLROnPlateau(optimizer, modemin, factor0.5, patience5, verboseTrue) best_val_loss float(inf) best_model_state None no_improve 0 for epoch in range(epochs): model.train() train_losses [] for X_batch, y_batch in train_loader: X_batch, y_batch X_batch.to(device), y_batch.to(device) optimizer.zero_grad() output model(X_batch) loss criterion(output, y_batch) loss.backward() # 梯度裁剪防止LSTM梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_losses.append(loss.item()) model.eval() val_losses [] with torch.no_grad(): for X_batch, y_batch in val_loader: X_batch, y_batch X_batch.to(device), y_batch.to(device) output model(X_batch) val_losses.append(criterion(output, y_batch).item()) avg_val_loss np.mean(val_losses) scheduler.step(avg_val_loss) if avg_val_loss best_val_loss: best_val_loss avg_val_loss best_model_state model.state_dict().copy() no_improve 0 else: no_improve 1 if no_improve patience: print(fEarly stopping at epoch {epoch}) break if (epoch 1) % 10 0: print(fEpoch {epoch1}, Train Loss: {np.mean(train_losses):.4f}, fVal Loss: {avg_val_loss:.4f}) model.load_state_dict(best_model_state) return modelclip_grad_norm_(max_norm1.0)是LSTM训练的标配空气监测数据里偶尔的浓度尖峰会导致梯度突然增大不裁剪的话模型参数可能一步跳飞。HuberLoss(delta1.0)的delta参数控制MSE和MAE的切换点归一化后的数据误差通常在0到1之间delta1.0意味着大部分误差按MSE处理只有极端误差才降级为MAE。3.3 预测与反归一化把模型输出变回真实浓度模型输出的是归一化后的值必须用训练时保存的scaler反归一化。注意目标列在特征矩阵中的位置反归一化时只取对应列。def predict(model, X_test, scaler, target_idx, devicecpu): 预测并反归一化 参数: target_idx: 目标列在特征矩阵中的索引 model.eval() model model.to(device) with torch.no_grad(): X_tensor torch.FloatTensor(X_test).to(device) pred_scaled model(X_tensor).cpu().numpy() # 构造与scaler维度一致的数组进行反归一化 dummy np.zeros((len(pred_scaled), scaler.n_features_in_)) dummy[:, target_idx] pred_scaled.flatten() pred_real scaler.inverse_transform(dummy)[:, target_idx] return pred_realtarget_idx是PM2.5在feature_cols列表中的位置这里是0。dummy数组的作用是满足inverse_transform对特征维度的要求只把目标列填进去其他列置零反归一化后只取目标列。这个技巧在只预测单个污染物时很实用避免了为每个目标单独训练一个scaler。4. 避坑与排查空气监测LSTM预测的5个血泪教训4.1 现象验证损失比训练损失低很多但测试集MAE爆炸原因训练集和验证集划分时没有按时间顺序而是随机打乱。空气监测数据有强时间自相关性随机划分导致验证集里的样本在训练集中有“未来信息”泄漏验证损失虚低。测试集是真正未来的数据模型没见过MAE自然爆炸。解决严格按时间顺序切分。前80%做训练中间10%做验证最后10%做测试。切分前不要做任何全局统计量的计算。4.2 现象模型预测值几乎是一条直线不随输入波动原因归一化时用了全量数据的min和max而训练集只覆盖了部分浓度范围。测试集里出现训练集没见过的极端值时归一化后超出[0,1]区间LSTM的sigmoid/tanh激活进入饱和区输出恒定。解决MinMaxScaler只在训练集上fit。如果测试集出现超出训练范围的值要么截断到训练集的min/max要么改用RobustScaler。我一般选择截断因为极端值本身可能是异常。4.3 现象训练loss正常下降但预测结果总是滞后实际值1-2小时原因LSTM学到了“用当前值预测当前值”的捷径。输入窗口的最后几个时间步和目标值高度相关模型直接复制了最后一个时间步的输入作为输出没有真正学习时序模式。解决在输入窗口中增加时间特征小时、星期、月份让模型有额外信息区分“当前时刻”和“预测时刻”。另外可以把目标列从输入特征中移除强制模型从其他污染物和气象参数中学习。4.4 现象GPU显存溢出batch_size降到1还是OOM原因seq_length设得太大比如用了168一周小时数加上10维特征和64个隐藏单元单个样本的中间激活就占了几十MB。LSTM的BPTT需要保存所有时间步的激活显存占用与seq_length成正比。解决seq_length控制在24到48之间。如果确实需要更长历史用两层LSTM第一层用较大步长提取局部特征第二层用降采样后的序列。或者改用GRU参数量比LSTM少三分之一。4.5 现象多步预测时预测步长越长误差越大24小时预测完全不可用原因直接多步预测pred_length24让LSTM一次性输出24个值但训练样本中每个时间步的误差反向传播路径太长模型倾向于输出平均值。解决改用滚动预测。先训练单步预测模型预测出t1的值后把预测值追加到输入窗口末尾滑动窗口再预测t2循环24次。误差会累积但比直接多步预测好得多。另一个方案是seq2seq结构编码器用LSTM解码器也用LSTM但实现复杂度高空气监测场景下滚动预测性价比更高。5. 进阶技巧用注意力机制提升LSTM对污染事件峰的捕捉能力单层LSTM在平稳天气下预测PM2.5够用但遇到沙尘暴、烟花爆竹集中燃放这类突发污染事件预测值往往比实际低30%以上。原因是LSTM的隐藏状态对所有时间步一视同仁24小时窗口里真正有预测价值的是最近3-6小时的突变信息但LSTM把24小时的信息压缩到一个固定长度的向量里早期信息被稀释了。加一个注意力层能明显改善。具体做法LSTM输出所有时间步的隐藏状态(batch, seq, hidden)用一个线性层计算每个时间步的注意力权重softmax归一化后对隐藏状态加权求和再送入全连接层。这样模型可以自动学会“重点关注最近几小时”或“关注昨天同一时段”。class AttentionLSTM(nn.Module): def __init__(self, input_size10, hidden_size64, output_size1): super(AttentionLSTM, self).__init__() self.lstm nn.LSTM(input_size, hidden_size, batch_firstTrue) self.attention nn.Linear(hidden_size, 1) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): lstm_out, _ self.lstm(x) # (batch, seq, hidden) # 计算注意力权重 attn_weights torch.softmax(self.attention(lstm_out), dim1) # 加权求和 context torch.sum(attn_weights * lstm_out, dim1) return self.fc(context)self.attention是一个hidden_size到1的线性层输出每个时间步的未归一化权重。softmax(dim1)在时间步维度归一化保证权重和为1。context是加权后的向量维度为(batch, hidden)。这个改动只增加了hidden_size个参数训练成本几乎不变但在污染事件峰值的捕捉上MAE通常能降10%-15%。验证注意力是否有效的方法把attn_weights打印出来看模型在预测高浓度时是否给最近时间步分配了更大权重。如果权重分布均匀说明注意力没学到东西可能需要增加训练数据或调整学习率。我自己的习惯是先用单层LSTM跑通全流程记录基线MAE再加注意力对比提升幅度。如果提升不到5%说明数据里的突变模式不够多注意力机制发挥空间有限不如把精力花在补充气象特征或引入邻近站点的数据上。做空气监测预测这几年最大的教训是——模型结构再花哨数据质量不过关都是白搭。把清洗和特征工程做扎实单层LSTM也能跑出不错的结果。希望帮到你。本文还有配套的精品资源点击获取