基于WOA-GRU的多输入回归预测Matlab实现与超参数优化
2026/9/11 16:31:41 网站建设 项目流程

简介:这是一套基于鲸鱼算法优化门控循环单元(WOA-GRU)的多输入单输出回归预测Matlab实现,适合电力负荷、气象、经济等领域的数值预测任务,也适用于需要对比优化算法效果的初学者和科研人员。压缩包共6个文件,约108KB,包含4个.m源码脚本与2个.xlsx数据文件;源码涵盖WOAGRU主程序、初始化、目标函数和性能指标计算,数据文件分别提供输入与输出样本,可在Matlab 2020及以上版本直接运行。已有203人学习下载,配套完整工程目录,便于读者快速复现鲸鱼算法搜索最优超参数并完成GRU训练预测的流程。通过该资源,使用者既能掌握WOA与GRU结合的代码实现细节,也能直接替换自己的数据集开展回归实验,减少建模调参时间。

1. 直接把多输入回归预测拍在桌面上

做回归预测时最痛的不是模型不先进,而是“超参数太多,调起来没完没了”。GRU 比 LSTM 少一个门,参数量和训练时间下来了,但学习率、隐层节点数、L2 正则化系数、MiniBatchSize 仍然要一个个试,碰上时序特征多、样本量又大的场景,手动调参成本高得离谱。WOA-GRU 的组合,就是拿鲸鱼优化算法去自动搜 GRU 的超参数:每只鲸鱼代表一组候选超参数,用验证集的回归误差当适应度,迭代更新鲸鱼位置,最终把“最优超参数 + 训练好的 GRU 模型”一起交给你。整个过程在 Matlab 里用脚本实现,输入是多个特征列、输出是单列连续值,正好覆盖了电力负荷、气象、故障特征值、经济指标这类多输入单输出的回归场景。适合想省掉调参时间、又不想换 Python 环境的研究生和算法工程师。

2. GRU 门控机制与鲸鱼算法适配逻辑:动手前先立住理论

2.1 GRU 为什么比 LSTM 更适合回归预测

GRU 把 LSTM 的遗忘门和输入门合并成更新门,用重置门去控制历史信息的忽略程度。更新门公式为z_t = sigmoid(W_z · [h_{t-1}, x_t]),重置门公式为r_t = sigmoid(W_r · [h_{t-1}, x_t]),候选隐藏状态为h_t~ = tanh(W · [r_t * h_{t-1}, x_t])。两个门的结构决定了 GRU 参数更少、训练更快,在小样本回归任务里不容易因为参数过多而过拟合。

在多输入单输出场景里,输入 X 通常是[样本数, 特征数]的表格数据,但 GRU 内部是按时间步展开计算的。常见做法是把每条样本当成一个长度为 1 的序列,特征维度放到输入层维度上,用sequenceInputLayer(featureNum)接收数据,再接一个gruLayer(numHiddenUnits)输出最后一步的隐藏状态,经过全连接层映射到 1 个输出节点,最后用regressionLayer计算均方误差损失。

在 Matlab 里,数据维度必须组织成[特征数, 序列长度, 样本数]。这句话说着容易,实际写代码时最容易出错:trainNetwork不接受普通矩阵,X 要 reshape 成三维数组,Y 要 reshape 成[1, 1, 样本数]。我曾见过有人把 X 直接塞进sequenceInputLayer,报错维度不匹配,查了半天才发现是矩阵排列方向的问题。

2.2 WOA 的三种搜索机制如何映射到超参数寻优

WOA 的三个核心机制——包围猎物、气泡网攻击、随机搜索——对应到超参数寻优里就是局部开发与全局探索的平衡。

  • 包围猎物:X(t+1) = X*(t) - A · |C · X*(t) - X(t)|,其中 A 随收敛因子线性下降,C 是随机系数,负责让鲸鱼围绕当前最优解附近搜索。对应到超参数寻优,就是在当前最优超参数组合邻域内小步试探。
  • 气泡网攻击:使用螺旋位置更新X(t+1) = X*(t) + D' · e^(b·l) · cos(2πl),同时以 50% 概率与包围机制切换。这个阶段负责精细搜索,适合在候选超参数组合周围做高精度扫描。
  • 随机搜索:当|A| ≥ 1时,鲸鱼随机选择一条鲸鱼的位置进行更新,强制跳出局部最优,避免训练得到的超参数组合卡在某个不理想的区域。

