TCN-LSTM-多头注意力模型超参数优化:WOA与SSA对比

发布时间:2026/10/25 17:44:54
TCN-LSTM-多头注意力模型超参数优化:WOA与SSA对比

简介:这是一份面向机器学习研究者和高级开发者的技术文档,聚焦时间序列预测场景,系统讲解如何利用鲸鱼算法WOA与麻雀算法SSA优化TCN-LSTM-Multihead-Attention组合预测模型。文档先拆解TCN的扩张卷积与残差连接、LSTM门控机制和Multihead-Attention注意力原理,说明三者如何分别捕捉短期波动、长期依赖并增强模型灵活性;随后阐述WOA与SSA的寻优流程,用于自动确定关键超参数;最后展示电力负荷预测从模型搭建到验证的完整过程。包体为单个docx文件,压缩包仅216KB,内容紧凑,包含模型总体设计、组件原理推导、优化算法流程及Python实现片段。目前已有222人学习,适合希望将多种深度学习组件与仿生优化算法结合落地到实际预测任务的研究者。阅读后能快速理解复合网络的搭建思路、超参数调优手段与实现细节,便于迁移到含短周期波动和长周期规律的时间序列数据中。

1. 为什么超参数是TCN-LSTM-Multihead-Attention模型的最后一道坎

做过lstm时间序列预测python落地的人大概都体会过:单跑一个LSTM预测模型,调几个超参还能接受,一旦把TCN、LSTM、Multihead-Attention三个模块串成一条流水线,可调参数立刻翻倍——学习率、TCN卷积核尺寸和通道数、LSTM隐藏层维度和层数、注意力头数、dropout,六七个参数都靠手试,一轮实验就是几小时。WOA(鲸鱼优化算法)和SSA(麻雀搜索算法)这类群体智能算法,正是用来把这堆超参数当成一个连续优化问题去自动寻优的。这篇内容适合做负荷预测、设备寿命预测、气象或金融时序的工程师,用WOA或SSA替你把超参搜索从“玄学调参”变成可复现的流程。

2. WOA与SSA是怎么找超参数的:两种群智能算法的寻优逻辑

2.1 WOA:鲸鱼群的气泡网策略如何搜索参数空间

WOA(Whale Optimization Algorithm)模拟座头鲸用气泡网围捕鱼群的行为。放在超参搜索场景里,每个粒子代表一组TCN-LSTM-Multihead-Attention的超参数,粒子在参数空间里移动,每次移动都参考当前全局最优解。WOA有三种位置更新策略。

收缩包围(encircle)的公式是:

X(t+1) = X*(t) - A·|C·X*(t) - X(t)|

其中A = 2a·r1 - a,C = 2·r2,a随迭代从2线性衰减到0。当A的绝对值小于1时,粒子向当前最优个体收缩;当A的绝对值大于等于1时,粒子会远离最优解,相当于撒开网做全局随机搜索。这个“远离”的机制是WOA前期探索的主要来源。

螺旋更新(bubble-net attacking)的公式是:

X(t+1) = D·e^(b·l)·cos(2πl) + X*(t),D = |X*(t) - X(t)|

这个更新让粒子以对数螺旋的轨迹逼近最优解,模仿鲸鱼旋转上升攻击鱼群的路径。实际实现时,用概率p=0.5在收缩包围和螺旋更新之间二选一,前期探索和后期开发的比例由a的衰减速度控制。

实现层面,WOA只引入A和C两个随机系数,结构非常干净,对种群数量不敏感,用5到10个粒子就能在低维问题里收敛。由于每个粒子都独立向最优位置靠拢,搜索行为相对直接,超参数维度不超过5时WOA往往比SSA更快见到适应度下降。

2.2 SSA:发现者-加入者-警戒者结构为什么适合高维

SSA(Sparrow Search Algorithm)把麻雀群分成三类角色,每类角色做的事不一样。

发现者负责大范围探索。当随机报警值R2小于安全阈值ST时,发现者按Xi·exp(-i/(α·T))更新,向更开阔的区域移动;当R2≥ST时,发现者快速向最优位置靠拢,模拟麻雀发现危险后逃向安全区。

加入者跟踪适应度最高的发现者。如果某个加入者的排名靠后(i > n/2),它就直接随机飞到一个新区域,避免所有个体挤在一起;排名靠前的加入者则向全局最优位置逼近,公式可以简化为Xbest + |Xi - Xbest|·A_inv,其中A_inv是±1构成的随机方向矩阵的伪逆。

