☰
MATLAB实现Stacking回归预测:PLS+SVM+BP+RF融合LSBoost实战
2026/10/8 15:26:22 网站建设 项目流程

做回归预测的朋友应该都见过这类需求:拿到一批数据,要用MATLAB做一个回归模型,要求精度尽可能高,最好还能解释得通。单模型不是不能用,但遇到特征维度高、样本量不大、变量之间存在非线性关系的数据时,PLS不够灵活,SVM调参痛苦,BP容易过拟合,RF又可能在局部数据上表现平平。这时候把几个模型组合起来,让它们投票说话,往往比硬挑一个模型要靠谱得多。

Stacking集成学习恰好就是干这个的。它的思路很直白:先训练几个不同的基学习器,再用一个元学习器去学习“如何最优地组合这些基学习器的预测结果”。我这次在MATLAB里实现了PLS、SVM、BP、RF四个基学习器,元学习器选的是LSBoost,跑下来效果比任何一个单独模型都要稳,测试集R2能稳定提升0.03到0.08。这篇文章就把完整思路、MATLAB代码、调参经验和踩过的坑一次性写清楚,适合正在搞回归预测的科研党、竞赛党,以及想系统了解Stacking实战的工程师。

1. 这个项目解决什么问题:Stacking回归预测的适用场景

1.1 标题拆解:四个基学习器加LSBoost意味着什么

先把这个标题逐层拆开看。标题里有两个关键词:Stacking和LSBoost。Stacking是一种集成学习框架,它把多个基础模型的输出作为新的特征,再交给一个高层模型训练。这里选的四个基学习器很有代表性:PLS是线性模型,SVM和BP是非线性模型,RF是Bagging类集成模型。这四种模型的误差来源不一样,预测偏差的结构也不一样,把它们放在一起,恰恰是Stacking能提升泛化能力的前提。

元学习器选LSBoost而不是常见的线性回归或逻辑回归,这一点值得说。LSBoost是Least Squares Boosting的缩写,在MATLAB里对应fitrensemble的'lsboost'选项,本质是一种以最小二乘损失为目标的梯度提升方法。用提升树来做stacking的元学习器,能捕捉基学习器之间的复杂交互关系,比如某一个基学习器在样本A上靠谱、另一个基学习器在样本B上靠谱,这种互补性靠简单的加权平均学不出来。

1.2 单一模型拟合的困境

我最早做这个项目,是想解决一个工业过程软测量问题:用几十个过程变量预测产品质量指标。数据量不大,只有几百个样本,但特征之间有强相关性,而且存在非线性。试过PLS,线性假设太强,残差明显带着结构;试过BP神经网络,训练集拟合得非常好,测试集却忽高忽低,典型的过拟合;试过SVM,调完核函数和惩罚系数之后精度有提升,但换一组数据表现又不稳定;RF倒是稳,精度却到不了我要的阈值。

后来我意识到一个问题:这几个模型根本不是互斥的,它们的错误是互补的。PLS在变量高度相关的场景下能把线性趋势抓得干净,SVM擅长小样本非线性边界,BP能逼近任意复杂函数但容易过拟合,RF通过多棵树平均抹平了方差但会在强非线性区域欠拟合。既然各有长短,与其纠结选哪个,不如全都用上,让上层模型去学“什么时候该相信谁”。

1.3 Stacking与Bagging、Boosting的根本区别

很多刚接触集成学习的人容易把Stacking和随机森林、梯度提升混在一起。随机森林是Bagging,对同一模型做多次采样训练然后投票,核心是降低方差;梯度提升是Boosting,按顺序训练模型不断拟合残差,核心是降低偏差。

Stacking不一样,它操作的是“模型的输出”而不是“样本的权重”。在Stacking框架里,我有四个完整的模型,每个模型都看过完整的数据,各自给出预测值。然后我把这些预测值拼成一个新矩阵,训练LSBoost去学习“面对一个新的样本,四个模型的预测值各占多少权重才是最优”。你可以把它理解成一个团队里既有线性思维强的、又有擅长捕捉非线性的,最后让一个决策者结合所有人的意见做判断。

