☰
GRNN神经网络MATLAB回归预测:从newgrnn建网到spread调参的完整实践
2026/10/21 7:25:46 网站建设 项目流程

简介:一份基于MATLAB的广义回归神经网络(GRNN)实现代码,面向需要处理非线性回归、预测建模的学生、工程师以及神经网络初学者,目标是帮助读者理解广义回归神经网络的原理并快速上手实践。程序基于径向基函数思想,围绕GRNN的四个组成部分——输入层、传播层、平滑层和输出层进行设计,利用固定宽度的高斯核函数完成输入样本的加权映射,并通过平滑参数降低对单个样本点的敏感度,从而减少过拟合。代码完整演示了从准备训练数据、初始化网络结构、创建高斯核矩阵、执行前向传播到按误差更新权重的全流程,同时包含基础注释,便于读者将公式与代码相互对照。压缩包内仅含1个.m脚本文件,整体大小约2KB,轻量简洁,适合逐行阅读、修改和调试,也可作为构建自定义回归模型的模板。目前已有1319人学习,能够帮助使用者掌握GRNN的建模逻辑,并可直接迁移到时间序列分析、系统辨识等实际应用场景中。

1. GRNN 神经网络:一个小众但好用的回归神器,这份 MATLAB 资源能干什么

做回归预测时,大家的第一反应往往都是 BP 或 LSTM,但碰到样本量小、特征和输出关系说不清楚的数据,GRNN(广义回归神经网络)常常是第一个能稳定出结果的模型。它的本质是做一次带高斯核的加权平均,不需要迭代训练,在 MATLAB 里 newgrnn 建网、sim 预测,五步以内跑完一次实验。这份 GRNN 神经网络.zip 给的是完整的 MATLAB 回归流程:数据加载、划分、归一化、建网、预测、反归一化和误差绘图全都串好,适合刚上手神经网络的本科生拿来跑第一个预测实验,也适合想快速拿一个强基线模型做对比的研究党。解压、改数据路径、运行 main,就能看到曲线和指标。

2. 网络原理与 MATLAB 入口:newgrnn 背后的四层结构

2.1 为什么不需要迭代训练:核回归与高斯核

在动手跑 main 之前,得先搞清楚一个问题:为什么 GRNN 不像 BP 那样需要 train。GRNN 的数学基础是统计里的核回归,说人话就是加权平均:对任意一个预测点 x,它把每个训练样本 xi 和它对应的 yi 拿出来,用高斯核计算出 xi 对 x 的“影响力”,再拿这些影响力对 yi 做加权平均。计算公式写成行内文本就是:

Y(x) = Σ(y_i × exp(-||x - x_i||² / (2σ²))) / Σ(exp(-||x - x_i||² / (2σ²)))

分子是每个训练输出 yi 按高斯核权重累加,分母是所有高斯核权重的总和,用来做归一化。σ 就是代码里的 spread,它控制曲线的松紧程度:σ 越小,只有离预测点很近的训练样本才起作用,预测曲线会贴着样本点剧烈起伏,过拟合风险高;σ 越大,远处样本也被平均进去,曲线被磨得很平,极端情况下退化成一条接近全体输出均值的直线。

所以 GRNN 的“训练”本质就是把训练样本原样记住。网络分四层:输入层、模式层、求和层、输出层。模式层每个神经元对应一个训练样本,神经元中心就是 xi,输出层的权重就是 yi。相比 RBF 网络要先做聚类找中心、再用最小二乘训练输出权重,GRNN 连中心都省了,直接拿样本当中心。代价是样本越多网络越胖,预测越慢,这个后面避坑章会专门讲。

2.2 一行建网:P、T、spread 三个参数怎么看

MATLAB 里建一个 GRNN 只需要一行代码:

% P: 特征×样本,R 行 Q 列 % T: 输出×样本,S 行 Q 列 % spread: 高斯核扩展常数,影响拟合平滑度 net = newgrnn(P, T, spread);

P 是输入矩阵,行数是特征数量,列数是训练样本数量;T 是目标矩阵,行数是输出数量,列数和 P 必须一致。如果原始数据是常见的“样本×特征”格式,导入后几乎必须转置一次,否则 newgrnn 会把每个样本当成一个特征,网络结构完全错掉。这个坑很多人踩过,我一般在代码里统一写成:

