简介基于麻雀搜索算法与长短时记忆网络SSA-LSTM的空气质量预测Python代码面向需要完成时序预测实验、研究超参数优化的在读学生及科研人员也可作为毕业设计或课程项目的核心代码直接扩展使用。项目从MLP基线、标准LSTM到SSA优化后的LSTM逐层递进清晰展示了利用麻雀搜索算法寻找最优学习率、隐藏层节点等超参数的完整过程。压缩包共16个文件5个Python脚本分步覆盖基线训练、LSTM建模、SSA参数寻优、最优参数回代以及模型对比绘图5张PNG图像直观呈现适应度曲线、学习率优化轨迹和两层隐藏层节点的寻优结果4个MAT文件保存了各模型的预测输出与误差数据便于进一步分析另有Excel汇总和README文档说明测试数据与运行方式。整个压缩包仅778KB轻量无冗余已有807人浏览学习。通过按序运行脚本可快速复现空气质量预测结果并观察SSA相对标准LSTM在回归精度上的提升适合具备基础Python语法、希望掌握群智能优化与深度时序预测结合实践的开发者参考。1. 空气质量预测被超参数卡脖子SSA-LSTM 到底解决什么问题做过空气质量预测的人都有一个共同感受数据不难找LSTM 也不难跑难的是让 LSTM 在你自己的数据上收敛得又快又不抖动。隐藏层神经元设多少、学习率给多少、批大小多大、正则化系数怎么定这些超参数一旦不合适同一个网络结构在训练集上拟合得很好换到验证集立刻崩掉。传统做法是手调或者用网格搜索但网格搜索在连续参数空间里非常慢而且维度一多组合数爆炸。基于 SSA-LSTM 的空气质量预测用到的是麻雀搜索算法Sparrow Search Algorithm去自动寻优靠它把 LSTM 的几个关键超参数在一次搜索里找出来再用最优参数训练最终的预测模型。适合的人群是会跑 Python、懂一点 LSTM 基础、但不想在调参上耗两周的工程师和研究生。这篇文章把原理、完整可运行的代码、参数边界和踩过的坑一次讲清。2. 先看明白再写代码麻雀搜索算法和 LSTM 是怎么耦合在一起的2.1 SSA 的搜索逻辑发现者、加入者和警戒者麻雀搜索算法是一种群体智能优化算法,模拟麻雀觅食和反捕食行为。种群里的每只麻雀就是一个候选解也就是一组 LSTM 超参数组合。搜索过程中麻雀被分成三个角色发现者负责在较大范围里找食物多的区域加入者跟在发现者附近继续开发警戒者负责发现危险并让种群跳出局部最优。每次迭代每只麻雀都会根据它的角色和当前最优位置更新自己的位置所有位置对应着一个适应度值。在 SSA-LSTM 这个场景里适应度函数就是 LSTM 在验证集上的误差。通常用均方根误差 RMSE 或者均方误差 MSE 作为适应度值值越小说明这组超参数越好。优化过程就是反复做三件事把麻雀的位置解码成超参数训练一个 LSTM返回验证集误差然后按 SSA 规则更新位置。这套逻辑比网格搜索聪明的地方在于每一轮迭代都会参考全局最优和个体历史最优所以同样的超参数搜索次数SSA 找到好参数的概率更高。2.2 SSA 优化 LSTM 时的位置解码超参数空间怎么映射做 SSA-LSTM 之前首先要决定一个问题到底优化哪些超参数。我常用的做法是优化五个值学习率、第一层 LSTM 神经元数、第二层 LSTM 神经元数、训练批大小、L2 正则化系数。其中学习率和正则化系数是对数尺度神经元数和批大小是整数尺度。麻雀位置是连续值所以解码时必须做边界处理和取整转换。解码函数我一般这样写import numpy as np def decode_position(pos, lb, ub): # pos 是归一化后的麻雀位置, 范围 [0, 1] # lb, ub 分别是超参数的下界和上界 real lb pos * (ub - lb) # 学习率、正则化系数保持连续值 lr real[0] reg real[1] # 神经元数、批大小必须转成整数 units1 int(round(real[2])) units2 int(round(real[3])) batch_size int(round(real[4])) # 防止取整后越界 units1 max(int(lb[2]), min(int(ub[2]), units1)) units2 max(int(lb[3]), min(int(ub[3]), units2)) batch_size max(int(lb[4]), min(int(ub[4]), batch_size)) return lr, reg, units1, units2, batch_size这段代码有几个关键点要注意。pos在种群初始化时是np.random.rand(N, dim)取值范围天然是 0 到 1但麻雀算法在更新位置时可能出现超出边界的值所以每次解码前最好把pos先 clip 到 0 和 1 之间。real是线性映射后的真实参数值。整数超参数必须先round再转int否则 LSTM 层会报类型错误。边界保护不只是为了防止越界更是为了不让搜索跑到明显不合理的地方比如批大小变成 1训练会非常不稳LSTM 神经元数变成 300显存和时间开销都受不了。学习率这个参数非常敏感通常下界给 1e-4上界给 1e-2如果给到 1e-1LSTM 很容易在训练初期就发散。2.3 数据形态设计滑动窗口和滞后步数回归预测的 LSTM 不是把整段历史丢进去而是按滑动窗口切样本。比如用过去 12 小时的污染物浓度和气象观测值预测未来 1 小时的 PM2.5 浓度。这个窗口长度叫 lookback在 SSA 搜索过程中先固定住一般取 12 或者 24后面我会在第 6 章讲怎么验证窗口长度的影响。每个样本的形状是(lookback, n_features)n_features 包括 PM2.5、PM10、NO2、SO2、CO、O3、温度、湿度、风速、气压这些列。这个输入特征设计是整个方案里最容易出问题的地方之一。很多人直接把所有列丢进窗口包括日期、站点编号这些无关列结果 LSTM 学了一堆没用的模式验证集误差始终下不去。我一般只保留物理上跟空气质量强相关的污染物浓度和气象变量日期和站点信息如果要用也应该单独做成分类特征拼在 LSTM 输出后再接全连接层而不是直接塞进序列。特征数量越多麻雀搜索里每次适应度评估的耗时越长因为模型输入维度大了参数量也上去了所以特征选择本身就是一次降本。2.4 为什么选 SSA 而不是粒子群或遗传算法做 LSTM 超参数搜索常见的还有粒子群算法 PSO 和遗传算法 GA。PSO 收敛快但容易早熟比如空气质量数据这种噪声较大的场景它经常在迭代到第 10 轮左右就停在同一个局部区域不动了超参数组合看着合理但换一段测试数据效果就掉。遗传算法全局搜索能力强交叉变异的随机性大但需要调交叉率和变异率相当于又多出两个超参数对新手不友好。SSA 的优势在于发现者和警戒者两个机制配合收敛速度接近 PSO而避免早熟的能力比 PSO 好。警戒者在每次迭代里会随机挑一部分麻雀重新扰动位置等于每轮都做了小规模重启这对 LSTM 这种训练一次成本高的模型非常关键。因为超参数搜索里最怕的就是花了五分钟训练一轮结果发现所有麻雀都挤在一个无效区域。另外 SSA 的实现代码量比 GA 少位置更新公式不涉及复杂的编码和解码核心循环一百行之内能写完这也是我推荐它作为入门优化器的理由。3. 完整可跑的 SSA-LSTM Python 代码预处理、搜索与训练三步走3.1 环境准备和数据预处理这个方案的运行环境不用太复杂TensorFlow 2.x、NumPy、Pandas、Scikit-learn 就够了。如果是用 GPU 训练记得提前把 CUDA 和 cuDNN 装好否则 LSTM 会退回 CPU 计算一次适应度评估要跑很久整个搜索过程会变得很难等。数据预处理里最关键的一步是归一化和样本划分的顺序这一步做错会让整个模型的结果虚高。import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split # 读取数据date 列解析成时间格式 raw pd.read_csv(air_quality.csv, parse_dates[date]) raw raw.sort_values(date).reset_index(dropTrue) # 只保留数值特征列剔除日期、站点等无关列 feature_cols [PM2.5, PM10, NO2, SO2, CO, O3, TEMP, RH, WS, PRES] target_col [PM2.5] data raw[feature_cols].copy() # 缺失值用前向填充空气质量数据常见连续缺失 1-2 小时的情况 data data.fillna(methodffill).dropna() # 按时间顺序划分不能随机打乱 total len(data) train_end int(total * 0.7) val_end int(total * 0.85) train_df data.iloc[:train_end] val_df data.iloc[train_end:val_end] test_df data.iloc[val_end:] # 用训练集 fit 归一化器验证集和测试集只 transform scaler_x StandardScaler() scaler_y StandardScaler() scaler_x.fit(train_df[feature_cols]) scaler_y.fit(train_df[target_col]) train_x scaler_x.transform(train_df[feature_cols]) train_y scaler_y.transform(train_df[target_col]) val_x scaler_x.transform(val_df[feature_cols]) val_y scaler_y.transform(val_df[target_col]) test_x scaler_x.transform(test_df[feature_cols]) test_y scaler_y.transform(test_df[target_col])这段代码里有两条规则是我一直坚持用的第一按时间顺序切分不能用train_test_split的默认随机切分时间序列随机切分会导致模型看到未来信息测试集误差低得离谱但这种低没有任何落地价值第二归一化器只能用训练集 fit验证集和测试集只 transform。如果把全量数据一起 fit均值和方差里混进了未来信息这是时间序列预测里最典型的泄露方式。空气质量数据的缺失值处理用前向填充就够了连续缺失超过 6 小时的我建议直接删掉这一段因为 LSTM 对这种空洞没有很好的建模能力。滑动窗口切样本的函数通常写成这样def create_sequences(x_data, y_data, lookback12, horizon1): X, y [], [] for i in range(len(x_data) - lookback - horizon 1): X.append(x_data[i:ilookback, :]) y.append(y_data[ilookbackhorizon-1, 0]) return np.array(X), np.array(y) lookback 12 horizon 1 train_X, train_Y create_sequences(train_x, train_y, lookback, horizon) val_X, val_Y create_sequences(val_x, val_y, lookback, horizon) test_X, test_Y create_sequences(test_x, test_y, lookback, horizon)lookback表示用过去多少个小时horizon表示预测未来多少个小时。这里预测的是 PM2.5 单变量所以y取的是目标列的第一个维度。如果你要同时预测 PM2.5、PM10 和 O3 三个变量create_sequences里的y就要改成返回多列。窗口切完之后要检查一件事train_X.shape[1]必须等于lookbacktrain_X.shape[2]必须等于特征数 10很多人后面报 shape 错误都来自这里。3.2 SSA 核心代码种群初始化、适应度评估、位置更新麻雀搜索算法的实现不需要太长的代码但结构要清楚。先把种群初始化和 LSTM 适应度函数写好。import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.regularizers import l2 from tensorflow.keras.optimizers import Adam # 超参数搜索范围 dim 5 lb np.array([1e-4, 1e-5, 32, 16, 16]) ub np.array([1e-2, 1e-2, 128, 64, 64]) # lb/ub 分别对应: 学习率下/上界, L2 系数下/上界, # 第一层神经元下/上界, 第二层神经元下/上界, 批大小下/上界 POP_SIZE 10 MAX_ITER 15 ST 0.7 # 安全阈值, 发现者位置更新用 PD 0.2 # 发现者比例 SD 0.2 # 警戒者比例 positions np.random.rand(POP_SIZE, dim) def train_single_lstm(params): lr, reg, units1, units2, batch_size params model Sequential([ LSTM(units1, input_shape(train_X.shape[1], train_X.shape[2]), return_sequencesTrue, kernel_regularizerl2(reg)), Dropout(0.2), LSTM(units2, return_sequencesFalse, kernel_regularizerl2(reg)), Dropout(0.2), Dense(1) ]) model.compile(optimizerAdam(learning_ratelr), lossmse) early_stop tf.keras.callbacks.EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue) history model.fit( train_X, train_Y, validation_data(val_X, val_Y), epochs30, batch_sizebatch_size, callbacks[early_stop], verbose0) val_pred model.predict(val_X, verbose0) rmse float(np.sqrt(np.mean((val_Y - val_pred.ravel()) ** 2))) return rmse def fitness(positions): scores [] for i in range(positions.shape[0]): pos np.clip(positions[i], 0, 1) params decode_position(pos, lb, ub) rmse train_single_lstm(params) scores.append(rmse) return np.array(scores)这段代码里我刻意加了两点一是EarlyStopping的patience设成了 5并且restore_best_weightsTrue因为 LSTM 训练后期容易过拟合验证集 loss 开始反弹时如果不回滚权重返回的 RMSE 会偏大二是verbose0搜索期间要训练几十次模型如果每次把训练过程打出来终端会刷屏刷到崩溃。kernel_regularizerl2(reg)加的非常轻通常下界 1e-5、上界 1e-2太大会把模型拉成欠拟合。接下来是麻雀算法的位置更新。完整的 SSA 包含发现者、加入者和警戒者三种更新规则我在这里给出一个可以运行的精简版本def ssa_update(positions, scores, iter_idx, max_iter): pop_size, dim positions.shape new_positions positions.copy() sorted_fit np.argsort(scores) # 适应度从小到大排列 # 发现者更新 pd_num max(1, int(POP_SIZE * PD)) R2 np.random.rand() for i in range(pd_num): idx sorted_fit[i] if R2 ST: new_positions[idx] positions[idx] * np.exp( -iter_idx / (max_iter * np.random.rand() 1e-8)) else: new_positions[idx] positions[idx] np.random.randn() * 0.1 # 加入者更新 for i in range(pd_num, pop_size): idx sorted_fit[i] best_idx sorted_fit[0] worst_idx sorted_fit[-1] if i pop_size / 2: new_positions[idx] np.random.rand(dim) else: new_positions[idx] positions[idx] np.random.rand(dim) * ( positions[best_idx] - positions[idx]) # 警戒者更新: 随机挑 SD 比例的个体 sd_num max(1, int(POP_SIZE * SD)) sd_indices np.random.choice(pop_size, sd_num, replaceFalse) best_idx sorted_fit[0] worst_idx sorted_fit[-1] best_score scores[best_idx] worst_score scores[worst_idx] for idx in sd_indices: if scores[idx] best_score: new_positions[idx] positions[best_idx] np.random.randn(dim) * ( positions[idx] - positions[best_idx]) else: new_positions[idx] positions[idx] 1.5 * np.random.rand(dim) * ( np.abs(positions[idx] - positions[worst_idx]) 1e-8) return np.clip(new_positions, 0, 1)位置更新这段我每次写都要小心三个边界情况。第一iter_idx / (max_iter * np.random.rand() 1e-8)里加1e-8是为了避免除以 0我在实际跑代码时真的遇到过某次随机到 0 导致位置变成 NaN之后整个种群全废的情况。第二np.random.randn()是标准正态随机数乘 0.1 是给发现者的扰动幅度空气质量数据适应度面比较平缓扰动太大反而容易跳过最优区域。第三警戒者更新里positions[worst_idx]是最差麻雀的位置它的作用是让警戒者往远离最差解的方向飞但如果只差麻雀本身也是警戒者更新公式可能把自己拉乱所以np.abs(positions[idx] - positions[worst_idx]) 1e-8取了绝对值来保证不为负。整个搜索循环是这样的best_rmse_curve [] best_params None for it in range(MAX_ITER): scores fitness(positions) best_idx int(np.argmin(scores)) if best_params is None or scores[best_idx] best_rmse_curve[-1]: best_params decode_position(positions[best_idx], lb, ub) best_rmse_curve.append(scores[best_idx]) else: best_rmse_curve.append(best_rmse_curve[-1]) print(fIter {it1}/{MAX_ITER}, best RMSE: {best_rmse_curve[-1]:.4f}) positions ssa_update(positions, scores, it, MAX_ITER)这个循环里我用了best_rmse_curve记录收敛曲线它将来可以画出来给汇报用也可以判断搜索有没有提前收敛。如果曲线在 5 轮之内就平了说明要么初始种群太集中要么适应度函数区分度不够需要检查是不是归一化出了问题。整轮搜索在 CPU 上跑大概 15 到 30 分钟GPU 上会快很多但要注意 GPU 跑单个小 LSTM 的启动开销比较大搜索轮次多的话并没有想象中那么快。3.3 用最优超参数训练最终模型SSA 搜索出来的best_params是验证集上的最优超参数但它是在epochs30、提前停止的条件下评估出来的最后还要用完整训练集重新训练一遍。做法是把验证集拼回训练集然后放宽epochs上限让模型在更多数据上充分收敛。final_X np.concatenate([train_X, val_X], axis0) final_Y np.concatenate([train_Y, val_Y], axis0) lr, reg, units1, units2, batch_size best_params final_model Sequential([ LSTM(units1, input_shape(final_X.shape[1], final_X.shape[2]), return_sequencesTrue, kernel_regularizerl2(reg)), Dropout(0.2), LSTM(units2, return_sequencesFalse, kernel_regularizerl2(reg)), Dropout(0.2), Dense(1) ]) final_model.compile(optimizerAdam(learning_ratelr), lossmse) early_stop tf.keras.callbacks.EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue) history final_model.fit( final_X, final_Y, validation_data(test_X, test_Y), epochs60, batch_sizebatch_size, callbacks[early_stop], verbose1) test_pred final_model.predict(test_X, verbose0)注意这里的validation_data用的是测试集这只是一个观测手段不能拿它来触发提前停止并回滚权重否则测试集就变成了验证集。如果你想严格一点应该只传final_X, final_Y给 fit让EarlyStopping把训练集末尾切一部分作为验证集。两种做法各有取舍我在实际项目里习惯保留validation_data因为它能随时看出模型在未见数据上的表现但要清楚这不是标准的评测流程。最终评价测试集时再单独算指标。4. 用 RMSE、MAE 和 R² 说话SSA-LSTM 和基线模型的对比实验4.1 三个评价指标的计算代码和解读方式空气质量预测的评测不像分类任务那样看准确率回归任务通常看三个指标RMSE、MAE 和 R²。RMSE 对离群点敏感适合反映重污染时段模型的糟糕程度MAE 更稳健反映平均偏差R² 反映模型相对「直接用均值预测」提升了多少越接近 1 越好接近 0 说明模型基本没学到东西。from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score inv_test_y scaler_y.inverse_transform(test_Y.reshape(-1, 1)) inv_test_pred scaler_y.inverse_transform(test_pred.reshape(-1, 1)) rmse float(np.sqrt(mean_squared_error(inv_test_y, inv_test_pred))) mae float(mean_absolute_error(inv_test_y, inv_test_pred)) r2 float(r2_score(inv_test_y, inv_test_pred)) print(fRMSE: {rmse:.3f} ug/m3) print(fMAE: {mae:.3f} ug/m3) print(fR2: {r2:.4f})这里必须把预测值和真实值做反归一化后再算指标如果直接在归一化空间算 RMSE得到的数值没有办法对应到真实的 PM2.5 浓度单位汇报时没有任何物理意义。scaler_y.inverse_transform是对应训练时scaler_y.fit(train_df[target_col])的注意传入的 shape 必须是二维的(n, 1)如果test_Y是(n,)的形状需要先reshape(-1, 1)。4.2 SSA-LSTM 和单 LSTM、PSO-LSTM 的基准对比为了让 SSA-LSTM 的效果更有说服力我通常拉两个基线跑同一份数据一个单 LSTM用论文里常见的经验参数比如隐藏层 64、学习率 0.001、批大小 32另一个是 PSO 优化的 LSTM迭代次数和种群大小与 SSA 完全一致。对比表格可以设计成下面这个样子具体数值以你的数据为准模型RMSEMAER²搜索耗时单 LSTM经验参数24.817.20.812无PSO-LSTM19.613.90.864约 22 分钟SSA-LSTM17.912.40.883约 24 分钟从这个典型结果能看到两件事一是搜索类的优化方法明显强于拍脑袋设超参数因为 LSTM 的隐藏层数和学习率对结果影响很大经验参数在不同城市、不同时间粒度的数据上并不可靠二是 SSA 和 PSO 的搜索耗时差不多但 SSA 在 RMSE 上有额外收益这个收益来源于它更强的探索能力。空气质量数据季节性明显比如冬季 PM2.5 偏高适应度面存在多个局部最优点PSO 容易收敛到某个局部区域而 SSA 的警戒者机制让种群在迭代后期仍然保持一定的随机扰动所以能找到更低谷值。4.3 搜索结果的可视化收敛曲线和散点图搜索过程的收敛曲线是判断优化器是否正常工作的第一视觉证据。每轮迭代记录全种群最优 RMSE然后画折线图。正常的曲线应该是前几轮快速下降后面缓慢逼近一个平台值。如果曲线是一根水平线说明种群初始范围太小或者 LSTM 适应度函数有问题如果曲线上下剧烈震荡说明警戒者扰动幅度太大或者训练不稳定导致同参数多次评估结果波动很大。我个人在做量化策略代码时也有同样习惯任何自动调参的东西最后都要看收敛曲线不然你根本不知道搜索有没有起作用。import matplotlib.pyplot as plt plt.figure(figsize(8, 4)) plt.plot(range(1, MAX_ITER 1), best_rmse_curve, markero) plt.xlabel(Iteration) plt.ylabel(Best RMSE on validation) plt.title(SSA Convergence Curve) plt.grid(True) plt.savefig(ssa_convergence.png, dpi150)测试集真实值和预测值的散点图也值得画出来。理想状态是散点贴在 yx 直线上如果明显偏离到一侧说明存在系统性偏差常见原因是预测值整体偏小这是 LSTM 对高浓度峰值预测不足的典型表现。空气质量预测里峰值的捕捉本身就是难点RMSE 主要就是被这些峰值拉高的。5. SSA-LSTM 的常见坑与排查5 条真实踩坑记录5.1 所有麻雀适应度相同loss 始终不变现象搜索循环跑了好几轮每只麻雀的 RMSE 都一样收敛曲线是一条水平线LSTM 训练时 loss 几乎不降。原因最常见的是数据归一化出了问题比如特征列里有 NaN 或者无穷大归一化之后数据仍然没有分布LSTM 输入里全是异常值另一种可能是positions在初始化时没有随机铺开比如直接用np.zeros初始化导致所有麻雀解码出的超参数一模一样。解决检查每个特征列是否有 NaN用np.isnan(data).sum()统计再把positions的初始化从np.random.seed(42)固定随机种子开始用np.random.rand(POP_SIZE, dim)生成打印前几只麻雀的位置确认它们不同。最后单独跑一次随机编码后的单个 LSTM确认 loss 能下降再进搜索循环。5.2 隐层神经元数是小数导致 LSTM 报类型错误现象执行LSTM(units1)时 TensorFlow 直接报错错误信息显示期望整数但给定的是30.0这种浮点数。原因解码函数里只做了线性映射没有取整。很多网上示例代码把这一步跳过了照抄后跑第一个 iteration 就崩。解决在decode_position里必须int(round(...))。我在 2.2 节代码里已经写了units1 int(round(real[2]))这个顺序是先round再int不能反过来先int再round否则int(30.5)会变成30损失精度。另外一个容易忽略的地方是batch_size直接写成浮点数传给 Keras 的fit也会报错同样要转成int。5.3 批大小不能整除样本数导致最后一次迭代形状不匹配现象LSTM 训练到最后一个 batch 时TensorFlow 报形状不一致错误错误信息里出现Incompatible shapes搜索中断。原因训练集样本数除以批大小有余数最后一个 batch 大小和前面不一致。TensorFlow 2.x 在 CPU 上偶尔能自己处理好这种情况但在 GPU 或某些数据格式下会直接报错。解决最简单的方法是在train_single_lstm里把训练样本截到批大小的整数倍train_X train_X[:train_len // batch_size * batch_size]对应的train_Y也同步截取。虽然会丢几个样本但 LSTM 训练是随机采样的损失可以忽略。也可以用tf.data.Dataset加drop_remainderTrue但为了搜索代码简洁我通常选截断。5.4 测试集效果好但实际应用时误差反弹现象测试集 RMSE 不高收敛曲线也正常但把模型部署到新数据上跑预测值明显偏大或偏小误差比测试集高出一截。原因十有八九是数据泄露。最常见的做法是先用全量数据 fit 了StandardScaler再切训练测试集这导致归一化时用到了未来数据的均值和方差测试集看起来被「校准」过。更隐蔽的一种泄露是缺失值填充时用了fillna(methodffill)但整个 fill 操作是在切分之前做的不过这种情况下泄露影响较小主要问题还是归一化。解决严格按 3.1 节代码的顺序先按时间切分再在训练集上fit归一化器。可以写一个自查函数分别用训练集、全量数据 fit 得到的两个归一化器去 transform 测试集对比 RMSE如果差异明显说明你的代码存在泄露。另外还要检查是否不小心把测试集的某个特征列当作标签喂给了模型这种特征泄露在特征选择时容易混进去。5.5 搜索时间太长单次适应度评估就要一分钟现象种群 10 只麻雀、15 轮迭代总共要训练 150 次 LSTM每轮几分钟整个搜索跑下来要一两个小时完全没法调试。原因LSTM 的epochs设太高比如直接给 100加上没有早停每个候选超参数都要完整训练 100 轮另一个原因是train_X样本量太大每轮训练要处理的数据太多。解决搜索阶段把epochs压到 20 到 30配合EarlyStopping(patience5)让模型在验证集不再改善时提前退出。搜索阶段的目的不是训练出最终模型而是比较不同超参数组合的相对好坏不需要充分收敛。如果样本量超过十万条可以考虑每轮训练只用一部分训练集比如抽样 30% 来评估这样搜索速度会快很多。最终的模型再拿到全量数据上训练。6. 验证与进阶序列长度、多步预测和参数边界6.1 lookback 的影响和快速验证方法空气质量数据的滞后性很强PM2.5 浓度不仅受当前气象条件影响还受过去几小时累积影响。lookback 取 1 和取 24 的 LSTM 预测效果差别很大但也不是越长越好。我的习惯是先固定 SSA 搜索出的最优模型然后用一个循环快速扫描 lookback比如 3、6、12、24、48各自训练一次 LSTM画出 RMSE 随 lookback 的变化线。如果 RMSE 在 12 以后明显上升说明序列太长引入了噪声如果 3 和 6 效果很差说明数据依赖的滞后长度确实长。这个快速验证法每次训练用早停控制十分钟左右能扫完一轮不要用 SSA 去搜 lookback因为没有必要为了一个值付出一整轮搜索成本。6.2 多步预测的两种做法搜索得到的最优超参数是针对单步预测的。如果要预测未来 6 小时或 24 小时有两种做法。迭代预测是把前一步预测值作为输入的一部分继续预测后一步直接多步是让模型输出一个长度为 6 的向量也就是把 Dense 层改成Dense(6)。迭代预测实现简单但误差会随时间步累积预测到第 24 小时时 RMSE 可能比第 1 小时翻一倍。直接多步的误差不会累积但训练时需要修改create_sequences的标签构造逻辑让每个样本的y包含未来多个时刻的目标值。两种做法对应的horizon和Dense节点数不同建议在验证集上各试一次再决定别只凭网络结构想当然。6.3 搜索边界应该怎么设先宽后窄超参数搜索范围不是一次定死的。我第一次跑 SSA-LSTM 时把学习率上界设到 0.05结果一半麻雀解码出的参数训练发散适应度全是 NaN整个收敛曲线直接崩坏。后来我改成先用大步长粗搜索找好区域再缩小边界做一次精搜索。具体做法是第一次搜索学习率范围 1e-4 到 1e-1、神经元 16 到 128迭代 10 轮如果最优学习率落在 1e-3 到 1e-2第二次把范围压缩到 5e-4 到 5e-3重新初始化种群再搜一轮。这个「先宽后窄」的做法只增加一轮成本但能明显提升稳定性。就像量化交易里先试方向再调仓位一样方向错了精细调参没有意义。最后说一个我已经养成习惯的操作每次 SSA 搜索开始前把随机种子固定比如np.random.seed(42)并在搜索结束后把best_params存成 JSON 文件。同一份数据换一个人跑随机种子不同搜出的最优参数往往不同但两个参数训练出来的模型效果可能差不多这说明搜索本身没问题只是超参数面太平。存下参数和收敛曲线既方便做实验记录也能在汇报时让人信服这套方案值得投入。希望这一篇能帮你把 SSA-LSTM 跑通在空气质量预测这个方向上走得更顺。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?