这个结构上的差异决定了Stacking的上限更高——它不要求基学习器的性能有多均衡,反倒是“差异越大的模型组合,Stacking提升越明显”。

2. 基学习器选型解析:PLS、SVM、BP、RF各司其职

2.1 PLS:线性特征提取的稳健选择

偏最小二乘回归在MATLAB里没有和fitrsvm平级的封装函数,通常用plsregress配合手动预测。它做的事情本质上是在X和Y之间找一组正交潜变量,使得这些潜变量既能解释X的变化,又能预测Y。对于特征维度高、样本量小、特征之间严重共线性的数据,PLS比普通最小二乘稳健得多。

在这个项目里为什么需要一个线性模型?因为非线性模型很容易把训练集里的噪声也学进去。PLS像是一个“锚”,它只捕捉全局的线性趋势,给Stacking提供一个相对保守但稳定的基线预测。即使SVM、BP在测试集上翻车,PLS的预测结果也不至于离谱到哪里去。

PLS的潜变量数量LVs需要确定,我一般用交叉验证。MATLAB里plsregress返回均方根误差曲线,选第一个误差明显下降后开始平缓的LVs值,通常10到20之间。太小欠拟合,太大过拟合。

2.2 SVM:小样本非线性的杀手锏

支持向量回归(SVR)在MATLAB里直接用fitrsvm,核函数我选的是高斯核(RBF)。SVR的核心思想是把数据映射到高维空间,在高维空间里找一个误差带内的回归函数。它的优势在于依赖结构风险最小化,小样本下泛化能力比神经网络强。

SVM有两个关键超参数:核函数的尺度参数KernelScale和误分类惩罚系数BoxConstraint。这两个参数如果手调会很痛苦,推荐用MATLAB的fitrsvm开启自动优化,也就是'OptimizeHyperparameters','auto',但要注意自动调参会占用大量时间。我自己会先在一个子集上粗调,再在全集上微调。

给SVM的数据必须做标准化。SVM对特征的尺度敏感,如果不归一化,数值范围大的特征会主导核函数的距离计算。这一点也是后面写代码时最容易踩的坑。

2.3 BP神经网络:非线性拟合的万金油

BP网络在MATLAB里有几种实现方式:老牌的feedforwardnet工具箱,或者新版的fitrnet。我在这个项目里用fitrnet,因为它可以直接纳入到一个统一的自定义训练流程里,而且支持标准化。

BP的核心是反向传播算法,通过梯度下降不断调整各层的权重,让输出不断逼近真实值。理论上来说,一个足够宽、足够深的BP网络可以逼近任意连续函数。但在小样本场景下,BP的问题也很突出:网络容量一大,训练集上的损失可以降到接近零,但测试集上一塌糊涂。所以BP在这个Stacking框架里承担的角色是“捕捉复杂的非线性模式”,但它的预测结果往往带着较大的方差,需要上层模型对它进行修正。

我用的是一个单隐藏层网络,隐藏层节点数设为15左右,激活函数用ReLU,求解器用LBFGS。数据依然要标准化,这一点和SVM一致。

2.4 随机森林:Bagging集成的成熟代表

随机森林在MATLAB里可以用TreeBagger或者fitrensemble('method','bag')实现。它通过自举采样生成多棵决策树,每棵树只在一个随机特征子集里做分裂,最终用所有树的平均作为预测。这个机制天然具备抗过拟合的能力,对异常值也不敏感。

RF在这个组合里扮演“稳定器”的角色。它几乎不需要太多参数调节,把树的数量设为300到500,最小叶子节点数设为5,效果就差别不大了。RF的预测方差小,但偏差偏大——尤其是在数据分布不均匀的区间,它倾向于回归到均值。Stacking正是利用这一点:RF给出一个稳但不够准的预测,SVM和BP给出锐利但偶尔离谱的预测,PLS保证线性趋势不错,LSBoost再根据实际情况决定最终采用谁的倾向。

2.5 为什么这四个模型搭在一起效果最好

