SSA-LSTM空气质量预测:麻雀搜索算法优化LSTM超参数实践

发布时间:2026/10/3 3:07:43
SSA-LSTM空气质量预测:麻雀搜索算法优化LSTM超参数实践 简介这套基于麻雀搜索算法和长短时记忆网络的空气质量预测Python代码面向环境数据分析者与深度学习者用于解决时序预测中的超参数调优与精度提升问题。项目以西安空气质量数据为例依次提供多层感知机基准、长短时记忆网络基准、麻雀搜索算法寻优、最优参数重训和多模型对比共五个可独立运行脚本并附说明文档。压缩包共十六个文件包括五个Python脚本、五个优化过程图、四个MAT结果文件、一张数据表格和一份说明文档整体约七百余KB轻量易用。当前已有八百余人浏览学习足见其参考价值。读者可复用完整流程从基础模型到智能调参直至结果可视化无需自行搭建代码框架适应度曲线、学习率寻优和隐藏层节点优化图能直观展示优化效果便于改造为其他空气质量或一般时序预测研究。1. 基于SSA-LSTM的空气质量预测让麻雀搜索算法替LSTM调参基于SSA-LSTM麻雀搜索算法和LSTM的空气质量预测把“LSTM 超参数怎么定”这个玄学问题变成了一次可复现的优化过程。做时间序列预测的人多半被第一层节点数、第二层节点数、学习率、训练轮次折磨过——每一个参数都直接影响最终误差却没有一个万能公式能算出来。这份代码包在西安空气质量数据集上先跑 MLP 和普通 LSTM 作为基线再用麻雀搜索算法SSA替 LSTM 搜索最优超参数最后把三个模型的结果统一对比。麻雀搜索算法属于群体智能优化算法搜索效率比网格搜索高也不像随机搜索那样全凭运气适合正在做 LSTM 时间序列预测 Python 工程、想摆脱手动调参的研究生和工程师。2. 拆开 SSA-LSTM 代码包五个脚本的分工与西安空气质量数据的加载2.1 五个脚本和结果文件各负责什么打开ssa_lstm_air_quality目录能看到 5 个 Python 脚本、dataset和result两个文件夹以及 README。README 前面写的是环境依赖和运行顺序建议先读它再动手我补充一下脚本之间的数据流关系文件作用输出1_mlp.py训练 MLP 基线模型result/MLP_result.mat2_lstm.py训练普通 LSTM 基线模型result/lstm_result.mat3_ssa_optimize_lstm_params.py用 SSA 搜索 LSTM 四个超参数优化曲线图、result/ssa_para.mat4_ssa_lstm_use_params_from_3.py读取 ssa_para.mat用优化后的参数重新训练result/ssa_lstm_result.mat5_comparison.py加载三个 mat 结果做统一对比画图对比图这是一个非常标准的“先基线、再优化、最后对比”实验结构。1 和 2 负责提供下界参照——如果 MLP 误差已经很低LSTM 的必要性就要打问号如果 LSTM 比 MLP 好但好得有限SSA 优化的价值就需要谨慎评估。3 是核心它把超参数搜索建模成一个连续优化问题用 SSA 去逼近最优解而不是靠人肉网格搜索。4 负责把 3 找到的参数落到真实训练上避免在优化过程中直接保存偏差较大的中间模型。5 是收口脚本把三个 mat 文件读进来画成一张图方便直接判断谁更好。result目录下的MLP_result.mat、lstm_result.mat、ssa_lstm_result.mat分别是三个模型的预测值和真实值ssa_para.mat保存的是 SSA 搜索到的最优超参数。优化过程中生成的fitness curve.png、first hidden nodes optim.png、second hidden nodes optim.png、learning rate optim.png、itration optim.png是 SSA 迭代过程的记录图后面第 5 章会专门讲怎么读这些图。有一点要注意脚本之间通过 mat 文件传递数据意味着 1 到 4 必须按顺序跑完5 才能画出完整对比图如果只跑 1 和 5对比图里就会缺两条曲线。2.2 读取西安空气质量数据的正确姿势数据集是dataset/xi_an目录名暗示了这是西安站点的观测数据。拿到这样的目录我习惯先看一眼实际文件格式再决定用 pandas 还是别的工具读避免凭经验猜文件名导致路径报错。import os import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler # 先看目录里到底是什么格式 data_dir dataset/xi_an files os.listdir(data_dir) print(files) # 以常见的逐小时CSV为例取第一个csv文件 csv_files [f for f in files if f.endswith(.csv)] df pd.read_csv(os.path.join(data_dir, csv_files[0]), parse_dates[date]) # 用常见的六项污染物作为特征 feature_cols [SO2, NO2, PM10, O3, CO, PM2.5] df df[feature_cols].dropna().reset_index(dropTrue) # LSTM对输入量纲敏感归一化到[0,1] scaler MinMaxScaler(feature_range(0, 1)) scaled scaler.fit_transform(df.values) def sliding_window(data, lookback24): X, y [], [] for i in range(len(data) - lookback): X.append(data[i:i lookback]) # 预测下一时刻的PM2.5feature_cols里-1对应最后一列 y.append(data[i lookback, -1]) return np.array(X), np.array(y) X, y sliding_window(scaled, lookback24) # 时间序列必须按顺序切分不能随机打乱 split int(len(X) * 0.8) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:]代码逻辑说明parse_dates把时间列解析成 datetime 类型保证后面按行切分时顺序是对的MinMaxScaler把 SO2、NO2、PM10、O3、CO、PM2.5 六个指标全部压到 0 到 1 之间。空气质量数据里 PM10 浓度可能上百SO2 可能只是个位数如果不归一化LSTM 的梯度更新会被大数值特征主导小数值特征的信息基本学不到。sliding_window里的lookback24表示用过去 24 个时刻的观测预测下一个时刻我手动写循环是为了让窗口构造逻辑透明——每一步取[i, ilookback)作为输入取ilookback这一时刻的 PM2.5 作为标签。如果数据是小时级24 对应“用过去一天预测下一小时”是个合理起点换成日级数据lookback就要改到 7 或 30。切分这里有个关键动作split int(len(X) * 0.8)是按位置切不是用train_test_split随机切。时间序列一旦随机打乱训练集里会出现未来数据模型等于开了天眼测试集上的误差会严重失真。这是空气质量管理里做预测最常见的错误之一后面避坑章节还会再提。2.3 基线模型的训练循环与验证集划分数据准备好之后MLP 和普通 LSTM 的训练脚本大同小异区别只在模型结构。MLP 的输入要把(lookback, n_features)展平成一条向量LSTM 则保留三维结构作为时间步输入。训练时我一般会加validation_split用训练集尾部一小部分监测过拟合from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam model Sequential([ LSTM(64, return_sequencesTrue, input_shape(24, 6)), LSTM(32), Dense(1) ]) model.compile(optimizerAdam(learning_rate0.001), lossmse, metrics[mae]) history model.fit(X_train, y_train, epochs50, batch_size32, validation_split0.1, verbose1)validation_split0.1表示从训练集末尾切 10% 做验证Keras 会按顺序切而不是随机切这一点对时间序列友好。batch_size32在数据量不大时是比较稳的选择显存或内存吃紧就降到 16。metrics[mae]注册平均绝对误差训练结束后可以直接看history.history[val_mae]判断模型是否过拟合——如果 val_mae 连续十来个 epoch 上涨而训练 mae 还在下降就说明模型开始记住训练集噪声了这时候要减小节点数或提前停止而不是继续加训练轮次。我建议把 1 和 2 两个脚本连着跑一遍先确认基线误差在什么量级再进入第 3 章的 SSA 优化。基线误差如果本身已经很小后面 SSA 折腾半天的收益会很有限甚至会得到“优化前后差不多”的结论这属于正常现象不代表代码有问题。3. SSA 参数优化实现麻雀搜索算法在搜哪四个 LSTM 超参数3.1 为什么基线要用 MLP 和普通 LSTM以及超参数为什么难定MLP 没有记忆结构输入窗口被展平成一条向量模型学不到时间顺序关系只能把每个时刻当成独立特征。LSTM 通过遗忘门、输入门、输出门控制信息在时间维度的流动能捕捉到小时级别甚至天级别的长期依赖所以理论上更适合空气质量这类时序数据。先跑这两个基线是为了知道后续 SSA-LSTM 的误差改进空间到底有多大。难点在于 LSTM 的四个关键超参数之间相互耦合第一层节点数决定了模型能从原始序列里提取多少时间特征第二层节点数决定在更高抽象层面上的组合能力学习率决定参数更新的步长训练轮次决定拟合到什么程度。节点数太小欠拟合太大过拟合而且训练变慢学习率太大梯度震荡太小半天不收敛。这四个参数没有任何一个能靠理论公式直接算出来只能搜索。网格搜索把每个参数等间隔切分组合数随维度爆炸随机搜索省时间但命中率低SSA 这类群体智能算法用种群迭代的方式在连续空间里逼近最优解是目前做这件事比较划算的方案。3.2 麻雀搜索算法的核心更新逻辑麻雀搜索算法的灵感是麻雀种群在觅食时的分工发现者负责大范围探索食物源跟随者围着发现者进食警戒者负责发现危险并随机扰动避免整个种群陷入局部最优。放到超参数搜索里每个麻雀就是一个四维坐标点坐标值分别对应四个超参数适应度函数就是“用这组超参数训练 LSTM 后得到的验证集 MAE”。MAE 越低说明这个麻雀的位置越好。import numpy as np def ssa_optimize(objective_func, lb, ub, dim4, pop10, max_iter30, producer_ratio0.2): lb np.array(lb, dtypefloat) ub np.array(ub, dtypefloat) # 初始化种群每个个体是一个四维参数向量 positions np.random.uniform(lb, ub, (pop, dim)) # 评估初始适应度objective_func内部会构建LSTM并返回验证MAE fitness np.array([objective_func(p) for p in positions]) best_idx int(np.argmin(fitness)) best_pos positions[best_idx].copy() best_fit fitness[best_idx] for _ in range(max_iter): order np.argsort(fitness) producer_n max(1, int(pop * producer_ratio)) producer_idx order[:producer_n] scrounger_idx order[producer_n:] # 发现者在当前位置附近扰动负责探索新区域 for idx in producer_idx: positions[idx] np.random.uniform(-1, 1, dim) * (ub - lb) * 0.1 # 跟随者向当前全局最优位置移动 for idx in scrounger_idx: positions[idx] (best_pos - positions[idx]) * np.random.uniform(0, 1.5, dim) # 警戒者随机选一小部分个体做反向扰动 guard_n max(1, int(pop * 0.1)) for idx in np.random.choice(pop, guard_n, replaceFalse): positions[idx] * np.random.uniform(0.8, 1.2, dim) # 边界限制防止节点数或学习率跑到无意义区间 positions np.clip(positions, lb, ub) # 重新评估适应度 fitness np.array([objective_func(p) for p in positions]) if fitness.min() best_fit: best_fit fitness.min() best_pos positions[int(np.argmin(fitness))].copy() return best_pos, best_fit这是教学版简化实现真实工程里发现者的步长会随迭代次数衰减跟随者还会引入随机扰动防止过早收敛。np.clip(positions, lb, ub)这一行很关键——节点数如果被扰动成负数或学习率变成 0LSTM 根本没法训练所以每一代更新完毕必须把粒子拉回搜索范围。objective_func接收一个四维向量内部要完成“构建 LSTM → 训练若干 epoch → 在验证集上算 MAE → 返回 MAE”四步。这意味着 SSA 每评估一个个体就要完整训练一次 LSTM。pop10, max_iter30意味着最坏情况下要训练 300 个模型这也是为什么我强烈建议把求值函数里的 epochs 调小到 10 到 15让 SSA 先找趋势找到最优参数后再把 epochs 放大做最终训练。如果一上来就用 100 个 epochs 跑完整搜索CPU 机器上跑几个小时很正常而且收益未必比小 epochs 搜索更好。3.3 把四维向量解码成 LSTM 模型四个参数的范围怎么定SSA 搜索到的四维向量不能直接塞给 Keras需要先解码成模型结构。这里给出 3 号脚本里最常见的做法def build_lstm_from_params(params, lookback24, n_features6): first_units, second_units, lr, epochs params model Sequential([ LSTM(int(first_units), return_sequencesTrue, input_shape(lookback, n_features)), LSTM(int(second_units)), Dense(1) ]) model.compile(optimizerAdam(learning_ratefloat(lr)), lossmse, metrics[mae]) return model, int(epochs)int(first_units)是因为 SSA 产生的是连续浮点数而节点数必须是整数float(lr)则相反学习率必须是浮点数。return_sequencesTrue保证第一层 LSTM 输出完整的时间步序列供第二层消费如果去掉这一句第二层拿到的是最后一帧网络结构就变成“单层 LSTM 全连接”表达能力明显下降。这里常被误写成return_sequencesFalse后面避坑章节会展开说。四个参数的搜索范围我一般这样设参数含义搜索范围采样方式first_hidden_nodes第一层 LSTM 隐藏节点数4 到 128整数均匀second_hidden_nodes第二层 LSTM 隐藏节点数4 到 128整数均匀learning_rateAdam 学习率1e-4 到 1e-2对数均匀iteration训练轮次10 到 100整数均匀学习率必须用对数均匀采样。1e-4 到 1e-2 之间相差两个数量级线性均匀采样大概率只在 1e-3 附近反复取值SSA 种群的多样性会被浪费搜索效率大打折扣。np.random.uniform(np.log10(lb), np.log10(ub))再10 ** x取回来是这类优化里很常用的处理方式。节点数的范围 4 到 128 是经验值低于 4 模型表达力不够高于 128 在空气质量这种小数据集上容易过拟合且训练明显变慢。3.4 优化完成后如何用参数重训并保存 MAT3 号脚本跑完会把最优参数写进result/ssa_para.mat4 号脚本再从同一个文件读回来重建模型。中间用 mat 文件传递而不是让 3 直接重训是为了解耦3 只负责搜索4 只负责最终训练5 只负责画图每个脚本都能独立从磁盘读取输入不用层层 import出问题也好排查。import scipy.io as sio import numpy as np # 保存最优参数注意value必须是numpy数组 sio.savemat(result/ssa_para.mat, { first_units: np.array([best_pos[0]]), second_units: np.array([best_pos[1]]), lr: np.array([best_pos[2]]), epochs: np.array([best_pos[3]]) }) # 读取时也要转成标量再使用 params sio.loadmat(result/ssa_para.mat) first_units int(params[first_units].flatten()[0]) second_units int(params[second_units].flatten()[0]) lr float(params[lr].flatten()[0]) epochs int(params[epochs].flatten()[0])flatten()[0]这个写法很重要loadmat读出来的 dict 里每个 value 都是二维数组直接传给int()会报错提示“only size-1 arrays can be converted”必须先用flatten()拉平再取第一个元素。保存阶段如果直接传 Python 的 int 或 floatsavemat也会因为类型不匹配而保存失败或生成空变量。把值包装成np.array([...])是 scipy.io 使用里最稳的姿势。ssa_para.mat保存的四个字段名与 4 号脚本里读取的字段名必须严格一致这个包里的命名是first_units、second_units、lr、epochs我自己写的时候会先print(params.keys())确认字段名防止大小写不匹配导致 KeyError。3.5 训练完成后如何保存结果 MAT 文件4 号脚本在模型训练完、拿到测试集预测值之后会把预测结果和真实值一起存入ssa_lstm_result.mat供给 5 号脚本画对比图。保存逻辑是# 假设model已经用最优参数训练完毕 y_pred model.predict(X_test).flatten() # 保存结果供后续对比脚本读取 sio.savemat(result/ssa_lstm_result.mat, { true: y_test, pred: y_pred, mae: np.array([float(np.mean(np.abs(y_test - y_pred)))]) })model.predict(X_test)返回的是一个二维数组形状是(样本数, 1)用flatten()转成一维后和y_test做差才不容易出现广播维度不一致的隐患。保存 MAE 是为了让 5 号脚本不用重新计算误差直接读 mat 就能在图上标注数值。到这里整个 SSA-LSTM 的主链路已经跑通数据加载 → MLP 基线 → LSTM 基线 → SSA 优化超参数 → 用最优参数重训 → 保存结果。后面只剩两件事——避坑和看结果。4. 避坑记录SSA-LSTM 从运行到出图最容易翻车的五个问题4.1 TensorFlow 2.x 与 Keras 3 的 API 差异导致模型构建失败现象脚本运行到model.fit时报TypeError: Cannot convert a symbolic Tensor或者在model.predict时提示某个 layer 不存在更常见的是一行from keras import ...直接 import 失败。原因本机装的是 TensorFlow 2.16 以上版本自带 Keras 3但项目脚本按 tf.keras 2.x 的写法来两个版本之间部分层和 API 签名有变化比如Dense(1)在 Keras 3 里需要显式指定activation的情况变多。如果你是在 VSCode 里配的 Python 环境还容易发生解释器指向虚拟环境、而 TensorFlow 装在系统环境的情况import 时拿到的是另一套 Keras。解决统一从tensorflow.keras导入不要单独写from keras import ...。具体来说就是from tensorflow.keras.models import Sequential、from tensorflow.keras.layers import LSTM, Dense。如果项目里原本就是tf.keras那问题多半是版本升级导致的兼容性可以降低 TensorFlow 版本到 2.15 或 2.10 试试这两个版本对 tf.keras 的兼容性最稳。4.2 归一化的数据泄漏导致测试误差虚低现象训练集 MAE 降到 0.01测试集 MAE 却高达 0.12或者反过来——训练误差高、测试误差反而低怎么看都不合理。原因MinMaxScaler在全体数据上做了fit_transform模型在训练阶段就用到了测试区间的最小值和最大值信息这叫数据泄漏。更隐蔽的是预测完成后做反归一化时如果拿训练集的 scaler 去inverse_transform测试集的预测值量纲会完全对不上画出来的曲线飘得离谱。解决严格按时间顺序先切分、再归一化。训练集上scaler.fit_transform(X_train)测试集上只做scaler.transform(X_test)。保存模型的同时把 scaler 也存下来预测完用同一个 scalerinverse_transform还原真实浓度值。判断有没有泄漏很简单——看归一化代码里fit的输入是不是只包含了训练区间。4.3 SSA 收敛太慢或适应度曲线锯齿状抖动现象fitness curve.png前面几代完全没有下降后面几代突然大幅跳变或者整条曲线像锯齿一样忽上忽下最终拿到的参数和随机初始化差不多。原因学习率搜索范围太大部分粒子探索到学习率接近 0.01 的位置时 LSTM 训练直接发散返回的 MAE 是无穷大或异常大数把种群适应度排序搅乱另一个原因是求值函数里 epochs 设置太大每个个体都训练很久导致迭代代次不足SSA 还没收敛就跑完了。解决学习率范围固定为 1e-4 到 1e-2 并用对数均匀采样不要用线性均匀求值函数内部训练轮次控制在 10 到 15让 SSA 快速评估种群数量保持 10 到 15迭代 30 代以上。如果曲线还是锯齿检查 loss 是否出现nan出现就说明学习率上界太高或数据里有缺失值没被 dropna 干净。4.4 预测曲线整体滞后一个时间步现象预测曲线形状和真实曲线几乎一样但整体向右平移了一个时间步误差计算出来却不小。原因构造滑动窗口时标签取了当前时间步的 PM2.5 而不是下一时间步相当于让模型拟合 t 时刻的值。LSTM 学到的最优策略是“复制上一个时刻的观测值”预测自然就滞后一拍。另一个隐蔽原因是训练时开了shuffleTrue把时序打乱了模型学到的依赖关系在测试集上失效。解决窗口构造时标签一定取data[i lookback, -1]这是 tlookback 时刻的值不是当前窗口内的值。全流程都不要用train_test_split默认的随机切分也不要给model.fit传shuffleTrue。做完这两件事滞后问题基本能消除。4.5 MAT 文件载入失败或字段名对不上现象sio.loadmat(result/ssa_para.mat)执行成功后打印params.keys()发现只有__header__、__version__、__globals__没有first_units这些字段或者有字段但取值时报ValueError: setting an array element with a sequence。原因savemat保存时传入的 value 是 Python 原生 int 或 float而不是 numpy 数组scipy.io 写入时产生了空变量还有可能是保存和读取时路径不一致——脚本在项目根目录运行时用了相对路径但你在别的目录下执行导致读到的是一个旧文件或根本没读到文件。解决保存前用np.array([value])包装所有字段读取后用int(params[first_units].flatten()[0])取值。路径统一用os.path.join(result, ssa_para.mat)并且保证所有脚本都在ssa_lstm_air_quality根目录下运行。我自己的习惯是每次 loadmat 后先打印params.keys()再继续处理花十秒钟确认字段比跑完整个脚本才发现 KeyError 要省事得多。5. 对比验证用收敛曲线和 MAT 结果判断 SSA-LSTM 值不值得用5.1 一次跑通全部脚本的顺序代码包里的 5 个脚本是按依赖顺序设计的最省心的做法是依次执行cd ssa_lstm_air_quality python 1_mlp.py python 2_lstm.py python 3_ssa_optimize_lstm_params.py python 4_ssa_lstm_use_params_from_3.py python 5_comparison.py3 号脚本通常耗时最长因为每个麻雀个体都要训练一次 LSTM跑的时候盯着命令行输出看 fitness 是否在下降即可。如果中途想验证链路通不通可以先只跑 1 和 2确认result目录下生成两个 mat 文件再跑 3 和 4。5_comparison.py 本身不重新训练模型它只负责加载三个 mat 文件并绘图所以速度很快。5.2 从图和 MAT 结果判断优化是否有效训练完成后打开result目录和 png 图按下面的方式快速判读文件判读方式fitness curve.png稳定下降并进入平台期说明 SSA 收敛first / second hidden nodes optim.png最优点贴近边界说明搜索范围设窄了learning rate optim.png最优点是否落在 1e-3 附近合理区间ssa_lstm_result.mat 与 lstm_result.mat对比 mae 字段改进不明显就观察预测曲线是否更贴近真实线收敛曲线如果前几代就降到很低说明这个问题本身好优化如果曲线一路缓慢下降直到第 30 代还在降说明迭代代次不够可以把max_iter从 30 加到 50 再跑一次。最优节点数如果落在 128 附近甚至贴着边界基本可以判断搜索区间设小了下一次把上界改成 256。学习率最优点如果在 1e-2 附近贴着边界说明收敛曲线可能受学习率上界限制建议把上界放宽到 5e-2 重新搜索。这些判断都不是玄学而是看粒子的落点是否被边界约束——被边界夹住就说明搜索空间给窄了。# 快速对比两个mat文件的MAE import scipy.io as sio lstm sio.loadmat(result/lstm_result.mat) ssa_lstm sio.loadmat(result/ssa_lstm_result.mat) print(LSTM MAE:, lstm[mae].flatten()[0]) print(SSA-LSTM MAE:, ssa_lstm[mae].flatten()[0])对比结论最好写成“SSA-LSTM 比 LSTM 降低了多少 MAE、比 MLP 降低了多少”而不是只说“优化后的曲线更近”。量化数值比肉眼判断更有说服力。从那以后我每次做 LSTM 时间序列预测都会强制走一遍“先基线、再优化、再对比”的流程没有对比图不放结果。希望帮到你。本文还有配套的精品资源点击获取