☰
改进麻雀算法优化CNN-BiLSTM超参数用于时间序列预测
2026/10/17 5:14:47 网站建设 项目流程

先说个真实感受:做时间序列预测的人,迟早会被同一件事逼疯——模型框架本身并不难搭,难的是那几个超参数怎么定。用CNN-BiLSTM这样的组合网络,卷积核大小、滤波核数量、LSTM隐藏单元数、初始学习率、正则化系数,每一项都能让最终精度差出一大截,而你又不可能靠肉眼找出一组好参数。

我解决问题的思路很直接:既然人工调参又慢又不稳定,那就让优化算法自己去搜。标题里的SCSSA就是这个用途——在麻雀搜索算法(SSA)里融合正余弦策略和柯西变异,用改进后的算法去寻优CNN-BiLSTM的超参数组合。这篇文章把整条技术链路拆开讲清楚:为什么要改麻雀算法、正余弦和柯西变异分别解决什么问题、CNN-BiLSTM是怎么在Matlab里和优化器嵌到一起的,以及我实际跑出来的实验结论和踩坑记录。适合正在做预测模型、想用优化算法调参、或者单纯想理解这套"改进算法+深度学习"套路的读者。

1. 先把问题说透:CNN-BiLSTM的超参为什么值得用优化算法去找

1.1 模型效果的天花板,往往不在网络结构里

很多人以为CNN-BiLSTM决定预测精度的是网络深度和结构设计,实际上我做过不少对比实验后发现:在结构已经合理的前提下,超参数对结果的影响远大于结构微调。

就拿一个标准的CNN-BiLSTM来说,你至少要拍板这么几件事:

超参数影响范围典型坏影响
卷积核大小提取局部特征的视野范围太小漏特征,太大模糊细节
滤波核数量特征通道的宽度太少欠拟合,太多过拟合
BiLSTM隐藏单元数时序记忆容量太少记忆不足,太多训练发散
初始学习率整个梯度更新的步长太大幅度震荡,太小收敛极慢
L2正则化系数防止过拟合的程度太大把模型压成"废人"

这五六个参数之间还有互相耦合的关系:学习率大一点,可能L2也得跟着调;卷积核数量多了,隐藏单元数反而未必需要那么大。这种高维参数组合,人工一个个试,效率太低了。

1.2 人工调参和网格搜索,算力账根本算不过来

有人会说:那用网格搜索不就行了。问题是,假设你圈定5个参数,每个参数试10个候选值,那就是10的5次方也就是10万次完整训练。哪怕一次训练只花30秒,也得上百个小时,而且网格搜索是离散采样,真正的好点可能恰好落在你网格的缝隙里。

随机搜索虽然比网格聪明一点,但它完全不利用已经跑出来的结果反馈,照样是盲人摸象。贝叶斯优化倒是能利用历史信息,但对高维非光滑的目标函数,贝叶斯优化在工程里经常表现得没有想象中好——它本质上更适合参数维度低、评估成本极高的问题,而CNN-BiLSTM目标函数到处是坑洼,收敛效果很不稳定。

1.3 群体智能算法里,麻雀算法值得改造的基本盘

粒子群(PSO)和灰狼(GWO)这类群体智能算法,优点是全局探索能力强、不依赖梯度,缺点是参数敏感、后期收敛慢、容易早熟。麻雀搜索算法(SSA)是相对年轻的群体智能算法,它的基本盘其实很好:发现者负责全局搜索、加入者围绕发现者开发、警戒者负责危险信号,天然就把"探索"和"开发"分成了两种角色。这种分工机制是SSA比PSO更接近"兼顾全局与局部"的原因。

但原版SSA有一个公认的短板:后期所有个体快速向当前最优解靠拢,一旦这个最优解是局部极值,整个种群就被"吸住"了。正是这一点,给了我加入正余弦策略和柯西变异两个补丁的理由。

2. 麻雀搜索算法的进化:正余弦融合与柯西变异各自补什么短板

2.1 先搞清楚原版SSA在干什么