P = data(:, 1:end-1)'; % 特征放在前几列 T = data(:, end)'; % 目标放在最后一列

newgrnn 返回的 net 是一个标准的 network 对象,但它和 fitnet 的差别很大:没有 net.trainFcn,不存在 epochs、learning rate 这些东西。打开这个 net 对象,能看到的几乎只有各层之间的权重矩阵,模式层的权重就是训练样本本身。换句话说,newgrnn 执行完之后,模型已经“训练”完毕,不需要再调用 train。想验证这个说法,可以在命令行看两眼:

size(net.IW{1}) % 应该是 Q×R,等于 P 的转置 size(net.LW{2,1}) % 和 T 的形状对应

2.3 sim 的尺寸陷阱:预测结果为什么总要多转一次置

预测同样只有一行:

Y_pred_row = sim(net, P_test); % 结果是 S×M,M 是测试样本数 Y_pred = Y_pred_row'; % 转回 M×S,和原始数据方向一致

sim 不是黑匣子,它就是重复执行 2.1 那个加权平均公式,把所有测试样本批量向量化算一遍。问题往往出在返回值的形状上:只要输出 S=1,sim 返回的就是一行,而 Y_test 是列向量,两者直接相减要么触发隐式扩展,要么画图时横轴对不上。所以每次预测后都补一行转置,是我写这份代码时的固定习惯。

另外一个容易忽略的点是:GRNN 天生支持多输出。newgrnn 的 T 可以是 S×Q 而不是 1×Q,输出层会为每个目标变量各自维护一套求和节点。很多回归案例只演示单输出,不代表它只能做单输出,这个特性到第六章二次开发时会直接用上。

3. 项目拆包与复现:目录职责与完整跑通步骤

3.1 解压之后有哪些文件:脚本职责与调用链

这份 GRNN 神经网络.zip 解压后,比较规范的脚本组织方式是 main 做总调度,数据读取、建网、预测分别拆成独立脚本。不同来源的包文件命名可能有差异,认职责比认文件名更重要。常规情况下你会看到这样一组文件:

文件(常见命名)职责依赖
main.m主入口,串联全部步骤其余全部脚本
load_data.m读取 Excel 数据并划分训练/测试集data.xlsx 等数据文件
grnn_train.m归一化 + newgrnn 建网load_data 的输出
grnn_predict.m预测、反归一化、画图、算指标grnn_train 生成的 net

调用链很清晰:main 先调 load_data 得到训练集和测试集,再调 grnn_train 得到 net,最后调 grnn_predict 拿到预测曲线和误差指标。把层次拆这么开不是多此一举,而是为了让二次开发更省事——想换数据只改 load_data,想对比 BP 网络只改 grnn_train 里的模型创建那几行,主流程一行都不用动。

如果你拿到的包里文件名对不上,也没关系,按函数内容认职责:出现 xlsread 或 readmatrix 的一定是数据加载,出现 newgrnn 的一定是模型创建,出现 figure 和 plot 的一定在预测后处理。只要抓住这三个锚点,任何变体包都能在三分钟内顺完。

3.2 三步跑通 main:从路径配置到看指标

先说路径。解压后我建议把整个文件夹放到纯英文路径下,比如 D:\Projects\grnn,避免老版本 MATLAB 对中文路径的编码问题。这个问题在中文 Windows 上很常见,跑不起来先检查这个,省得浪费半小时。

cd('D:\Projects\grnn'); main;

第一步,把 load_data.m 里的数据文件路径改成你机器上的实际位置。注意 Excel 文件的组织方式:默认情况下每一行是一个样本,每一列是一个特征,最后一列是预测目标。如果你的数据格式不是这样,优先改 load_data,而不是改后面的模型代码。第二步,命令行执行 main,脚本会按序弹出两张图:一张是训练集拟合对比,一张是测试集预测对比,命令窗口同时打印 MAE 和 MAPE。第三步,看指标判断是否正常:对常见的回归数据,MAPE 能落在 10%~20% 以内就算基线合格;如果远超这个范围,先别急着怀疑算法,回数据里查异常值和量纲差异。