警戒者每轮随机挑一部分粒子做扰动。如果它当前适应度比全局最差好,就往最优位置靠拢;如果自己就是全局最优,则向群体中心移动,防止被“一网打尽”。

这三层结构本质上把搜索分成了三个层次:发现者探索新区域,加入者做局部开发,警戒者负责跳出局部最优。当优化维度上升到6个以上时,SSA的种群多样性保持得比WOA好,不容易出现所有粒子挤在同一个局部最优附近的情况。代价是每一轮排序和角色切换引入了额外开销,且ST、PD、SD这几个参数需要人工设定,设得不好会明显影响收敛速度。

2.3 先选哪个:WOA和SSA的适用边界

从工程取舍的角度,我给两个判断标准:维度小于等于5、只想快速拿到一组可用参数,用WOA,粒子和迭代轮次可以少设一些,单轮实验时间更短;维度大于等于6、或者你怀疑收敛曲线会卡在同一个适应度值上,用SSA,它牺牲了一部分收敛速度,换来了跳出局部最优的可能。

TCN-LSTM-Multihead-Attention的常用搜索维度是7:学习率、TCN通道数、TCN卷积核、LSTM隐藏层、LSTM层数、注意力头数、dropout。按上面的规则,SSA更稳妥。但我实际项目里通常两种都跑一遍,因为它们的随机性来源不同,最终得到的超参组合常常不是一个结果,谁低选谁。

对比网格搜索和贝叶斯优化,元启发式算法的优势体现在参数类型上。网格搜索在7个维度上哪怕每个只抽5个值,组合数就是5的7次方,接近8万组,根本跑不完;贝叶斯优化在连续参数上效果好,但遇到TCN卷积核、LSTM层数、注意力头数这类整型参数时,需要额外做离散化。WOA和SSA对参数类型不敏感,连续值在解码时自己取整就行,实现最简单。劣势则是没有利用历史评估的梯度信息,碰到高维度连续参数空间时收敛速度不如贝叶斯优化。所以如果项目后期已经确定了大致范围,想精细收一下精度,也可以考虑换贝叶斯优化继续跑。

3. 搭TCN-LSTM-Multihead-Attention基线模型:PyTorch实现与超参设定

3.1 数据预处理:滑动窗口与归一化的标准流程

做时间序列预测,第一件事是切分数据。我用前70%到80%作为训练集,剩下部分做验证和测试,绝不打乱顺序。很多人一上来就用train_test_split随机切,结果模型“偷看”到未来的信息,验证集指标虚高,换到真实数据上立刻翻车。时序数据必须按时间顺序切分,这是一条铁律。

import numpy as np from sklearn.preprocessing import MinMaxScaler def create_dataset(series, window=24, horizon=1): X, y = [], [] for i in range(len(series) - window - horizon + 1): X.append(series[i:i+window]) y.append(series[i+window:i+window+horizon]) return np.array(X), np.array(y) # 只对训练集做归一化,测试集用训练集的scaler做transform scaler = MinMaxScaler(feature_range=(0, 1)) train_scaled = scaler.fit_transform(train_data.reshape(-1, 1)) test_scaled = scaler.transform(test_data.reshape(-1, 1)) X_train, y_train = create_dataset(train_scaled, window=24, horizon=1) X_test, y_test = create_dataset(test_scaled, window=24, horizon=1)

窗口长度一般取序列周期的1到2倍,日负荷数据取24或48,horizon是预测的未来步数,通常取1。归一化只用训练集的scaler做fit_transform,测试集只transform,这一步是为了避免测试集的信息泄露到训练过程。最后评估误差时要inverse_transform还原成真实量纲。

3.2 tcn模型结构:从因果卷积到残差连接

TCN的核心是因果卷积加膨胀卷积。因果卷积保证当前时刻的输出只看过去信息,不会引入未来时刻的数据;膨胀卷积让感受野指数级扩大,用较少的层数就能覆盖足够长的历史窗口。这两个机制是tcn模型结构里最值得理解的部分。