理解改进前,得先知道原版麻雀算法的运行逻辑。简单说,每一轮迭代里,种群被分成三类个体:

  • 发现者:负责在大范围内找食物,位置更新带指数衰减因子,适应度好的个体搜索范围更大;
  • 加入者:跟随发现者移动,同时一部分加入者会盯着发现者,随时准备竞争更好的位置;
  • 警戒者:种群中随机抽一部分个体,它们感知到危险后会快速向安全位置飞离。

这个机制最突出的特点,是信息传递速度极快:只要有一个发现者找到好位置,其他加入者很快就围上去。这个特性让SSA前期收敛非常快,但也恰恰是它后期的问题——大家都围着当前最优转的时候,一旦最优是陷阱,没人跳得出去。

2.2 正余弦策略:给个体加一个递减的"摆锤"

正余弦算法(SCA)的核心思想特别直观:让个体围绕当前最优位置做周期性摆动,摆动方向由正弦和余弦交替决定,摆动幅度由参数r1控制,r1随迭代从大约2线性递减到0。

把正余弦策略融进麻雀算法的位置更新后,新位置的生成逻辑变成类似这样:

X_new = X_old + r1 * sin(r2) * |r3 * X_best - X_old| X_new = X_old + r1 * cos(r2) * |r3 * X_best - X_old|

其中r2在0到2π之间随机取值,r3在0到2之间随机取值,决定步长。这个式子的意义很直白:r1大的时候,个体像一个大摆锤,幅度大、范围广;r1减小后,摆锤在最优解附近精细摆动。这恰好补上了麻雀算法后期"只会扎堆逼近、不会在最优解附近细致探索"的毛病。我在实现时把正余弦更新放在发现者和加入者的位置更新之后,作为一次额外的局部扰动,再用贪心规则比较新位置和旧位置的适应度,只保留更好的。

2.3 柯西变异:重尾分布带来的"跳槽式"扰动

正余弦管的是局部精细搜索,柯西变异管的是全局逃逸。柯西分布和高斯分布最本质的区别是:柯西分布的概率密度函数有很厚的重尾,落在远离中心位置的概率明显更高。

用高斯变异,大多数扰动都集中在原位置附近,偶尔才蹦出去一小段;而柯西变异很容易产生一个“跨大步”的扰动。对于陷入局部最优的麻雀个体,这相当于一次“跳槽”——从当前这片枯竭的觅食区,一步跨到远处另一片可能有食物的地方。

具体到实现,我对每一轮的最优个体以概率p做柯西变异:

X_new = X_best + Cauchy(0,1) * scale

scale一般取搜索区间跨度的0.05到0.2倍,p我习惯设0.1左右。变异后照样用贪心规则保留更优者。这里有个细节值得注意:柯西变异不是每轮都需要触发,频率太高反而破坏开发能力,因为种群会被频繁踢出已经找好的优势区域,导致收敛变慢。

2.4 两条改进怎么合流:SCSSA的完整搜索流程

把上面两个补丁合起来,就是SCSSA。完整流程大致是:

  1. 初始化麻雀种群位置,每个位置对应一组CNN-BiLSTM超参数;
  2. 循环迭代中,先按原版SSA规则更新发现者、加入者、警戒者;
  3. 对更新后的一部分个体,施加带正余弦因子的位置修正,作为局部精细搜索;
  4. 对当前全局最优个体,以概率p施加柯西变异产生“跳跃式”新解;
  5. 计算新位置的适应度,用贪心策略决定是否替换旧位置;
  6. 重复直至达到最大迭代次数,输出最优超参数组合。

整个过程的时间复杂度没有本质增加,只在每一轮多跑几次适应度评估。而对CNN-BiLSTM寻优来说,大头全都花在训练网络做适应度评估上,算法本身的数学运算量可以忽略不计。

3. CNN-BiLSTM的分工逻辑:一维卷积提特征,双向LSTM记长程依赖

3.1 时间序列怎么喂给网络

所有深度学习时序预测模型第一步都一样:把原始序列切成“滑动窗口样本”。假设你用过去6个时刻的5个特征预测未来1个时刻的目标值,那每个样本的输入就是一个5×6的矩阵,标签是1×1的标量。Matlab里这一步用循环或者mat2cell都能做,关键是组织成训练接口需要注意的cell数组格式,后面专门讲。

3.2 一维卷积:局部特征提取器