跑通之后有一个值得做的小实验:把 spread 改到 0.01 重跑一次,再改到 5 重跑一次。你会看到训练集拟合曲线一个比一个贴合,而测试集误差先降后升。这个实验做一次,比看十篇教程都更理解 GRNN 的脾气。

3.3 跑不起来怎么定位:按报错行号往回推

如果 main 直接报错,不用慌张,基本都能按行号定位。

  • 如果是 load_data 那一行报错,大概率是路径写错、Excel 格式不符,或者文件里有空单元格。xlsread 遇到空单元格会返回 NaN,后面 newgrnn 会把 NaN 传进高斯核,输出一堆 NaN。这种情况先用 rmmissing 清理,或者读文件时用 readmatrix 并指定范围跳过空行。
  • 如果是 newgrnn 那一行报错,基本是 P 和 T 的列数不一致,回到 2.2 检查转置。
  • 如果是绘图那一段报错,通常是 Y_pred 和 Y_test 长度对不上,检查 split 计算和索引。
  • 如果完全不报错但预测曲线乱成一片,重点查 spread 和数据归一化,和维度没有关系。

这种“按报错行号反推”的排查方式不是我偷懒,而是 MATLAB 脚本大多数错误就出在数据格式上,算法本身的报错反而很少。把第 5 章的避坑清单过一遍,基本能覆盖这份代码 90% 的意外情况。

4. 核心代码逐段拆解:样本从加载到预测的完整旅程

4.1 数据读取与划分:先固定随机种子再打乱

第三章把流程顺了一遍,这里开始逐段看代码。先从 load_data 说起。

data = xlsread('data.xlsx'); % 读出全部数据,行为样本、列为特征 rng(42); % 固定随机种子,结果可复现 n = size(data, 1); % 样本总数 idx = randperm(n); % 打乱样本顺序 split = round(n * 0.8); % 80% 训练、20% 测试 train_data = data(idx(1:split), :); test_data = data(idx(split+1:end), :);

xlsread 读出来的是普通 double 矩阵,行为样本、列为特征,这是 MATLAB 里最顺手的组织方式。rng(42) 这行很多人会删掉,我强烈建议留着。GRNN 本身训练不需要随机初始化,但数据划分的随机性会让同样的代码两次跑出不同的指标,固定随机种子是唯一能让你在实验记录里写“可复现”的办法。split 用 round 取整,样本数比较多时差一两个影响不大,少样本数据集上四舍五入也比向下取整合理。

这段代码有两个可以定制的位置:一是如果原始数据本身就是乱序的,randperm 可以注释掉;二是如果数据是“前 300 条训练、后 100 条测试”这种按时间划分的时序数据,就不要用随机划分,直接按行号切片,否则时间信息泄漏会让测试结果虚高,模型在真实场景里根本达不到那个准确度。

4.2 归一化处理:测试集为什么只能用训练集的参数

特征归一化是 GRNN 的高优先级操作。高斯核里计算的是欧氏距离,如果某一列特征量纲特别大,距离会被这一列主导,spread 再怎么调也救不回来。

[X_train_norm, ps] = mapminmax(train_X', 0, 1); % 用训练集计算缩放参数 X_test_norm = mapminmax('apply', test_X', ps); % 测试集沿用同一套参数

