简介:鱼鹰算法(OOA)是近年提出的元启发式优化算法,通过模拟鱼鹰俯冲捕鱼时的搜索策略,兼顾全局探索与局部开发。将其用于BP神经网络的权值阈值初始化,可有效缓解传统BP对初值敏感、易陷入局部最优的问题,从而提升回归预测精度与稳定性。这份Matlab源码包正是基于这一思路,实现了多输入单输出回归预测的完整流程,适合从事数据预测、机器学习算法改进的研究人员及高年级学生使用。压缩包共6个文件,整体仅19KB,其中4个m脚本构成核心代码,分别实现主程序、OOA优化、参数初始化与目标函数计算;1个xlsx文件提供可直接加载的案例数据;1个asv文件为编辑器自动备份,不影响使用。目前已有181人学习下载,运行主程序即可输出预测值与误差指标,也方便替换自有数据进行不同领域的回归预测,是快速复现OOA-BP算法、开展课程设计或论文实验的轻量参考。
1. 多输入单输出回归预测:OOA-BP 这套 Matlab 源码到底能省掉什么
做回归预测的同行应该都见过这种尴尬:数据整理好了,BP 神经网络结构也定得差不多,随手一跑,训练集拟合漂亮,测试集却像另一个数据集;换个初始权重,结果又变一个样。OOA-BP 这套 Matlab 源码解决的正是这个「初始权重看运气」的问题——用鱼鹰算法(Osprey Optimization Algorithm,OOA)在训练开始前把 BP 的初始权值和阈值搜出来,相当于把随机碰运气改成主动找一套好起点。适合谁?手里有多输入单输出数据、想在 Matlab 里快速出一个能交代的回归模型的人,不管你是做风速、负荷还是工业参数预测,拿到源码改数据就能跑。
2. 鱼鹰算法优化 BP 的原理:先搞清它动了哪块奶酪
2.1 鱼鹰捕鱼的两个阶段:探索与开发的交替
鱼鹰算法是 2023 年前后提出的一种元启发式优化算法,模仿的是鱼鹰捕鱼的完整过程。第一阶段叫“定位并攻击鱼”:每个鱼鹰会在当前种群中随机选定一个优质解当作猎物,然后朝这个猎物方向大步跳一步,跳多远由一个随迭代变化的邻域半径控制,这个半径直接按 (ub - lb) 缩放。所以这一阶段的搜索范围大、跳脱性强,目的是在全局范围里快速圈定“大致还不错”的区域——对应优化里的 exploration。第二阶段叫“把鱼叼到安全处”:鱼鹰把猎物带到自己熟悉的区域进食,算法就在当前解周围生成一个小邻域做精细搜索,半径随迭代次数线性收缩,越到后期越精准——对应 exploitation。
这个“先广撒网、再精准捞”的结构,是它区别于 PSO 和遗传算法的核心:PSO 要维护速度项和两个加速因子,遗传算法要操心交叉变异概率,OOA 只靠“朝优质解跳跃 + 邻域收缩”两套规则交替,实现同样效果的同时少了一堆玄学参数。对回归预测场景来说,少两个手调参数,就少两处翻车点。
2.2 鱼鹰位置如何编码一份完整网络:维度公式
要搞清楚 OOA 每轮在改什么,先得把网络参数“拍扁”成一个向量。以一个 n_in 输入、n_hidden 隐层、单输出的三层 BP 为例,决策变量由四段拼成:
| 参数 | 数量 |
|---|---|
| 输入层→隐层权值 w1 | n_in × n_hidden |
| 隐层阈值 b1 | n_hidden |
| 隐层→输出权值 w2 | n_hidden × 1 |
| 输出阈值 b2 | 1 |
| 合计 dim | n_hidden × (n_in + 2) + 1 |
每个鱼鹰的位置就是一个 dim 维向量,前 n_in×n_hidden 个分量是 w1,接着 n_hidden 个是 b1,再往后是 w2 和 b2。这个编码顺序在源码的适应度函数里是写死的,后面第 4 章我会专门说换数据时为什么维度会错。这里先记住结论:改输入特征个数或者隐层节点数,dim 就会变,lb/ub 声明和种群初始化必须跟着更新。
解码后的评估逻辑,代码层面长这样:
function mse = ObjFun(x, hiddennum, Xtr, Ytr) % 把鱼鹰位置向量 x 解码成 BP 初始权值和阈值 n_in = size(Xtr, 2); cnt1 = n_in * hiddennum; % w1 占用的分量数 cnt2 = cnt1 + hiddennum; % w1+b1 占用的分量数 w1 = reshape(x(1:cnt1), hiddennum, n_in); b1 = x(cnt1+1 : cnt2); w2 = reshape(x(cnt2+1 : end-1), 1, hiddennum); b2 = x(end); % 前向计算隐层和输出,输出层直接用纯线性 a1 = tansig(w1 * Xtr' + b1); % hiddennum × 样本数 yh = w2 * a1 + b2; % 1 × 样本数 mse = mean((yh - Ytr').^2); % 训练集 MSE 作为适应度 end这段代码的意图要讲清楚:鱼鹰优化阶段不做 BP 反向传播,只做一次前向计算,拿训练集 MSE 当作“这组权值有多好”的分数。好处是快,100 次迭代、30 个鱼鹰也就 3000 次前向计算;坏处是这个分数没有经过梯度精修,和最终 train 完之后的误差不是严格等价的。所以正规流程都是 OOA 搜完最优解 → 用最优解初始化 BP → 再跑完整 BP 训练,而不是直接用 OOA 的结果当最终模型。我见过有人把 OOA 搜索完的权值直接拿去做预测,效果差一截,就是这个原因。
2.3 为什么选鱼鹰而不是 PSO 或灰狼
很多人的第一反应是“我手头就有 PSO 的代码,为啥非要换 OOA”。我的看法分三点。第一,PSO 有 c1、c2 两个加速因子要调,灰狼算法有 a、A、C 三组系数,OOA 的主控参数基本就是种群大小 N 和迭代次数 T,BP 初始权重优化恰恰是几十维的中等规模问题,在这个量级上少两个参数意味着更容易复现。第二,鱼鹰算法把全局搜索设计成“向随机选中的优质猎物方向跳跃”,这种带随机性的大步长在跳出局部极小方面比 PSO 的线性速度更新更粗暴也更有效,代价是后期精度稍弱,但这正好被 BP 的 LM 精修补上。第三,这套源码在 ObjFun 这一层对优化器完全透明,你不想要 BP 收尾,把解码后的赋值目标换成 RBF 或 GRU 的初始化表达式也行,后面第 6 章我会讲怎么改验证。
理解到这里就够了:鱼鹰算法是一个用 MSE 做评分卡的“初始解搜索器”,真正的精度由后续 BP 训练负责。搞明白这个分工,后面跑代码、换数据、看指标才不会本末倒置。
3. 源码运行链路:从数据读入到出图的每一步
3.1 文件清单与 MATLAB 环境检查
打开压缩包,按我过去拆这类包的经验,核心文件通常是这几类:一个主脚本(一般是 main.m 或 Run_OOA_BP.m),一个鱼鹰算法函数 OOA.m,一个适应度函数(ObjFun.m 或 fitness.m),一个数据文件(data.xlsx 或 .csv / .mat),外加一段把最优解解码并重新训练 BP 的脚本。你这套源码标题里写着“完整源码和数据”,跑通之前先确认三件事。
第一,MATLAB 版本。R2016a 以上基本都没问题,这里用到的 xlsread、mapminmax、newff、sim 都是老接口,新版全兼容。第二,工具箱依赖。newff、mapminmax、trainlm 都来自 Deep Learning Toolbox(老版本叫 Neural Network Toolbox),如果安装时没勾这个组件,第一行 newff 就会报 Undefined function,命令行执行 ver 可以查。第三,工作目录。先 cd 到解压目录再运行,脚本里如果是 xlsread('data.xlsx') 这类相对路径,目录不对会直接报“文件不存在”。
3.2 主程序跑通一次:加载到出图的四段代码
主脚本逻辑高度模板化,拆开看就是四步。第一步加载数据并归一化:
%% 1. 加载数据并归一化 data = xlsread('data.xlsx'); % 行是样本,列是特征 X = data(:, 1:end-1); % 输入特征在前 Y = data(:, end); % 单输出在最后一列 [X_norm, ps_input] = mapminmax(X', 0, 1); % 输入规约到[0,1] [Y_norm, ps_output] = mapminmax(Y', 0, 1); % 输出规约到[0,1] X_norm = X_norm'; Y_norm = Y_norm';注意 mapminmax 的操作对象是“特征×样本”矩阵,所以这里传的是转置后的 X',归一化完再转置回来。ps_input 和 ps_output 这两个结构体必须留到后面给测试集做同样变换,这是新手最容易弄丢的“后悔药”。
第二步划分训练测试集:
%% 2. 划分训练集和测试集(8:2) n = size(X_norm, 1); rng(42); % 固定随机种子,保证可复现 idx = randperm(n); train_idx = idx(1: floor(n*0.8)); test_idx = idx(floor(n*0.8)+1 : end); Xtr = X_norm(train_idx, :); Ytr = Y_norm(train_idx, :); Xte = X_norm(test_idx, :); Yte = Y_norm(test_idx, :);第三步是 OOA 本体。优化器这边的入口通常是:
%% 3. OOA 搜索 BP 初始权值阈值 SearchAgents_no = 30; % 鱼鹰种群数,常见 20~50 Max_iteration = 100; % 最大迭代次数,常见 50~200 hiddennum = 10; % 隐层节点数,先默认,第4章讲怎么调 dim = hiddennum*(size(Xtr,2)+2) + 1; % 决策变量维度 lb = -5*ones(1, dim); % 权值下界 ub = 5*ones(1, dim); % 权值上界 [Best_solution, Best_mse, ConvergenceCurve] = OOA(... SearchAgents_no, Max_iteration, lb, ub, dim, ... @(x) ObjFun(x, hiddennum, Xtr, Ytr));这一段把整个初始解搜索理解成“在 [-5,5] 的 dim 维空间里找 MSE 最小的点”。lb/ub 取 ±5 是一般经验值,因为 tansig 的敏感区间就在 ±5 附近,范围再大梯度就饱和了,后面第 5 章会写一个因为这个范围设置引发的典型翻车。
第四步用最优解初始化 BP 并训练:
%% 4. 解码最优解,初始化 BP 并完整训练 net = newff(Xtr', Ytr', hiddennum, {'tansig','purelin'}, 'trainlm'); % 按2.2的切分规则把四段一一赋给网络 net.IW{1} = reshape(Best_solution(1: size(Xtr,2)*hiddennum), hiddennum, size(Xtr,2)); net.b{1} = Best_solution(size(Xtr,2)*hiddennum+1 : size(Xtr,2)*hiddennum+hiddennum)'; net.LW{2,1} = reshape(Best_solution(end-hiddennum : end-1), 1, hiddennum); net.b{2} = Best_solution(end); net = train(net, Xtr', Ytr'); % LM 精修 Yte_pred_ = sim(net, Xte'); Yte_pred = mapminmax('reverse', Yte_pred_, ps_output); % 反归一化这里四个赋值语句少一个都不行,尤其是 net.b{1} 和 net.b{2} 的维度一个是 hiddennum×1、一个是 1×1,赋错方向会直接报维度错误。反归一化用的是训练时保存的 ps_output,而不是对预测值重新做一次 mapminmax,这是原则问题,反了指标全错。
3.3 跑完之后先看这四个量
脚本正常结束后,命令窗口至少会出现三张图:训练误差曲线、OOA 收敛曲线、测试集拟合对比图。我的检查顺序是固定的。
先看 OOA 收敛曲线是否单调下降——正常的鱼鹰搜索应该是前期陡降、后期平缓趋稳;如果曲线反复横跳,多半是 lb/ub 给太大或种群数太少,详见第 5 章第 3 条。然后看测试集拟合对比图,判断标准不是曲线贴合得多好看,而是看峰谷位置是否对得上——相位对不上说明输入特征和输出之间的映射关系没学好,这时调参没用,要加特征或换网络结构。最后回到工作区看三个数值:Best_mse、训练集 R²、测试集 R²。前两个决定优化是否到位,最后一个才是你对外汇报的指标。
4. 换成自己的数据集:三处修改与四个隐藏规约
4.1 数据替换:不只改文件名
实操里最常犯的错是只把新的 data.xlsx 灌进去就跑,两步之内必翻车。第一步,列顺序。源码约定“输入在前所有列、输出在最后一列”,如果你的 Excel 里目标变量夹在中间,X 和 Y 会互相串。我一般先在工作区执行 size(data) 确认列数,再用头部几行确认最后一列是不是目标变量,然后才继续。第二步,输入输出个数变了之后,ObjFun 里的维度切分要跟着变,但因为你用的是 2.2 节那个通用公式,只要 hiddennum 定了,dim、lb、ub 会自动跟着变,不需要手改切分索引。
另外注意特征质量。如果某个输入列范围是 0.001 到 0.002,另一个是 1000 到 5000,mapminmax 会统一压到 [0,1],这没问题;但如果某列全是同一个常数,归一化后整列变 0,这一路输入信息量为零。看到哪列方差为 0 直接删掉,别指望优化器帮你救。
4.2 隐含层节点数的确定方法
这套源码里 hiddennum 是唯一需要手填的结构参数。常见做法是先用经验公式估算候选区间:hidden 取 sqrt(n_in + 1) 再加一个 1 到 10 的常数,或者按 n_in 的两倍向上取整。比如 5 个输入,候选就是 5 到 15。我的习惯是先用 hiddennum=10 跑通流程、确认数据链路无误,然后做一个小网格搜索:
%% 隐层节点数网格搜索:只比较测试集 RMSE hidden_candidates = [5, 8, 10, 13, 15]; for k = 1:numel(hidden_candidates) hiddennum = hidden_candidates(k); dim = hiddennum*(size(Xtr,2)+2) + 1; [Best_solution, ~, ~] = OOA(SearchAgents_no, Max_iteration, ... lb, ub, dim, @(x) ObjFun(x, hiddennum, Xtr, Ytr)); % 解码、train、预测测试集,记录 RMSE rmse_list(k) = RMSE; end注意:选隐层节点数时只看测试集 RMSE,不要看训练集 R²。训练集拟合必然随隐层增多而变好,那个数字没有选择意义。
这个循环是 OOA 完整跑 N 遍,很吃时间,我一般只测 3 到 5 个值。选完把最优 hiddennum 回填到主脚本,再完整重跑一遍。
4.3 训练测试划分与多次重复的约定
划分比例我固定用 8:2,数据量少于 200 条时改 7:3,但不要低于 6:4,否则测试集只有几十条,任何指标都带巨大方差。换数据时同步检查一件事:训练集是否覆盖全范围。用 min(Xtr) 和 max(Xtr) 与全量对比,如果极端值恰好全掉进测试集,训练集外推能力会奇差,遇到这种就把极端样本人工挪进训练集。
更重要的约定是随机种子。脚本开头写死 rng(42),可以让同一份数据每次跑的结果一致;不固定种子,第一次 R²=0.93、第二次 0.91、第三次 0.88,你根本没法判断是算法问题还是取样问题。我的标准流程是固定种子跑一遍拿到基准,然后注释掉 rng 连续跑 5 次,记录均值±标准差,汇报的时候两组数都放,别人问起来也答得清楚。
5. 避坑与常见问题:我跑 OOA-BP 踩过的五个坑
5.1 预测曲线是一条水平直线,R² 为负数
现象:测试集预测值全部落在同一个常数附近,画出来一条横线,R² 甚至小于 0。
原因主要有三个:一是输出层用了饱和型激活函数而输出没归一化,大数值把梯度压死,学习率再小也推不动;二是反归一化写错,直接对归一化后的预测值算指标;三是隐层节点太少,网络根本没学会非线性映射。
解决:确认 Y 在训练前做了 mapminmax 到 [0,1],hiddennum 从 10 起步往 20 试;计算指标前必须 mapminmax('reverse') 还原真实量纲。排查顺序是先查反归一化,再看隐层节点数,最后才怀疑优化器。
5.2 训练集 R² 接近 1,测试集 R² 只有 0.4
现象:两边指标断崖式分化,典型的过拟合脸谱,但很多人第一次用这套源码会误以为是鱼鹰算法搜出了过拟合解。
原因:训练集太小而隐层节点过多,或者 trainlm 迭代次数默认拉满,LM 在训练集上精修过度。OOA 只负责初始解,过拟合发生在 BP 精修阶段。
解决:把 hiddennum 减半重跑,观察测试集 R² 是否回升;给 train 函数加 net.trainParam.epochs 上限,我一般用 500,再加 net.trainParam.goal=1e-5 早停;训练测试划分尽量保证样本量在 300 以上。
5.3 OOA 收敛曲线前三代猛降,之后完全不动
现象:ConvergenceCurve 在第 3 到第 5 代就开始走平,后面 95 次迭代全在浪费,Best_mse 却不理想。
原因:通常是 lb/ub 范围太大(比如 ±20、±50),鱼鹰在初期随机撞到一个局部好点后,邻域半径按线性收缩,后面够不到更优区域;或者种群太少(N 小于 15),全局搜索能力不足。
解决:把 lb/ub 收到 ±3 到 ±5,种群提到 30 到 50。注意收敛曲线“早平”不等于失败,只要 Best_mse 已经足够小,后续 BP 精修照样出成果;真正要警惕的是曲线还有明显下降趋势却被 Max_iteration 拦腰切断,这时把迭代次数加到 150。
5.4 报错 Matrix dimensions must agree
现象:改了输入特征个数之后,ObjFun 里 reshape 报维度错误,或者 net.IW{1} 赋值时直接崩。
原因:输入列数变了,但 w1 的切分长度 size(Xtr,2)*hiddennum 没同步更新;或者旧工作区里残留着上一个数据集的 Best_solution 变量,脚本没覆盖它,继续拿旧向量初始化新网络。这是典型的“脏工作区”问题。
解决:换数据前先 clear all,确认工作区没有残留变量;然后验证 dim = hiddennum*(size(Xtr,2)+2)+1 是否随新输入列数更新,解码前加一行 assert(numel(Best_solution)==dim)。这一行 assert 能省掉一半这类崩溃。
5.5 同样代码两次运行结果差异极大
现象:固定了 rng 还是对不上,或者注释掉 rng 后五次运行 R² 从 0.85 到 0.95 乱跳。
原因分两半。前半是 randperm 的划分变了,训练样本集本身不同,这是正常的随机性;后半更隐蔽——trainlm 内部也有自己的随机化,即使固定了 OOA 部分的 rng,如果没有在 train 之前再固定一次,反向传播中的扰动仍会引入差异。
解决:两个 rng 调用缺一不可,一个在数据划分前,一个在 net = train 之前。想汇报稳定结果,就按 4.3 的约定跑五次取均值。不要追求五次一模一样,那是自欺欺人;追求“均值稳定、标准差小”才是可复现的模型行为。
6. 验证结果是否可信:四个指标与一步对比实验
6.1 指标读取顺序
出图后我按固定顺序读四个数:RMSE 看误差量级是否被业务容忍,MAE 看有没有被个别离群点拉高,R² 看整体解释力,MAPE 看相对误差对百分比场景是否友好。前三个用这段代码一次算全:
%% 回归指标计算(全部用反归一化后的真实量纲) Yte_true = mapminmax('reverse', Yte_norm, ps_output); RMSE = sqrt(mean((Yte_true - Yte_pred).^2)); MAE = mean(abs(Yte_true - Yte_pred)); R2 = 1 - sum((Yte_true - Yte_pred).^2) / sum((Yte_true - mean(Yte_true)).^2); MAPE = mean(abs((Yte_true - Yte_pred) ./ Yte_true)) * 100; % 数据含0时慎用判断建议:R² 低于 0.7 先不要动优化器,回头查特征;RMSE 比 Y 的标准差还大,说明模型连均值水平都没达到;MAPE 只在 Y 全部为正数时用,样本里出现 0 或负值这个指标就没有意义,别硬报。
6.2 和普通 BP 做一次同条件对比
为了让 OOA-BP 的结果能站住,我习惯在同一份数据、同一个 hiddennum、同一个划分种子下,用不带 OOA 的普通 BP 跑一遍当基准。实现上只需要把第 3.2 节第 4 步里的四行初始化解码删掉,直接用 newff 默认随机初始化的 net 去 train。对比看两个数字:训练集 R² 谁高,测试集 R² 谁高。
如果 OOA-BP 的测试集 R² 只比普通 BP 高 0.01 以内,说明你的数据本身好拟合,初始解优化收益不大;如果高了 0.05 以上,这套资源对你的场景就是有效投入。我还见过 OOA-BP 反而不如普通 BP 的情况,原因基本都在 5.3 条,收敛没到位或者边界设置失误,排查比换算法更优先。
6.3 我现在的固定收尾流程
我现在的习惯是每跑完一个回归实验都强制走一遍:clear all 清工作区 → 检查 data 列顺序 → rng 固定种子跑基准 → 连续 5 次无种子跑记录均值±标准差 → 用 save 把 net、ps_input、ps_output、Yte_pred 一起落盘。模型文件不存归一化参数的话,下次预测新数据还要拿旧数据重新算 ps,新样本的归一化就会和训练时对不上,指标立刻变差,这是我最开始吃过的亏。从那以后我每次跑这类优化加网络训练的出图脚本都强制走这一遍。后续你要改成多输出,只需要把最后一列 Y 换成 N 列并同步调整输出层节点数和 ObjFun 的输出维度,别的不用动。希望帮到你。
本文还有配套的精品资源,点击获取