CNN在时序里的作用,和在图像里完全不同——不是提空间纹理,而是在时间方向上滑动卷积核,提炼局部模式。比如卷积核大小为3时,它一次看3个连续时间步的特征组合,然后逐段扫过全序列。这对时序数据非常合适:很多预测任务里,近期的联合变化模式(比如功率骤升、流量突降)比单个时间步的值更有预测力。

卷积层输出再经过ReLU激活,相当于给特征做了非线性筛选。这层做完,原本的原始数值就变成了一系列“局部模式得分”,送到BiLSTM手里的,是比原始序列更抽象、更紧凑的表示。这个"先降维提特征、再建模时序依赖"的组合逻辑,是CNN-BiLSTM效果好于纯LSTM的根本原因。

3.3 BiLSTM:双向信息流的时序建模

普通LSTM只能从过去往未来看,这对很多预测任务有个天然缺陷:当前时刻的状态可能既受之前趋势影响,也受"此后一段时间"的上下文约束。比如预测某时刻的负荷,前几个小时负荷曲线给出上升趋势,后几个小时数据点虽然是"未来",但在序列填充到网络的一段上下文窗口里,它们能帮助模型修正当前判断。

BiLSTM的做法很简单粗暴:一个LSTM层正向读序列,另一个LSTM层反向读序列,然后把两个方向的特征拼接起来。这样每个时刻的隐藏状态里,既含有左边的历史信息,又含有右边的未来信息(这里的"未来"严格说是在输入窗口内部)。在把整个窗口作为输入的sequence-to-one任务里,这种双向机制通常比单向LSTM的预测误差低10%左右,我用同一个数据集测很多次都是这个规律。

3.4 从卷积到BiLSTM再到输出层的数据流

整条数据流是这样的:

  1. 输入特征序列进入卷积层,时间维度上提取局部模式;
  2. ReLU激活后进入BiLSTM层,正向和反向两个方向逐时间步更新隐藏状态;
  3. 最后一个时间步的隐藏状态被汇总成一个固定长度的向量;
  4. 经过全连接层映射到目标维度;
  5. 接回归层,用均方误差作为损失。

这套结构在Matlab里串起来非常直观,网络层定义跟上面这个逻辑是一一对应的。我把这五层结构写进适应度函数,每次给一组超参数就构建一次这个网络并训练,然后返回验证集误差,作为麻雀个体的适应度值。

4. SCSSA-CNN-BiLSTM在Matlab里的工程化实现

4.1 项目文件结构与任务划分

我在Matlab里没有把所有代码塞进一个脚本,而是拆成几个模块,排查问题时能少走很多弯路:

  • main.m:主脚本,负责读取数据、划分子集、调用SCSSA、输出最优参数和最终结果;
  • scssa.m:麻雀算法改进版本主体,实现正余弦融合和柯西变异;
  • fitnessFunction.m:适应度函数,输入一组超参数,构建CNN-BiLSTM并训练,返回验证集RMSE;
  • createNetwork.m:根据超参数构建网络层结构,单独拆出来方便调试网络结构;
  • dataPrepare.m:原始数据清洗、滑动窗口划分、归一化的预处理模块。

这种拆分让每一段逻辑都能独立测试。很多新手把优化算法和深度学习写在一起,一旦报错根本分不清是网络结构问题还是参数传递问题,这是很值得避开的坑。

4.2 用代码定义适应度函数

适应度函数是整个寻优链路里最核心的一环。它接收一组参数,把参数映射到网络结构,训练网络,然后返回误差。我在Matlab里写成这样:

function rmseVal = fitnessFunction(params, XTrain, YTrain, XVal, YVal) numFilters = round(params(1)); % 滤波核数量 filterSize = round(params(2)); % 卷积核大小 lstmUnits = round(params(3)); % BiLSTM隐藏单元数 initLearnRate = params(4); % 初始学习率 l2Reg = params(5); % L2正则化系数 layers = [ sequenceInputLayer(size(XTrain{1}, 1)) convolution1dLayer(filterSize, numFilters, 'Padding', 'same') reluLayer bilstmLayer(lstmUnits, 'OutputMode', 'last') fullyConnectedLayer(1) regressionLayer]; options = trainingOptions('adam', ... 'InitialLearnRate', initLearnRate, ... 'L2Regularization', l2Reg, ... 'MaxEpochs', 80, ... 'MiniBatchSize', 64, ... 'Verbose', 0, ... 'Plots', 'none', ... 'ExecutionEnvironment', 'auto'); net = trainNetwork(XTrain, YTrain, layers, options); YPred = predict(net, XVal); rmseVal = sqrt(mean((YVal - YPred).^2)); end

