
简介面向需要研究郑州地区空气质量预测的Python开发者与数据科学学习者项目以完整源码形式给出了从历史数据处理、模型构建到训练评估与可视化的一体化实现。资源共20个文件包含4个Python核心代码数据预处理、模型定义、训练与预测、5个XML工程配置、5个文本说明、3张结构/损失/结果分析图以及HDF5权重与数据文件压缩包约652KB适合作为环境科学相关课程设计或毕业设计的参考方案。模型围绕郑州本地气象与污染物数据展开便于读者理解输入特征组织、模型训练流程和结果展示逻辑。目前已有396人学习浏览可辅助快速上手此类型预测项目。1. 郑州空气质量预测源码包能训练、能预测、能自己改城市的数据闭环郑州的空气质量数据有一个很典型的现象污染过程会持续数天浓度曲线有明显的自相关相隔几小时的数据通常不是独立的。所以基于历史序列做预测比单纯拿当天数值外推更可靠。这套源码包就是一个完整的本地训练方案不是只有几张效果图的展示项目train_model.py 负责训练predict.py 负责预测model_weights.h5 是训练后保存的权重data.txt 是历史序列核心输入。它能解决的问题很实在——给定过去一段时间的浓度变化输出未来一段时间的变化趋势直接跑通能得到 loss 曲线和预测图。适合三类人一是做课程设计或毕业设计的学生二是刚接触 LSTM 时序预测的开发者三是手里有其他城市数据、想替换后重训练的工程师。2. 文件分工与数据流从 readme.txt 跑到 model_weights.h5把源码包解压后第一感觉是文件多且杂Python 脚本、PNG 图片、HDF5 权重、txt 文本、XML 配置混在一起。真正决定这个项目能不能跑起来的只有两层数据层和模型层。数据层是 data.txt 输入原始序列x_params_list.txt 与 y_params.txt 保存归一化参数模型层是 train_model.py、model.py、predict.pyutils.py 则是数据层和模型层之间的胶水。2.1 先读两个文本readme.txt 和 record.txt 里藏着运行顺序我拆这类源码包的习惯是打开后先看 readme.txt再看 record.txt最后才碰代码。readme.txt 会写明环境依赖和运行顺序record.txt 记录上一次训练用的轮数、样本窗口和误差指标。即使你只打算做预测也必须看归一化参数和权重的对应关系否则输出值和原始数据对不上量纲。文件在流程中的位置作用data.txt输入起点提供原始时间序列一行一个采样值缺失或异常值会直接带进训练utils.py预处理层负责读取、归一化、切窗等辅助操作训练和预测都会调用model.py模型定义定义 LSTM 网络结构训练和预测共用同一个结构train_model.py训练入口读数据、构造样本、训练并保存权重与归一化参数model_weights.h5训练产物模型权重预测阶段直接加载x_params_list.txt / y_params.txt伴随产物保存归一化的边界值预测时用来还原真实量纲loss.png / model_struct.png结果图训练收敛曲线与模型结构示意图用来判断训练是否正常数据流可以简单写成一条链训练阶段是 data.txt → utils.py 预处理 → model.py 构建网络 → train_model.py 执行 fit → 生成 model_weights.h5 与参数文件预测阶段是 model_weights.h5 加上参数文件 → model.py 重建网络 → predict.py 输出未来值。这里有个值得注意的设计预测阶段不重新训练只复用权重和归一化参数所以这两个文本文件和权重文件是绑定的关系少一个预测结果就会失真。2.2 data.txt 到归一化参数先确认数据范围和缺失值拿到 data.txt 后不要急着训练先看数据行数和数值范围。郑州的空气质量数据如果存的是 AQI 指数量级一般在 20 到 500 之间如果存的是 PM2.5 浓度正常范围在 10 到 300 之间。数值范围能帮你判断这份数据的单位也能帮你判断训练时归一化参数是否合理。# 检查 data.txt 的数据长度、最小最大值和缺失情况 python -c import numpy as np; dnp.loadtxt(data.txt); print(d.shape, d.min(), d.max(), np.isnan(d).sum())这条命令会输出三个关键值数据维度、最小最大值、NaN 数量。只要 NaN 数量不是 0训练时 loss 就可能变成 NaN必须先做填充或删除。我一般会再顺手画一条折线图看数据是逐小时还是逐天采样这个节奏直接决定后面的 look_back 参数怎么设。# 用 h5py 查看权重文件里有哪些层确认模型结构和 model.py 是否一致 import h5py with h5py.File(model_weights.h5, r) as f: print([key for key in f.keys()])这段代码输出的是权重文件里的层级名称。如果输出里的层数或名字和 model.py 不一致加载权重时会直接报错。新手最容易忽略这一点只换 model.py 的结构而不重新训练然后拿旧权重硬加载结果模型结构不匹配。2.3 四个 Python 脚本的职责训练、模型、预处理、预测谁依赖谁train_model.py 是入口负责串起整个流程model.py 只做网络结构定义本身不训练utils.py 里的函数会被 train_model.py 和 predict.py 共同 importpredict.py 负责加载权重、读入新数据、输出预测结果。这四个脚本的依赖关系是train_model.py 依赖 utils.py 和 model.pypredict.py 依赖 utils.py 和 model.pymodel.py 不依赖任何自定义脚本。# 推荐运行顺序先训练后预测训练前确认没有遗留的旧权重干扰 python train_model.py python predict.py如果你只是复现训练过程执行第一行就够了。执行 train_model.py 前建议把原来的 model_weights.h5 备份一下因为训练会直接覆盖权重文件。至于 .idea 目录下的 XML 和 iml 文件只影响 PyCharm 打开项目的方式不影响训练和预测可以完全忽略。3. 模型设计的关键结构LSTM 时间窗与训练超参数这套源码包的核心预测逻辑是时序建模。空气质量数据不是独立的随机变量今天的浓度和过去几十个小时的浓度高度相关尤其是静稳天气下污染物会持续累积。因此模型需要具备“记住过去一段状态”的能力这正是 LSTM 这类循环网络擅长的事情。3.1 为什么这里更该用 LSTM 而不是普通回归如果只拿当天的气象条件和污染源排放做线性回归模型很难捕捉污染过程的持续性。郑州的霾过程往往持续 3 到 5 天凌晨的低风速和高湿度会让 PM2.5 浓度缓慢爬升白天扩散条件变好后又逐步下降。这种长期依赖关系普通全连接网络很难直接表达因为它把每个时刻当成独立特征处理。LSTM 通过门控机制保留历史信息遇到突变时又会丢弃旧状态对空气质量这类缓慢变化的时间序列比较合适。源码包里出现 model_weights.h5 和时序文本从结构上看也更偏 LSTM 方案而不是 CNN。CNN 更适合空间特征提取单序列浓度预测用它的收益不明显。3.2 model.pyLSTM Dropout Dense 的定义与参数选择model.py 的结构在类似项目中通常写成下面的形式。代码不复杂但每一层的意义要清楚后面改多特征输入也要从这里下手。# model.py: 基于 LSTM 的单变量多步预测结构 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout def build_model(look_back24, future_steps1, lstm_units64): model Sequential([ LSTM(lstm_units, input_shape(look_back, 1), return_sequencesTrue), Dropout(0.2), LSTM(max(16, lstm_units // 2), return_sequencesFalse), Dense(32, activationrelu), Dense(future_steps, activationlinear) ]) model.compile(optimizeradam, lossmse, metrics[mae]) return model第一层 LSTM 设置了 return_sequencesTrue目的是让输出保持完整的时间步序列方便第二层 LSTM 继续处理时序信息。中间插入 Dropout(0.2) 是为了防止过拟合空气质量数据噪声比较大污染物浓度受风向、湿度、排放的随机影响明显不加 Dropout 训练后期验证 loss 容易反弹。第二层 LSTM 把序列压成一个向量后面接 Dense 层做非线性映射最后一层 Dense 输出 future_steps 个预测值。激活函数用 linear 而不是 relu是因为浓度值经过归一化后在 0 到 1 之间不需要强制非负linear 的输出范围更灵活。3.3 look_back、units 和 epochs训练前先定好这三件事训练前最重要的不是急着调参而是先确定三个基本量look_back 是看多长的历史窗口future_steps 是预测未来多少个时间点lstm_units 是网络宽度。它们之间的关系直接影响样本数量和模型容量。参数推荐值说明look_back24 或 16824 表示用过去 24 个采样点预测适合逐小时数据168 表示过去 7 天适合周期明显的序列future_steps1 或 241 是预测下一个采样点24 是预测未来一天输出维度会随它变化lstm_units32 到 128数据量小时优先 32 或 64units 翻倍参数量也翻倍dropout0.1 到 0.3数据噪声大用 0.2过拟合明显再上调batch_size32 或 64几百条样本时 32 更稳定epochs50 到 200配合早停最好不要硬跑满这里有个容易忽略的细节样本数量等于 len(scaled) - look_back - future_steps 1。look_back 设得越大能构造的样本数就越少。如果 data.txt 只有几百行拿 168 做 look_back 会直接砍掉一大半样本。数据量不够时小一点的时间窗反而比强行拉长历史效果更好。4. 完整复现训练数据归一化、切窗与收敛判断训练脚本不是直接把 data.txt 扔给模型中间要经过三个步骤归一化、切窗、按时间顺序划分训练集和验证集。每一步出错都会让结果变得不可解释尤其是归一化和切窗的顺序。4.1 读数据与 MinMax 归一化一个不能跳过的预处理空气质量浓度的绝对值受单位影响很大AQI 可能是 300PM2.5 浓度可能是 80直接放进模型会让数值较大的特征支配梯度更新。源码包里把归一化参数单独存成 x_params_list.txt 和 y_params.txt说明训练时用的是 MinMax 归一化把数据压到 0 到 1 之间。# utils.py 中的归一化与反归一化对应 x_params_list.txt / y_params.txt def minmax_fit(series): arr series.reshape(-1, 1) return arr.min(axis0), arr.max(axis0) def minmax_transform(series, min_val, max_val): return (series - min_val) / (max_val - min_val 1e-8) def minmax_inverse(series, min_val, max_val): return series * (max_val - min_val 1e-8) min_val这段代码做了三件事minmax_fit 计算序列的最小值和最大值minmax_transform 做归一化minmax_inverse 做反归一化。加 1e-8 是为了防止某一段数据最大值等于最小值时出现除零。x_params_list.txt 保存的就是输入数据的最小值和最大值y_params.txt 保存的是预测目标的最小值和最大值。为什么不用 StandardScaler 那种均值方差归一化因为 MinMax 对浓度这种非负序列更直观反归一化时只需乘差再加底不容易出错。4.2 切窗与顺序划分时间序列不能随机打乱切窗是把一长串时间序列变成监督学习样本。给定 look_back24代表用第 0 到第 23 个点预测第 24 个点然后窗口右移一位用第 1 到第 24 个点预测第 25 个点。滑动窗口构造出的 X 形状是 (样本数, look_back, 1)y 形状是 (样本数, future_steps)。# 切窗data.txt 转成 X/y注意循环上界 X, y [], [] for i in range(len(scaled) - look_back - future_steps 1): X.append(scaled[i:i look_back]) y.append(scaled[i look_back:i look_back future_steps]) X np.array(X).reshape(-1, look_back, 1) y np.array(y).reshape(-1, future_steps) # 按时间顺序切分前 80% 训练后 20% 验证 split int(len(X) * 0.8) train_x, test_x X[:split], X[split:] train_y, test_y y[:split], y[split:]切窗后必须按顺序划分不能用 sklearn 的 train_test_split 并设置 shuffleTrue。时间序列样本之间存在重叠窗口打乱后训练集和验证集会包含几乎相同的时间片段模型相当于提前看到了验证集的内容。验证集 loss 会很低但真实预测时效果很差。这个坑在时序项目里非常常见。源码包里的 loss.png 如果表现正常大概率就是按顺序切分的。4.3 训练、保存和看 loss.png什么样的曲线才算收敛训练主流程在 train_model.py 里执行 fit 之后把权重和归一化参数保存下来。# train_model.py 的拟合与保存部分 history model.fit(train_x, train_y, validation_data(test_x, test_y), epochs80, batch_size32, verbose2) model.save_weights(model_weights.h5) np.savetxt(x_params_list.txt, x_params) np.savetxt(y_params.txt, y_params)注意 save_weights 只保存权重不保存模型结构。预测时要用 model.py 重新构建网络然后把权重加载进去。这也是为什么 model.py 和 model_weights.h5 必须放在同一目录下且结构不能随意改动。训练结束后看 loss.png重点看两条曲线train_loss 持续下降val_loss 也在下降且差距不大说明收敛正常。如果 val_loss 先降后升就是过拟合优先调大 dropout 或减少 epochs。如果 train_loss 和 val_loss 都在高位震荡不下降先检查归一化是否做了再看学习率是否过大。record.txt 里建议手动追加一条训练记录把 look_back、units、mae 写清楚不然过两周再回来看图完全想不起来用的什么参数。# 追加一次训练记录方便后面对比 echo look_back24 units64 mae8.30 val_mae9.12 record.txt5. 避坑指南复现时最常见的五个坑这部分是从实际复现中总结出来的问题基本覆盖了拿到这个源码包后最容易翻车的五个位置。每条按现象、原因、解决来写复现时可以直接对号入座。5.1 训练阶段数组重排溢出与 loss 变成 NaN现象train_model.py 运行到 X np.array(X).reshape(-1, look_back, 1) 时报错 “cannot reshape array of size x into shape (?, 24, 1)”。原因数据长度和切窗循环上界对不上。最常见的是数据尾部多出几行没有被完整切进任何样本或者把 look_back 和 future_steps 搞混循环范围算窄了导致 X 的元素总数不等于样本数乘以 look_back。解决打印 len(data) 和 len(X)手工核对样本数是否等于 len(data) - look_back - future_steps 1。我一般会在切窗后面加一行断言assert len(X) len(data) - look_back - future_steps 1过不了就先看数据行数。现象训练过程中 loss 变成 NaN或者前几个 epoch 就出现巨大跳变。原因data.txt 里存在 NaN 或 Inf或者数据的数值范围异常。MinMax 归一化时如果最大值和最小值是 nan算出来的所有值都是 nan。另一个原因是学习率默认值偏大碰上浓度序列里的极端峰值梯度直接溢出。解决先用 np.isnan(data).any() 和 np.isfinite(data).all() 检查数据缺失值用前后均值填充把优化器的学习率显式设置为 0.001 或 0.0001。不要以为默认 adam 就一定稳批量数据里有极端值时并不保险。现象训练 loss 很低但验证集 loss 高预测曲线和真实曲线明显错位。原因切窗后用了 train_test_split 并开了 shuffle把时间顺序打乱了。相邻样本窗口有重叠乱序后验证集里混进了与训练集几乎一样的时间片段模型实际上是在“背答案”同时对长期时间依赖的学习也被破坏了。解决只用顺序切分前 80% 数据训练后 20% 数据验证。时序预测里不要贪图随机打乱带来的多样性。5.2 预测阶段权重结构不匹配与反归一化遗漏现象predict.py 加载 model_weights.h5 时报错提示权重与模型结构不匹配。原因model.py 被改过比如 lstm_units 从 64 改成 128或者多接了一个 Dense 层但权重文件还是旧的。Keras 加载权重时按层名和形状严格匹配任何不一致都会直接中断。解决要么把 model.py 改回训练时的结构要么用当前结构重新训练并重新保存权重。改模型前先确认你确实需要改结构否则旧权重就是白训的。这个源码包里权重文件和模型结构本来就是绑定的预测前先打印一下 h5 里的层名和 model.py 对照。现象predict.py 输出的预测值全都在 0 到 1 之间曲线像一条压扁的水平线和真实浓度差了一个量级。原因模型输出是归一化后的值predict.py 没有用 y_params.txt 做反归一化。训练时把 y 压到了 0 到 1预测时直接拿 0 到 1 的值画图当然对不上。解决预测输出后调用 minmax_inverse并把 y_params.txt 里的最小值和最大值传进去。如果 y_params.txt 已经丢了不要自己猜范围回到训练阶段重新生成。这个文件很小以后每次训练完记得单独备份。现象换一个城市的数据直接预测效果比郑州本地差一大截。原因模型是在郑州历史数据的分布上训练的权重里记录的规律包含了当地的风速、温度、湿度、排放结构。不同城市的数据量级和采样频率不同直接用旧权重预测新城市等于拿一套旧标定去解释新环境。解决把新城市数据整理成和 data.txt 同样格式后重新训练。如果只是想验证模型结构可以先用少量 epoch 跑通流程确认效果再拉长训练轮数。数据单位也要确认AQI 和 PM2.5 浓度范围完全不是一回事先画折线图看清量级再动手。6. 进阶验证与接口化多特征、滚动回测和自动预测训练跑通只是第一步真正想把这套模型用在课程设计或日常空气质量分析里还需要做两件事扩展特征和验证稳定性。6.1 多特征输入扩大 input_shape 的三个改动点data.txt 如果只有一列模型只能看到浓度自身的历史。实际影响空气质量的因素还有风、湿度、温度和前一天排放把这些列拼进来能明显提升预测上限。改动点有三个data.txt 变成多列归一化时逐列计算 min 和 maxmodel.py 的 input_shape 第二维从 1 改成特征数。# 多特征扩展X 的形状从 (样本数, look_back, 1) 变成 (样本数, look_back, 7) feature_num 7 # 污染物 气象要素 X np.array(X).reshape(-1, look_back, feature_num) y np.array(y).reshape(-1, future_steps) # model.py 里的 input_shape 同步调整 LSTM(lstm_units, input_shape(look_back, feature_num), return_sequencesTrue)多特征训练时归一化必须逐列做不能把风和浓度合并计算 min 和 max否则数值小的特征会被直接压没。6.2 滚动回测和定时预测让模型经得起真实使用静态训练集上验证一次并不够更稳的做法是滚动回测固定一个训练窗口预测未来一段时间然后窗口向右移动把真实值并入下一轮训练。这样检验的是模型持续预测能力而不是单次幸运命中。# 滚动回测骨架每滚动一次用截止到当前时间点的数据训练并预测 for start in range(0, len(scaled) - window_len - horizon, step): train_block scaled[start:start window_len] test_block scaled[start window_len:start window_len horizon] # 用 train_block 训练或继续拟合然后预测 test_block 对应时间段 # 下一轮 start 右移真实值被纳入历史窗口实际做成每日自动预测也很简单每天早上用截至当天的数据重新训练一轮保存权重然后预测未来 24 小时。这类单变量 LSTM 模型在本地 CPU 上几分钟就训练完不需要 GPU。从那以后我每次拿到带 h5 权重和文本参数的时序项目都会先跑一遍 predict.py 再谈训练先确认归一化参数和权重的匹配关系再动任何结构。希望你拿到这套源码包后也按这个顺序来能少走很多弯路。本文还有配套的精品资源点击获取