import torch.nn as nn class TCNBlock(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, dilation, dropout=0.2): super().__init__() padding = (kernel_size - 1) * dilation self.conv1 = nn.utils.weight_norm(nn.Conv1d( in_channels, out_channels, kernel_size, padding=padding, dilation=dilation)) self.conv2 = nn.utils.weight_norm(nn.Conv1d( out_channels, out_channels, kernel_size, padding=padding, dilation=dilation)) self.dropout = nn.Dropout(dropout) self.relu = nn.ReLU() # 残差连接,维度不同时用1x1卷积对齐 self.residual = nn.Conv1d(in_channels, out_channels, 1) \ if in_channels != out_channels else nn.Identity() def forward(self, x): out = self.relu(self.conv1(x)) out = self.dropout(out) out = self.relu(self.conv2(out)) out = self.dropout(out) return self.relu(out + self.residual(x))

这段代码有三个关键点:padding取(kernel_size-1)*dilation是为保证卷积输出长度和输入一致;用weight_norm替代BatchNorm,能避免小batch训练时BatchNorm统计量漂移造成验证误差忽高忽低;残差连接只有在输入输出通道数一致时才能直接相加,不一致时用1×1卷积对齐。

TCN整体就是按dilation=1、2、4叠两到三层这样的TCNBlock。叠太少感受野不够,叠太多参数量上涨快且训练变慢,三层是我在大多数项目里的默认值。

3.3 lstm神经网络怎么与Multihead-Attention拼接:关键代码与形状对齐

下面这段lstm模型代码把LSTM输出接到多头注意力上,是整套模型结构里最容易写错的地方。

class TCNLSTMAttention(nn.Module): def __init__(self, n_features, tcn_channels=64, tcn_kernel=3, lstm_hidden=128, lstm_layers=2, n_heads=4, dropout=0.2): super().__init__() self.tcn = nn.Sequential( TCNBlock(n_features, tcn_channels, tcn_kernel, dilation=1, dropout=dropout), TCNBlock(tcn_channels, tcn_channels, tcn_kernel, dilation=2, dropout=dropout), TCNBlock(tcn_channels, tcn_channels, tcn_kernel, dilation=4, dropout=dropout), ) self.lstm = nn.LSTM(tcn_channels, lstm_hidden, lstm_layers, batch_first=True, dropout=dropout) self.attention = nn.MultiheadAttention( lstm_hidden, n_heads, dropout=dropout, batch_first=True) self.fc = nn.Linear(lstm_hidden, 1) def forward(self, x): # x: (batch, seq_len, n_features) x = x.permute(0, 2, 1) # (batch, n_features, seq_len) x = self.tcn(x) x = x.permute(0, 2, 1) # (batch, seq_len, tcn_channels) x, _ = self.lstm(x) # (batch, seq_len, lstm_hidden) attn_out, _ = self.attention(x, x, x) return self.fc(attn_out[:, -1, :])

这段代码有两个容易踩的坑:LSTM输出形状是(batch, seq_len, hidden),多头注意力要求query、key、value的序列长度对齐,这里直接把同一个x传进去做self-attention,batch_first=True后返回张量形状是(batch, seq_len, hidden);预测目标只取最后一个时间步的输出,如果horizon大于1,需要把fc的输出维度改成horizon。另外,当lstm_layers=1时,nn.LSTM里的dropout参数不会生效,但这不影响搜索流程,只是知道即可。

3.4 基线超参数范围表

在跑WOA和SSA之前,先给出一组手工基线参数,后面所有优化结果都和它对比。超参搜索的上下界我一般设成这样:

超参数搜索下限搜索上限采样方式
学习率1e-41e-2log均匀
TCN通道数32128整型
TCN卷积核25整型
LSTM隐藏层维度32256整型
LSTM层数13整型
注意力头数18整型
dropout0.00.5连续

这张表直接决定了WOA和SSA搜索空间的形状。学习率用log均匀采样而不是普通均匀采样,因为1e-2到1e-4这个区间里,线性采样会把大量个体丢在1e-2附近,而1e-3左右才是大多数场景的有效区。注意力头数必须能被LSTM隐藏层维度整除,否则MultiheadAttention在forward时会直接报错,这个合法性检查要写进目标函数里。

4. 把WOA和SSA接到模型上:目标函数、搜索维度和完整代码

4.1 适应度函数怎么写才不骗优化器

WOA和SSA只靠一个值判断一组超参好不好,就是适应度。这个值写不好,整个搜索就是碰运气。我的做法是:每个个体解码超参,构建模型,在训练集上用Adam训练若干轮,每轮在验证集上算一次MSE,记录验证集上的最小loss作为适应度返回。