如果你去查相关论文,会看到Stacking的基学习器强烈推荐“模型多样性”。所谓多样性,就是不同模型的决策边界、偏差方差特性要有差异。PLS和SVM的差异在于线性与非线性;SVM和BP的差异在于小样本泛化与全局拟合能力;RF和BP的差异在于Bagging方差控制与单一模型的过拟合倾向。这四个模型两两之间的相关性都不算太高,融合时能够提供互补信息。

我做了一个简单的对比实验:单独用RF跑,测试集R2在0.86左右;单独用SVM,R2在0.84到0.88波动;BP单独跑,R2只有0.80到0.87,极不稳定。把这四个做成Stacking后,测试集R2稳定在了0.92以上。注意,这个提升不是某个模型变强了,而是“组合出来的预测比任何一个成员都更接近真实值”。

3. 元学习器LSBoost:如何让融合更聪明

3.1 最小二乘提升原理

LSBoost在MATLAB中对应的是一种梯度提升机(Gradient Boosting Machine)的特殊形式。它用一系列弱学习器(通常是小决策树)去拟合当前残差,每一步都在减少损失函数的最小二乘误差。和经典的AdaBoost不同,LSBoost不需要调整样本权重,而是直接在残差空间里做加法模型。

在Stacking框架里,元学习器的输入是四个基学习器的预测列向量,输出是真实值。LSBoost在训练时,会不断比较“当前融合模型预测值与真实值的残差”,然后用新的决策树去拟合这个残差。每棵树学到的是“前几轮融合模型哪里做得还不够好”。最终预测值是所有树的累加。

采用LSBoost代替简单线性回归的好处很明显:第一,它能找到四个基学习器预测结果之间的非线性组合关系;第二,它自带特征选择能力,会在分裂过程自动抛弃不重要的基学习器;第三,它对基学习器的共线性不那么敏感,因为每棵树只用部分特征。

3.2 为什么不能简单平均

许多人做集成时习惯直接把几个模型的预测结果取平均,这个方法在某些竞赛里能直接超越单一模型,但它的上限很低。平均假设了所有模型“靠谱程度相同”,但实际上模型之间的性能差异很大,而且在不同样本上表现各异。有些样本对SVM友好,有些对BP更友好,平均之后,这种局部优势被稀释了。

LSBoost则不同。它通过训练阶段不断拟合残差,自动学习了不同区域的置信度分配。比如在某个区间内,SVM的预测和真实值偏差小,LSBoost就会在那一轮用SVM的预测作为主输入,把其他模型的残差当作噪声处理。这种“自适应加权”能力,是简单平均无法做到的。

3.3 元学习器训练时的输入设计

Stacking元学习器的训练不是直接把基学习器在整个训练集上的预测拿过来训练,而是需要Out-of-Fold(OOF)预测。也就是把训练集分成K折,每个基学习器在K-1折上训练、在剩下1折上预测,循环K次后,所有样本都得到了一个“没见过这个样本才做出的预测”。这些OOF预测组成新特征矩阵,交给元学习器。

这样做的目的很直接:如果元学习器看到的是基学习器在训练集上的预测,那这些预测本身就是基学习器“背过答案”的结果,泛化性极差。用OOF预测训练的元学习器,才能模拟测试时的真实状态。我在初版代码里犯过这个错误,直接拿模型的训练集预测去做元学习器训练,结果测试集R2直接掉到了0.75,而OOF训练后是0.92。

4. MATLAB实操:从数据到Stacking代码

4.1 准备工作:数据划分与归一化

不管用什么模型,数据先行。假设原始数据是X(n行p列)和Y(n行1列)。先做一步分层划分,保证训练集和测试集的分布相似。

rng(42); cvp = cvpartition(size(X,1), 'Holdout', 0.2); idxTrain = training(cvp); idxTest = test(cvp); XTrain = X(idxTrain, :); YTrain = Y(idxTrain, :); XTest = X(idxTest, :); YTest = Y(idxTest, :);

归一化处理:对训练集计算均值和标准差,然后用同一组参数归一化测试集。这里是真正的坑点:如果在归一化时把全部数据混合在一起计算,哪怕是只在归一化阶段引入测试集信息,也会造成数据泄露,测试集指标虚高。我见过很多朋友在这个细节上翻车。

