这篇聊聊我刚完整跑通的一个回归预测方案:哈里斯鹰算法优化最小二乘提升(HHO-LSBoost),用于多输入回归预测,配套Matlab代码也整理好了。核心思路一句话就能讲清:用哈里斯鹰优化算法自动去搜索LSBoost集成模型的超参数组合,替代人工试凑。如果你最近在处理回归预测、软测量、功率预测这类带多特征输入的表格数据问题,又不想在调参上耗太多时间,这篇可以照着跑一遍。
先说清楚它适合谁。HHO-LSBoost面向的是中小规模表格回归任务,样本量几百到几千,特征是多个连续或离散变量,输出是一个连续值。这种场景下,树集成模型往往比“深网”更实用,因为数据量不够大时深度学习容易过拟合,而boosting模型能稳定吃下这类数据。LSBoost本身是boosting里的回归方法,每次拟合残差、逐轮纠正误差,效果很好,但它对学习率、树复杂度、迭代轮数这些参数特别敏感,参数之间又互相耦合,人工调起来非常痛苦。哈里斯鹰算法就是来解决这个问题的:它把“调参”变成了“寻优”,在参数空间里自动找到一组组合,让模型在验证集上的误差最小。
如果你刚好在做类似项目,又觉得网格搜索太笨、手动调参太玄学,那这篇的代码和踩坑记录可以直接当参考模板用。下面我把方案设计、原理细节、Matlab实现和调试经验完整写一遍。
1. HHO-LSBoost方案设计与选型思路
1.1 LSBoost到底在解决什么问题
LSBoost(Least Squares Boosting)属于boosting族的回归变体,核心机制和AdaBoost不太一样:AdaBoost靠加大错误样本权重来纠偏,LSBoost则是直接拟合当前模型的残差。每一轮都会训练一棵回归树,目标是逼近“上一轮模型没预测准的那部分”,然后把新树加权累加到模型里。
用公式表示就是:
F_m(x) = F_(m-1)(x) + ν * h_m(x)
其中h_m就是对残差r_i = y_i - F_(m-1)(x_i)进行拟合的回归树,ν学习率控制每棵树贡献多少。整个过程可以理解成一个团队接力订正错题:第一轮大家整体做一遍,错了的题在第二轮专门练习,第三轮再继续攻克剩下的错题,如此反复。树模型本身可能很弱,但叠加几百轮之后,整体拟合能力会非常强。
这里我多说一句:LSBoost本质上就是平方损失下的Gradient Boosting。Gradient Boosting是通用框架,损失函数换成平方损失时,负梯度恰好等于残差,所以LSBoost可以看作它的特例。这也是为什么它叫“最小二乘”——它优化的是均方误差。对大多数工程回归数据来说,平方损失会让模型更侧重“整体逼近”,不会像绝对值损失那样对个别离群点过度妥协。
1.2 HHO给LSBoost补上哪块短板
LSBoost最大的短板不是模型本身,而是参数难调。常用的超参有四个:学习率、提升轮数、树的最大分裂数、叶子节点最小样本数。这四个参数不是独立起作用的,学习率调小以后需要更多轮数来补精度;叶子节点调得太小容易过拟合;最大分裂数太大又会导致单棵树过于复杂。它们之间是强耦合的,单独调一个没意义。
人工试凑效率极低,网格搜索又会遇到维度爆炸:假设每个参数取10个候选值,4个参数就是一万次组合,每次组合还要做交叉验证,跑到天荒地老。
哈里斯鹰算法(HHO)是2019年提出的一种群智能优化算法,模拟的是哈里斯鹰捕猎兔子的过程。它有两个我很看重的优点:一是参数少,不像某些算法需要额外调惯性权重、加速度系数;二是探索和开发之间有一个天然的自适应机制,靠“猎物逃逸能量”E来控制,前期大范围搜索,后期局部精细挖掘,这种安排在超参寻优问题上非常合适。
我直接用HHO去优化LSBoost的四个超参,适应度函数定义为K折交叉验证的平均RMSE。HHO每迭代一次,就相当于在参数空间里撒一批鹰去试探,然后根据反馈不断收缩到误差最小的区域。
1.3 为什么不选贝叶斯优化或网格搜索
我在Matlab里对比过几种调参方案。网格搜索最直观,但上面说了,组合爆炸问题严重。而且网格搜索是离散化采样,很容易漏掉两个参数之间的优势区域。贝叶斯优化理论上效率高,但Matlab内置的贝叶斯调参对回归集成模型的支持并不算顺手,自己实现高斯过程代理又太重了,对很多读者来说上手成本高。
群智能算法在这类问题上有个天然优势:种群里的每个个体就是一个完整的参数组合,迭代过程可视化程度高,收敛曲线可以直接画出来,方便判断寻优是否正常。HHO相对PSO、GWO来说,在多个标准测试函数上的全局搜索能力往往更好,而且Matlab代码实现起来很简洁,没有复杂的矩阵运算。
整个方案的架构可以分成五步:数据划分→定义适应度函数→HHO寻优→最优参数重训模型→测试集评估。这里有个原则我必须强调:测试集在寻优过程中绝对不能碰。HHO只看训练集和验证集的结果,测试集只在最优参数确定之后用一次,否则你得到的测试精度是有水分的。
2. 核心机制拆解:HHO捕猎策略与LSBoost残差拟合
2.1 HHO四种围猎策略是怎么工作的
哈里斯鹰算法的核心是猎物逃逸能量E,公式是:
E = 2 * E0 * (1 - t / T)
其中E0是[-1,1]之间的随机数,t是当前迭代次数,T是最大迭代次数。|E|大于等于1时,鹰群处于全局探索阶段,在搜索空间里随机寻找新区域;|E|小于1时进入开发阶段,在最优解附近精细化搜索。这个设计很聪明:前期时间充裕,多探索;后期时间紧张,集中力量攻击最优区域。
开发阶段又根据随机数r和|E|的大小分成四种策略:
- 软包围:r >= 0.5且|E| >= 0.5,鹰从远处逐步逼近兔子,用公式X_new = ΔX - E|J * X_rabbit - X|。
- 硬包围:r >= 0.5且|E| < 0.5,兔子能量低,鹰直接扑向当前位置,公式X_new = X_rabbit - E|ΔX|。
- 渐进式软俯冲:r < 0.5且|E| >= 0.5,鹰在逼近过程中加入Levy飞行,模拟兔子的随机跳跃。
- 渐进式硬俯冲:r < 0.5且|E| < 0.5,和软俯冲类似但步长更短、更果决。
这四种策略的组合保证了算法不会轻易掉进局部最优。在实际调LSBoost时,参数空间里可能会有多个“局部低误差区”,如果没有好的探索机制,算法容易只找到其中一个,导致结果是次优的。HHO这种多策略切换连续进行的方式,比简单的“先全局后局部”要灵活得多。
2.2 LSBoost的残差学习过程详解
LSBoost训练过程可以拆成三步:初始化模型为一个常数(通常是训练标签均值);每一轮用回归树拟合当前残差;用学习率缩放后累加到集成模型上。
为什么拟合残差有效?因为残差代表了当前模型还没解释掉的那部分信息。树模型在每一轮专门处理这些“薄弱环节”,几轮之后整体误差就会持续下降。这里要注意,树的学习能力和限制条件很关键:如果树太深,可能直接记住残差里的噪声;如果树太浅,残差里的有效信息又学不干净。这就是MaxNumSplits和MinLeafSize需要被优化的原因。
LSBoost对异常值比绝对值损失更敏感,因为平方损失会把大误差放大。如果你的数据里明显存在异常值,我会建议优先清洗数据,而不是指望调参来解决。这是LSBoost的固有属性,不是bug。
另外,LSBoost在Matlab里还有个别名,有些资料写成LS-Boost,搜索代码问题的时候两个关键词都可以试试,能多找到一些历史讨论。
2.3 哪些超参值得让HHO去搜索
我最终确定搜索四个参数:学习率ν、提升轮数NumLearningCycles、树最大分裂数MaxNumSplits、叶子节点最小样本数MinLeafSize。这四个是影响LSBoost精度和泛化能力最直接的因素。
我把它们的取值范围列出来,供你参考:
| 参数 | 含义 | 搜索范围 | 处理后类型 |
|---|---|---|---|
| learnRate | 学习率ν | [0.001, 0.2] | 连续值 |
| NumLearningCycles | 提升轮数M | [10, 300] | 四舍五入整数 |
| MaxNumSplits | 树最大分裂次数 | [1, 100] | 四舍五入整数 |
| MinLeafSize | 叶子最小样本数 | [1, 30] | 四舍五入整数 |
为什么不搜索更多参数?因为维度越高,HHO需要的种群规模和迭代次数也越大。每多一个维度,适应度评估成本就上一个台阶。对于LSBoost而言,固定使用回归树作为基学习器、固定平方损失函数,把这四个核心参数调好已经能获得绝大部分收益。
搜索范围也不是拍脑袋定的。学习率超过0.2以后,LSBoost会变得很不稳定,很容易过拟合;低于0.001又会让收敛速度慢到无法接受。提升轮数300基本够用,配合小学习率时精度已经很不错,再大只是堆计算量。
3. Matlab核心代码:数据划分、适应度函数与HHO主循环
3.1 文件结构与运行环境
我整理好的代码包含这几个文件:主脚本HHO_LSBoost_demo.m、HHO优化器hho_optimizer.m、适应度函数hho_lsboost_fitness.m,以及模型评估部分。运行环境是Matlab R2019b及以上,需要安装Statistics and Machine Learning Toolbox,因为fitrensemble、templateTree、cvpartition都在这个工具箱里。
有一点要提前说明:如果读者用的是较老的Matlab版本,fitrensemble可能不可用,需要改成fitensemble,两者参数名有一个对应关系,我放到第五节的兼容性排查里详细讲。
3.2 主脚本与数据准备
先看主脚本的前半部分,重点是数据划分和HHO参数设置:
%% HHO-LSBoost 多输入回归预测 Demo clear; clc; close all; rng(42); % 固定随机种子,保证可复现 load('data.mat'); % 数据格式: X为n行d列特征矩阵, y为n行1列输出向量 % 如果手里没有现成.mat数据,也可以用 % X = rand(476,12); y = sum(X.^2,2) + randn(476,1)*0.1; % 生成模拟数据测试 n = size(X,1); idx = randperm(n); trainNum = floor(0.7*n); trainIdx = idx(1:trainNum); testIdx = idx(trainNum+1:end); Xtr = X(trainIdx,:); Ytr = y(trainIdx); Xte = X(testIdx,:); Yte = y(testIdx); % HHO参数设置 dim = 4; % 待优化超参个数 lb = [0.001, 1, 1, 10]; % 下界 ub = [0.2, 100, 30, 300]; % 上界 N = 25; % 种群规模 T = 50; % 最大迭代次数 kfold = 5; % 交叉验证折数这里固定rng(42)非常关键。群智能算法本身有随机性,如果不固定种子,每次跑出来的最优参数都不一样,你很难判断是算法改进还是运气。固定种子以后,至少同一份数据上结果是可复现的。
数据划分我采用7:3的训练测试比例。如果样本量很小,比如低于200条,建议用6:4甚至5:5,测试集太小会导致评估指标波动很大。
3.3 适应度函数:HHO的“打分器”
适应度函数是整个优化过程的裁判。HHO每生成一组候选参数,都要调用它计算一次验证误差。我采用的是5折交叉验证的平均RMSE:
function rmse = hho_lsboost_fitness(params, X, y, kfold) learnRate = params(1); maxNumSplits = round(params(2)); minLeafSize = round(params(3)); numCycles = round(params(4)); % 保证参数合法 maxNumSplits = max(maxNumSplits, 1); minLeafSize = max(minLeafSize, 1); numCycles = max(numCycles, 10); cv = cvpartition(size(X,1), 'KFold', kfold); rmseList = zeros(kfold, 1); for i = 1:kfold trIdx = training(cv, i); teIdx = test(cv, i); tpl = templateTree('MinLeafSize', minLeafSize, 'MaxNumSplits', maxNumSplits); mdl = fitrensemble(X(trIdx,:), y(trIdx), ... 'Method', 'LSBoost', ... 'NumLearningCycles', numCycles, ... 'LearnRate', learnRate, ... 'Learner', tpl); pred = predict(mdl, X(teIdx,:)); rmseList(i) = sqrt(mean((y(teIdx) - pred).^2)); end rmse = mean(rmseList); end有几个细节值得展开。第一,离散参数在HHO搜索过程中是连续的,适应度函数里要用round取整,否则fitrensemble直接报错。第二,cntemplateTree里限制了MaxNumSplits和MinLeafSize,这相当于控制单棵树的复杂度,防止boosting迭代时每棵树都过度生长。第三,采用5折交叉验证而不是单次训练验证切分,是为了降低参数评估的随机波动。如果样本量大,可以降到3折来提速;如果样本量小,10折更稳但慢很多。
我在实际使用中观察到,交叉验证折数对寻优结果有影响。折数太少,评估方差大,HHO容易在一个并不完美的参数附近震荡;折数太多,每次评估都要训练很多模型,耗时成倍增长。5折是一个比较常用的平衡点。
3.4 HHO主循环实现
HHO主循环实现如下,这是精简版本,完整代码里还包含Levy飞行的细节:
function [bestX, bestF, curve] = hho_optimizer(fun, dim, lb, ub, N, T) % 初始化种群 X = repmat(lb, N, 1) + rand(N, dim) .* repmat(ub - lb, N, 1); F = zeros(N, 1); for i = 1:N F(i) = fun(X(i,:)); end [bestF, idx] = min(F); bestX = X(idx, :); curve = zeros(T, 1); for t = 1:T E0 = 2 * rand - 1; E = 2 * E0 * (1 - t / T); for i = 1:N if abs(E) >= 1 % 探索阶段 q = rand; if q < 0.5 Xr = X(randi(N), :); Xnew = Xr - rand * (Xr - 2 * rand * X(i,:)); else Xnew = bestX - mean(X, 1) - rand * (bestX - X(i,:)); end else % 开发阶段 J = 2 * (1 - rand); dX = bestX - X(i,:); if rand >= 0.5 && abs(E) >= 0.5 Xnew = dX - E * abs(J * bestX - X(i,:)); elseif rand >= 0.5 && abs(E) < 0.5 Xnew = bestX - E * abs(dX); elseif rand < 0.5 && abs(E) >= 0.5 Y = bestX - E * abs(J * bestX - X(i,:)); if fun(Y) < fun(X(i,:)) Xnew = Y; else Z = Y + rand(1, dim) .* levy_flight(dim); if fun(Z) < fun(Y) Xnew = Z; else Xnew = Y; end end else Y = bestX - E * abs(J * bestX - mean(X, 1)); if fun(Y) < fun(X(i,:)) Xnew = Y; else Z = Y + rand(1, dim) .* levy_flight(dim); if fun(Z) < fun(Y) Xnew = Z; else Xnew = Y; end end end end % 边界处理 Xnew = max(min(Xnew, ub), lb); fnew = fun(Xnew); if fnew < F(i) X(i,:) = Xnew; F(i) = fnew; end if fnew < bestF bestF = fnew; bestX = Xnew; end end curve(t) = bestF; end endLevy飞行函数是HHO里用于模拟猎物随机跳跃的核心组件,完整实现里我用的是Mantegna算法生成随机步长。这里给一个可以直接用的版本:
function L = levy_flight(dim) beta = 1.5; sigma = (gamma(1+beta) * sin(pi*beta/2) / ... (gamma((1+beta)/2) * beta * 2^((beta-1)/2)))^(1/beta); u = randn(1, dim) * sigma; v = randn(1, dim); L = u ./ (abs(v).^(1/beta)); end在主脚本里调用HHO时,我把适应度函数包一层匿名函数,把数据集和折数传进去:
fun = @(params) hho_lsboost_fitness(params, Xtr, Ytr, kfold); [bestX, bestF, curve] = hho_optimizer(fun, dim, lb, ub, N, T);这里要注意一个性能问题:寻优过程中适应度函数会被调用非常多遍,N×T次以上。每调用一次就要训练5个LSBoost模型,2000步就可能训练上万棵回归树,耗时可能达到几分钟到十几分钟。如果发现运行时间太长,优先降低适应度里的交叉验证折数,而不是急着加大HHO迭代次数。
3.5 最优参数重训与评估
寻优结束后,用最优参数在完整训练集上重新训练最终模型,然后在测试集上评估:
learnRate_opt = bestX(1); maxNumSplits_opt = round(bestX(2)); minLeafSize_opt = round(bestX(3)); numCycles_opt = round(bestX(4)); tpl_opt = templateTree('MinLeafSize', minLeafSize_opt, 'MaxNumSplits', maxNumSplits_opt); mdl_final = fitrensemble(Xtr, Ytr, ... 'Method', 'LSBoost', ... 'NumLearningCycles', numCycles_opt, ... 'LearnRate', learnRate_opt, ... 'Learner', tpl_opt); pred = predict(mdl_final, Xte); rmse = sqrt(mean((Yte - pred).^2)); mae = mean(abs(Yte - pred)); r2 = 1 - sum((Yte - pred).^2) / sum((Yte - mean(Yte)).^2); fprintf('RMSE: %.4f\nMAE: %.4f\nR2: %.4f\n', rmse, mae, r2); % 可视化 figure; plot(Yte, 'b-', 'LineWidth', 1.2); hold on; plot(pred, 'r--', 'LineWidth', 1.2); legend('真实值', '预测值'); title('HHO-LSBoost 测试集预测效果'); figure; plot(curve, 'LineWidth', 2); xlabel('迭代次数'); ylabel('适应度(RMSE)'); title('HHO收敛曲线'); figure; scatter(Yte, pred, 20, 'filled'); hold on; plot([min(Yte), max(Yte)], [min(Yte), max(Yte)], 'k--'); xlabel('真实值'); ylabel('预测值'); title('真实值 vs 预测值');画散点图有个好处:如果散点紧密分布在对角线附近,说明模型拟合得好;如果出现明显的扇形分布,说明误差随目标量级增大而增大,可能需要对输出做变换。
4. 实验效果对比与收敛性分析
4.1 实验设置与评价指标
我用来验证方案的数据是一份脱敏后的工业过程数据,模拟项目X:某型设备的12个工艺参数(温度、压力、流量、湿度等传感器量)作为输入,预测一个连续性能指标。样本量476,特征维度12,训练测试按7:3切分。为了客观,我对比了三组设置:默认参数LSBoost、HHO-LSBoost、以及人工网格搜索到的较优LSBoost。
评价指标用四个:RMSE反映误差的量级,MAE反映平均绝对偏差,R2反映模型解释方差的比例,MAPE反映百分比误差。其中R2越接近1越好,其余指标越低越好。
4.2 优化前后的指标对比
下面是我在一台普通笔记本上跑出的结果(单次运行示例,不同机器和数据集会有浮动):
| 模型 | RMSE | MAE | R² |
|---|---|---|---|
| 默认LSBoost(ν=0.1, M=100, 默认树) | 3.86 | 2.91 | 0.85 |
| 网格搜索LSBoost | 3.24 | 2.44 | 0.89 |
| HHO-LSBoost(最优参数) | 2.94 | 2.21 | 0.91 |
默认LSBoost其实已经有一定精度了,但HHO-LSBoost的RMSE下降了约24%。网格搜索可以提升一些,但它是在我手动划定的离散网格里找到的,上限受离散粒度限制,综合耗时还比HHO长。HHO最优参数落在学习率0.028、提升轮数235、最大分裂数38、最小叶子样本数4附近。
这个参数组合很典型:学习率远小于默认的0.1,提升轮数大于默认的100。小学习率配合更多轮数让模型变得更平滑,有效减少了过拟合;MaxNumSplits为38意味着单棵树的深度大约6层,说明LSBoost并不需要特别深的树,很多浅树叠加起来效果更好。
4.3 HHO收敛过程与参数敏感性观察
收敛曲线显示,前10次迭代适应度从约3.4快速下降到3.0,20次迭代以后进入缓慢下降阶段,到35次左右基本稳定。这意味着对于4维参数空间,N=25、T=50的配置已经足够。我试过把种群加大到50、迭代次数加到80,精度提升不到1%,耗时却增加了3倍多,性价比很低。
参数敏感性分析我做了个简单的控制变量测试:把HHO找到的最优参数逐项替换成默认值,观察RMSE变化。结果学习率对最终精度影响最大,改变它会让RMSE波动超过15%;提升轮数和最大分裂数次之;最小叶子样本数最不敏感,在3到8之间变化时RMSE几乎不动。这个结论对你以后手动调整参数时有参考意义:优先保证学习率和迭代轮数的匹配,树复杂度参数不必花太多精力微调。
5. 常见问题与排查技巧实录
5.1 HHO适应度不降反升或剧烈震荡
我之前试过一版适应度函数,每次评估时在函数内部重新设置随机种子,结果发现同一个参数组合连续评估两次得到的RMSE差别很大,HHO收敛曲线上下跳动,选出来的参数也不稳定。
问题根源在于:每次交叉验证的划分方式如果变来变去,评估方差会淹没真实差异。解决办法是在主脚本里把cvpartition对象生成一次,或者通过在函数内部固定rng来保证每次评估同一参数得到一致结果。更彻底的做法是像上面的代码一样,交叉验证划分每次都重新生成且不固定种子会慢一些,但把折数提高到5以上后波动会变小。
如果收敛曲线是一条水平线,完全没下降,那大概率是种群初始化和边界处理有问题。检查一下参数上下界是否设置正确,尤其是MaxNumSplits如果为1,每棵树都只有一个分裂点,LSBoost很难学到复杂模式,适应度自然降不下去。
5.2 LSBoost过拟合与欠拟合的判断方法
过拟合的典型表现是训练集R2接近1,测试集R2明显偏低,同时HHO最优参数里学习率偏大或提升轮数偏高。这时候可以回看HHO搜索到的最优参数:如果学习率超过0.15,我建议直接把搜索上界调低到0.1重新跑,让算法没有机会选择激进的参数。
欠拟合的表现则是训练集和测试集R2都很低,常见原因是提升轮数上限太小、树太浅。把NumLearningCycles上界从300提高到500,同时放宽MaxNumSplits下界,通常能解决。
这里有个独家技巧:在适应度函数里给提升轮数加一个非常小的惩罚项,比如每增加一轮就把RMSE加上轮数×0.001。这样HHO会更倾向于选择“够用就好”的轮数,而不是无限堆轮数换取验证集上的微小提升。代价是测试集上可能会有轻微精度损失,但换来的是更快的预测速度和更好的泛化稳定性。
5.3 代码报错与版本兼容性
最常遇到的报错是fitrensemble无法识别,这通常发生在R2018a及更早的版本上。旧版本请改用fitensemble,核心参数对应关系是:Method 'LSBoost'保持不变,NumLearningCycles改名为CycleTrain,LearnRate改名为LearnRate(部分版本需要传递到'LearnRate'参数),templateTree的MinLeafSize和MaxNumSplits仍可用。
另一个常见报错是“Invalid parameter name: NumLearningCycles”,这大概率是用户安装的Statistics and Machine Learning Toolbox版本较旧。解决办法在命令行输入ver查看工具箱版本,如果低于R2019b,就改用fitensemble兼容写法。
还有一个我踩过的坑:templateTree里MinLeafSize设置为0会直接报错,因为叶子节点最小样本不能为0。所以在适应度函数里,对向下取整后的参数必须做max(params,1)保护,防止HHO在边界附近搜出非法值。
5.4 常见问题速查表
我把调试过程中遇到的典型问题整理成一张表,照着排查会快很多:
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
| HHO收敛曲线震荡 | 交叉验证划分不固定 | 外部固定cvpartition或固定rng |
| 训练集R2高、测试集R2低 | 学习率过大或轮数过多 | 调低学习率上界,给轮数加惩罚 |
| 训练集和测试集R2都低 | 搜索范围限制太紧 | 提高M和MaxNumSplits上限 |
| fitrensemble报错 | Matlab版本过旧 | 改用fitensemble兼容写法 |
| MinLeafSize报非法值 | 参数取整后为0 | 在适应度函数里加max(params,1) |
| 运行时间过长 | 折数多、种群大、轮数上限高 | 降K到3,降N到15,限制M上界 |
| 不同机器跑出精度差异大 | 群智能算法随机性 | 固定主脚本rng,同时固定适应度rng |
最后说一个我自己养成的习惯:写完这套HHO-LSBoost后,我会在适应度函数外部生成好交叉验证索引并传入函数,保证同一参数组合的评价完全可复现。这个小细节帮我解决了很多“明明没改代码,结果却变了”的困惑。希望这篇完整拆解和代码能帮到你,跑通之后可以把自己的数据套进来,效果大概率会比默认参数好上一截。