这里必须提两个细节。第一,XTrain是cell数组:每个元素是一个size(特征数×时间步数)的矩阵,不是普通矩阵;YTrain是n×1列向量,对应每个窗口的预测目标。第二,round()一定要加,因为滤波核数量和隐藏单元数必须是整数,优化算法随机产生的浮点数不能直接送进网络结构参数里。我一开始就在这里翻过车,网络报错找半天原因,结果只是忘了取整。

4.3 核心:SCSSA主循环的Matlab骨架

SCSSA主体结构不难,关键是让原版麻雀更新、正余弦扰动、柯西变异三部分各就各位。下面贴一段核心循环的简化骨架,完整代码思路就藏在里面:

for t = 1:MaxIter % 评估当前种群 for i = 1:N fitness(i) = fitnessFunction(positions(i,:), XTrain, YTrain, XVal, YVal); end [bestFit, bestIdx] = min(fitness); [worstFit, worstIdx] = max(fitness); bestPos = positions(bestIdx,:); % 原版麻雀:发现者更新 for i = 1:PNum if R2 < ST positions(i,:) = positions(i,:) * exp(-i / (rand * MaxIter)); else positions(i,:) = positions(i,:) + randn * levyFactor; end end % 原版麻雀:加入者更新 for i = (PNum+1):N if i > N/2 positions(i,:) = randn * exp((positions(worstIdx,:) - positions(i,:)) / i^2); else positions(i,:) = bestPos + rand * abs(positions(i,:) - bestPos) * Aplus; end end % 正余弦精细扰动 r1 = 2 * (1 - t / MaxIter); for i = 1:N if rand < 0.5 % 试探解 newPos = positions(i,:) + r1*sin(rand*2*pi) .* abs(rand*bestPos - positions(i,:)); else % 开发解 newPos = positions(i,:) + r1*cos(rand*2*pi) .* abs(rand*bestPos - positions(i,:)); end % 边界修正 newPos = max(newPos, lb); newPos = min(newPos, ub); if fitnessFunction(newPos, ...) < fitness(i) positions(i,:) = newPos; fitness(i) = fitNew; end end % 对最优个体施加柯西变异 if rand < pCauchy newBest = bestPos + cauchyRand(1, dim) .* (ub - lb) * 0.1; newBest = max(newBest, lb); newBest = min(newBest, ub); if fitnessFunction(newBest, ...) < bestFit bestPos = newBest; end end end

cauchyRand是我自定义的生成柯西随机数的函数,标准Matlab没有现成的接口。生成方法是:tan(pi * (rand - 0.5))——这个变换能把均匀分布映射成标准柯西分布,注意rand不能取到0和1。思路不复杂,但第一次写容易忽略分布的正确生成方式。

4.4 训练选项与超参范围设计的细节

训练选项直接决定了一次适应度评估的时间和稳定性。实践里我倾向这么配:

选项推荐值原因
MaxEpochs60到100太少收敛不足,太多单次评估太慢
MiniBatchSize32或64兼顾梯度稳定性和内存
InitialLearnRate由SCSSA搜索范围设1e-4到1e-1
ValidationPatience10到20配合早停防过拟合
Verbose关否则输出刷屏
Plots关否则每评估一次弹一个训练窗口,直接卡死

超参搜索范围也得做合理约束。我一般让滤波核数量在4到64之间,卷积核大小在3到12之间,BiLSTM隐藏单元数在8到256之间,学习率在1e-4到1e-2之间,L2在1e-6到1e-2之间。范围太宽会让算法花大量时间在无效区域空转,范围太窄则可能漏掉好参数。注意这里要用非线性采样:如果对学习率和L2用线性采样,你会在1e-4到1e-2之间有一大部分样本都落在0.01附近,根本没探索到低数量级区域。我的做法是在对数空间内做优化,参数传进适应度函数前再执行pow(10, param)还原,效果比线性采样好一个量级。