muX = mean(XTrain); sigX = std(XTrain); muY = mean(YTrain); sigY = std(YTrain); XTrainNorm = (XTrain - muX) ./ sigX; XTestNorm = (XTest - muX) ./ sigX; YTrainNorm = (YTrain - muY) ./ sigY; YTestNorm = (YTest - muY) ./ sigY;

预测完成后再反归一化回到原始的物理量纲,方便计算真实误差。

4.2 搭建Stacking整体框架

Stacking框架的核心是一个双层结构,第一层是基学习器,第二层是元学习器。在MATLAB里,我建议先写一个函数train_predict_model,每一种模型都接受训练特征、训练标签、测试特征三个输入,返回训练集OOF预测和测试集预测。

外层用三层嵌套循环:第一层遍历K折,第二层遍历四个基学习器,第三层在每一个基学习器内部完成训练和预测。整个过程用细胞数组保存预测结果。

K = 5; cv = cvpartition(YTrainNorm, 'KFold', K); % 保存OOF预测和测试预测 oofPreds = zeros(size(XTrainNorm, 1), 4); testPreds = zeros(size(XTestNorm, 1), 4); for m = 1:4 for k = 1:K idxTr = cv.training(k); idxVa = cv.test(k); % 根据模型索引m,在当前折上训练并预测 % 预留函数入口,下面各小节给出具体实现 predVal = base_model_predict(m, XTrainNorm(idxTr,:), YTrainNorm(idxTr,:), XTrainNorm(idxVa,:)); oofPreds(idxVa, m) = predVal; testPreds(:, m) = testPreds(:, m) + base_model_predict(m, XTrainNorm(idxTr,:), YTrainNorm(idxTr,:), XTestNorm) / K; end end

这个过程的计算量不小,尤其是BP和SVM训练五轮,再乘以四个模型。实测400个样本、30个特征时,整个流程跑下来大约需要五到十分钟。建议每完成一个模型就打印一行日志,避免等待时心里没底。

4.3 基学习器具体实现代码

下面是四个基学习器的核心实现函数。PLS我用plsregress,预测时把系数矩阵应用到测试集上。注意plsregress返回的BETA是包含截距项的,所以测试集要拼接一列1。

function yhat = pls_predict(Xtr, Ytr, Xte, nLV) [XL, YL, XS, YS, BETA] = plsregress(Xtr, Ytr, nLV); yhat = [ones(size(Xte,1),1), Xte] * BETA; end

SVM直接使用fitrsvm,这里我固定使用RBF核,标准化已提前做,所以不用再开Standardize选项。

function yhat = svm_predict(Xtr, Ytr, Xte) md = fitrsvm(Xtr, Ytr, 'KernelFunction', 'rbf', ... 'KernelScale', 'auto', 'BoxConstraint', 60, ... 'Epsilon', 0.05); yhat = predict(md, Xte); end

BP网络使用fitrnet。节点数15,半精度训练关闭,开标准化。fitrnet在R2021a之后可用,如果你的版本旧,需换成feedforwardnet配合手动train和sim。

function yhat = bp_predict(Xtr, Ytr, Xte) md = fitrnet(Xtr, Ytr, 'LayerSizes', [15], ... 'Activations', 'relu', 'Standardize', true, ... 'Solver', 'lbfgs', 'Verbose', 0); yhat = predict(md, Xte); end

随机森林用fitrensemble,方法选bag,这是MATLAB官方支持的Bagging回归集成。

function yhat = rf_predict(Xtr, Ytr, Xte) md = fitrensemble(Xtr, Ytr, 'Method', 'Bag', ... 'NumLearningCycles', 300, 'Learners', templateTree('MinLeafSize', 5)); yhat = predict(md, Xte); end

把这些函数统一包装成base_model_predict,用switch根据模型索引分发。这样可以避免在双层循环里堆砌重复代码。

4.4 生成OOF预测与元学习器训练

四个基学习器的OOF预测拼成矩阵Z_train,测试集预测拼成Z_test,然后用LSBoost训练元学习器。

