从手动调参到自动搜参:ISSA-CNN-BiLSTM多输入单输出回归的完整落地记录
上个月在做一个多特征时间序列回归任务时,我用CNN-BiLSTM搭了个基准模型,结果验证集MAE始终在0.08来回横跳,手动调卷积核数量、LSTM隐层节点和学习率调了整整三天,每次改一个参数都要重跑一遍,效率极低。后来索性把麻雀搜索算法(SSA)搬进来做超参数自动寻优,再把标准的SSA做了三处针对性改进,形成了ISSA-CNN-BiLSTM组合方案,验证集MAE降到了0.062,训练轮数也压缩了近三分之一。这篇博文就把从算法原理、改进思路到Python代码实现的完整过程记录下来,给正在折腾CNN、BiLSTM做多输入单输出回归、又苦于参数难调的朋友一个可以直接抄作业的参考。
1. 从手动调参到自动搜参:为什么要做ISSA-CNN-BiLSTM
1.1 模型选型的基本逻辑
先明确一个基础认知。多输入单输出回归任务里,CNN和BiLSTM的分工本质上是不同的:CNN通过一维卷积在局部窗口内提取特征模式,对相邻时间步之间的短期关联非常敏感,比如风速预测中前几个时刻的突变趋势;BiLSTM则通过正向和反向两个LSTM单元分别捕捉时间序列的历史信息和未来信息,适合处理长距离依赖,比如负荷预测中前一周同时间段的规律。二者串联后,CNN先做特征抽取,BiLSTM再做时序建模,最后接一个全连接回归头输出单维连续值,这是被大量实验验证有效的结构组合。
但组合模型最大的麻烦不在结构设计,而在超参数。卷积核数量取32还是64,LSTM隐层节点取32还是128,学习率取0.001还是0.0005,dropout设0.2还是0.4,这些参数之间不是独立的。卷积核偏少可能导致特征提取不充分,但LSTM节点很多时又会过拟合;学习率大了收敛不稳,小了训练时间翻倍。我没有办法通过拍脑袋或者看一两个曲线就定位到最优组合,这是所有做回归预测的人都会撞上的墙。
1.2 四种超参数搜索方案的对比
手动调参不行,剩下可选的方案就是网格搜索、随机搜索、贝叶斯优化和群智能优化算法。我把它们放在一起对比过,各自特点非常明显:
| 搜索方法 | 核心思路 | 优点 | 缺点 |
|---|---|---|---|
| 网格搜索 | 穷举所有参数组合 | 实现简单,结果可复现 | 维度爆炸,训练成本极高 |
| 随机搜索 | 参数空间内随机采样 | 比网格搜索高效,不容易漏极值 | 仍有大量无效尝试 |
| 贝叶斯优化 | 用代理模型拟合参数与目标的关系 | 迭代次数少,适合连续参数 | 对高维离散参数处理较弱,实现稍复杂 |
| 麻雀搜索算法 | 模仿麻雀觅食与反捕食行为 | 全局搜索能力强,收敛快,天然处理连续参数 | 标准算法易早熟,需要改进 |
网格搜索对于CNN-BiLSTM这种动辄每轮迭代要训练几十次甚至上百次网络的模型来说基本不现实。我一开始试过在5个参数维度上各取4个候选值,组合数是4的5次方等于1024轮,每轮如果训练50个epoch,换算下来要跑几天。随机搜索比网格好一些,但缺少"利用已有经验继续朝更优区域搜索"的机制。贝叶斯优化理论上很合适,但在处理kernel_size这类取值不连续的整数参数时有天然的短板,而且代理模型本身的超参数也需要调。
相比之下,麻雀搜索算法在这类问题上的优势在于:它是一个群体智能算法,前期能在整个解空间均匀撒点,后期通过发现者、加入者、警戒者的角色分工逐步逼近最优区域,不需要计算梯度,对CNN-BiLSTM这种"黑盒"寻优任务非常友好。
1.3 为什么还要做改进
标准麻雀搜索算法不是没有毛病。它在迭代后期种群多样性会快速下降,所有麻雀都挤到局部最优附近,导致最终搜出来的参数组合只是"次优解"。而且标准算法的初始种群完全随机生成,如果初始分布不均匀,第一轮迭代就失去了全局探索的机会。说白了,CNN-BiLSTM的搜索空间不是光滑的凸函数,存在大量局部极小值,标准SSA很容易一头扎进去出不来。
2. 麻雀搜索算法基础与它的两个硬伤
2.1 三类麻雀的角色分工
要理解ISSA,必须先吃透标准SSA。算法把种群分成三类角色:发现者、加入者和警戒者。
发现者的职责是为整个种群寻找食物方向,位置更新规则是:
- 当安全阈值R2小于警戒阈值ST时,说明当前区域安全,发现者可以大范围搜索:
[
X_{i,j}^{t+1} = X_{i,j}^{t} \cdot \exp\left(-\frac{i}{\alpha \cdot T_{max}}\right)
] - 当R2大于等于ST时,说明有捕食者靠近,发现者需要迅速转移到安全区域:
[
X_{i,j}^{t+1} = X_{i,j}^{t} + Q \cdot L
]
加入者跟随发现者觅食,适应度高的加入者会靠近当前最优位置,适应度低的则飞到远处重新寻找食物。警戒者通常占种群总数的10%-20%,它们一旦发现危险,就把整个种群引向安全区域,位置更新同时受到全局最优和自身适应度的影响。
这个机制本身是优雅的。麻雀的三种角色正好对应了搜索的三种策略:发现者做全局勘探,加入者做局部开发,警戒者防止种群过早收敛。
2.2 硬伤一:随机初始化导致的种群分布不均
标准SSA用均匀随机数生成初始位置。在低维问题上问题不大,但在CNN-BiLSTM这种5到6维的搜索空间中,随机生成的点很容易扎堆在某个区域,另一些区域完全没有麻雀覆盖。比如首个卷积核数量初始值如果全部落在20到40之间,那算法基本不会去探索100以上的区域,即使那里藏着更优解也发现不了。
2.3 硬伤二:迭代后期陷入局部最优
标准SSA的发现者位置更新公式中,步长衰减只跟迭代次数t和个体序号i有关,没有引入"当前位置与最优位置的差距"这样的反馈信息。当种群逐渐聚集到某个局部最优附近时,发现者依然在用固定的指数衰减策略搜索,很难跳出去。警戒者虽然能在一定程度上维持多样性,但它的随机扰动范围有限,一旦种群整体收缩到局部最优的邻域内,单靠警戒者那点扰动根本不够。
这也是我一直不太建议直接拿标准SSA去优化深度学习模型的原因。神经网络超参搜索空间大、单次评估代价高,算法的探索效率直接决定了总耗时。
3. ISSA的三个改进点:公式、理由与落地细节
3.1 改进一:Tent混沌映射初始化种群
既然问题出在初始种群分布不均,最常见的解法就是引入混沌映射代替均匀随机数。Tent混沌映射能在[0,1]区间内生成分布均匀、又有良好遍历性的序列。我选Tent而不是Logistic,原因是Tent映射生成的序列在定义域内分布更均匀,且迭代速度快,非常适合作为种群初始化的前置生成器。
Tent映射的迭代公式是:
[
z_{n+1} = \begin{cases} 2z_n, & 0 \le z_n < 0.5 \ 2(1-z_n), & 0.5 \le z_n \le 1 \end{cases}
]
实现时,对每个麻雀个体的每一维都生成一个混沌值,再映射到搜索空间的上下界。实际测试中,Tent初始化让初始种群的覆盖率比随机初始化提高了不少,第一个迭代周期结束后,适应度最优值就比对照组低了约8%。这个改进几乎不增加计算量,属于"必做项"。
3.2 改进二:动态自适应权重修正发现者步长
标准SSA的发现者在R2小于ST时的步长因子只跟迭代次数相关,同样的衰减策略用在整个搜索过程中,前期探索范围不够大,后期收敛又不够精细。我的做法是给发现者的更新公式加上一个随迭代次数线性递减的自适应权重 (w),同时引入全局最优位置的引导项。
改进后的发现者位置更新公式:
[
X_{i,j}^{t+1} = w \cdot X_{i,j}^{t} \cdot \exp\left(-\frac{i}{\alpha \cdot T_{max}}\right) + c \cdot r \cdot (X_{best}^{t} - X_{i,j}^{t})
]
其中w从0.9随迭代次数线性递减到0.2。当迭代刚开始时权重偏大,麻雀尽量在原有方向上大步探索;后期权重变小,配合全局最优引导项,让个体更倾向于朝当前最优位置收敛。这个"全局最优引导"其实借鉴了粒子群算法的思想,能明显加快后期收敛速度。
3.3 改进三:柯西变异策略增强跳出局部最优的能力
光有初始化和动态权重还不够。当种群收敛到某个局部最优附近时,如果没有外部扰动,所有麻雀都会停在原地,这在优化深度学习超参数时是致命的。我选择对每次迭代筛选出的全局最优个体施加柯西变异,生成一个候选位置,如果新位置的适应度更好就替换原最优位置,否则保留原位置。
柯西分布的随机数可以用以下方式生成:
python复制def cauchy_random(scale=1.0):
return scale * np.random.standard_cauchy()
变异后的候选解为:
[
X_{new} = X_{best} + cauchy_random \cdot \beta
]
这里的 (\beta) 设为当前搜索空间范围的5%,太小了扰动不起作用,太大了会把最优个体踢太远反而浪费评估次数。
这个改进的价值在模型超参数寻优时体现得很明显。CNN-BiLSTM的损失面极度崎岖,一个局部最优和全局最优之间的适应度可能只差0.005,但对应的参数组合完全不同。柯西变异每次以较大概率产生一个远离当前最优的候选解,保证每隔几轮迭代就有一次"远程射击"的机会。
3.4 三个改进的协同逻辑
单独加任何一个改进都能提升效果,但组合起来提升最大:Tent混沌映射解决初始覆盖问题,动态自适应权重平衡探索与开发,柯西变异提供后期逃逸能力。这三者分别作用于算法的初始化阶段、主循环阶段、收尾阶段,互不干扰但互相补位。我跑过对照组实验,只用混沌初始化提升约8%,只用自适应权重提升约12%,加上柯西变异后整体提升约21%。
4. 多输入单输出回归模型:CNN与BiLSTM如何衔接才合理
4.1 两种"多输入"的理解
先说清楚"多输入"的定义。我在实际项目里遇到两种情况。
第一种是把多个特征序列拼接成一个三维张量,形状是(samples, timesteps, features),输入给Conv1D,卷积层的通道数自然就是特征维度。比如预测风电功率时,输入特征包括历史功率、风速、温度、湿度四个特征,时间步长取24,那么单个样本就是一个24x4的矩阵。
第二种是多个独立输入分支,每个分支有各自的处理网络,最后合并。比如一部分输入是气象站数据,另一部分是历史运行状态数据,两者的采样频率和物理含义差异很大,强行拼成一个矩阵会弱化各自的局部模式。这时候就要用Keras的多输入API。
本文的ISSA代码以第一种为主,但在模型构建函数里我会额外说明如何改成第二种。
4.2 滑动窗口构造样本的细节
多输入单输出回归的第一步是构造有监督样本。假设原始数据是一个长度为N的多特征序列,形状为(N, n_features),目标变量是其中的某一列或一个独立序列。使用滑动窗口切分:
python复制def make_dataset(data, target, timesteps=24, pred_len=1):
X, y = [], []
for i in range(len(data) - timesteps - pred_len + 1):
X.append(data[i:i + timesteps])
y.append(target[i + timesteps:i + timesteps + pred_len])
return np.array(X), np.array(y)
timesteps的选择直接影响模型性能。太小了模型看不到足够的上下文,太大了训练数据量骤减且引入无关噪声。我一般先做自相关分析,取目标变量自相关系数跌到0.2以下对应的滞后期作为参考值。对于小时级负荷数据,24到48通常是不错的范围。
4.3 网络结构设计
ISSA要优化的就是下面这个结构中的关键超参数:
python复制def build_model(filters, kernel_size, lstm_units, dropout_rate, lr, input_shape):
model = Sequential()
model.add(Conv1D(filters=filters, kernel_size=kernel_size,
activation='relu', padding='same',
input_shape=input_shape))
model.add(MaxPooling1D(pool_size=2))
model.add(BatchNormalization())
model.add(Bidirectional(LSTM(units=lstm_units,
dropout=dropout_rate,
return_sequences=False)))
model.add(Dense(16, activation='relu'))
model.add(Dense(1))
model.compile(optimizer=Adam(learning_rate=lr),
loss='mse', metrics=['mae'])
return model
几个容易踩坑的衔接细节:
- Conv1D默认在时间维度上滑动,输入必须是(samples, timesteps, features),特征维度不等于时间维度,很多人栽在这里。
- MaxPooling1D会将时间步减半,如果原始timesteps是奇数,池化后会和后续LSTM的时间维度对不上,建议要么在Conv1D里设padding='same',要么控制timesteps为偶数。
- Bidirectional(LSTM(..., return_sequences=False))返回的是正向和反向最后一个时间步隐藏状态的拼接,维度是2*lstm_units。很多人以为自动降维了,实际上就是拼接,在接Dense层时不用特别处理,但心里要清楚参数量的增长。
- 如果时序关系很重要,return_sequences可以设为True,再接一层BiLSTM,但这样参数量翻倍,小数据集很容易过拟合。我的经验是第一层BiLSTM用return_sequences=False就够了,除非数据量在十万级往上。
BatchNormalization放在池化后的位置,能加速收敛,但要注意预测阶段它依赖训练集的统计量,Keras会自动处理,不需要手动设置。
4.4 损失函数与评估指标
回归任务首选mse作为损失函数,因为它对大误差的惩罚力度强,能加速收敛。但评估指标我会同时看mae和rmse。mae直观反映平均误差,rmse能暴露大偏差样本。如果项目中离群点较多、不想让个别异常样本主导训练,可以把损失函数换成huber_loss。ISSA的适应度函数用的是验证集mae,因为它最稳定,不容易被个别极端样本带偏。
5. 完整Python实现:ISSA寻优与CNN-BiLSTM训练的耦合写法
5.1 环境准备
我用的是Python 3.10 + TensorFlow 2.13 + NumPy。装环境时容易被卡住的几个包,建议直接用如下组合:
bash复制pip install numpy pandas matplotlib scikit-learn tensorflow
如果机器有NVIDIA显卡,务必先装好CUDA和cuDNN对应的版本,再装tensorflow,否则会默认使用CPU训练,慢到怀疑人生。
5.2 搜索空间定义与麻雀个体编码
ISSA的每个麻雀个体代表一组CNN-BiLSTM超参数组合。我定义了6个待优化维度:
| 参数 | 含义 | 搜索范围 | 取值处理 |
|---|---|---|---|
| filters | 卷积核数量 | [16, 128] | 向下取整为整数 |
| kernel_size | 卷积核大小 | [2, 8] | 向下取整为奇数 |
| lstm_units | BiLSTM隐层节点数 | [16, 128] | 向下取整为整数 |
| dropout_rate | dropout比例 | [0.1, 0.5] | 保留两位小数 |
| learning_rate | 学习率 | [0.0001, 0.01] | 保持连续值 |
| batch_size | 批大小 | [16, 64] | 向下取整为整数 |
在ISSA迭代中,麻雀个体位置是一个6维连续向量,每一维解算适应度时再映射到具体的超参数。kernel_size这种需要整数的参数,取整后还要强制判断是否为奇数,如果不是就减1。
5.3 麻雀搜索算法主体实现
核心类的实现可以精简为下面这个结构:
python复制class ISSAOptimizer:
def __init__(self, dim, lb, ub, pop_size=10, max_iter=15):
self.dim = dim
self.lb = np.array(lb)
self.ub = np.array(ub)
self.pop_size = pop_size
self.max_iter = max_iter
self.p_num = int(pop_size * 0.7) # 发现者数量
self.s_num = int(pop_size * 0.2) # 警戒者数量
self.population = self._tent_init()
self.fitness = np.full(pop_size, np.inf)
self.best_pos = None
self.best_fit = np.inf
Tent混沌初始化用如下方式生成整个种群:
python复制def _tent_init(self):
pop = np.zeros((self.pop_size, self.dim))
for i in range(self.pop_size):
x = np.random.rand()
for d in range(self.dim):
if x < 0.5:
x = 2 * x
else:
x = 2 * (1 - x)
pop[i, d] = self.lb[d] + x * (self.ub[d] - self.lb[d])
return pop
这里的混沌序列虽然是同一个起点迭代出来的,但每一维都独立迭代,避免了各维度完全相关。
主循环中,每轮迭代要完成四件事:计算所有个体的适应度、按适应度排序并更新最优、更新发现者位置、更新加入者位置、更新警戒者位置,最后对最优个体施加柯西变异。适应度计算是大头,因为要完整训练一个CNN-BiLSTM模型。
5.4 适应度函数的写法
适应度函数是整个耦合过程的关键。我把它设计成接收一个麻雀个体位置向量,解码成超参数后构建模型,在训练集上训练固定epoch数,在验证集上返回mae:
python复制def fitness_func(position):
filters = int(position[0])
kernel_size = int(position[1])
if kernel_size % 2 == 0:
kernel_size -= 1
lstm_units = int(position[2])
dropout_rate = round(position[3], 2)
learning_rate = position[4]
batch_size = int(position[5])
model = build_model(filters, kernel_size, lstm_units,
dropout_rate, learning_rate, (timesteps, n_features))
early_stop = EarlyStopping(monitor='val_loss',
patience=8, restore_best_weights=True)
model.fit(X_train, y_train,
validation_data=(X_val, y_val),
epochs=60, batch_size=batch_size,
verbose=0, callbacks=[early_stop])
val_loss, val_mae = model.evaluate(X_val, y_val, verbose=0)
return val_mae
注意两个细节。第一,验证集必须是模型完全没见过的数据,我在做数据切分时严格按时间顺序切,避免随机打乱导致未来数据泄漏到训练集。第二,早停的patience设8,epoch上限设60,这样每个个体平均训练30到40个epoch就能完成评估。如果某个参数组合明显不合理,模型会提前停掉,不会浪费太多时间。
5.5 加入者与警戒者的位置更新
加入者的更新逻辑保留了标准SSA的分段思想:
python复制def _update_followers(self, sorted_idx):
for i in range(self.p_num, self.pop_size):
if i > self.pop_size / 2:
q = np.random.standard_normal(self.dim)
self.population[sorted_idx[i]] = q * np.exp(
(self.worst_pos - self.population[sorted_idx[i]]) / (i ** 2))
else:
a = np.random.rand(self.dim)
a[a <= 0.5] = -1
a[a > 0.5] = 1
a_plus = a.T @ np.linalg.inv(a @ a.T + 1e-8)
self.population[sorted_idx[i]] = self.best_pos + np.abs(
self.population[sorted_idx[i]] - self.best_pos) @ a_plus
self._check_bound()
警戒者只更新随机选出的s_num个个体,分为适应度等于全局最优和高于全局最优两种情况处理,这里不再展开代码,核心逻辑是给不同状态的个体施加不同的逃离扰动。
5.6 主循环
python复制optimizer = ISSAOptimizer(dim=6,
lb=[16, 2, 16, 0.1, 0.0001, 16],
ub=[128, 8, 128, 0.5, 0.01, 64],
pop_size=10, max_iter=15)
for t in range(optimizer.max_iter):
for i in range(optimizer.pop_size):
optimizer.fitness[i] = fitness_func(optimizer.population[i])
optimizer.update_best()
w = 0.9 - 0.7 * (t / optimizer.max_iter) # 自适应权重
optimizer.update_producers(w)
optimizer.update_followers()
optimizer.update_scouts()
optimizer.apply_cauchy_mutation(fitness_func)
print(f'iter {t+1}, best mae: {optimizer.best_fit:.6f}')
我设了10个个体、15次迭代,总共最多训练150次模型。每个模型平均训练40个epoch,在中等规模数据集(约5000个样本)上单次评估约20到30秒,整个寻优过程大约50分钟。如果时间充裕,把pop_size提升到15、max_iter加到20,效果会更好。
5.7 效率优化技巧
ISSA跑深度学习模型最烦的就是慢。三个优化手段能显著压缩时间:
第一,快速评估阶段用少量epoch。寻优过程中不需要让模型完全收敛,只要不同参数组合的相对优劣关系稳定即可。我试过把epoch上限从60降到40,寻优结果几乎不变,总时间节省了35%。
第二,用早停做动态epoch控制。无效参数组合会在十几轮内快速过拟合或在验证集上表现很差,早停能自动掐断这类评估。
第三,如果机器有多个GPU或CPU核心,可以对种群中的多个个体做并行评估。我当时没搞并行就已经能接受50分钟的耗时,但如果你的数据量大,建议用multiprocessing或concurrent.futures把适应度计算并行化。
6. 实测结果与坑位总结:从MAE到训练稳定性
6.1 实验基准与对比结果
我在某个公开的风电功率预测数据集上做了三组对照实验:手工调参的CNN-BiLSTM、标准SSA优化的CNN-BiLSTM、ISSA优化的CNN-BiLSTM。评价指标用验证集MAE和RMSE,结果如下:
| 方案 | 验证集MAE | 验证集RMSE | 搜索耗时 |
|---|---|---|---|
| 手工调参 | 0.0831 | 0.1124 | 3天 |
| SSA-CNN-BiLSTM | 0.0706 | 0.0962 | 约55分钟 |
| ISSA-CNN-BiLSTM | 0.0623 | 0.0857 | 约50分钟 |
ISSA搜出的最优参数组合是filters=96、kernel_size=5、lstm_units=64、dropout=0.31、learning_rate=0.0008、batch_size=32。这个组合和标准SSA搜出来的差异主要在filters和lstm_units上,ISSA更倾向于选择较大的卷积核数量配合适中的LSTM隐层节点,原因是柯西变异帮助算法跳出了一个小filters区域的局部最优。
6.2 数据泄漏:最隐蔽的坑
我在第一次跑实验时犯过一个典型的时序数据泄漏错误。当时为了省事,直接把数据用train_test_split随机切分,结果验证集MAE只有0.04,看着特别漂亮,但放到真实场景就崩了。原因很简单:时间序列样本之间不是独立的,前一个样本末尾的时间步很可能是后一个样本的开头,随机切分后训练集和验证集高度重叠,模型等于"见过"验证集数据。
正确的做法是按时间顺序切分:
python复制train_size = int(len(X) * 0.7)
X_train, X_val = X[:train_size], X[train_size:]
y_train, y_val = y[:train_size], y[train_size:]
如果数据本身有季节性周期,比如每天一个周期,建议按整周期切分而不是按样本数硬切,否则训练集可能只学到某个季节段内的规律。
6.3 随机种子对寻优结果的影响
神经网络训练本身有随机性,这就导致同一个参数组合在不同随机种子下评估出的MAE会有波动。ISSA在迭代过程中比较两个参数组合的优劣时,如果波动幅度大于组合间的真实差异,就可能选错方向。
对策有两个。第一,在构建模型时固定随机种子:
python复制def set_seed(seed=42):
tf.random.set_seed(seed)
np.random.seed(seed)
random.seed(seed)
第二,对排名靠前的几个个体做二次评估,每个个体用3个不同种子各训练一次,取MAE均值作为最终排名依据。这个操作只在寻优结束后做,不会增加太多时间成本,但能显著提升最终选参的可靠性。
6.4 多输入多输出场景的扩展方向
如果任务从单输出变成多输出,比如同时预测未来三个小时的功率,改动不大:把输出层Dense(1)改成Dense(pred_len),损失函数改成mse即可,ISSA的搜索空间和时间步构造逻辑都不用变。如果数据集里有多个独立数据源、不适合拼接成单一三维张量,可以用Keras的多输入API:
python复制def build_multi_input_model(filters, kernel_size, lstm_units, lr, input_shape1, input_shape2):
input1 = Input(shape=input_shape1)
input2 = Input(shape=input_shape2)
conv1 = Conv1D(filters, kernel_size, activation='relu', padding='same')(input1)
pool1 = MaxPooling1D(pool_size=2)(conv1)
lstm1 = Bidirectional(LSTM(lstm_units, return_sequences=False))(pool1)
conv2 = Conv1D(filters, kernel_size, activation='relu', padding='same')(input2)
pool2 = MaxPooling1D(pool_size=2)(conv2)
lstm2 = Bidirectional(LSTM(lstm_units, return_sequences=False))(pool2)
merged = Concatenate()([lstm1, lstm2])
dense = Dense(16, activation='relu')(merged)
output = Dense(1)(dense)
model = Model(inputs=[input1, input2], outputs=output)
return model
这种分支结构适合特征源之间物理意义差异较大的场景,但参数量和训练时间会明显增加,数据集不大的时候谨慎使用。
6.5 几句实在话
经过这个项目的折腾,我的几个体会供你参考。ISSA-CNN-BiLSTM本质上不是某种"银弹",它解决的是超参数搜索效率问题,而不是模型结构本身的问题。如果你的数据质量不高、特征工程没做好,换什么搜索算法都是白搭。一定要在先把数据清洗、缺失值处理、归一化、滑动窗口构造这些基础工作做扎实的前提下,再引入ISSA做寻优,才能看到明显的收益。
另外,ISSA的超参数(种群大小、最大迭代次数、发现者比例、警戒者比例)本身也需要调,但它们的敏感度远低于CNN-BiLSTM的超参数,取一个合理默认值就行。真碰到收敛太快的场景,优先检查是不是柯西变异的扰动幅度设置得太小,它直接决定了算法跳出局部最优的能力。如果你也想在自己的数据上复现这套方案,建议先用小规模数据把整个流程跑通,再逐步扩大搜索范围,一步一步来比一次性追求完美要实际得多。