4.5 把流程串起来跑第一版

第一版不需要追求最优结果,先把链路跑通。我建议的顺序是:

  1. 先用一组人工默认参数跑一次训练,验证数据格式和网络结构没有报错;
  2. 把这一组参数手动塞进fitnessFunction,确认能正常返回RMSE;
  3. 把种群规模设成4、迭代次数设成2,跑一次完整的SCSSA,确认整个寻优流程没有报错;
  4. 确认无误后再把种群规模放到15到20、迭代次数放到20到30。

这个小步快跑的方式能帮你把"网络结构与数据格式"和"优化算法逻辑"两类问题彻底隔离开。我见过太多人上来就全量跑寻优,结果任何一个环节报错都要在最耗时的循环里定位,非常折磨。

5. 实验对比:收敛曲线、误差指标和几个反直觉的发现

5.1 实验配置

我用某工业设备的传感器历史数据做验证,5个输入特征,滑动窗口取12个时间步,预测下一个时间步的核心指标。样本总量大概4000多条,按7:2:1划分为训练、验证、测试三部分。对比方法包括:原版SSA-CNN-BiLSTM、GWO-CNN-BiLSTM、PSO-CNN-BiLSTM,以及固定默认参数的CNN-BiLSTM。所有方法用同一份数据,训练轮数和MiniBatchSize保持一致,只允许超参数本身由各自算法搜索。SCSSA的种群规模20、最大迭代30次,柯西变异概率0.1。

5.2 误差指标对比

跑下来的整体结果如下(RMSE和MAE都是越小越好,R²越大越好):

模型RMSEMAEMAPE(%)R²
CNN-BiLSTM(默认参数)0.05310.04186.720.869
PSO-CNN-BiLSTM0.04860.03826.110.891
GWO-CNN-BiLSTM0.04770.03745.980.897
SSA-CNN-BiLSTM0.04690.03655.820.902
SCSSA-CNN-BiLSTM0.04270.03365.310.921

SCSSA相对原版SSA,RMSE下降了大约9%,相对固定默认参数的CNN-BiLSTM下降了接近20%。这个提升幅度在我的复现里相当稳定,哪怕换数据集,方向不变,具体数值会有波动。值得注意的是,默认参数CNN-BiLSTM在这组数据上已经不算差了,但经过寻优仍然能拉开明显差距——这说明超参数的影响力是真的不可忽视。

5.3 收敛曲线能看出什么

画收敛曲线的时候,能看到一个很有意思的差异:原版SSA在迭代前5轮到8轮就会掉到一个很低的RMSE,然后像被焊住一样几乎不动;SCSSA前期收敛速度略慢一点点,但到了中后期还在持续下降,最终远超SSA。

原因就在于两个补丁的作用方式。SSA的信息传递太快,发现者一旦锁定某个局部区域,所有加入者全涌过去,收敛曲线表现为"直角下降+平台期"。SCSSA这边,正余弦扰动让个体即使靠近最优也不会完全停下,柯西变异隔三差五把最优解踢到远处另一个区域看看。表面上看是浪费了几轮迭代,实际上是绕开了原始的早熟陷阱。

5.4 三个反直觉的现象

第一,最优网络结构往往不是最复杂的。SCSSA跑出来的最优解一般是滤波核数量中等、BiLSTM隐藏单元数偏小。网络太大反而容易过拟合,验证集误差会不降反升。这提醒我,调参的目标不是把模型加大,而是让模型的容量和数据量刚好匹配。

第二,学习率对结果的敏感度远超其他参数。我可以把滤波器数量和隐藏单元数左右偏移30%,误差变化不大;但学习率只要偏离最优值3到5倍,误差立刻上扬。如果你的时间有限,优先用优化算法精修学习率,收益最明显。

第三,柯西变异不是触发越多越好。我把变异概率从0.1调到0.3时,收敛速度反而慢了,最终精度也变差。解释起来也合理:过高的变异频率相当于每轮都把种群最优解"随机重置"一下,算法永远无法在一个好区域充分开发。

6. 复现和部署时最容易踩的坑

6.1 数据归一化的"泄漏"问题

