简介:基于Sine混沌映射改进的麻雀搜索算法SSA优化BP神经网络的MATLAB实现代码包,面向需要提升回归预测精度的研究人员、算法学习者及MATLAB开发者。资源将混沌映射的全局探索能力与SSA快速收敛特性结合,针对BP神经网络易陷入局部最优和收敛慢的问题,提供了完整的优化训练方案。压缩包共6个文件,含4个.m脚本、1个.mat数据文件和1个Excel数据集,代码结构清晰,可直接运行并替换数据使用。包体约198KB,轻量易部署。目前已吸引713人学习下载,适合用于学术实验、课程设计或工程预测任务。内容涵盖数据预处理、Sine混沌初始化、SSA寻优、BP训练及误差评估等完整流程,并附有适应度与预测对比可视化,便于理解算法原理与调参思路,可快速迁移到其他回归场景。
1. 从麻雀搜索到参数初值:为什么BP回归要先做权重初始化
做 BP 回归预测,很多人第一反应是调隐层节点数、改学习率,却忽略了最开始的权重和阈值其实决定了你后面所有迭代的天花板。直接net = newff(...)生成的随机初始参数,多跑几次会发现指标忽高忽低;而换成群智能算法先搜一组初值,再交给 BP 反向传播精调,往往能同时改善收敛速度和最终精度。这个 MATLAB 工程正是在做这件事:先用 Sine 混沌映射初始化麻雀搜索算法(SSA)的种群,让 SSA 在解空间里找一组权重阈值,再赋给 BP 网络做回归预测。代码里包含main.m、fitness.m、calc_error.m、sineInitialization.m和一份数据.xlsx,适合预测建模或算法对比实验时直接改数据复跑。
2. Sine混沌映射与麻雀搜索算法的原理与选型
2.1 Sine混沌映射的迭代公式与分布特征
Sine 混沌映射的迭代形式很简洁:
x_{n+1} = r \cdot \sin(\pi \cdot x_n),r \in (0, 1]
当r接近 1 时,序列长期运行不会收敛到周期点,而且对初值极其敏感。它的输出大致落在[0, 1]区间,实际应用中通常通过lb + (ub - lb) * x平移到 BP 的权重阈值边界内。相比rand生成的均匀随机数,混沌序列有两个容易被忽略的优势:一是相邻样本之间有确定的相关性;二是当序列长度较长时,它不会像随机数那样在某些区间聚集。对 SSA 来说,初始种群的分布质量直接决定前期探索是否充分,Sine 映射在这里不是花架子,而是把随机初始化换成非线性动力系统采样。
% sine_sequence.m 示例:生成 30 个混沌值 N = 30; r = 1; x = zeros(N, 1); x(1) = 0.31; % 初始值避开0、0.5、1 for n = 1:N-1 x(n+1) = r * sin(pi * x(n)); end上面这段代码的关键参数是r和x(1)。r取 1 时映射处于满混沌状态,序列的遍历性最好;x(1)如果取 0、0.5 或 1,迭代会直接进入不动点,整个正弦链失效。实际使用时,把x映射到[lb, ub]就得到了一组初始解。
提示:Sine 映射对初值敏感,调试时需要固定
seed。在后面的工程里到处改seed会浪费时间,先固定一个安全初值,把流程跑通再讨论随机性。
2.2 麻雀搜索算法中的发现者、追随者与警戒者
SSA 把种群分成三类角色。发现者一般为种群中适应度较好的前 20% 个体,负责大范围扫描;追随者围绕发现者周围进一步细化;警戒者占比 10%~20%,作用是跳出局部最优。常见实现中,发现者的位置更新会先判断预警值R2是否小于安全阈值ST,小于则按指数项逐步收缩,大于则直接跳跃到随机位置。追随者则根据自己在种群中的排名,决定是去发现者附近进食,还是飞到更远的方向觅食。警戒者的更新通常会保留当前最优位置,并让其余个体向安全位置移动。
这三类角色配合后,SSA 在低维连续优化问题上比标准 PSO 收敛更直接,但也有一个缺点:如果初始种群全部挤在某个局部区域,后续角色更新很难救回来。这正好是引入 Sine 混沌初始化的理由。原始 SSA 多用均匀随机数生成初始种群,样本量小时容易出现聚类现象;用 Sine 映射生成的位置序列在区间内分布更均匀,且相邻个体之间保持连续过渡,让发现者在第一轮迭代就能扫到距离更远的位置。
2.3 改进点:Sine混沌初始化与适应度桥接
改进后的流程很清晰:先由sineInitialization.m生成pop个长度为dim的个体,每个个体对应一组 BP 权重和阈值;然后fitness.m把该个体还原成网络参数,在训练集上做一次前向计算,返回 MSE 作为适应度;SSA 依据适应度排序确定发现者、追随者和警戒者,更新位置后继续下一轮。和随机初始化对比:
| 对比项 | 随机初始化 | Sine混沌初始化 |
|---|---|---|
| 生成方式 | rand独立抽样 | 混沌序列迭代 |
| 分布均匀性 | 样本量小时容易聚集 | 低维投影更均匀 |
| 与原 SSA 耦合 | 直接替换初始化即可 | 需额外维护序列生成 |
| 可复现性 | 需rng固定 | 固定初值即可 |
| 对早熟的影响 | 可能因聚集陷入局部最优 | 有助于前期的全局探索 |
实际代码里不需要改 SSA 主循环,只要把X0 = lb + (ub - lb) * rand(pop, dim)换成X0 = sineInitialization(pop, dim, lb, ub, 0.31),其余逻辑不变。这也是这个压缩包最容易迁移的部分:把随机初始化替换成混沌初始化,对 PSO、GWO、WOA 等算法同样适用。
3. MATLAB工程结构:从数据导入到SSA优化BP的实现
拿到压缩包,先不要急着双击main.m。建议按下面顺序浏览:数据.xlsx是原始数据,最后一列通常是被预测的目标;data1.mat是中间结果或备用数据;sineInitialization.m只负责生成初始种群;fitness.m用于计算每个个体的 MSE;calc_error.m最后输出 RMSE、MAE、R²;真正把组织和训练串起来的是main.m。下面逐个拆关键点。
3.1 文件清单与运行入口
| 文件 | 作用 |
|---|---|
main.m | 主流程:读数据、归一化、调用SSA、训练与验证 |
sineInitialization.m | 用 Sine 混沌映射生成初始种群 |
fitness.m | 将个体解码为 BP 参数并计算 MSE |
calc_error.m | 计算 RMSE、MAE、R² |
数据.xlsx | 原始数据集 |
data1.mat | 备用数据或中间结果 |
运行入口只有一个:在 MATLAB 命令窗口进入解压目录后执行run('main.m'),或者直接按 F5。由于main.m内通常嵌入了 SSA 的发现者、追随者、警戒者更新逻辑,断点调试时建议在for t = 1:T这一行和fitness调用处分别下断点。
3.2 main.m 主流程的关键代码
%% main.m 主流程骨架 clc; clear; close all; % 1) 读取数据 if isfile('数据.xlsx') data = xlsread('数据.xlsx'); else load('data1.mat', 'data'); end X = data(:, 1:end-1); Y = data(:, end); % 2) 训练/测试划分 rng(42); n = size(data, 1); idx = randperm(n); trainNum = floor(0.8 * n); X_train = X(idx(1:trainNum), :); Y_train = Y(idx(1:trainNum), :); X_test = X(idx(trainNum+1:end), :); Y_test = Y(idx(trainNum+1:end), :); % 3) 归一化,只 fit 训练集 [Xn_train, psX] = mapminmax(X_train', 0, 1); [Xn_test, ~] = mapminmax('apply', X_test', psX); [Yn_train, psY] = mapminmax(Y_train', 0, 1);isfile('数据.xlsx')是 MATLAB R2017b 之后才有的函数,如果你的版本较老,建议改成exist('数据.xlsx', 'file')。rng(42)固定数据划分,保证两次实验之间只有算法参数不同;mapminmax对每行归一化,所以输入需要转置,并且归一化参数只能从训练集计算。
% 4) BP网络结构 inputNum = size(X, 2); hiddenNum = 10; outputNum = 1; dim = inputNum*hiddenNum + hiddenNum + hiddenNum*outputNum + outputNum; % 5) Sine混沌初始化 pop = 30; lb = -3 * ones(1, dim); ub = 3 * ones(1, dim); [X0, ~] = sineInitialization(pop, dim, lb, ub, 0.31); % 6) SSA迭代 T = 100; historyBest = zeros(T, 1); for t = 1:T f = arrayfun(@(i) fitness(X0(i,:), Xn_train, Yn_train, ... inputNum, hiddenNum, outputNum), 1:pop); [bestF, bestIdx] = min(f); historyBest(t) = bestF; bestX = X0(bestIdx, :); % 这里省略标准SSA的角色更新公共段 % 实际工程中通常是:发现者用指数搜索,追随者向最优挪动,警戒者随机扰动 enddim是权重阈值总个数,必须和fitness.m里的reshape长度一致。这段骨架没有直接用newff,而是把权重阈值展平成向量手动做前向计算,因为 SSA 每次迭代都要重算几十次适应度,用工具箱的net对象反复setwb/getwb反而拖慢速度。lb/ub取正负 3 是经验值,数据归一化到[0,1]时权重不会太大,边界过宽会浪费搜索。pop=30, T=100适用于中小数据集;如果样本量超过 10 万,建议pop=50, T=150。
3.3 sineInitialization.m 的设计与边界处理
function [X0, seq] = sineInitialization(pop, dim, lb, ub, seed) % Sine混沌映射生成初始种群 % pop: 种群规模, dim: 个体维度 x = zeros(pop, dim); seq = zeros(pop, dim); r = 1; x(1, 1) = seed; seq(1, 1) = x(1, 1); for i = 1:pop if i > 1 x(i, 1) = r * sin(pi * x(i-1, dim)); % 跨个体继续迭代 end for j = 2:dim x(i, j) = r * sin(pi * x(i, j-1)); end seq(i, :) = x(i, :); X0(i, :) = lb + (ub - lb) .* x(i, :); end end这里用一整条混沌链生成所有个体:第一个个体第一维来自seed,后面的值全部由前一个位置迭代得到,所以个体之间不是独立随机数,而是保持了混沌序列的连续性。X0(i,:) = lb + (ub - lb) .* x(i,:)做逐元素映射,因为lb和ub都是向量。seq保存未映射前的混沌值,方便排查边界映射是否正确。如果dim很大,比如超过 50,这条链生成的序列仍然有很好的全局均匀性;如果嫌长链条耗时,可以在每个个体内部重置seed,但那样会削弱混沌遍历性,我一般不做。
3.4 fitness.m 与 calc_error.m 的联动
function mse = fitness(ind, Xn, Yn, inputNum, hiddenNum, outputNum) dim = length(ind); W1 = reshape(ind(1:inputNum*hiddenNum), hiddenNum, inputNum); B1 = ind(inputNum*hiddenNum+1 : inputNum*hiddenNum+hiddenNum)'; W2 = reshape(ind(inputNum*hiddenNum+hiddenNum+1 : dim-outputNum), ... outputNum, hiddenNum); B2 = ind(dim-outputNum+1 : dim)'; H = 1 ./ (1 + exp(-(W1 * Xn + B1))); % 隐层 logsig O = W2 * H + B2; % 输出层 purelin mse = mean((Yn - O).^2, 'all'); endB1转置成列向量是为了和W1 * Xn做广播加法。mean((Yn-O).^2, 'all')是 MATLAB R2018b 之后的写法,老版本请改成mean(mean((Yn-O).^2))。这里故意没有做反向传播,因为 SSA 只需要前向计算出来的 MSE 作为适应度;真正训练和测试时的误差评估交给calc_error.m:
function [rmse, mae, r2] = calc_error(Ytrue, Ypred) err = Ytrue - Ypred; rmse = sqrt(mean(err.^2, 'all')); mae = mean(abs(err), 'all'); ss_res = sum(err.^2, 'all'); ss_tot = sum((Ytrue - mean(Ytrue, 'all')).^2, 'all'); r2 = 1 - ss_res / ss_tot; end注意使用顺序:优化结束后先用bestX计算测试集预测值,如果输出层做过归一化,必须先mapminmax('reverse', Ypred, psY)还原,再调calc_error。否则r2虽然也能算出来,但反映的是归一化空间的表现,和原始数据的 RMSE 不在一个量纲上。
4. 数据归一化、隐层结构与SSA迭代参数整定
4.1 数据读取与归一化的三种方式
数据.xlsx默认读取第一个 sheet,如果数据不在第一个 sheet,用xlsread('数据.xlsx', 'Sheet2')指定。MATLAB R2019a 以后更推荐readmatrix,它对数值型数据的读取更稳健。data1.mat里我一般习惯保存已经清洗好的data矩阵,避免每次xlsread重复解析,也方便在没有 Excel 的 Linux 环境下直接运行。
归一化方式直接决定隐层激活函数的选择。mapminmax(X_train', 0, 1)把每行数据映射到[0,1],对应logsig的输出范围;mapminmax(X_train', -1, 1)映射到[-1,1],对应tansig。zscore(X_train)是另一种常见做法,它按列标准化为均值 0、方差 1,适合输入特征量纲差异大的情况:
% 两种常见预处理 [Xn_train, psX] = mapminmax(X_train', 0, 1); [Zn_train, muX, sigmaX] = zscore(X_train);如果训练集只有几十个样本,zscore比mapminmax更不容易受离群点影响。但不管选哪种,测试集必须复用训练集得到的psX或muX/sigmaX,不能独立计算,否则数据分布不一致会让测试集指标虚高。
另外要注意,这份代码默认数据是无序的,randperm打乱后划分训练集和测试集。如果数据.xlsx是时间序列,比如天气或负荷数据,打乱会导致未来数据混进训练集,应该改成按时间顺序切分,保留最后 20% 作为测试集。这是回归预测任务里比调参更容易踩的坑。
4.2 隐层节点数与激活函数的权衡
隐层节点数是 BP 回归里最敏感的结构参数。常见经验公式是hiddenNum = round(sqrt(inputNum + outputNum) + a),其中a取 1 到 10,也可以从round((inputNum + outputNum) * 2 / 3)开始试。对于本项目的数据量,建议设hiddenNum = 10起跑,然后按 2 步长递增,观察测试集 RMSE。节点太少模型欠拟合,节点太多会出现训测差距快速拉大的过拟合信号。
激活函数方面,输出层用purelin,隐层用logsig还是tansig取决于归一化范围。用[0,1]归一化就配logsig,用[-1,1]归一化就配tansig,这是比较省心的组合。反向传播本身的线性层对初始权重很敏感,所以 SSA 搜索出的最优个体不要直接投入训练,而是作为 BP 的初始权重,再用trainlm精调几个 epoch,能拿到更好的局部精修。
| 参数 | 建议范围 | 调整方向 |
|---|---|---|
| hiddenNum | sqrt(inputNum+outputNum)+1~ +10 | RMSE 回升说明过拟合,降节点 |
| pop | 20 ~ 50 | dim 大或数据量大时调大 |
| T | 50 ~ 200 | 收敛曲线后段仍下降就调大 |
| 发现者比例 | 0.2 | 想加强全局探索可到 0.25 |
| 警戒者比例 | 0.1 ~ 0.2 | 频繁早熟时调大 |
| lb / ub | [-3, 3] | 权重发散时缩到[-2, 2] |
4.3 SSA迭代参数对收敛的影响与观测曲线
调参不能只靠感觉,要把每代最优适应度画出来。historyBest在main.m的循环里已经被记录,用semilogy画:
figure('Color', 'w'); semilogy(1:T, historyBest, 'LineWidth', 1.6); grid on; xlabel('迭代次数'); ylabel('训练集 MSE(log)'); title('Sine-SSA 适应度收敛曲线');用semilogy而不是plot,是因为 MSE 经常从1e-1下降到1e-3,跨越数量级后plot的前半段会被压成一条直线。如果曲线在 20 代内就水平,说明种群多样性消失,可以调大发现者比例到 0.25,或者把 Sine 映射的r从 1 降到 0.95,给迭代增加一点扰动。如果曲线一直锯齿状跳动,大概率是lb/ub太宽,导致个体频繁跳出优质区域,此时把边界缩到[-2, 2]会更快稳定。如果historyBest在最后一代仍在下降,不要只加T,还要检查是不是数据划分导致测试集过于简单,换一份数据或换随机种子跑一次,排除数据层面的偶然。
5. 回归预测结果验证:RMSE、R²和混沌初始化重复性检查
5.1 用 calc_error.m 得到最终指标
最终预测时不要重新写一套误差计算,直接复用calc_error.m。关键在于输出还原:
% bestX 是 SSA 找到的最优个体 % 按 bestX 重建网络,对 Xn_test 前向计算得到 Y_pred_norm Y_pred_norm = W2_best * H_test + B2_best; Y_pred_raw = mapminmax('reverse', Y_pred_norm, psY); [rmse, mae, r2] = calc_error(Y_test, Y_pred_raw); fprintf('RMSE=%.4f, MAE=%.4f, R2=%.4f\n', rmse, mae, r2);如果Y_test本身也做过归一化,那么calc_error的第一个参数也要换成mapminmax('reverse', Y_test, psY)。不少新手只还原预测值不还原真实值,算出来的 RMSE 总是接近 0,然后找不到原因。
5.2 固定 seed 做重复性检查
Sine 初始化比随机初始化稳定,但它的混沌链对seed依然敏感。调试阶段建议固定seed = 0.31,把算法跑通;做实验对比时再换 0.11、0.21、0.31 各跑一次,每次保持rng(42)使得数据划分一致,记录三次测试集 R2。如果三次 R2 的极差大于 0.05,说明搜索不够充分,优先增大T,其次再考虑调大pop。混沌初始化不是消除随机性,而是把随机性收敛到可复现的序列上,所以只跑一次就拿出去对比,结果没有说服力。
5.3 预测结果异常时的快速排查
几个高频问题对照:
reshape报错:检查length(ind)是否等于inputNum*hiddenNum + hiddenNum + hiddenNum*outputNum + outputNum。- R2 高但 RMSE 异常小:多半用了归一化后的数据算误差,记得逆向还原。
- 测试 R2 比训练低很多:隐层节点过多或迭代次数过长,BP 精调阶段发生过拟合。
- 多次运行结果差异巨大:先检查数据划分是否固定,再检查
sineInitialization的seed是否固定。
调试时把historyBest前 10 代打印出来,如果第一次迭代的 MSE 已经接近最终值,说明初始解运气好,不能作为改进依据;真正有效的改进应该让不同 seed 下的收敛曲线都比随机初始化更平滑,波动更小。调整时优先看测试集 RMSE 而不是训练集 MSE,混沌初始化的 seed 固定下来后,每次对比才有意义。
本文还有配套的精品资源,点击获取