import torch from torch.utils.data import DataLoader, TensorDataset def make_objective(X_train, y_train, X_val, y_val, device='cuda', max_epochs=30, patience=5, seed=42): def objective(hyperparams): torch.manual_seed(seed) np.random.seed(seed) lr, tcn_ch, tcn_k, lstm_hidden, lstm_layers, n_heads, dropout = hyperparams tcn_ch = int(tcn_ch) tcn_k = int(tcn_k) lstm_hidden = int(lstm_hidden) lstm_layers = int(lstm_layers) n_heads = int(n_heads) # 合法性检查:多头注意力的头数必须能整除隐藏层维度 if lstm_hidden % n_heads != 0: return 1e9 model = TCNLSTMAttention( n_features=X_train.shape[-1], tcn_channels=tcn_ch, tcn_kernel=tcn_k, lstm_hidden=lstm_hidden, lstm_layers=lstm_layers, n_heads=n_heads, dropout=float(dropout) ).to(device) train_loader = DataLoader(TensorDataset( torch.tensor(X_train, dtype=torch.float32), torch.tensor(y_train, dtype=torch.float32)), batch_size=64, shuffle=True) optimizer = torch.optim.Adam(model.parameters(), lr=lr) criterion = torch.nn.MSELoss() best_val = float('inf') bad_epochs = 0 for epoch in range(max_epochs): model.train() for Xb, yb in train_loader: Xb, yb = Xb.to(device), yb.to(device) optimizer.zero_grad() loss = criterion(model(Xb), yb) loss.backward() optimizer.step() model.eval() with torch.no_grad(): y_pred = model(torch.tensor(X_val, dtype=torch.float32).to(device)) val_loss = criterion(y_pred, torch.tensor(y_val, dtype=torch.float32).to(device)) if val_loss < best_val: best_val = val_loss bad_epochs = 0 else: bad_epochs += 1 if bad_epochs >= patience: break return float(best_val.cpu().numpy()) return objective

这个目标函数有三个要点。第一,固定随机种子只是起点,即使固定了torch和numpy的seed,CUDA后端的卷积运算仍有不确定性,同一组超参跑两次可能得到略有差异的适应度。若项目对可复现性有硬要求,可以把每次评估改成训练两次取均值。第二,早停的patience设5,大部分个体到不了30轮就提前停掉,整体搜索时间可控。第三,非法参数组合直接返回1e9,不要抛异常,因为优化算法内部会调用几百次目标函数,任何一次异常都可能让整个流程中断。

4.2 WOA优化TCN-LSTM-Multihead-Attention的核心代码

import numpy as np def woa_search(objective, dim, lb, ub, pop_size=10, max_iter=30): lb = np.array(lb, dtype=float) ub = np.array(ub, dtype=float) pos = np.random.uniform(lb, ub, (pop_size, dim)) score = np.array([objective(p) for p in pos]) best_idx = np.argmin(score) best_pos = pos[best_idx].copy() best_score = score[best_idx] for t in range(max_iter): a = 2 - 2 * t / max_iter # 线性衰减,前期探索,后期开发 for i in range(pop_size): r1, r2 = np.random.rand(2) A = 2 * a * r1 - a C = 2 * r2 p = np.random.rand() if p < 0.5: if abs(A) < 1: D = abs(C * best_pos - pos[i]) pos[i] = best_pos - A * D else: rand_idx = np.random.randint(pop_size) rand_whale = pos[rand_idx] D = abs(C * rand_whale - pos[i]) pos[i] = rand_whale - A * D else: b = 1.0 l = np.random.uniform(-1, 1) D = abs(best_pos - pos[i]) pos[i] = D * np.exp(b * l) * np.cos(2 * np.pi * l) + best_pos pos[i] = np.clip(pos[i], lb, ub) score[i] = objective(pos[i]) if score[i] < best_score: best_score = score[i] best_pos = pos[i].copy() return best_pos, best_score

收缩包围和螺旋更新是二选一的概率关系,各占50%。A的绝对值大于1时粒子远离最优,相当于一次随机探索;小于1时粒子向最优收拢。WOA的这个特性在低维搜索里不容易陷入局部极值,但种群太小时,粒子一旦收敛到同一区域就很难再分散。所以pop_size建议至少8,max_iter至少20,否则搜索效果不稳定。

4.3 SSA优化TCN-LSTM-Multihead-Attention的核心代码