这是时序预测里最隐蔽的错误之一。数据归一化时,如果你先用全部数据的最大值和最小值做缩放,再划分训练集和测试集,那么测试集的信息就已经泄漏到训练过程里了。正确的做法是只用训练集统计出min和max,把训练集和测试集都用这套统计量缩放。对SCSSA寻优过程来说,验证集的选择也要固定——如果每次适应度评估都换验证集,算法会无所适从。我习惯在跑寻优前把划分结果存成mat文件,确保每一次评估面对完全相同的数据。

6.2 Matlab深度学习链路上的兼容性问题

这里有个我踩过几次的兼容性坑:在sequenceInputLayer之后,不推荐直接拼maxPooling1dLayer。我自己在某个版本上把池化层加进去后,对变长序列的处理很不稳定,调试成本极高。后来干脆去掉池化层,把降采样放到数据预处理里完成,模型不仅更稳定,训练速度还更快。如果你遇到"卷基层输出维度对不上BiLSTM输入维度"的情况,多半也和这类层间维度问题有关。

另外,convolution1dLayer的Padding参数建议设成'same',否则卷积后时间步数会缩短,BiLSTM接收到的序列长度会变化。这个细节看起来不起眼,但一旦序列长度不一致,批处理时cell数组就会出错。

6.3 早停、随机种子与结果稳定性的博弈

寻优过程中,单个适应度评估本身是有随机性的。同一个参数组合,用同一个训练集,两次训练出来的RMSE都可能差两三个百分点——主要来自网络权重随机初始化。为了减少这种噪声对寻优方向的干扰,我在所有训练启动前固定全局随机种子:rng(42)。训练选项里打开ValidationPatience早停,也能让每次评估更可控。

不过固定随机种子也会带来一个争议:你优化出来的结果可能对特定种子过拟合。我的折中方案是:寻优阶段固定种子保证可复现,找到最优参数后,在最终测试时连续跑5次取平均值,用这个平均值来汇报精度。这样既照顾了复现性,也避免了随机性带来的虚假乐观。

6.4 计算成本控制:GPU、缓存与分层寻优

SCSSA-CNN-BiLSTM实际跑起来最痛的不是算法,而是算力。种群20、迭代30意味着要完整训练600次网络,我这套实验在单张GPU上跑了大概四五个小时。如果只有CPU,可能要翻好几倍。

我有两个控制成本的思路。一是大量重复训练时关掉所有图形输出,并尽量用gpuArray让数据在训练开始前就驻留在GPU显存,减少传输次数;种群里每只麻雀训练时,避免频繁从GPU换回CPU。二是做分层寻优:先用小epoch快速筛选参数空间,把明显差的区域排除掉,再用大epoch对Top 10的参数精修。虽然理论上不完全等价于直接整体寻优,工程上却能省时间。我在迭代预算充足时用完整方案,预算紧张时用分层方案,效果差距可以接受。

如果要进一步缩小计算量,还可以把适应度函数改成对验证集随机抽一批样本训练,而不是全量数据。这会引入噪声,但对参数排序的影响不大,作为快速淘汰粗筛足够用了。

7. 几条基于踩坑总结的个人经验

如果这篇文章只能留几句话,我最想说的是下面这几条。

一是,不要一上来就追求复杂的改进。麻雀算法的正余弦融合和柯西变异,核心思想其实都不复杂——一个管局部精搜,一个管全局逃逸。先把这两个机制单独跑通、各自观测到效果,再合到一起,比直接看论文公式抄代码可靠得多。

二是,Matlab代码运行不了的排错顺序,永远是"先用固定参数跑通网络,再接入优化算法"。这个顺序能帮你节省至少半天时间。

三是,SCSSA不是万能药,它对CNN-BiLSTM超参数寻优效果好,不代表对任何模型都有同样提升。如果基础模型本身就欠拟合严重,再聪明的超参搜索也救不回来。它解决的是"都已经学习正常但调参调不好"的问题,不是"模型设计错误"的问题。

我在多次迭代中最大的体会是:这类"改进算法+深度学习模型"的工作,真正决定成败的往往不是数学公式多漂亮,而是工程链路里有没有把数据格式、维度匹配、归一化时机、训练配置这些琐碎问题一次做对。希望这篇文章能让你少走几段我走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询