Z_train = oofPreds; % n x 4 Z_test = testPreds; % m x 4 metaMd = fitrensemble(Z_train, YTrainNorm, 'Method', 'LSBoost', ... 'NumLearningCycles', 200, 'Learners', templateTree('MaxNumSplits', 5), ... 'LearnRate', 0.1);

LSBoost的核心参数有三个:NumLearningCycles控制迭代轮数,Learners选择弱学习器复杂度,LearnRate是学习率。我在项目里用的弱学习器是深度很浅的决策树,MaxNumSplits设为5,学习率0.1,200轮迭代。如果测试集指标不理想,优先调MaxNumSplits和LearnRate,而不是盲目增加轮数。

4.5 完整预测流程与评估指标

预测时,先用基学习器对测试集做出四列预测,再把四列预测输入元学习器。评估指标我采用R2、均方根误差RMSE和平均绝对误差MAE。

YTestPredNorm = predict(metaMd, Z_test); YTestPred = YTestPredNorm * sigY + muY; % 反归一化 R2 = 1 - sum((YTest - YTestPred).^2) / sum((YTest - mean(YTest)).^2); RMSE = sqrt(mean((YTest - YTestPred).^2)); MAE = mean(abs(YTest - YTestPred));

R2接近1说明模型解释了大部分方差,RMSE和MAE则给出实际误差的绝对尺度。这三个指标一起看,才能评判一个回归模型的好坏——只看R2容易忽略误差分布,只看RMSE又看不出拟合优度。

5. 实测经验与常见问题速查

5.1 OOF预测 vs 训练集预测:Stacking最大的坑

这个问题我在前面已经点过一次,这里再强调一遍。Stacking元学习器的训练特征必须来自OOF预测,如果你图省事直接拿基学习器在训练集上的predict结果来训练元学习器,那测试阶段的表现会断崖式下跌。原理在于,基学习器的训练集预测是“事后的答案”,元学习器从中学不到任何关于泛化的信息。

有一个简单的验证方法:查看元学习器在OOF特征上的R2,如果高得离谱(接近0.99),但测试集R2很低,基本可以判断是特征泄露了。正常情况下的OOF特征R2应该在0.85到0.95之间,不会太高,因为每个基学习器都在OOF折上表现略差一些。

5.2 基学习器参数对Stacking结果的影响

Stacking的好处是降低了单一模型调参的敏感度。我在试验中发现,基学习器不需要各个达到最优,只需达到“合理的次优”即可。比如SVM的BoxConstraint从50调到100,单独预测的R2变化约0.02,但Stacking最终结果几乎不受影响。原因是元学习器会自动调整信任度,一个稍微弱一点的基学习器会被其他模型补回来。

当然这不意味着可以不调参。如果某个基学习器的预测水平太差,比如BP网络的层数设成50层在小样本上几乎崩溃,那它在Stacking里就是纯噪声。至少要保证每个基学习器的单独测试集R2都在0.7以上,否则建议先调好这个模型再放进Stacking。

5.3 LSBoost参数调节经验

LSBoost做元学习器时,MaxNumSplits的取值很关键。我的经验是4到10之间。设得太浅(比如1),每棵树只依赖一个基学习器,学不到交互;设得太深,元学习器会过度拟合OOF特征中的噪声,毕竟OOF预测只有四列特征,树太深很容易把这些有限信息全背下来。

学习率默认是1,我建议调到0.05到0.1之间。学习率越低,需要越多的迭代次数,但泛化效果更稳。先用200轮0.1的组合跑一遍,如果验证指标还有下降趋势,再加到500轮同时把学习率降到0.05。

一个值得注意的现象是:LSBoost对四个基学习器的利用不是均匀的。打印md.EnsemblePredictor的树结构会发现,SVM和BP的预测列在很多分裂节点上被使用,PLS的预测列使用频率略低。这说明对于这份数据,非线性模型提供的信息占比更大。

5.4 常见报错信息排查表

下面这些错误我在跑代码时都实际遇到过,整理成速查表:

