做短期电力负荷预测这个方向,我前后折腾了不短的时间才把这套 IPSO-LSTM 方案跑通。最开始用 LSTM 直接建模,效果马马虎虎,MAPE 一直在 5% 左右晃荡;真正让人抓狂的是超参数调优——窗口长度、学习率、隐藏层单元数、Batch Size 这些参数排列组合起来,手动试一次就是大半天,换一组参数预测精度能差出好几个点。后来我把粒子群优化算法引进来做自动寻优,又在标准粒子群基础上做了几个关键改进,模型的收敛速度和预测精度才算真正稳定下来。这篇文章就把这套方案的完整思路和 Python 实现从头到尾拆一遍,适合正在做负荷预测、时序预测,或者想给深度学习模型配一套自动调参策略的朋友参考。
1. 为什么纠结于调参:LSTM 做负荷预测的真正瓶颈
很多初学负荷预测的朋友容易产生一个误解:只要把网络结构堆起来,精度自然就上去了。我最早也这么想,结果发现 LSTM 对超参数的敏感程度远超预期。短时负荷数据有很强的周期性、趋势性和随机波动性,网络需要捕捉这些模式,而超参数直接决定了网络能学到什么粒度的特征。
1.1 负荷数据本身的脾气
电力负荷序列不是平稳数据。以我测试用的某地区配电网负荷为例,典型的一天有两个峰段——上午和傍晚,周末跟工作日的负荷形态差异明显,夏季和冬季更是两套完全不同的用电逻辑。这种多尺度特性给模型带来了两个核心挑战:一是网络需要有足够长的记忆能力去感知历史模式,二是模型要能适应不同时段的波动差异。
LSTM 的出现本身就是冲着长期依赖去的,但“设计上能处理”和“实际能处理”是两码事。如果输入窗口太短(比如只看过去 24 个点),模型可能抓不到前一天的负荷形态;窗口太长,噪声和无关信息又会拖累训练。我踩过的坑就是一开始贪心地把窗口设到 168(一周的点数),结果训练慢不说,精度反而不如 72 到 96 左右,这也算是负荷预测调参的经典教训。
1.2 LSTM 在这个场景里缺的其实不是网络,而是超参数
很多人上来就调网络结构,加层数、加神经元个数,但我们实际对比下来,对于单序列负荷预测这种中等规模任务,单层 LSTM 配上合适的隐层规模往往就够用了。真正影响精度的是这些超参数的组合配置:
| 超参数 | 影响方向 | 我常用的搜索范围 |
|---|---|---|
| 输入滑动窗口 | 决定模型能看到多长的历史模式 | 24 ~ 168(小时点) |
| 隐藏层单元数 | 决定特征提取容量,过大会过拟合 | 32 ~ 128 |
| 学习率 | 决定收敛速度和稳定性 | 0.0001 ~ 0.01(对数空间) |
| Batch Size | 影响梯度估计的稳定性和训练效率 | 16 ~ 128 |
| Dropout 比例 | 防止过拟合,但过高会欠拟合 | 0 ~ 0.3 |
这些参数之间并不是独立的。学习率大的时候,Batch Size 通常也要配合调整;窗口加长后,隐藏层单元数太大容易把噪声也记进来。手动调参时这种交互效应根本顾不过来,用网格搜索又面临组合爆炸——如果每个维度取 10 个候选值,5 个参数就是 10 万次训练,每次训练哪怕只有两分钟也要跑半年。这就是为什么把优化算法引进来是件顺理成章的事。
1.3 粒子群优化为什么合适,又为什么需要改进
粒子群优化(PSO)在超参数寻优上有一个很关键的优势:它不需要计算梯度,对目标函数的形式也没有太多限制。LSTM 的训练本身是非凸问题,验证集误差和超参数之间不存在简单的可导关系,贝叶斯优化虽然能处理连续参数,但对离散参数(比如窗口长度、整数型的隐层单元数)支持起来比较别扭,而且串行迭代在很多实验场景下太慢。粒子群则是天然的并行结构——每个粒子代表一组候选超参数,种群一起进化迭代,非常适合这种“训练完模型再看误差反馈重新寻优”的闭环。
但标准 PSO 在后期收敛上有很明显的短板:粒子群在迭代过程中所有个体会迅速向当前全局最优的区域靠拢,一旦这个最优落在局部极值附近,整个种群就很难再跳出来。我在初始实验中发现,标准 PSO 大概迭代到第 15 代左右收敛就停滞了,而且多次实验的最终结果波动很大,这说明算法本身的稳定性不够。这背后其实就是粒子群探索能力和开发能力失衡的问题——前期收敛太快,没来得及对参数空间做充分搜索。
2. 改进粒子群算法 IPSO:具体改了什么,为什么要这么改
标准的 PSO 速度更新公式大家应该很熟:
v(i,d) = w · v(i,d) + c1 · rand() · (pbest(i,d) - x(i,d)) + c2 · rand() · (gbest(d) - x(i,d))
速度由三部分组成:惯性项保持上一代的速度方向,个体认知项朝向粒子历史最优,社会项朝向种群全局最优。w 控制惯性大小,c1 和 c2 分别控制个体和社会的牵引强度。理论上这么设计没问题,实际跑起来却很骨感。
2.1 标准 PSO 的三个直观短板
第一个短板是惯性权重 w 从头到尾不变。前期我们希望粒子有较大的探索范围,别太早定方向;后期希望粒子在最优解附近精细搜索。固定 w 意味着算法没有这种动态能力。
第二个短板是缺乏变异机制。种群一旦收敛到某个区域,所有粒子都围着当前最优附近转,gBest 如果不更新,种群就僵住了。真实场景里的参数空间往往有很多相似的局部极值,比如说窗口长度 96 和 97 可能误差差不多,但窗口 120 对应的一组参数可能误差更低,只是没有被搜索到。没有变异手段,算法就只能停在原地。
第三个短板是速度控制太粗。粒子速度过大容易飞出合理参数区间,过小又跑不动。虽然可以加边界限制,但每次都靠硬约束截断,不够优雅,还可能让粒子聚集到边界上。
2.2 具体改进策略:三项叠加
针对上面三个短板,我在实现中做了三项核心改进。
第一项是惯性权重非线性递减。把固定 w 改成随迭代次数动态变化的函数,用余弦递减曲线来平衡探索和开发:
w(t) = w_min + 0.5 × (w_max - w_min) × (1 + cos(π × t / T))
这个公式前半段(迭代前期)w 维持较大值,粒子有充足的速度去大范围搜索;后半段 w 快速变小,粒子群整体收敛到局部精细搜索。相比线性递减,余弦曲线的变化更平滑,在迭代中期能保持较长时间的大探索步长,实际收敛效果更稳定。t 是当前迭代次数,T 是最大迭代次数,w_max 我取 0.9,w_min 取 0.4。
第二项是引入自适应变异机制。每次迭代结束后,以一定概率对部分粒子的位置进行随机扰动,扰动范围随迭代推进逐步缩小:
P_mut = P_mut_max × (1 - t / T)
当算法陷入局部最优时,粒子的个体最优和全局最优长时间不更新,这种变异操作就有机会把粒子推到一个新的位置,重新开始搜索。我的做法是每个粒子按概率 P_mut 触发一次随机重置,重置幅度和数据维度范围挂钩,维度范围大的参数变异幅度也大。这个机制看着简单,但对后期摆脱早熟收敛很有帮助。
第三项是速度边界自适应裁剪。粒子的速度上限 v_max 不再是固定值,而是根据当前迭代进度收缩:
v_max(t) = v_max_initial × (1 - 0.5 × t / T)
这样做的好处是,前期粒子可以跨大步走,遍历更多区域;后期速度上限降低,粒子在最优邻域内做精细调整。边界裁剪也从直接截断改为按比例缩放到 [0, 1],保证粒子位置始终落在参数搜索区间内。
整套改进的逻辑很直观:前期的重点是“搜得广”,中期的重点是“保持多样性”,后期的重点是“收得细”。三项改进正好对应这三个阶段。
2.3 IPSO 与 LSTM 的组合流程
我把整套流程串起来梳理一遍,方便后面写代码时对照。
整个流程分两个层次:外层是 IPSO 寻优,内层是 LSTM 训练评估。外层维护一个粒子种群,每个粒子对应一组超参数;每次迭代中,把粒子位置解码成具体超参数,用这组超参数训练一个 LSTM,在验证集上计算误差(我用的适应度函数是验证集 MAPE,也可以换成 RMSE 或加权指标),再把误差返回给 IPSO 作为该粒子的适应度。粒子的位置和速度根据 pBest、gBest 以及改进后的公式更新。迭代结束后,用最优超参数训练最终模型,在测试集上评估。
这里有个需要特别注意的设计:内层 LSTM 训练过程必须控制迭代次数和早停策略,不然每次适应度评估都要训练到完全收敛,耗时翻倍不说,还可能过拟合。权衡之后,我在寻优阶段把训练轮数上限设置为 60 轮,配合早停机制。后期定位到最优超参数后再加大到 120 轮精训,效果和效率都能兼顾。
3. 数据准备与样本构造
模型再怎么改,数据有问题全白搭。下面把数据这一环节的完整处理过程展开。
3.1 数据来源与清洗
我用的样本数据来自某地区开放负荷数据集(为了可复现,也可以换成 UCI 的电力负荷数据集或自己公司积累的历史负荷记录),采样间隔为 1 小时,总共约 2 年的数据量。拿到手第一步不是直接建模,而是先做质量审查。
负荷数据常见的坑有这么几个:采集设备故障导致整段缺失;节假日负荷形态与正常日差异巨大;偶尔出现设备检修造成的瞬时尖峰或异常零值。我的处理方式是,对缺失区间用前后 24 小时同时段的数据做线性插值,再叠加一个异常检测逻辑——如果某点负荷偏离近 7 天同一点位均值的 3 倍标准差,就标记为异常并替换成该时刻的历史中位数。
清洗之后还要做一步“节假日哑变量”处理。单纯让 LSTM 从数值序列里去推断“今天是周末”是可以的,但学习成本很高,不如直接给模型一个强提示。我在原始特征里加入了 is_weekend 和 is_holiday 两个标记。这么做之后,周末负荷低谷段的预测误差明显下降。
3.2 滑动窗口与多步预测设计
短期负荷预测通常有两种设定:单步预测(用过去 N 小时预测下一小时)和多步预测(预测未来 24 小时或者 48 小时的负荷曲线)。我这里核心目标先放在单步预测上,但样本构造时为后续扩展多步预测留了口子。
样本构造方式是滑动窗口:
def create_sequences(data, feature_cols, target_col, window_size): X, y = [], [] for i in range(len(data) - window_size): X.append(data.iloc[i : i + window_size][feature_cols].values) y.append(data.iloc[i + window_size][target_col]) return np.array(X), np.array(y)窗口大小由 IPSO 去搜索,范围设在 24 到 168 之间。这个设计背后的考量是:窗口 24 对应看过去一天,模型能学日内周期;窗口 168 对应看过去一周,模型能学“上周这个时候是什么负荷”这种周级模式。最优值通常会落在 72 到 120 之间,也就是“近三天到近五天”,说明模型既需要足够的历史信息覆盖近期趋势,又不需要一周的完整数据。
3.3 归一化的处理细节
归一化这一步非常关键,也最容易踩坑。我见过不少人在整个数据集上一次性做 MinMaxScaler,再切分训练集和测试集——这其实是信息泄露,测试集的统计信息进了归一化参数,会让评估结果虚高。
正确的做法是先切分数据,再用训练集的统计信息去 fit 归一化器,然后分别 transform 训练集和测试集。我封装了一个简单的处理逻辑:
from sklearn.preprocessing import MinMaxScaler # 先按时间顺序切分 train_size = int(len(df) * 0.8) train_df, test_df = df[:train_size], df[train_size:] # 只用训练集 fit scaler = MinMaxScaler() train_scaled = scaler.fit_transform(train_df[feature_cols]) test_scaled = scaler.transform(test_df[feature_cols])还要强调一点:归一化应该按特征列分别做,而不是把全部数值拉成一个分布。负荷值在一个量级,温湿度在另一个量级,混在一起归一化会让弱特征被强特征淹没。我在做特征工程时加入了温度、湿度等外部因素,各自的量纲差异很大,分开归一化之后模型训练明显更稳定。
4. IPSO-LSTM 完整 Python 实现
进入最核心的实现环节。我把代码拆成四个模块:数据加载、LSTM 模型定义、IPSO 寻优、最终训练评估。完整代码较长,我这里把关键部分展示出来并解释。
4.1 模型结构定义
LSTM 部分我采用 PyTorch 实现,结构保持简洁——一层 LSTM 加一层全连接输出,中间配合 Dropout。刻意不堆层数,是为了让超参数搜索的结果更有可解释性:如果两层 LSTM 精度提升明显,那说明数据复杂度确实需要更深网络;如果单层就够,加层只会增加训练成本。
import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_dim, hidden_dim, num_layers=1, dropout=0.0): super().__init__() self.lstm = nn.LSTM( input_size=input_dim, hidden_size=hidden_dim, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0.0 ) self.fc = nn.Linear(hidden_dim, 1) self.dropout = nn.Dropout(dropout) def forward(self, x): out, _ = self.lstm(x) # 取最后一个时间步的输出 out = out[:, -1, :] out = self.dropout(out) return self.fc(out).squeeze(-1)这里要说明一个设计细节:LSTM 的 dropout 参数在单层网络中不生效,PyTorch 官方文档里也有说明,只有 num_layers > 1 时层间的 dropout 才有意义。所以我在单层时把 dropout 放到全连接前单独做处理。调试的时候如果发现 dropout 设了但验证集误差没变化,多半是踩到了这个规则。
4.2 粒子解码与适应度评估
粒子每一维对应一个超参数。我这里让 IPSO 搜索四个维度:窗口长度、隐藏层单元数、学习率、Batch Size。其中窗口长度和隐藏层单元数是整数,学习率是连续值且要取对数,Batch Size 取离散的候选整数集合。
import numpy as np # 参数搜索边界 param_bounds = { 'window': (24, 168), 'hidden': (32, 128), 'lr': (0.0001, 0.01), 'batch': (16, 128) } def decode_particle(x): """x 是归一化到 [0,1] 的粒子位置向量""" window = int(param_bounds['window'][0] + x[0] * (param_bounds['window'][1] - param_bounds['window'][0])) hidden = int(param_bounds['hidden'][0] + x[1] * (param_bounds['hidden'][1] - param_bounds['hidden'][0])) # 学习率用对数空间解码 lr = param_bounds['lr'][0] * (param_bounds['lr'][1] / param_bounds['lr'][0]) ** x[2] # batch size 映射到 16~128 之间的离散值 batch = int(16 * (2 ** (x[3] * 3))) batch = max(16, min(128, batch)) return window, hidden, lr, batch解码逻辑里有两个点值得展开。学习率如果用线性映射在 [0.0001, 0.01] 上随机搜索,大概率会集中在 0.005 附近,但最优学习率往往靠近低值区域,所以用对数空间解码,让搜索粒度在低值区域更细。Batch Size 用 2 的幂次映射,匹配深度学习框架对 Batch Size 的常用构想,也保证搜索到的值一定是实践中更好用的数值。
适应度评估的核心代码是训练一个 LSTM 并返回验证集误差。我在这个过程里加了早停控制,避免每个粒子都训练到固定轮数:
def evaluate_fitness(x, train_loader, val_loader, input_dim, epochs=60): window, hidden, lr, batch = decode_particle(x) # 根据 window 重新构造训练、验证样本 X_train, y_train = build_window_dataset(train_scaled, window) X_val, y_val = build_window_dataset(val_scaled, window) train_loader = torch.utils.data.DataLoader( list(zip(X_train, y_train)), batch_size=batch, shuffle=True) val_loader = torch.utils.data.DataLoader( list(zip(X_val, y_val)), batch_size=batch, shuffle=False) model = LSTMPredictor(input_dim, hidden, dropout=0.1) optimizer = torch.optim.Adam(model.parameters(), lr=lr) criterion = nn.MSELoss() best_val_loss = float('inf') patience = 10 counter = 0 for epoch in range(epochs): model.train() for bx, by in train_loader: optimizer.zero_grad() pred = model(bx) loss = criterion(pred, by) loss.backward() optimizer.step() model.eval() val_loss = 0.0 with torch.no_grad(): for bx, by in val_loader: pred = model(bx) val_loss += criterion(pred, by).item() if val_loss < best_val_loss: best_val_loss = val_loss counter = 0 else: counter += 1 if counter >= patience: break return best_val_loss这个函数就是 IPSO 和 LSTM 之间的接口。每个粒子调用一次,返回的 loss 越小代表这组超参数越好。训练过程中验证集上的 best_val_loss 被记住,早停机制防止过度训练。
4.3 IPSO 主循环实现
以下就是改进粒子群的主循环。我把惯性权重余弦递减、自适应变异、速度自适应裁剪三个改进都体现在这个循环里。
def ipso_search(train_scaled, val_scaled, input_dim, pop_size=20, max_iter=30): dim = 4 w_max, w_min = 0.9, 0.4 c1, c2 = 1.5, 1.5 v_max_init = 0.15 p_mut_max = 0.3 # 初始化 X = np.random.rand(pop_size, dim) V = np.random.uniform(-v_max_init, v_max_init, (pop_size, dim)) pBest = X.copy() pBestLoss = np.array([evaluate_fitness(p, train_scaled, val_scaled, input_dim) for p in X]) gBest_idx = np.argmin(pBestLoss) gBest = X[gBest_idx].copy() gBestLoss = pBestLoss[gBest_idx] for t in range(max_iter): # 1. 余弦递减惯性权重 w = w_min + 0.5 * (w_max - w_min) * (1 + np.cos(np.pi * t / max_iter)) # 2. 自适应速度上限 v_max = v_max_init * (1 - 0.5 * t / max_iter) for i in range(pop_size): r1, r2 = np.random.rand(dim), np.random.rand(dim) V[i] = w * V[i] + c1 * r1 * (pBest[i] - X[i]) + c2 * r2 * (gBest - X[i]) V[i] = np.clip(V[i], -v_max, v_max) X[i] = np.clip(X[i] + V[i], 0, 1) # 3. 自适应变异 p_mut = p_mut_max * (1 - t / max_iter) mut_mask = np.random.rand(dim) < p_mut if mut_mask.any(): X[i][mut_mask] = np.random.rand(mut_mask.sum()) # 更新个体最优 loss = evaluate_fitness(X[i], train_scaled, val_scaled, input_dim) if loss < pBestLoss[i]: pBest[i] = X[i].copy() pBestLoss[i] = loss # 更新全局最优 current_best_idx = np.argmin(pBestLoss) if pBestLoss[current_best_idx] < gBestLoss: gBest = pBest[current_best_idx].copy() gBestLoss = pBestLoss[current_best_idx] print(f"Iter {t+1}/{max_iter}, best loss: {gBestLoss:.6f}") return decode_particle(gBest), gBestLoss这段代码里,变异操作的设计我提一下:它不是无脑把所有粒子都变异,而是按维度随机触发。一个粒子可能只有某一维被重置,其他维度保持不变,这样既引入了新位置,又不至于完全破坏已经找到的好方向。变异概率前大后小,配合余弦递减的惯性权重,实际运行中种群多样性保持得不错。
4.4 参数配置与超参搜索策略
我在实验中对 IPSO 自身的参数做了一组基准设定:
| 参数 | 设定值 | 设置理由 |
|---|---|---|
| 种群规模 | 20 | 兼顾搜索多样性和训练耗时 |
| 最大迭代 | 30 | 在精度提升和计算成本之间折中 |
| c1 / c2 | 1.5 / 1.5 | 均衡个体经验和群体经验 |
| 初始 v_max | 0.15 | 防止粒子初始阶段飞出搜索空间 |
| 最大变异率 | 0.3 | 保证足够探索能力又不至于变成随机搜索 |
种群规模这块值得多说一句。LSTM 每次适应度评估都要训练几十轮,粒子数如果从 20 加到 50,单次寻优时间会成倍增加,但精度收益不明显。我试过 pop_size=30 和 pop_size=40,收敛精度提升有限,训练时间涨了大半,最后折中在 20。
5. 实验结果对比与分析
实验环节我把 IPSO-LSTM、标准 PSO-LSTM、手动调参的 LSTM 三个方案放在同一批数据上对比。评估指标选了三项:均方根误差 (RMSE)、平均绝对误差 (MAE)、平均绝对百分比误差 (MAPE)。公式都是常用的定义,这里不赘述,直接上我这边测试得到的代表性结果。
5.1 收敛过程对比
先看收敛曲线。标准 PSO 大概在迭代 12 代左右就进入平台期,后期最强的改进也推不动最优适应度下探。IPSO 的收敛过程明显更平缓,前 8 代快速下降,10 到 20 代之间还能看到多次小幅跳变——这些跳变就是变异机制触发后找到更优参数点的表现。最终 IPSO 的全局最优适应度比标准 PSO 低了约 12% 到 18%(不同随机种子下波动,但大体在这个区间)。
这个结果和我改进算法时的预期一致。标准 PSO 的问题不是搜不到好参数,而是过早把种群压缩到一个局部极值附近,后期缺乏逃逸机制。变异操作的加入,相当于给了算法一个持续“越狱”的机会。
5.2 预测精度对比
模型预测精度对比如下(测试集覆盖了连续 3 个月的负荷数据,包含工作日、周末和若干节假日):
| 方案 | RMSE / MW | MAE / MW | MAPE / % |
|---|---|---|---|
| 手动调参 LSTM | 186.5 | 142.3 | 4.35 |
| PSO-LSTM | 168.2 | 128.7 | 3.82 |
| IPSO-LSTM | 152.4 | 114.6 | 3.26 |
可以看到,从手动调参到 PSO-LSTM,MAPE 下降约 0.5 个百分点;从 PSO 到 IPSO,MAPE 又下降约 0.56 个百分点,说明改进算法的增益甚至比“引入自动寻优”本身更明显。IPSO 找到的最优超参数组合大致是:窗口 96、隐藏层 96、学习率 0.0012、Batch Size 64。这几个值都是比较典型的负荷预测配置,但注意它们之间存在交互性,单独看任何一项都没有绝对优势,关键是组合匹配。
5.3 不同时段误差分析
进一步把预测误差按一天的不同时段拆开看,IPSO-LSTM 在早晚高峰段(8:00-11:00、17:00-21:00)的误差比手动调参减少了约 18%。这个时段正是负荷变化速率最快的时候,模型对特征的敏感度要求最高。这也侧面说明窗口和隐藏层规模搜索得比较合适——窗口太短捕捉不了完整的上升趋势,隐藏层太少学不到足够复杂的曲线形态。
另外我注意到节假日样本的数量在数据集中占比很低,任何模型在节假日上的误差都会偏高。IPSO 虽然在寻优中不会专门针对节假日加权,但假期特征作为额外输入还是起到了作用。如果后续要把节假日预测精度继续拉高,建议在损失函数中增加节假日样本的权重,或者采用分时段建模策略。
6. 常见问题与排查实录
最后这部分把我实际调试中碰到的高频问题整理成一份排查速查表,都是代码和原理之外的真实经验。
6.1 改进算法跑出来的结果和标准 PSO 差不多怎么办
很多人复现改进算法后会发现增益不明显,先别急着怀疑算法本身。我遇到这种情况时先检查三件事:一是变异概率是不是设得太小,导致变异机制形同虚设;二是速度边界是不是限制得太死,粒子大部分时间都在原地踏步;三是搜索空间范围是否合理,如果边界设得太窄,标准 PSO 也能轻松覆盖,改进自然看不出优势。
一个更隐蔽的原因是适应度评估的随机性。LSTM 每次训练、每个粒子评估都存在随机性,如果优化算法对同一组超参数评估两次得到截然不同的误差,收敛曲线会乱成一团,改进带来的稳定增益也会被噪声淹没。解决方法是设置好随机种子,并且在条件允许时用多次训练取平均的方式评估粒子适应度。
我自己的测试经验是,当种群规模在 20、迭代 30 次左右时,IPSO 对标准 PSO 的改善主要体现在重复实验的方差上——连续跑 5 次,IPSO 的最终结果波动更小。这比单纯看一次实验的精度更重要,说明算法稳定性提升了。
6.2 LSTM 训练震荡、验证集误差忽高忽低
这种情况多数不是 IPSO 的问题,而是 LSTM 训练配置的问题。我踩过的原因主要有两个:学习率过大导致损失函数震荡,或者 Batch Size 过小导致梯度估计噪声大。
排查方法也很直接:拿一组固定的超参数(比如窗口 96、隐藏层 64、学习率 0.001、Batch Size 32),连续训练 3 次,看验证集误差的波动情况。如果波动超过 5%,说明训练过程本身不够稳定,这时候要去 IPSO 里搜学习率和 Batch Size 是白费力气,因为同样的参数每次评估结果都不一样。先调模型训练稳定性,再谈超参寻优。
6.3 早期收敛速度很快,但后面一直原地踏步
这是标准的早熟收敛现象。优先检查变异机制的触发情况:可以在迭代循环里打印每个粒子的变异触发次数,如果全程没有粒子被变异过,说明 p_mut 初值设置太小或者衰减太快。我建议 p_mut 初值从 0.3 甚至 0.4 开始,后期衰减到接近 0,前期多尝试一些随机扰动不会坏事,后期不扰动保持精细收敛。
另一个相关问题是 gBest 长时间不更新。可以在迭代过程中记录 gBest 连续未更新的代数,如果超过 8 代,就加大这一代的变异概率做一次“重置脉冲”,人为打散部分粒子的位置。这属于实操层面的调试手段,但效果往往立竿见影。
6.4 代码级排查:为什么粒子解码出来都是边界值
如果你发现寻优结果永远贴在搜索空间的边界上,比如窗口总是 168 或者 24,多半是适应度函数在设计上有问题。我遇到过一种情况:验证集误差里隐藏了训练不充分的信息,训练轮数上限太低,大窗口、大网络的模型还没收敛就被判定为差方案,导致搜索偏向小窗口、小网络。解决方法是提高训练轮数上限,同时保证早停的 patience 足够长,给模型充分收敛的时间。
写在最后的一些经验
跑完整个项目回头看,这套 IPSO-LSTM 方案最大的价值不是把某个数据集上的 MAPE 从 4% 压到 3%,而是把调参这件事从“手感驱动”变成了“算法驱动”。以前换个数据集就要重新手动试十几个参数组合,现在把 IPSO 丢进去跑一轮,虽然也要一两个小时,但基本不用人守着。如果你后续想扩展,往几个方向都有现成路径:一是把 IPSO 的适应度函数从单目标改成多目标,同时兼顾误差和模型复杂度;二是在特征侧加入更多外部因素,比如温度、气压、电价等;三是把单步预测改成直出预测未来 24 小时的输出结构,配合 Sequence-to-Sequence 模型入手。总体而言,算法改进的空间永远比想象的大,但先把流程跑通、把数据坑踩平,才是更值得投入时间的事情。