def ssa_search(objective, dim, lb, ub, pop_size=10, max_iter=30, pd=0.2, sd=0.2, st=0.8): lb = np.array(lb, dtype=float) ub = np.array(ub, dtype=float) pos = np.random.uniform(lb, ub, (pop_size, dim)) score = np.array([objective(p) for p in pos]) gbest_idx = np.argmin(score) gbest_pos = pos[gbest_idx].copy() gbest_score = score[gbest_idx] n_pd = int(pop_size * pd) n_sd = int(pop_size * sd) for t in range(max_iter): order = np.argsort(score) sp = pos[order].copy() sc = score[order].copy() if sc[0] < gbest_score: gbest_score = sc[0] gbest_pos = sp[0].copy() # 发现者更新 for i in range(n_pd): r2 = np.random.rand() if r2 < st: sp[i] = sp[i] * np.exp(-i / (np.random.rand() * max_iter + 1e-8)) else: sp[i] = sp[i] + np.random.randn(dim) * (sp[i] - gbest_pos) # 加入者更新 for i in range(n_pd, pop_size): if i > pop_size / 2: sp[i] = np.random.uniform(lb, ub) else: A = np.random.choice([-1, 1], dim) A_inv = 1.0 / (A + 1e-8) sp[i] = gbest_pos + np.abs(sp[i] - gbest_pos) * A_inv # 警戒者更新 for i in range(n_sd): idx = np.random.randint(pop_size) if sc[idx] > gbest_score: sp[idx] = gbest_pos + np.random.randn(dim) * np.abs(sp[idx] - gbest_pos) else: sp[idx] += np.random.randn(dim) * (sp[idx] - np.median(sp, axis=0)) pos = np.clip(sp, lb, ub) score = np.array([objective(p) for p in pos]) if score.min() < gbest_score: gbest_score = score.min() gbest_pos = pos[np.argmin(score)].copy() return gbest_pos, gbest_score

SSA和WOA有两个显著差异。每一轮按适应度排序后再更新,个体的角色随适应度动态变化,不是一开始就固定死。警戒者的更新带随机扰动,容易让种群跳出局部最优。在优化TCN-LSTM-Multihead-Attention这种非凸、多峰的适应度面上,SSA找到的最优值往往比WOA更低,但代价是每一轮排序和角色切换增加了计算开销。

4.4 搜索维度与边界设置

调用上面两个函数前,把超参上下界写成同样长度的数组。注意WOA和SSA内部都在连续空间做位置更新,只有送进objective时才取整:

dim = 7 lb = [1e-4, 32, 2, 32, 1, 1, 0.0] ub = [1e-2, 128, 5, 256, 3, 8, 0.5] best_w, score_w = woa_search(objective, dim, lb, ub, pop_size=10, max_iter=30) best_s, score_s = ssa_search(objective, dim, lb, ub, pop_size=10, max_iter=30)

连续值取整后,2.2和2.8都会落到2,所以TCN卷积核这类整型参数在搜索时有概率被“截断”成边界值。这是群体算法做离散优化的固有问题,不用太纠结。如果确实要精细搜出中间值,可以把对应维度的边界收窄,比如卷积核的上界下界定为2.5到4.5,让取整后的有效区域集中在中间。

5. 避坑:WOA、SSA搭配TCN-LSTM-Multihead-Attention的5个翻车现场

下面这5个问题都是实际调参时容易遇到的翻车现场,每一条按现象、原因、解决的顺序写,方便对照排查。

5.1 适应度曲线不下降但验证集误差也没降

现象:woa_search跑完30轮,保存的收敛曲线从头到尾几乎是一条横线,适应度在0.012到0.015之间乱跳。

原因:目标函数内部没有固定随机种子。每一轮评估同一组超参得到的适应度波动,大于不同超参之间的差异,优化算法看到的是一个噪声面,根本找不到搜索方向。

解决:在objective入口调用torch.manual_seed(seed)和np.random.seed(seed)。如果仍抖动,把max_epochs加大到50甚至80,让模型训练得更充分,不同超参的差距才会显现出来。另外注意DataLoader的shuffle=True会在每个epoch改变数据顺序,固定随机种子能让shuffle的模式在每次评估间保持一致。

5.2 SSA早熟收敛到局部最优

现象:SSA在第12轮左右就停止更新,之后无论跑多少轮,适应度都不再改变。

原因:警戒者比例sd设得太大,比如0.4,会破坏发现者和加入者的搜索节奏;安全阈值st设得太小,比如0.2,会让发现者过早进入避险模式,整个种群快速收拢到一个小区域。