WOA 相比网格搜索的优势是:网格搜索要尝试的组数是各维度参数候选数量的乘积,而 WOA 每一轮迭代只需要评估种群数量个超参数组合,迭代次数可以控制在 10 到 30 次左右,实际训练时间从几小时压缩到几十分钟。和遗传算法相比,WOA 没有交叉和变异算子,参数少、实现简单,在连续型超参数搜索问题上收敛速度通常更快。

3. 在 Matlab 里搭建 WOA-GRU 基线环境:工具箱、数据划分和归一化

3.1 工具箱与最低版本要求

WOA-GRU 需要两个核心工具箱:Deep Learning Toolbox(训练 GRU)和 Global Optimization Toolbox(可选,如果想用内置优化算法做对比实验)。Matlab R2019b 之后的版本都能跑通,但要注意trainNetwork对 GPU 的支持需要 Parallel Computing Toolbox 和 CUDA 计算能力 3.0 以上的显卡。没有 GPU 环境就用 CPU 训练,把trainingOptions里的'ExecutionEnvironment'设置为'cpu'即可,只是速度会明显变慢。

建议在脚本开头加一段环境检查代码:

% 检查工具箱是否可用 if ~license('test', 'Deep_Learning_Toolbox') error('未安装 Deep Learning Toolbox,请先安装该工具箱'); end % 检查 GPU 是否可用,不可用则回退 CPU try canUseGPU = canUseGPU(); catch canUseGPU = false; end if canUseGPU execEnv = 'auto'; % 优先使用 GPU else execEnv = 'cpu'; % 回退到 CPU end fprintf('执行环境: %s\n', execEnv);

这个检查逻辑很简单但很实用:用license('test', ...)判断工具箱是否存在,用canUseGPU()判断硬件是否支持。execEnv变量后面会传给trainingOptions'ExecutionEnvironment'参数,避免不同机器上跑同一份代码时因为环境差异报错。

3.2 数据划分:训练集 / 验证集 / 测试集三份,缺一不可

WOA 在寻优过程中用验证集误差作为适应度,测试集必须从头到尾隔离,只做最后一轮评估用。如果拿测试集去指导超参数搜索,相当于把测试集数据参与了训练,最后报告的性能会有虚高。

data = readmatrix('data.csv'); % 假设最后一列是目标值 X_raw = data(:, 1:end-1); % 多输入特征 Y_raw = data(:, end); % 单输出目标 rng(42); % 固定随机种子,保证结果可复现 cv = cvpartition(size(X_raw, 1), 'HoldOut', 0.2); testIdx = test(cv); trainValIdx = training(cv); % 在训练集+验证集上再切分一次 cv2 = cvpartition(sum(trainValIdx), 'HoldOut', 0.15); valIdxInTrainVal = test(cv2); X_train = X_raw(trainValIdx & ~valIdxInTrainVal, :); Y_train = Y_raw(trainValIdx & ~valIdxInTrainVal, :); X_val = X_raw(find(trainValIdx & valIdxInTrainVal), :); Y_val = Y_raw(find(trainValIdx & valIdxInTrainVal), :); X_test = X_raw(testIdx, :); Y_test = Y_raw(testIdx, :);

这里用cvpartition做两次划分:第一次切出 20% 测试集,第二次在剩余 80% 数据里切出 15% 验证集。

这样做的好处是每条样本只属于一份数据,不会出现验证集和训练集重叠的尴尬情况。rng(42)必须放在cvpartition前面,否则每次运行划分结果不同,调参结果不可复现。

3.3 归一化:用训练集统计量归一化,测试集只做变换

GRU 使用 sigmoid 和 tanh 激活函数,输入数据不归一化会导致梯度消失或振荡。建议使用 z-score 归一化:x_norm = (x - μ) / σ。关键在于 μ 和 σ 必须从训练集计算,验证集和测试集直接套用训练集的统计量,不能在完整数据集上计算归一化参数,那样会造成数据泄漏。

% 计算训练集统计量 mu_X = mean(X_train, 1); sigma_X = std(X_train, 0, 1); mu_Y = mean(Y_train, 1); sigma_Y = std(Y_train, 1); % 转换三份数据 X_train_norm = (X_train - mu_X) ./ sigma_X; Y_train_norm = (Y_train - mu_Y) ./ sigma_Y; X_val_norm = (X_val - mu_X) ./ sigma_X; Y_val_norm = (Y_val - mu_Y) ./ sigma_Y; X_test_norm = (X_test - mu_X) ./ sigma_X; Y_test_norm = (Y_test - mu_Y) ./ sigma_Y;

