简介:这份资源提供基于鲸鱼算法优化深度置信网络(WOA-DBN)的Matlab多输入回归预测完整实现,面向需要高精度拟合与模型调参的科研人员和工程师。核心代码在交叉验证框架下自动寻优DBN的隐藏层节点数、反向迭代次数与反向学习率,有效抑制过拟合,并给出R²、MAE、MSE、RMSE、MAPE及RPD等全套评价指标,可直接对照运行结果。压缩包共29个文件,包含19个m脚本、1个Excel数据文件、8张结果图以及一份zip子包,整体仅420KB,轻量且结构清晰,便于替换数据和二次开发。已有331人学习使用,代码注释完整,适合快速上手深度置信网络回归预测的入门与进阶实践。实际评测中R²达0.98912,MAE为0.17649,也侧面印证了算法的实用效果。
1. WOA-DBN 回归预测:先看这个标题里到底藏着多少次训练
很多人第一次看到 WOA-DBN 这个名字,以为它是一个新网络结构,其实它是一套两段式训练方案:深度置信网络(DBN)负责构建多输入回归预测模型,鲸鱼算法(WOA)在外面负责搜索 DBN 容易卡住的初始权值与学习率。DBN 先通过 RBM 逐层预训练把输入特征转成高层表达,再整体反向传播微调;WOA 则把 DBN 的训练代价当成黑箱目标函数,用包围猎物与气泡网捕食两种策略去搜更好的一组网络参数。真正跑起来你会发现最耗时的不是单次前向或 BP,而是每一次适应度评估都要完整跑一遍网络、每一代种群又包含几十个候选个体。下面按一个可维护的 Matlab 工程顺序来拆,读完能解释 WOA-DBN 在回归场景下的训练流程,也能照着代码骨架改参数。
2. WOA-DBN 原理与选型:鲸鱼算法在优化 DBN 初值与学习率时靠什么收敛
2.1 DBN 回归训练存在两个独立的搜索面
DBN 做多输入回归预测时的网络结构一般是:输入层接若干个 RBM,每个 RBM 把上一层输出映射到一个新的隐藏表达,最后一层接一个线性输出单元。训练分成两部分,第一部分是逐层贪婪预训练,用对比散度 CD-1 让每个 RBM 在自己的视野内拟合输入分布;第二部分是把所有层展开,用 BP 对整个网络做有监督微调。很多只跑过普通神经网络的人会忽略一个事实:DBN 的预训练结果本身带有随机性,CD-1 只用一步吉布斯采样,权值初始点不同,预训练后落点就不同;而后续 BP 又在非凸误差面上做局部搜索,预训练落点直接决定了微调能走多远。于是回归预测任务里出现两个独立的搜索面:预训练阶段的初始权值,以及微调阶段的学习率、动量等超参数。
WOA-DBN 的常见做法是把第二个搜索面扩大:先用 DBN 自己的预训练得到一组还不错的权重,再把所有权重和偏置展开成一个连续向量,作为 WOA 的个体位置。这样鲸鱼搜索的空间不是随机权值空间,而是“预训练权值附近”的连续空间。预训练负责给语义特征,WOA 负责在这些特征表达附近找更平坦的回归误差位置,最后再补一轮短 BP 微调。这个方案比让 WOA 从随机权值开始搜要可靠得多,也避免了随机初始化让实验不好复现。
2.2 WOA 的三个更新机制
WOA 模拟座头鲸捕食,数学上只做三件事:包围猎物、螺旋泡泡网、随机搜索。把猎物位置记为 X*,当前鲸鱼位置记为 X,第 t 代的控制参数 a 从 2 线性减小到 0,三个更新规则分别是:
包围阶段:D = |C·X* - X(t)|,X(t+1) = X* - A·D,其中 A = 2a·r1 - a,C = 2r2。r1、r2 是 [0,1] 均匀随机数。A 的绝对值大于 1 时鲸鱼远离猎物做探索,小于 1 时靠近猎物做开发。
泡泡网阶段:令 D' = |X* - X(t)|,X(t+1) = D'·e^(b·l)·cos(2πl) + X*。b 通常取 1,l 是 [-1,1] 随机数,这个式子让位置沿着对数螺旋线逼近最优解。
随机搜索:当 p < 0.5 且 |A| ≥ 1 时,随机选一个种群个体 X_rand 代替 X*,再做包围更新。这个分支是专门用来跳出局部最优的。p 是每次更新前生成的 0~1 随机数,p ≥ 0.5 走螺旋,p < 0.5 再按 |A| 判断走包围或随机搜索。
对 DBN 的权重向量而言,这三个操作都是对向量的平移和缩放,不涉及离散编码,所以 WOA 适合直接优化连续权值空间。计算时只要把个体的位置向量 reshape 回各层权值矩阵,跑一次前向,返回验证集 RMSE 作为适应度。
2.3 为什么不用网格搜索、遗传算法或粒子群
常见替代方案是网格搜索、GA、PSO。网格搜索只能搜少数超参数,DBN 的网络权值动辄几百上千维,没法铺网格;GA 需要设计交叉和变异算子,对连续权值向量还要考虑染色体长度;PSO 粒子少收敛快,但速度更新参数多,在 DBN 这种高维误差面上早熟概率不小。WOA 只有 A、C、p 三个随机控制点,不用管速度、惯性权重,代码量小,螺旋更新让种群既能围绕当前最好解开发,又保留随机跳出机制。Matlab 优化工具箱里提供了 ga 和 particleswarm,但没有现成的 WOA,所以一般是自己写一个五六十行的主循环,反而更可控。
| 方案 | 搜索空间 | 单次代价 | 主要问题 |
|---|---|---|---|
| 网格搜索 | 离散超参 | 训练一次 DBN | 权值维度高,组合爆炸 |
| 遗传算法 GA | 连续/离散 | 训练一次 DBN | 算子多,参数繁琐 |
| 粒子群 PSO | 连续 | 训练一次 DBN | 速度项容易早熟收敛 |
| 鲸鱼算法 WOA | 连续 | 训练一次 DBN | 参数少,螺旋机制保持探索 |
要提醒的是,优化算法不能解决数据划分导致的过拟合。如果训练集和验证集没有分开,或者归一化参数用了全样本统计量,WOA 会很快把验证集 RMSE 压得很低,换到未知数据就崩。我一般先划分训练、验证、测试三段,WOA 内部只用验证集参与适应度计算,测试集留到最后打分。
3. Matlab 搭建 DBN 回归预测:预训练与微调的代码骨架
3.1 用结构体把网络参数一次性定义下来
先把网络结构和训练超参集中在一个结构体里,后续 WOA 的适应度函数、网络解包都会用到它。下面是常见的字段设计:
% dbn 结构体集中放网络结构与训练超参 dbn.inputDim = 8; % 输入特征数,由训练集特征列数决定 dbn.hiddenDim = [16 8]; % 两层RBM,隐藏节点数可按数据量调整 dbn.outputDim = 1; % 多输入回归预测的输出是单连续值 dbn.rbmEpochs = 60; % 每层RBM的CD-1预训练迭代次数 dbn.fineTuneEpochs = 200; % 整体BP微调轮数 dbn.lrRBM = 0.01; % RBM预训练学习率 dbn.lrBP = 0.003; % BP微调学习率 dbn.momentum = 0.9; % 动量系数 dbn.batchSize = 32; % 小批量大小把结构和超参放一个结构体而不是一堆全局变量,是因为 WOA 适应度每次要复制网络、对个体解包、前向,函数签名只能是“结构体 + 位置向量”,这样传参最干净。hiddenDim 用两层是常见默认做法,节点数超过 32 后需要更强的正则,否则回归任务容易过拟合。
3.2 RBM 预训练:CD-1 的核心片段
单个 RBM 的训练核心是 CD-1。以下代码接受输入矩阵 X,用对比散度更新权重和偏置,最后把所有参数拼成一个长向量返回:
function Wb = trainRBM(X, hNum, opts) % 输入X形状为[N x vNum],每个数据一行 [N, vNum] = size(X); W = 0.01 * randn(vNum, hNum); % 可见层到隐藏层的权重 vb = zeros(1, vNum); % 可见层偏置 hb = zeros(1, hNum); % 隐藏层偏置 numBatch = floor(N / opts.batchSize); for epoch = 1:opts.epochs idx = randperm(N); for k = 1:numBatch v1 = X(idx((k-1)*opts.batchSize + 1 : k*opts.batchSize), :); hProb = 1 ./ (1 + exp(-(v1 * W + repmat(hb, opts.batchSize, 1)))); hState = double(hProb > rand(size(hProb))); v2 = 1 ./ (1 + exp(-(hState * W' + repmat(vb, opts.batchSize, 1)))); h2Prob = 1 ./ (1 + exp(-(v2 * W + repmat(hb, opts.batchSize, 1)))); % CD-1 梯度,按 batch 平均 gW = (v1' * hProb - v2' * h2Prob) / opts.batchSize; gv = mean(v1 - v2); gh = mean(hProb - h2Prob); W = W + opts.lr * gW; vb = vb + opts.lr * gv; hb = hb + opts.lr * gh; end end Wb = [W(:); vb(:); hb(:)]; % 返回展开后的参数,便于拼接下一层 end这段代码省略了权重衰减和动量项,实际使用时要自行加opts.l2到gW上。opts.lr来自外部传入的dbn.lrRBM。连续值特征要先归一化到 [0,1],否则 sigmoid 输入过大容易饱和,CD-1 的梯度会迅速归零。
3.3 把多层 RBM 拼成回归网络
预训练时每一层 RBM 的输出要作为下一层的输入,所以trainDBN的循环里每次都要把数据矩阵乘上新一层的权重再过 sigmoid。输出层用线性激活,不套 sigmoid:
function dbn = trainDBN(Xtr, dbn) % 逐层预训练,只搭网络和 RBM,BP微调单独封装 data = Xtr; numLayers = numel(dbn.hiddenDim); dbn.W = cell(1, numLayers + 1); dbn.b = cell(1, numLayers + 1); for k = 1:numLayers opts = struct('epochs', dbn.rbmEpochs, ... 'batchSize', dbn.batchSize, ... 'lr', dbn.lrRBM); Wb = trainRBM(data, dbn.hiddenDim(k), opts); inDim = size(data, 2); dbn.W{k} = reshape(Wb(1:inDim*dbn.hiddenDim(k)), inDim, dbn.hiddenDim(k)); dbn.b{k} = Wb(inDim*dbn.hiddenDim(k)+1 : inDim*dbn.hiddenDim(k)+dbn.hiddenDim(k))'; data = 1 ./ (1 + exp(-(data * dbn.W{k} + repmat(dbn.b{k}, size(data,1), 1)))); end % 线性输出层,用于回归 dbn.W{numLayers + 1} = 0.01 * randn(dbn.hiddenDim(end), 1); dbn.b{numLayers + 1} = 0; end对应的预测函数更简单,隐藏层逐层走 sigmoid,最后一层只做矩阵乘法:
function yPred = predictDBN(X, dbn) h = X; for k = 1:numel(dbn.W) - 1 h = 1 ./ (1 + exp(-(h * dbn.W{k} + repmat(dbn.b{k}, size(h,1), 1)))); end yPred = h * dbn.W{end} + dbn.b{end}; end这个预测函数会被 WOA 的适应度函数反复调用,所以它的向量化程度直接决定优化过程耗时。Minimal 的写法就是上面这种,不推荐在循环里用for i = 1:N逐样本前向,那样速度会慢一个数量级。
3.4 回归任务为什么不能用分类 DBN 的输出层
分类 DBN 的最后一般接 softmax 和交叉熵,输出每个类别的概率。回归预测要求输出连续实数,所以输出层必须用线性激活,损失函数用均方误差。如果强行把回归值切成区间做分类,再用 softmax,会损失分辨率,尤其是测试集里出现训练区间之外的数值时,预测会被钳制在最后一类。这也是“多输入回归预测”和一般 DBN 分类代码之间最容易被忽略的差异。
4. WOA 优化 DBN 的 Matlab 主循环:适应度、位置编码与参数配置
4.1 把预训练后的权值展开成鲸鱼个体
既然要用 WOA 搜 DBN 的权值,第一件是把网络参数展开成一维向量,同时保留解包回结构体的方法。展开顺序固定为“所有权重矩阵按列拼接,再拼接所有偏置向量”:
function pos = flattenDBN(dbn) weightBlock = cell(numel(dbn.W), 1); for k = 1:numel(dbn.W) weightBlock{k} = dbn.W{k}(:); end biasBlock = cat(1, dbn.b{:}); pos = cat(1, weightBlock{:}, biasBlock); endunpackDBN按相反顺序切分向量,逐个 reshape 回各层权重和偏置。初始种群要从预训练后的initPos附近采样,不能从随机空间重新开始。原因是随机初始化会让网络丢失预训练得到的特征表达,适应度普遍差,WOA 大部分迭代都浪费在找回语义信息的路上。
4.2 适应度函数写一次前向
适应度只做前向计算,不做 BP 微调。这是保证 WOA-DBN 能跑完的关键:如果每个候选个体都做完整微调再评估,时间和算力成本会大到无法接受。常见做法是让 WOA 搜“不微调也足够好”的权重,最后只对最优个体补一轮 BP:
function cost = fitWOA(pos, Xva, yva, dbn) dbn = unpackDBN(pos, dbn); yPred = predictDBN(Xva, dbn); cost = sqrt(mean((yva - yPred).^2)); end参数说明:Xva、yva是验证集输入和输出,在 WOA 开始前固定下来;pos是当前鲸鱼个体的位置向量;每次调用都要unpackDBN一次,这是必要的开销但不要在里面叠加多层循环。如果验证集样本少,RMSE 方差大,可以把评估指标改成0.5 * trainRMSE + 0.5 * valRMSE做加权,避免 WOA 过度拟合一小段验证集。
4.3 WOA 主循环代码
下面是完整的主循环框架,直接复制到脚本里就能替换掉常见的 GA 和粒子群逻辑:
function [bestPos, bestCost] = woaDBN(initPos, Xva, yva, dbn, opt) dim = numel(initPos); lb = initPos - opt.searchR; % 搜索半径下界 ub = initPos + opt.searchR; % 搜索半径上界 % 以 initPos 为中心生成初始种群 pop = repmat(initPos, opt.popSize, 1) + opt.searchR * 0.05 * randn(opt.popSize, dim); pop = min(max(pop, lb), ub); cost = zeros(opt.popSize, 1); for i = 1:opt.popSize cost(i) = fitWOA(pop(i,:), Xva, yva, dbn); end [bestCost, bestIdx] = min(cost); bestPos = pop(bestIdx, :); for t = 1:opt.maxIter a = 2 - 2 * t / opt.maxIter; % a 随迭代从 2 降到 0 for i = 1:opt.popSize r1 = rand; r2 = rand; A = 2 * a * r1 - a; C = 2 * r2; p = rand; if p < 0.5 if abs(A) < 1 % 包围猎物 D = abs(C .* bestPos - pop(i,:)); newPos = bestPos - A .* D; else % 随机搜索,跳出局部最优 randIdx = randi(opt.popSize); D = abs(C .* pop(randIdx,:) - pop(i,:)); newPos = pop(randIdx,:) - A .* D; end else % 螺旋泡泡网 l = 2 * rand - 1; D = abs(bestPos - pop(i,:)); newPos = D .* exp(opt.b .* l) .* cos(2 * pi * l) + bestPos; end newPos = min(max(newPos, lb), ub); % 边界截断 newCost = fitWOA(newPos, Xva, yva, dbn); if newCost < cost(i) pop(i,:) = newPos; cost(i) = newCost; end end [minCost, minIdx] = min(cost); if minCost < bestCost bestCost = minCost; bestPos = pop(minIdx, :); end end end说明:这里的A用了标量,即所有维度使用同一个缩放系数,这是一些公开实现里的常见简化。如果想让每个维度的探索能力不同,可以把r1 = rand; r2 = rand;改成r1 = rand(1, dim); r2 = rand(1, dim);,A和C会变成向量,后面的加减乘法依旧成立。边界处理用了截断法,简单稳定;如果发现大量个体被压在边界上,可以把截断改成反射操作newPos = 2*lb - newPos,让越界个体落回搜索空间内部。
4.4 WOA-DBN 常用参数配置表
WOA 本身的参数不多,但每个参数对终点精度和运行时间的影响都很大:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| popSize | 20 ~ 40 | 每加一个个体等于多一次完整前向 |
| maxIter | 30 ~ 80 | 通常 20 代前收敛曲线下降明显,过多会过拟合验证集 |
| searchR | 0.3 ~ 0.5 | 搜索半径,作用在展开后的权重向量上 |
| b | 1 | 螺旋形状常数,一般不动 |
| p 阈值 | 0.5 | 螺旋与包围各占一半概率 |
| l | [-1, 1] | 螺旋更新中的步长控制 |
种群规模从 20 起步观察收敛曲线。如果 30 代内 RMSE 还在明显下降,说明搜索空间没走完,可以加到 40;如果 10 代就平台了,说明初始半径searchR太大,个体早在第一轮就飞远。时间预算吃紧时,先跑 15 代看趋势,再决定要不要正式跑 60 代。
lb和ub的设置最容易踩坑。不要用整个网络的绝对最大最小值做边界,比如[-1, 1]这种;要以initPos为中心,只允许个体在预训练权重附近浮动。这样保持了 DBN 预训练提取到的特征,WOA 只是在同一语义空间里找更平缓的误差曲面。
5. 回归预测验证与加速:从 RMSE 曲线到 MEX 编译
5.1 三个指标和一张图
最终评估不能只看一个 RMSE。把测试集预测结果画成散点图,同时算三个指标更完整:
yPred = predictDBN(Xte, bestDbn); e = yte - yPred; rmse = sqrt(mean(e.^2)); mae = mean(abs(e)); sst = sum((yte - mean(yte)).^2); r2 = 1 - sum(e.^2) / sst; figure; plot(yte, yPred, '.'); hold on; lim = [min([yte;yPred]), max([yte;yPred])]; plot(lim, lim, 'r--'); xlabel('真实值'); ylabel('预测值');R² 接近 1 说明模型学到了数据中的主要方差;R² 为负说明预测还不如直接用均值,优先检查归一化、输出层线性激活、数据泄漏三个位置。散点图如果整体偏离对角线,通常是输出层误用了 sigmoid;如果两端散得更开,说明测试集包含训练区间之外的样本,回归模型本身不改,只能通过清洗异常样本缓解。
WOA 的收敛曲线也要画,用semilogy(curve)看 RMSE 下降趋势。如果曲线一开始就很平,先检查initPos是否来自预训练后的网络,再看searchR是否设得过小。种群内大部分个体没有更新也是常见现象,说明新的位置适应度都不如当前解,这时可以适当加大searchR。
5.2 固定随机种子才可复现
Matlab 的 RBM 采样、randperm划分批次、WOA 中的rand都依赖全局随机流。不固定随机种子,前后两次跑出来的 RMSE 不一样,很难判断是参数改动带来的收益还是随机波动:
rng(42);把rng放在main脚本开头,最好放在数据划分之前。这样批次划分、种群初始化、WOA 内部随机数全部可复现。固定随机种子不是为了让模型一定最优,而是让实验对比有迹可循。
5.3 提速三板斧:向量化、MEX 编译与边界判断
第一个提速点是predictDBN中的 sigmoid 计算。Matlab 的1 ./ (1 + exp(-x))已经向量化,但如果单次调用次数达到几十万次,可以把 sigmoid 抽成 MEX 函数:
mex -O sigmoid_mex.cC 文件里只需要实现y = 1.0 / (1.0 + exp(-x))的循环,前向时间通常能减少三分之一以上。第二个提速点是外层用parfor替换 WOA 的种群循环。多核机器上可以把for i = 1:opt.popSize改成parfor,但要注意每个 worker 的随机数流要独立设置,否则每一代的结果不可复现。第三个提速点是提前判断边界。newPos = min(max(newPos, lb), ub)每次更新都执行,换成只在newPos越界时做反射运算,能少做两次全向量比较。
把rng(42)、上面的三个评估指标和散点图放到 main 脚本开头,之后每次调整网络结构或数据划分,你都能从同一套随机基线下看出差别,这是 WOA-DBN 调参阶段最值得先搭起来的基础设施。
本文还有配套的精品资源,点击获取