麻雀算法优化GRNN的Matlab实现:多变量回归预测完整项目
发布时间:2026/10/2 5:02:47来源:尧图网络
简介这是一份面向具备Matlab与机器学习基础的研发人员、数据分析从业者的完整项目案例聚焦麻雀搜索算法SSA优化广义回归神经网络GRNN的多变量回归预测重点解决高维、噪声数据下传统模型易陷入局部最优、预测精度不足等问题。压缩包内仅含1个docx文档整体大小56KB文档以章节形式完整覆盖项目背景、目标与意义、挑战及解决方案、特点创新、应用领域、模型架构、代码示例、系统部署与应用、未来改进方向等模块并配有GUI设计说明与关键代码详细注解。已有55人学习浏览。读者可从数据预处理、模型训练优化到实时预测与可视化界面对照实践理解SSA-GRNN原理并迁移到金融、气象、环境、制造等回归预测任务还可参考增量学习、引入深度学习等扩展思路适合作为课程设计或科研实验起点。1. 机器学习里做多变量回归预测Matlab实现麻雀算法SSA优化广义回归神经网络GRNN核心就一句话GRNN只有一个超参数spread值得调而SSA恰好擅长在连续空间里找全局最优于是SSA-GRNN就成了很多项目里的默认组合。它不挑数据规模几十个样本也能跑特别适合做论文对比和仿真验证。这篇文章写给正在写代码的人不管你是课设、课题还是企业仿真都希望拿到一份能直接复现的SSA-GRNN完整项目而不是原理翻来覆去、代码缺胳膊少腿的帖子。我会把四个层面讲透——GRNN结构、SSA搜索逻辑、Matlab完整实现、GUI封装与踩坑。读完你可以做两件事按我的代码把整套流程跑通然后替换成你自己的多变量数据看着适应度曲线和误差指标判断模型到底行不行。2. 先看懂GRNN的四层结构与唯一超参数spread为什么不能随手乱设2.1 输入层、模式层、求和层、输出层各做了什么GRNN全称Generalized Regression Neural Network是一种基于核回归的概率神经网络。它最吸引人的地方是不需要迭代训练也不用反向传播样本一次性进网络就完成了“建网”。Matlab里一条newgrnn命令就能把网络建好省掉了大部分调参时间代价是你得理解它内部那套非参数回归逻辑。网络结构固定是四层。输入层的节点数等于你的特征维度只负责把样本送入。模式层最特殊每个训练样本对应一个神经元神经元中心就是这个样本的输入向量。预测一个新点时模式层逐个计算该点与所有样本中心的距离再套一个径向基函数。求和层里有两类节点一类对模式层输出做加权求和另一类做普通求和。输出层把加权和与普求和相除得到最终的回归估计值。这里没有可学习的权重也没有训练轮次唯一影响预测形态的就是spread。spread是径向基函数的扩展速度在newgrnn(P,T,spread)里直接作为第三个参数传入。spread越小基函数越尖锐新点只受周围极少数样本影响spread越大基函数越平缓预测结果逐渐趋向所有训练样本输出的平均值。所以你会发现一个典型现象spread设成0.001训练集误差低到夸张但测试集预测曲线震荡得像锯齿spread设成100预测曲线几乎变成一条平线。这不是玄学是基函数宽度在起作用。2.2 先用网格扫描摸清spread的脾气一段可复现的Matlab代码正式上SSA之前我建议先做一次最原始的网格扫描。这一步有两个目的一是确认你的数据和GRNN是否兼容二是给后面SSA的搜索范围一个靠谱的上下界。代码里我按“每行一个样本、最后一列是输出”的常见格式处理。% 固定随机种子让结果可重复 rng(1); % 读数据最后一列为因变量前面所有列是自变量 data xlsread(data.xlsx); X data(:, 1:end-1); Y data(:, end); N size(X, 1); % 划分训练集和测试集 idx randperm(N); trainNum floor(0.75 * N); X_train X(idx(1:trainNum), :); Y_train Y(idx(1:trainNum)); X_test X(idx(trainNum1:end), :); Y_test Y(idx(trainNum1:end)); % 归一化newgrnn要求“每列一个样本”所以需要转置 [Xn_train, psX] mapminmax(X_train, 0, 1); [Yn_train, psY] mapminmax(Y_train, 0, 1); Xn_test mapminmax(apply, X_test, psX); % 对spread做网格扫描 spreads 0.05:0.05:2; rmseTrain zeros(size(spreads)); rmseTest zeros(size(spreads)); for i 1:length(spreads) net newgrnn(Xn_train, Yn_train, spreads(i)); Yn_hat sim(net, Xn_train); Y_hat mapminmax(reverse, Yn_hat, psY); rmseTrain(i) sqrt(mean((Y_hat - Y_train).^2)); Yn_hat_t sim(net, Xn_test); Y_hat_t mapminmax(reverse, Yn_hat_t, psY); rmseTest(i) sqrt(mean((Y_hat_t - Y_test).^2)); end % 画出两条误差曲线 figure; plot(spreads, rmseTrain, o-, spreads, rmseTest, s-); legend(训练集RMSE, 测试集RMSE); xlabel(spread); ylabel(RMSE);这段代码里的关键点是mapminmax。它默认对矩阵的每一行做归一化而newgrnn要求输入是“特征×样本”的矩阵所以必须先把常见的数据表转置。后面mapminmax(apply, X_test, psX)表示用训练集的归一化参数去处理测试集不能重新计算测试集自己的最小值和最大值。网格扫描的结果通常会告诉你两件事训练集RMSE随着spread增大而快速上升因为网络变平滑后拟合能力下降测试集RMSE则是一个U型曲线最低点就是当前数据下的较优spread。如果最低点落在区间的边缘说明你的扫描范围不对。比如最低点出现在0.05那么真实最优可能在更小的位置SSA搜索时就要把下界继续往下压。网格扫描的局限是只试了离散点而且遇到多个局部低谷时会漏掉更优的位置这时候才轮到麻雀算法出马。3. 麻雀算法优化GRNN三个角色、位置更新与适应度函数设计3.1 发现者、加入者、侦察者如何在连续空间里合作搜索麻雀算法是2020年前后提出的群智能优化算法模拟麻雀觅食和反捕食的行为。整个种群分成三个角色分工明确。发现者占种群的一部分通常设成20%。它们适应度好离食物近负责在当前位置附近精细搜索同时引导其他麻雀飞过来。发现者更新时有一个安全阈值ST当预警值小于ST时说明周围安全就在当前位置附近小步移动当预警值大于等于ST时说明有捕食者接近整群麻雀需要迅速飞到安全区域此时位置会进行一次较大幅度的随机跳跃。加入者是剩余的大多数跟着发现者走。适应度排在后面的一半加入者会直接飞到一个远离当前最优的随机位置保证种群不会过早聚在一起适应度相对靠前的加入者会向当前最优位置靠拢并加入随机扰动避免原地打转。侦察者比例一般占10%负责监视危险。如果自己不是最优就向最优位置靠近如果自己已经是最优反而要远离最差位置以此产生跳出局部最优的逃逸行为。这三个角色的配合对一维spread搜索来说可能显得“杀鸡用牛刀”但要注意GRNN的spread不是越接近某个固定值越好它和数据分布、样本量、特征尺度都有关。SSA的优势在于它不依赖梯度也不要求目标函数连续可导给一个适应度函数就能搜而且收敛速度在低维问题上明显快于网格扫描和随机搜索。3.2 适应度函数用验证集MSE而不是训练集MSESSA-GRNN里麻雀个体的每一个位置就是一个候选的spread值。适应度函数负责回答“这个spread好不好”。最简单的写法是用训练集MSE但这是很多新手的坑。因为spread越小GRNN对训练样本的记忆越强训练集MSE一定漂亮可到了新样本上预测曲线会剧烈震荡掉进过拟合。我通常会把原来的训练集再挖出一块验证集用验证集的MSE做适应度。验证集不参与newgrnn建网只用来评估“没见过”数据上的误差。这样SSA搜索出的spread会偏向光滑一些的位置泛化能力更好。function mse fitness_fun(spread, Xn_tr, Yn_tr, Xn_va, Yn_va) % 用训练集建网用验证集评估 net newgrnn(Xn_tr, Yn_tr, spread); Yn_hat sim(net, Xn_va); mse mean((Yn_hat - Yn_va).^2); end这个函数保存为fitness_fun.m放在当前目录。注意newgrnn每次调用都会重新建网所以SSA每评估一个个体就建一次网种群数30、迭代50次就是1500次建网。听起来多但GRNN建网只是复制样本没有迭代训练所以速度完全能接受。如果样本量过万模式层神经元太多可以考虑先对训练样本聚类减量不过这是后话。还有一个细节newgrnn建网时如果spread恰好是0或负数会直接报错。所以不管SSA怎么飞位置必须做边界限制。常见做法是给spread设上下界比如[0.001, 10]每次位置更新后执行越界截断。3.3 SSA主循环骨架边界处理与收敛曲线下面给一个麻雀算法主循环的核心骨架一维搜索方便你理解加入者、发现者、侦察者是怎么协同工作的。% SSA参数 pop 30; % 种群数量 M 50; % 最大迭代次数 dim 1; % 优化一个spread lb 0.001; ub 10; PD 0.2; % 发现者比例 SD 0.1; % 侦察者比例 ST 0.8; % 预警安全阈值 % 初始化种群 X lb rand(pop, dim) * (ub - lb); fit zeros(pop, 1); for i 1:pop fit(i) fitness_fun(X(i, 1), Xn_tr, Yn_tr, Xn_va, Yn_va); end [bestFit, bestIdx] min(fit); bestX X(bestIdx, :); curve zeros(M, 1); for t 1:M % 按适应度排序 [fitSorted, sortIdx] sort(fit); Xsorted X(sortIdx, :); pNum round(pop * PD); % 发现者更新 R2 rand; for j 1:pNum if R2 ST Xsorted(j) Xsorted(j) * exp(-j / (pNum * M)); else Xsorted(j) Xsorted(j) randn; end end % 加入者更新 for j pNum1:pop if j pop / 2 Xsorted(j) randn * exp((Xsorted(end) - Xsorted(j)) / j^2); else % 一维下朝当前最优方向移动 Xsorted(j) Xsorted(1) 0.5 * (2*rand-1) * abs(Xsorted(j) - Xsorted(1)); end end % 侦察者更新 for j 1:round(pop * SD) idx_s randi(pop); if fitSorted(idx_s) bestFit Xsorted(idx_s) bestX randn * abs(Xsorted(idx_s) - bestX); else Xsorted(idx_s) Xsorted(idx_s) rand * ... (Xsorted(idx_s) - Xsorted(end)) / (fitSorted(idx_s) - fitSorted(end) eps); end end % 边界处理并重新计算适应度 Xsorted max(min(Xsorted, ub), lb); for i 1:pop fitSorted(i) fitness_fun(Xsorted(i), Xn_tr, Yn_tr, Xn_va, Yn_va); end X Xsorted; fit fitSorted; [bestFit, bestIdx] min(fit); bestX X(bestIdx); curve(t) bestFit; fprintf(迭代%d, 最优MSE%.6f, spread%.4f\n, t, bestFit, bestX); end这个骨架我把原始论文里的矩阵运算简化成了一维运算实际效果完全够用。边界处理放在每个个体重新计算适应度之前保证任何越界spread都不会传给newgrnn。收敛曲线curve记录每一代的全局最优MSE后面画出来就能直观判断算法是否收敛。如果你发现曲线最后几个点还在明显下降说明最大迭代次数太小需要增加M。4. Matlab完整项目实例数据划分、SSA主程序、训练与回归评估4.1 数据读取、训练/验证/测试三份划分与归一化网上很多例程只分训练集和测试集但SSA-GRNN建议分三份训练集用来建网验证集用来给SSA当适应度测试集只做最终评估。这样能避免“用测试集调过参数再拿测试集报告成绩”的自欺欺人。clear; clc; close all; rng(42); % 固定随机种子保证结果可复现 % 读数据每行一个样本最后一列是输出 data xlsread(data.xlsx); X data(:, 1:end-1); Y data(:, end); N size(X, 1); % 按 60% 训练 / 20% 验证 / 20% 测试 划分 idx randperm(N); nTr floor(0.6 * N); nVa floor(0.2 * N); X_train X(idx(1:nTr), :); Y_train Y(idx(1:nTr)); X_val X(idx(nTr1:nTrnVa), :); Y_val Y(idx(nTr1:nTrnVa)); X_test X(idx(nTrnVa1:end), :); Y_test Y(idx(nTrnVa1:end)); % 归一化newgrnn要求每列一个样本所以转置 [Xn_tr, psX] mapminmax(X_train, 0, 1); [Yn_tr, psY] mapminmax(Y_train, 0, 1); Xn_va mapminmax(apply, X_val, psX); Yn_va mapminmax(apply, Y_val, psY); Xn_te mapminmax(apply, X_test, psX); Yn_te mapminmax(apply, Y_test, psY);这段代码里mapminmax的第二个输入0,1表示把数据映射到[0,1]区间。为什么不直接用原始值因为GRNN的径向基函数依赖样本欧氏距离如果某个特征量纲很大、另一个量纲很小距离就会被大量纲特征主导spread怎么调都救不回来。归一化之后每个特征都落在一个量级spread才具有全局可比性。注意psX保存了训练集的最小值和最大值验证集和测试集都用apply调用同一个变换。这是为了模拟真实预测场景。如果你的测试集里出现比训练集最大值还大的数mapminmax会把它压到超出[0,1]范围但这是正常现象程序不会报错。4.2 适应度函数与SSA主程序完整实现先写适应度函数文件。把下面代码保存为fitness_fun.m。function mse fitness_fun(spread, Xn_tr, Yn_tr, Xn_va, Yn_va) net newgrnn(Xn_tr, Yn_tr, spread); Yn_hat sim(net, Xn_va); mse mean((Yn_hat - Yn_va).^2); end然后写SSA主程序。这里给出一个可以直接执行的完整版本使用的数据和上一节工作区中的Xn_tr等变量一致。% SSA优化GRNN的spread pop 30; M 50; dim 1; lb 0.001; ub 10; PD 0.2; SD 0.1; ST 0.8; X lb rand(pop, dim) * (ub - lb); fit zeros(pop, 1); for i 1:pop fit(i) fitness_fun(X(i), Xn_tr, Yn_tr, Xn_va, Yn_va); end [bestFit, bestIdx] min(fit); bestX X(bestIdx); curve zeros(M, 1); for t 1:M [fitSorted, sortIdx] sort(fit); Xsorted X(sortIdx); pNum round(pop * PD); % 发现者 R2 rand; for j 1:pNum if R2 ST Xsorted(j) Xsorted(j) * exp(-j / (pNum * M)); else Xsorted(j) Xsorted(j) randn; end end % 加入者 for j pNum1:pop if j pop / 2 Xsorted(j) randn * exp((Xsorted(end) - Xsorted(j)) / j^2); else Xsorted(j) Xsorted(1) 0.5 * (2*rand-1) * abs(Xsorted(j) - Xsorted(1)); end end % 侦察者 for j 1:round(pop * SD) idx_s randi(pop); if fitSorted(idx_s) bestFit Xsorted(idx_s) bestX randn * abs(Xsorted(idx_s) - bestX); else Xsorted(idx_s) Xsorted(idx_s) rand * ... (Xsorted(idx_s) - Xsorted(end)) / (fitSorted(idx_s) - fitSorted(end) eps); end end % 边界处理 Xsorted max(min(Xsorted, ub), lb); % 重新计算适应度 for i 1:pop fitSorted(i) fitness_fun(Xsorted(i), Xn_tr, Yn_tr, Xn_va, Yn_va); end X Xsorted; fit fitSorted; [bestFit, bestIdx] min(fit); bestX X(bestIdx); curve(t) bestFit; fprintf(迭代%2d, 最优MSE%.6f, spread%.4f\n, t, bestFit, bestX); end % 画出收敛曲线 figure; plot(1:M, curve, b-, LineWidth, 1.5); xlabel(迭代次数); ylabel(验证集MSE); title(SSA优化GRNN收敛曲线); grid on;这个版本的一个简化点是在加入者更新里用了带符号的随机系数替代原始论文里对矩阵求伪逆的操作。原因是dim1时伪逆只有一个标量符号正负不影响随机性。如果你想扩展成同时优化多个超参数需要重新引入矩阵运算一维版的随机系数就不适用了。侦察者更新时我用了fitSorted(idx_s)判断个体是否优于当前全局最优。注意这里可能会重复选中同一个个体但不影响整体收敛因为每次迭代都有随机扰动。如果希望更规范可以遍历所有侦察者并保证不重复。实际测试区别很小。4.3 用最优spread建网、预测并输出RMSE、MAE、R2SSA搜索结束后bestX就是当前数据下的最优spread。接下来要做两件事一是把训练集和验证集合起来重新建网尽可能多利用样本二是在测试集上评估最终效果。% 用训练集验证集一起建网 X_all [Xn_tr, Xn_va]; Y_all [Yn_tr, Yn_va]; net newgrnn(X_all, Y_all, bestX); % 测试集预测 Yn_pred sim(net, Xn_te); Y_pred mapminmax(reverse, Yn_pred, psY); Y_pred Y_pred(:); Y_test Y_test(:); % 计算回归指标 RMSE sqrt(mean((Y_test - Y_pred).^2)); MAE mean(abs(Y_test - Y_pred)); R2 1 - sum((Y_test - Y_pred).^2) / sum((Y_test - mean(Y_test)).^2); fprintf(\n最终结果RMSE%.4f, MAE%.4f, R2%.4f\n, RMSE, MAE, R2); % 测试集预测对比图 figure; plot(1:length(Y_test), Y_test, o-, LineWidth, 1); hold on; plot(1:length(Y_pred), s--, LineWidth, 1); legend(真实值, SSA-GRNN预测值); xlabel(测试样本序号); ylabel(输出值); grid on;这段代码里的mapminmax(reverse, Yn_pred, psY)是把归一化后的预测结果变回原始量纲否则RMSE和MAE都是[0,1]尺度上的误差没法直接理解。Y_pred Y_pred(:)和Y_test Y_test(:)是为了防止一个是行向量、一个是列向量导致维度不一致。R2的计算用的是标准的回归决定系数越接近1说明模型越能解释数据波动。一种更保守的做法是只用训练集建网测试集评估。这样评估结果更真实但会浪费验证集的样信息。我的选择是在最终模型里合并训练和验证集因为SSA选spread用的是验证集MSE没有让spread直接接触到测试集所以测试集评估仍然相对可信。5. SSA-GRNN排查指南5个让人翻车的Matlab细节5.1 每次跑出来的最优spread都不一样是不是算法不稳定现象同样一份数据、同一套参数每次运行得到的bestX都不同有时候从0.3跳到2.1最终测试集误差也忽高忽低。原因麻雀算法和其他群智能算法一样初始化种群用的是随机数位置更新里也有rand和randn。种群没有覆盖到足够空间或者迭代次数不够时每次搜索的路径都会不一样。解决先在程序开头执行rng(default)或rng(42)固定随机种子保证一键复现。但这只是掩盖问题真实做法是让种群数量和迭代次数足够大。我常用pop30, M50起步如果收敛曲线末尾还在下降就把M加到100。如果spread仍然在两个极端之间跳说明适应度函数有多个局部最优数据可能有问题见5.2。5.2 预测曲线几乎是一条水平线spread被搜到了上限附近现象SSA给出的最优spread等于10也就是你设置的上边界。测试集的预测值全部落在训练集输出的平均值附近RMSE虽然不高但没有任何趋势。原因spread过大径向基函数变得太平网络把所有训练样本的权重做成了一个几乎均匀的加权平均。另一种可能是数据本身的特征与输出之间相关性很弱GRNN学不到有效规律于是最优选择就是输出一个接近均值的平滑值。解决先画散点图看每个自变量与输出之间有没有线性或单调关系。如果相关性太弱该换特征而不是继续调spread。如果数据确有规律把ub从10缩小到2或1让SSA在合理区间里搜索。网格扫描的结果能直接帮你确定这个区间别跳过。5.3 newgrnn报错“Inputs and targets have different number of columns”现象代码写好一运行newgrnn这一行直接抛错提示输入和输出的列数不一致。原因newgrnn期望输入矩阵是R×Q输出矩阵是S×Q其中Q是样本数R是特征数S是输出维度。但大多数人读取Excel后得到的是“每行一个样本、每列一个特征”直接把这样的矩阵传进去矩阵方向正好相反。解决给训练数据加转置。例如Xn_tr mapminmax(X_train, 0, 1)其中X_train是行样本、列特征。如果仍然报错在命令窗口输入size(Xn_tr)和size(Yn_tr)确认第一维是不是特征数。另外Y_train也要变成1×Q的行向量newgrnn输出层节点数等于行数这才能匹配单变量回归。5.4 训练集误差很低测试集却一塌糊涂现象SSA收敛曲线降到极低比如MSE只有1e-6但是最终测试集RMSE反而比网格扫描最差点还要高。原因这是一个典型的过拟合信号。你的适应度函数可能用了训练集MSE而不是验证集MSE。spread被搜到了很小的值比如0.001附近此时GRNN退化成近似最近邻查表训练样本被完全记住但测试样本只要稍有偏移预测就剧烈震荡。解决把适应度函数换成验证集MSE也就是第3.2节给的那种写法。同时把lb从0.001提高到0.05或者更大一些避免SSA进入极端尖峰区域。我通常从0.05开始观察收敛曲线最低点有没有压在边界上再决定是否继续下调。5.5 GUI一点“开始训练”就转圈卡死界面无响应现象在App Designer里放了按钮把SSA主程序直接写进按钮回调。点下去整个窗口变成“未响应”等很久之后才跳出结果。原因SSA-GRNN里的newgrnn和sim虽然是离线训练但种群30、迭代50就对应上千次建网和预测这一串操作放在GUI线程里同步执行会阻塞界面的消息循环。drawnow只能刷新当前帧没法解决整体阻塞。解决把SSA优化过程封装成独立函数在按钮回调里使用parfeval或timer后台执行执行完再更新界面。如果不想引入异步复杂度至少要在回调开头把按钮设为禁用并加一个waitbar循环里每算10个个体制更新一次进度条。实际项目里我发现waitbar结合drawnow能挽回不少体验后台异步才是最终方案。6. 把SSA-GRNN封装成GUIApp Designer关键回调与两个验证习惯模型训练完只是第一步交付时点按钮才能出结果的方式才勉强算个工具。Matlab里GUI我一般用App Designer原因是从2020年以后新版对GUIDE的更新已经很少App Designer的坐标轴、编辑框、标签组件布局更省事。你要做的核心是把第4章的SSA主程序包成一个函数输入种群数和迭代次数返回最优spread和收敛曲线function [bestSpread, curve] runSSAGRNN(pop, M) % 内部加载数据、归一化、调用SSA主循环 bestSpread ...; curve ...; end然后在App Designer里放一个按钮、一个坐标轴、三个编辑框和几个标签。按钮回调这样写function RunButtonPushed(app, event) app.StatusLabel.Text SSA正在搜索spread...; drawnow; [bestSpread, curve] runSSAGRNN(... app.PopEditField.Value, ... app.IterEditField.Value); plot(app.UIAxes, curve, b-); app.SpreadEditField.Value bestSpread; app.StatusLabel.Text 训练完成; end界面回显最优spread坐标轴画出收敛曲线这样用户至少能看到优化过程。训练完成后别忘了把网络和归一化参数存成mat文件不然下次关掉GUI又得重跑save(ssa_grnn_model.mat, net, psX, psY);以后预测新样本时流程是mapminmax(apply, X_new, psX)、sim(net, ...)、mapminmax(reverse, ...)三步缺一不可。关于验证我有两个一直保留的习惯。第一每次看完收敛曲线才认结果曲线末尾还在下降就加迭代次数曲线平了但spread落在边界说明搜索范围有问题。第二同一份数据用rng换三个种子各跑一遍取spread的中位数作为最终值能有效避开单次随机性。这两个习惯让我少翻了很多次车至少遇到奇怪的预测结果时能判断是算法没收敛还是数据本身不行。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网