std(X_train, 0, 1)std(X_train, 1)的差别在于分母用的是 n-1 还是 n。对归一化来说差异不大,但保持一致即可。反归一化放在最后评估阶段做:Y_pred = Y_pred_norm * sigma_Y + mu_Y,否则 RMSE 的数值是在归一化空间里的,无法原尺度解释。

在把数据送入trainNetwork之前,还要将二维表格 reshape 成 GRU 要求的三维数组:

numFeatures = size(X_train_norm, 2); numTrain = size(X_train_norm, 1); X_train_seq = reshape(X_train_norm', [numFeatures, 1, numTrain]); Y_train_seq = reshape(Y_train_norm', [1, 1, numTrain]);

这里把每条样本当成序列长度为 1 的输入,特征维度在前。reshape操作要注意 MATLAB 列优先填充:转置后再 reshape,才能保证每个样本的特征正确排列到第一个维度上。

4. 跑通 WOA-GRU 核心代码:适应度函数设计、鲸鱼位置更新与 GRU 训练回滚

4.1 整体脚本结构和变量约定

WOA-GRU 的完整流程可以拆成四个模块:超参数编解码、适应度函数、WOA 主循环、最终模型训练与评估。

%% WOA-GRU 主脚本 clc; clear; close all; % 数据加载和预处理(上一章代码) % ... % 定义搜索空间:每只鲸鱼位置是 4 维向量 lb = [0.0001, 10, 1e-6, 16]; % [学习率, 隐层节点, L2, MiniBatchSize] ub = [0.01, 150, 1e-2, 64]; dim = length(lb); SearchAgents_no = 8; % 种群数量 Max_iter = 15; % 迭代轮数 % 调用 WOA 主函数 [best_pos, best_fitness, convergence_curve] = WOA_GRU(...);

参数说明:SearchAgents_no建议设为 8 到 10,超出后单轮训练时间线性增加但搜索性能提升不明显。Max_iter设为 15 到 20,因为每一轮要进行SearchAgents_no次 GRU 训练,迭代太多意味着总训练次数会到几百次,CPU 环境下不现实。lbub限定了四个超参数的搜索区间,区间范围不要设太宽,尤其是学习率,超过 0.01 时 GRU 容易发散。

4.2 适应度函数:把超参数映射为验证集误差

适应度函数是 WOA 和 GRU 之间的桥梁。它接收一条鲸鱼的位置向量,把四个分量解码成训练参数,然后训练 GRU 并返回验证集 RMSE。

function fitness = WOA_GRU_Fitness(solution, X_train_seq, Y_train_seq, X_val_seq, Y_val_seq, numFeatures, execEnv) % 解码超参数 learnRate = solution(1); numHiddenUnits = round(solution(2)); l2Regularization = solution(3); miniBatchSize = round(solution(4)); % 构建网络层 layers = [ sequenceInputLayer(numFeatures) gruLayer(numHiddenUnits, 'OutputMode', 'last') fullyConnectedLayer(1) regressionLayer]; % 训练选项 options = trainingOptions('adam', ... 'InitialLearnRate', learnRate, ... 'L2Regularization', l2Regularization, ... 'MiniBatchSize', miniBatchSize, ... 'MaxEpochs', 50, ... 'Verbose', false, ... 'ExecutionEnvironment', execEnv, ... 'Shuffle', 'every-epoch'); % 训练网络 net = trainNetwork(X_train_seq, Y_train_seq, layers, options); % 验证集预测 Y_val_pred = predict(net, X_val_seq, 'ExecutionEnvironment', execEnv); mse = mean((Y_val_pred - Y_val_seq).^2, 'all'); fitness = sqrt(mse); end

这段代码有几个细节值得解释。gruLayer'OutputMode'必须设置为'last',因为我们做的是单步回归预测,只需要最后一个时间步的隐藏状态,不需要输出完整序列。round(solution(2))round(solution(4))是因为隐层节点数和 MiniBatchSize 必须是整数,而 WOA 的位置更新会产生连续值。Shuffle设为'every-epoch'可以防止 GRU 在时序数据上记住样本顺序,但要注意这里没有使用'Plots',因为训练过程中频繁画图会严重拖慢 WOA 迭代速度。

4.3 WOA 主循环:三种位置更新策略

WOA 主循环按标准算法实现,每一轮迭代对每条鲸鱼独立计算新的位置。