解决:sd保持在0.15到0.25之间,st保持在0.6到0.8之间。发现者比例pd控制在0.2左右。第一轮跑完后如果还是觉得收敛太快,把max_iter加到50,不要急着调这些角色参数,SSA对它们比较敏感,改一个参数经常牵动其他方面的表现。

5.3 WOA在低维下比高维更稳定:A和C的衰减

现象:同一份数据、同一组超参边界,WOA连续跑三次,三次找到的最优适应度都不一样,方差很大。

原因:WOA的a虽然从2线性衰减到0,但每轮迭代中的r1和r2都是独立采样的,A和C的跳变幅度很大。高维搜索空间里,这种随机性让最终结果变得不稳定。

解决:把max_iter加大到50以上,或者同一轮里跑WOA和SSA各10次,取最优的作为最终参数。做对比实验时,我习惯用10次运行的中位数来评估算法,而不是单次的最好值,因为单次最好值很容易被随机性带偏。

5.4 训练时间爆炸:每轮评估的epoch数设置

现象:pop_size=10、max_iter=30跑了一晚上还没出结果。

原因:目标函数里每个个体的训练是一次完整训练流程。10个个体乘以30轮,每轮最多30个epoch,即使有早停,理论最坏情况也是接近9000次前向反向传播,在CPU上跑一天都不奇怪。

解决:先做小规模试跑,把max_epochs降到15、patience降到3,确认目标函数没报错、适应度确实在下降,再做完整搜索。时间预算紧张的话,把batch_size从64调到128,单轮训练时间能缩短近一半,对最终精度影响很小。

5.5 优化结果低于基线

现象:WOA和SSA找到的最优超参,换上之后的效果竟然比手工设置的那组基线还差。

原因:目标函数里的验证集和最终测试集来自同一条时间序列,验证集占比太小(比如只有5%)会让搜索结果过拟合验证集;另一种情况是搜索边界设得太窄,把真正好的参数挡在边界外。

解决:验证集至少占全样本的15%到20%。边界方面,学习率上限放宽到2e-2,TCN通道数上限放宽到160,让搜索空间覆盖常见的有效区。放宽之后解的方差会变大,但不代表算法变差,先看收敛曲线的最终值,如果比原来好很多,再逐步缩窄边界。

提示:搜索边界、验证集比例、随机种子这三个因素叠加在一起,会把WOA和SSA的搜索结果完全改变。排查时优先按这个顺序查,不要一上来就怀疑算法本身。

6. 验证优化效果:适应度曲线、预测误差和一次完整的对比流程

光看目标函数返回的验证集loss还不够,换到测试集上重新对比才有说服力。我的完整流程是:固定随机种子,用一组基线超参训练模型,记录测试集上的RMSE、MAE、MAPE和R²;然后分别用WOA和SSA找到的最优超参重新训练,再对比测试集指标。注意不要直接把优化过程中验证loss最小的那一组参数当成最终模型,因为早停是在验证集上生效的,验证集和测试集的数据分布仍有差异,必须重新完整训练一次。

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score def evaluate_model(model, X_test, y_test, scaler, device): model.eval() with torch.no_grad(): pred = model(torch.tensor(X_test, dtype=torch.float32).to(device)) pred = scaler.inverse_transform(pred.cpu().numpy().reshape(-1, 1)) y_true = scaler.inverse_transform(y_test.reshape(-1, 1)) rmse = np.sqrt(mean_squared_error(y_true, pred)) mae = mean_absolute_error(y_true, pred) r2 = r2_score(y_true, pred) mape = np.mean(np.abs((y_true - pred) / (y_true + 1e-8))) * 100 return rmse, mae, r2, mape

最后判断优化这项工作值不值得做,标准不是训练loss比基线低了多少,而是测试集RMSE有没有下降5%以上。下降不到5%时,我倾向把优化结论记成“超参敏感度低”,说明模型对这组参数不敏感,省下时间不如去改数据窗口长度或特征工程。这是我踩过几次坑之后形成的习惯:先用最便宜的设定验证搜索流程能跑通,再投入完整资源,最后用多项指标对比,而不是只盯一个验证loss。

我的习惯是WOA和SSA各跑10次,把这20次的最优score列出来,选中位数最小的那组参数作为最终结果。如果两种算法最终得分差距很小,我会优先选SSA,因为它跳出局部最优的能力更强,在后续换数据、换预测步长时通常更稳。希望帮到你。

本文还有配套的精品资源,点击获取