mapminmax 的默认设计是按行处理,所以这里要把“样本×特征”转置成“特征×样本”。ps 是输出结构体,里面存了每一行的 xmin 和 xmax。关键在第二行:测试集归一化必须用 'apply' 加 ps,而不是再调用一次 mapminmax(test_X', 0, 1)。如果各自归一化,训练集和测试集就是两把不同的尺子,预测结果会整体偏移。这种“归一化泄漏”是回归实验里最常见的隐性错误,而且它不报错,只能靠代码审查发现。

至于目标值 Y 要不要归一化,我一般不归一化。GRNN 是回归网络,输出本质是加权平均,直接学习原始量纲能让你一眼看出误差多大;对 Y 归一化反而引入一次反归一化的误差传递。当然,如果目标值动态范围特别大,比如跨了三个数量级,也可以对 Y 归一化,但此时训练和测试必须共用同一套输出缩放参数,不能像特征那样各行其是。

顺带说一句,这也是 GRNN 和树模型的一个本质区别。随机森林这类模型对特征的单调变换不敏感,特征缩放不影响分裂点;而 GRNN 是距离驱动的模型,特征尺度直接决定距离计算,所以归一化在这里是必要前处理,不是可选项。

4.3 建网与预测:spread 从哪个数开试

模型创建和预测的代码很短,真正的调参压力全在 spread 上:

spread = 0.5; % 归一化到 [0,1] 时从 0.1~1 起步 net = newgrnn(X_train_norm, Y_train, spread); Y_pred_row = sim(net, X_test_norm); Y_pred = Y_pred_row';

spread 的初值选择有套路:特征做 mapminmax 归一化到 [0,1] 后,特征空间里大部分点对之间的距离在 0~1 这个量级,spread 从 0.1 到 1 之间起步比较合理;如果数据没有归一化,距离动辄几十上百,spread 就必须放大到 1~10 甚至更大,否则高斯核在远处样本处衰减到零。换句话说,spread 的取值是相对于特征空间尺度而言的,脱离数据谈 spread 没有意义。

我不建议一上来就精调参数,而是先跑一组快速实验观察趋势:

for s = [0.05 0.1 0.2 0.5 1 2] net_tmp = newgrnn(X_train_norm, Y_train, s); yp = sim(net_tmp, X_test_norm)'; mae = mean(abs(Y_test - yp)); fprintf('spread=%.2f, MAE=%.4f\n', s, mae); end

这组循环跑完,你会看到 MAE 先降后升,谷底附近就是当前数据集下比较稳的取值。如果谷底落在试探区间边界上,说明区间选得不对,往相应方向外扩再跑一轮。注意这个过程必须在测试集上完成并固定下来,不要在选定 spread 之后再用测试集反复“找补”,那就成了拿测试集当验证集,指标会失真。更严格的做法是把训练集再切一小块当验证集,或者直接上第 6 章的 k 折交叉验证。

4.4 误差计算与绘图:只看训练集拟合是自欺欺人

预测完成之后是误差评估,常规写法是三件套:

MAE = mean(abs(Y_test - Y_pred)); RMSE = sqrt(mean((Y_test - Y_pred).^2)); MAPE = mean(abs((Y_test - Y_pred) ./ (Y_test + eps))) * 100;

MAE 和 RMSE 的单位和原始数据一致,报告时带上单位会更有说服力。MAPE 是百分比,直观但有个致命陷阱:只要 Y_test 里出现一个 0 或接近 0 的值,除法结果就是无穷大。我见过不少脚本因为这个直接算出 NaN 还不知道怎么回事,所以在分母上加 eps 是常规保护操作。如果数据集里存在 0 值,建议只用 MAE 和 RMSE,别强行报 MAPE。

绘图部分通常是测试集对比加误差曲线:

figure; plot(Y_test, 'b-o', 'LineWidth', 1); hold on; plot(Y_pred, 'r-*', 'LineWidth', 1); grid on; legend('真实值', '预测值'); xlabel('测试样本序号'); ylabel('预测目标值');

这里要特别注意:改 spread 时一定是“训练集和测试集同时看”。spread 调到 0.01 时训练集拟合近乎完美,但测试集曲线会抖成锯齿。如果把训练集误差当作模型好坏的标准,你会在过拟合的路上越走越远。正确的习惯是每次调参都把 train MAE 和 test MAE 同时打印出来,两者差距越小越健康。

5. 避坑手册:GRNN 实验里最常翻车的五个场景

5.1 归一化“泄漏”:测试误差低到离谱时先查它

现象:测试误差低得不真实,有时 R² 超过 0.99,但换个数据文件表现立刻崩掉。

原因:代码在划分数据集之前对全量数据做了一次 mapminmax,缩放参数里混入了测试集的信息,相当于模型在训练时已经“见过”测试集的范围,得到的是一个被污染的评估结果。

解决:先划分、后归一化是铁的纪律。训练集归一化得到 ps,测试集用 'apply' 沿用 ps。每次写完这段代码,都手动确认 ps 是从哪个矩阵算出来的。

5.2 spread=1:工具箱默认值不一定适合你

现象:预测曲线几乎趴在均值线上,真实值的波动一点都没捕捉到,误差比直接取均值还差。

原因:newgrnn 的 spread 缺省给 1,但经过 [0,1] 归一化之后,1 已经属于比较大的带宽,高斯核在很多点上衰减不明显,输出被迫向全局均值回归,网络退化成一个“平均模型”。

解决:把 spread 当超参数而不是默认参数。从 0.1 开始成倍往上试,0.1、0.2、0.5、1、2,选测试误差最低的那个。这是 GRNN 唯一的自由度,值得花十分钟专门摸一遍。

5.3 “输入尺寸不一致”的报错先查转置

现象:newgrnn 或 sim 报错,提示和维度有关,比如“Matrix dimensions must agree”,有时报错信息很隐晦。

原因:把“样本×特征”的矩阵直接当成 P 传给了 newgrnn,而接口要求的是“特征×样本”。sim 的输入也有同样要求。

解决:照着 2.2 的写法统一转置,预测完再转回来。维度口诀只有一句:训练样本数永远在矩阵的列方向。

提示:如果你把 P 和 T 都转置正确依旧报错,再看一眼数据文件里有没有空行或 NaN。NaN 会一路传播到 sim 的高斯核计算里,报错信息有时非常迷惑。

5.4 小样本指标虚高:几十条数据就别单次划分了

现象:总共 80 个样本,随机划分后测试集只有 16 个点,MAE 很小,曲线看着也贴得漂亮,但换一次随机种子结果天差地别。

原因:样本量少时,单次划分的测试集过于随机,几个点碰巧落在好预测的区域就能把指标拉得很高,这反映的是运气而不是模型能力。

解决:样本量少于 100 时,用留一法或 5 折交叉验证代替单次划分。GRNN 不需要训练,交叉验证的成本就是多建几次网,时间完全可接受。报告里写“留一法 MAE”也比写单次划分的 MAE 更有说服力。

5.5 训练样本上到几千之后预测越来越慢

现象:newgrnn 建网很快,但 sim 预测几百个点时界面卡住,等好几秒甚至十几秒才出结果。

原因:GRNN 模式层神经元数量等于训练样本数,预测每个点都要对全部训练样本计算一次高斯核,复杂度是 O(M×N),M 是预测点数,N 是训练样本数。这个慢是结构性的,换台机器也解决不了。

解决:常见做法是把训练集样本量压到 2000 以内再用 GRNN;或者先做一次均匀抽样,让模式层神经元数量降下来。如果数据量实在大,换 RBF 网络用可控数量的中心,或者直接上 fitnet 的 BP 网络,性价比更高。

6. 二次开发:从单输出改成多输出预测与自动调参

6.1 改成多输出:newgrnn 天生支持多个目标

这份资源默认演示的是单输出回归,但前面提过 newgrnn 的 T 支持多行。改成多输出只需要动一行代码:把原来选最后一列当 T 的地方,改成选最后两列。

T = data(:, [end-1 end])'; % 同时预测两个目标,S=2 net = newgrnn(X_train, T, spread);

预测返回的 Y_pred 会变成 M×2,画图时逐列对比即可。这次改造对网络结构几乎没有额外负担,因为每个输出目标对应独立的求和节点,互不干扰,比分别训练两个单输出网络省事得多。

6.2 自动选 spread:一个 k 折循环搞定调参

手动试 spread 可行但不够严谨,遇到验证集划分敏感的数据会误导。我改成 5 折交叉验证之后,选参就成了一个很机械的循环:

cv = cvpartition(size(X_train,2), 'KFold', 5); for s = 0.1:0.2:2 err = zeros(5,1); for k = 1:5 tr = cv.training(k); netk = newgrnn(X_train(:,tr), Y_train(:,tr), s); yk = sim(netk, X_train(:,~tr))'; err(k) = mean(abs(Y_train(:,~tr)' - yk)); end fprintf('spread=%.1f cvMAE=%.4f\n', s, mean(err)); end

cvpartition 生成分层划分索引,每一折的训练/验证比例固定,循环里用 ~tr 取验证集那一半。把各折误差平均后,取 cvMAE 最小的 spread,再用全量训练集重建最终模型。这套做法跑一次也就十几秒,比手试更稳,写完也能直接写进实验报告的参数选择部分。从那以后,我每次拿到回归数据,都会先跑一遍这个 k 折基线,再决定要不要上更复杂的模型。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询