function [best_pos, best_fitness, convergence_curve] = WOA_GRU(... lb, ub, dim, SearchAgents_no, Max_iter, ... X_train_seq, Y_train_seq, X_val_seq, Y_val_seq, numFeatures, execEnv) % 初始化种群位置 Positions = rand(SearchAgents_no, dim) .* (ub - lb) + lb; fitness = inf(SearchAgents_no, 1); for i = 1:SearchAgents_no fitness(i) = WOA_GRU_Fitness(Positions(i, :), ... X_train_seq, Y_train_seq, X_val_seq, Y_val_seq, numFeatures, execEnv); end [best_fitness, best_idx] = min(fitness); best_pos = Positions(best_idx, :); convergence_curve = zeros(Max_iter, 1); for t = 1:Max_iter a = 2 - t * (2 / Max_iter); % 收敛因子从 2 线性降到 0 for i = 1:SearchAgents_no r1 = rand(); r2 = rand(); A = 2 * a * r1 - a; C = 2 * r2; p = rand(); l = (t / Max_iter) - 1 + rand(); if p < 0.5 if abs(A) < 1 % 包围猎物:朝当前最优解移动 D = abs(C .* best_pos - Positions(i, :)); new_pos = best_pos - A .* D; else % 随机搜索:随机选一条鲸鱼作为参照 rand_idx = randi(SearchAgents_no); D = abs(C .* Positions(rand_idx, :) - Positions(i, :)); new_pos = Positions(rand_idx, :) - A .* D; end else % 气泡网攻击:螺旋更新 D_star = abs(best_pos - Positions(i, :)); new_pos = D_star .* exp(0.8 .* l) .* cos(2 * pi * l) + best_pos; end % 边界处理和适应度评估 new_pos = min(max(new_pos, lb), ub); new_fitness = WOA_GRU_Fitness(new_pos, ... X_train_seq, Y_train_seq, X_val_seq, Y_val_seq, numFeatures, execEnv); if new_fitness < fitness(i) Positions(i, :) = new_pos; fitness(i) = new_fitness; end if fitness(i) < best_fitness best_fitness = fitness(i); best_pos = Positions(i, :); end end convergence_curve(t) = best_fitness; fprintf('迭代 %d/%d, 最优适应度: %.4f\n', t, Max_iter, best_fitness); end end

边界处理使用min(max(new_pos, lb), ub)强制把位置拉回搜索空间内,避免学习率变成负数或隐层节点数跑出上限。每轮迭代只评估一次新位置的适应度,如果新位置的适应度没有变好就保留旧位置,这是标准的“贪心选择”策略。

这段代码在实际跑的时候要注意一个性能瓶颈:每轮迭代每个个体都调用WOA_GRU_Fitness,而训练一个 GRU 在 CPU 上可能耗时 10 到 30 秒,所以 8 个种群 × 15 轮最多可能产生几百次训练,整体耗时在 1 到 3 小时。想要更快可以用上一轮的最优解初始化种群中的部分个体,例如把第一个个体的位置设为上一轮best_pos

4.4 训练完成后用最优超参数重建模型并评估测试集

WOA 搜索完成后,best_pos就是找到的最优超参数组合。用这些超参数在 训练集+验证集 的合并数据上重新训练最终模型,然后在测试集评估性能。

X_trainval_norm = [X_train_norm; X_val_norm]; Y_trainval_norm = [Y_train_norm; Y_val_norm]; X_trainval_seq = reshape(X_trainval_norm', [numFeatures, 1, size(X_trainval_norm,1)]); Y_trainval_seq = reshape(Y_trainval_norm', [1, 1, size(Y_trainval_norm,1)]); finalLayers = [ sequenceInputLayer(numFeatures) gruLayer(round(best_pos(2)), 'OutputMode', 'last') fullyConnectedLayer(1) regressionLayer]; finalOptions = trainingOptions('adam', ... 'InitialLearnRate', best_pos(1), ... 'L2Regularization', best_pos(3), ... 'MiniBatchSize', round(best_pos(4)), ... 'MaxEpochs', 100, ... 'ExecutionEnvironment', execEnv, ... 'Verbose', false); finalNet = trainNetwork(X_trainval_seq, Y_trainval_seq, finalLayers, finalOptions);

把训练集和验证集合并再训练是回归预测中常见的提升技巧:WOA 只是用验证集选参数,最终模型应该在更大的训练样本上吃饱数据。测试集从头到尾没有参与任何训练步骤,这时的评估结果才可信。

5. 核心参数调优清单:训练效率提升与三个必调项