报错信息原因解决方法
The tail value of X must be a scalarplsregress的输入维度不匹配检查X是否为数值矩阵,Y是否为列向量
Invalid argument at position 2fitrsvm的超参数名称拼写错误对照文档检查参数名,如KernelFunction不能写成Kernel
Layer sizes must be a positive integerBP层数设成了0或负数fitrnet的LayerSizes至少为1
Parameter 1 must be a M-by-N matrix测试集特征列数和训练集不一致检查原始数据是否有多余列,特征选择时保持一致
Cannot use LSBoost with non-scalar Y元学习器训练时Y不是数值列向量确认YTrainNorm是n乘1的double
Out of memory树的数量或BP网络规模过大降低NumLearningCycles,或者减少BP隐藏层节点数

5.5 一些独家的小技巧

第一,MATLAB的fitrensemble在'Method','LSBoost'时弱学习器必须是回归树,不能换成别的模型。如果你想把元学习器换成SVM或BP,就要手动构建两层结构,先训练基学习器得到OOF预测,再用fitrsvm或fitrnet去拟合Z_train和YTrainNorm。

第二,Stacking的OOF预测计算是整个流程中最耗时的部分。如果你的样本数量超过几千,建议把每个基学习器的训练代码套在parfor里并行跑。需要注意的是,parfor里无法直接使用cvpartition对象索引,需要提前把训练索引和验证索引提取出来。

第三,如果你要做特征重要性分析,可以通过MATLAB的oobPermutedPredictorDeltaError对随机森林进行分析,再把重要性排名靠前的特征与Stacking结果对比。这样能验证模型是否学到了合理的物理规律,而不是纯靠硬拟合。

第四,对回归问题,我建议把结果用散点图展示出来。横轴是真实值,纵轴是预测值,如果点紧密分布在y=x直线附近,说明模型在量纲范围内都是可靠的。如果某个区间偏离明显,说明该区间样本较少,需要补充数据或增加该区间的样本权重。

6. 扩展方向与个人体会

6.1 从回归到分类的迁移思路

这套Stacking框架稍微改一下就可以用于分类问题。基学习器的代码改成fitcecoc做SVM多分类、fitcnet做BP分类、TreeBagger设置分类模式,元学习器把LSBoost换成的AdaBoostM2或者fitcensemble对应方法。核心的OOF机制和双层结构完全不需要变。我做回归做得顺手之后,把同一份数据换成分类标签,测试集准确率提升了约4%,这个框架的可迁移性确实不错。

6.2 增加基学习器数量的思路

四类基学习器是最常见的组合,但不是上限。你还可以加入核岭回归(KRR,用MATLAB的fitrkernel)、广义线性模型(fitglm)、K近邻(fitrknn)作为第五、第六个基学习器。只要这些模型和现有模型的相关性够低,Stacking表现会继续小幅上升。不过要注意,基学习器增加到七八个之后,收益会显著递减,同时OOF计算成本线性上升,建议用特征重要性检验哪些基学习器真正在元学习器中被使用,把不重要的丢弃。

6.3 踩过几次坑之后我的体会

这个项目让我最深刻的体会是:Stacking工程实现的门槛不高,真正拉开差距的是细节。数据泄露、OOF策略、归一化时机、元学习器复杂度控制,每个环节都可以让测试集指标上下浮动0.05以上。初版代码跑出来后,我以为万事大吉,结果发现LSBoost的树深度设为默认时,元学习器几乎把OOF特征全部记住了,测试集R2只比单一模型好一点点。把MaxNumSplits调到5之后,效果一下就上去了。

所以在跑完Stacking之后,我建议你务必做一次“元学习器自身的交叉验证”:把OOF特征矩阵再做一次五折交叉验证,看元学习器的平均R2。如果这个值远高于你在留出测试集上的实际R2,说明元学习器过拟合了,优先降低树的复杂度、增加学习率、减少迭代轮数这三板斧。

最后再说一个小技巧:每次运行前固定随机种子rng(42),保证SVM、BP、RF在OOF交叉验证时随机初始化一致。这样你调整参数时看到的性能变化才是真实的,而不是随机性带来的波动。别小看这一行代码,它能帮你省掉大量排查“为什么改完参数反而变差”的时间。

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

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

立即咨询