参数作用建议范围调优方向
learnRate学习率,控制梯度更新步长0.0001 ~ 0.01如果验证误差发散发散就下调,收敛太慢就上调
numHiddenUnitsGRU 隐层节点数10 ~ 150小数据集取 20~50,大数据集取 80~120
L2Regularization正则化系数,抑制过拟合1e-6 ~ 1e-2验证误差与训练误差差距大时增大
MiniBatchSize批量大小16 ~ 64影响训练稳定性和显存占用
MaxEpochs训练轮数30 ~ 100用于适应度评估时少设,最终模型多设
Wait提前停止的容忍轮数5 ~ 10在验证损失上升时自动终止训练

第三个必调项是trainingOptions里的'Plots',在 WOA 迭代过程中宁可设为'none'也不要开'training-progress',因为 Matlab 的画图刷新会显著拖慢训练循环。想要可视化收敛过程的话,用 WOA 返回的convergence_curve在全部迭代结束后画一张适应度下降曲线,既省时间又直观。

WC 算法本身还有一个容易被忽略的细节:a = 2 - t * (2 / Max_iter)让 A 随迭代线性下降。如果Max_iter太小,A 下降太快,全局探索阶段被压缩,WOA 容易过早收敛到局部最优。建议观察convergence_curve的走势:如果在前几轮就平了,说明Max_iter设置过短,可以增加到 20 到 30;如果曲线在最后几轮仍在明显下降,说明还可以继续迭代,但考虑训练成本,可以先接受当前结果。

6. 用验证集判据防御过拟合:一个可落地的防偏技巧

WOA-GRU 最容易出现的坑不是模型不收敛,而是“调节超参数的时候过拟合了验证集”。由于 WOA 每一轮都是朝着验证集误差最小的方向搜索,经过十几次迭代后,验证集误差被压到很低,但测试集误差可能反而偏高。可以用以下三步来防偏。

第一步,不要用单次验证集误差作为适应度,而是用两次不同随机种子下的验证集预测平均 RMSE:

rng1 = 1; rng2 = 2; mse1 = evaluateWithSeed(net, X_val_seq, Y_val_seq, rng1); mse2 = evaluateWithSeed(net, X_val_seq, Y_val_seq, rng2); fitness = sqrt((mse1 + mse2) / 2);

这里的evaluateWithSeed是在预测前设置随机数种子、固定 dropout 和 BatchNorm 的随机行为。GRU 的预测虽然不需要随机采样,但trainNetwork的训练过程受随机种子影响,多次训练取平均能降低偶然性。

第二步,在 WOA 结束后、做测试集评估之前,检查一个早停判据:假设你已经用训练集训练了 100 轮,如果最终模型的验证集误差比 WOA 搜索过程中记录的best_fitness还要低很多,比如低了 10% 以上,说明训练轮数太少,模型在训练集上还没吃饱。把这个基于验证集误差轨迹的判断写在评估代码里,能及时发现欠训练而不是急着分析测试数据。

第三步,测试集的评估必须只做一次。如果你拿测试集测完发现效果不好,回头改了超参数再测一次,这时的测试集已经在参与选择了,报告的数字自然偏高。常见做法是把测试集误差输出到一个独立日志文件,整个 WOA 调参过程中不去读它,等全部调参完成后才把最终模型的预测结果和真实值放一起画散点图:

Y_test_pred = predict(finalNet, X_test_seq, 'ExecutionEnvironment', execEnv); Y_test_pred_raw = Y_test_pred * sigma_Y + mu_Y; Y_test_raw = Y_test * sigma_Y + mu_Y; rmse = sqrt(mean((Y_test_pred_raw - Y_test_raw).^2)); mape = mean(abs((Y_test_pred_raw - Y_test_raw) ./ Y_test_raw)) * 100; fprintf('测试集 RMSE: %.4f, MAPE: %.2f%%\n', rmse, mape); figure; plot(Y_test_raw, 'b-', 'LineWidth', 1); hold on; plot(Y_test_pred_raw, 'r--', 'LineWidth', 1); legend('实际值', '预测值'); xlabel('测试集样本序号'); ylabel('目标值'); title('WOA-GRU 测试集预测结果对比');

这一章的技巧只讲一个核心:验证集误差在 WOA 循环里是搜索目标,在循环外是早停判据,但它永远不能代替测试集的单次最终判决。把这三步固化到脚本里,你的 WOA-GRU 就不只是能跑通,而是经得起质疑。

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

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

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

立即咨询