拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MATLAB实现RF-RFE特征选择与BP神经网络回归预测

MATLAB实现RF-RFE特征选择与BP神经网络回归预测 做回归预测项目时最头疼的往往不是算法本身而是摆在你面前的那张特征表。特征几个还好说一旦上了几十个上百个很多模型的精度反而会直线下滑——这就是常说的“维数灾难”。我最近在MATLAB里把随机森林递归特征消除RF-RFE和BP神经网络回归预测组合在一起跑通了一套从特征筛选到回归建模的完整流程。这套组合对高维小样本的工业数据、经济数据预测特别实用今天把代码逻辑和踩过的坑一起整理出来。RF-RFE这个名字拆开看其实很直白RF是随机森林Random ForestRFE是递归特征消除Recursive Feature Elimination后面再接一个BP神经网络做回归。整个链路解决的核心问题是——特征太多、样本太少、回归精度上不去。下面我会从原理到代码把这个流程逐步拆开。1. 高维回归为什么“人多反而误事”RF-RFE这个组合的价值起点1.1 维数灾难在回归里的具体表现教科书上特别喜欢把BP神经网络描述成“万能逼近器”但真到高维场景下使用你会发现它并没有那么万能。特征维度上去之后训练样本在高维空间里会被冲得非常稀疏每个样本之间的距离变得几乎差不多模型的泛化能力就跟着崩了。我自己遇到的一个典型情况是手上有一批流程工业的传感器数据原始特征60多个维度样本量却只有两三百条。直接用BP神经网络做回归预测训练集上R²能到0.95以上一到测试集直接跌到0.3左右。这就是典型的过拟合——模型把训练样本中的噪声和随机波动都记住了并没有真正学到特征与目标之间的映射关系。更深层的问题在于高维特征里往往藏着大量的冗余信息和无关信息。有些特征和预测目标之间是线性相关有些是非线性相关有些干脆就是噪声还有一些特征之间彼此高度共线。把这些一股脑全喂给BP网络不仅训练速度变慢还会让梯度更新被无关维度干扰模型的稳定性和可解释性都会变差。1.2 RFE到底在做什么递归消除的思路递归特征消除的思路其实不复杂从完整的特征集出发每一次训练一个模型用这个模型给每个特征的重要程度排个序然后把最不重要的那个特征删掉再用剩下的特征重新训练模型重复这个过程直到特征数达到你想要的规模。这里有一个关键点经常被误解RFE并不是跑一次模型、根据重要性排序直接砍掉一批特征就完事了。因为特征之间存在交互作用一个特征单独看不重要和其他特征组合在一起却可能很重要反过来也一样。一次性砍掉太多特征很容易误伤这些“组合才有价值”的特征。递归、逐步地删除每一步都基于当前特征子集重新评估重要性这才是RFE的核心意义。打个比方这有点像团队裁员。你不能光看个人KPI就把人裁掉还得考虑这个人和团队的协作价值。每一轮裁掉当前状态下贡献最低的人再重新评估剩下的团队配置——这个过程比一次性大裁员要稳妥得多。1.3 为什么评估器偏偏选随机森林RFE中间需要一个模型来评估特征重要性这个模型可以是线性回归、SVM、决策树但我实践下来随机森林是综合表现最稳的选择。第一随机森林对非线性关系敏感。BP神经网络能捕捉非线性但训练成本高、不稳定线性模型又只能捕捉线性关系。随机森林作为树模型的集成天然能处理非线性、特征交互和复杂的决策边界和BP网络回归的逻辑比较匹配。第二随机森林有内建的OOBOut-of-Bag验证机制。每棵决策树大约用63.2%的样本训练剩下的样本就是这棵树的OOB样本。训练结束后可以用OOB样本直接估计泛化误差和特征重要性不需要额外划分验证集。这一点在小样本场景下特别珍贵省出来的样本可以全用在训练里。第三随机森林对噪声和过拟合的容忍度比较高。只要树的棵数够、深度控制得当它对个别异常值的反应不像单棵决策树那么剧烈给RFE提供了一个相对稳定的“打分器”。2. RF-RFE特征筛选的MATLAB实现随机森林回归到底在“筛”什么2.1 MATLAB里训练随机森林回归的正确姿势MATLAB里训练随机森林回归最常用的接口是TreeBagger它属于Statistics and Machine Learning Toolbox。有些人会推荐新版里的fitrensemble接口更现代但TreeBagger有一个优势——特征重要性相关的属性非常直观而且大量老代码和教程都用它网上遇到问题也更容易搜到解决方案。下面是一个典型的随机森林回归训练代码%% 读取数据 data xlsread(sample_data.xlsx); % 最后一列是目标值 X data(:, 1:end-1); Y data(:, end); %% 训练随机森林回归 rng(42); % 固定随机种子保证实验可复现 numTrees 200; rfModel TreeBagger(numTrees, X, Y, ... Method, regression, ... OOBPrediction, on, ... MinLeafSize, 5, ... NumPredictorsToSample, all);这里有几个参数值得说一下。Method必须指定为regression因为我们要做的是回归预测而不是分类OOBPrediction要打开后续计算OOB误差和特征重要性都依赖它MinLeafSize控制叶子节点最小样本数默认值是1但回归任务里我一般会设到5左右这个值太小容易过拟合太大又会让模型过于粗糙需要根据样本量微调。2.2 特征重要性的两种度量方式与选择TreeBagger里特征重要性主要有两种形态很多新手容易混淆。一种是基于分裂增益的度量对应属性是DeltaCriterionDecisionSplit。它的逻辑是在构建每棵树时每当某个特征被选作分裂变量就记录下分裂前后误差准则回归任务里是均方误差的下降量然后把这个下降量累加到对应特征上。这种度量计算快但有一个偏置问题——取值域更宽的特征比如数值波动范围大的传感器读数天然有更多分裂机会重要性容易被高估。另一种是基于OOB置换的度量对应属性是OOBPermutedPredictorDeltaError。做法是先用OOB样本算出模型的预测误差然后对某一列特征做随机打乱permutation再次计算OOB误差两次误差的差值就反映了这个特征对预测的贡献程度。如果打乱某个特征后OOB误差变化很大说明模型非常依赖它重要性就高反之就是可有可无的特征。我在RFE流程里优先会选择置换重要性因为它不依赖具体的分裂过程对特征尺度的偏置也小得多。下面这段代码可以直接提取特征重要性%% 提取OOB置换重要性 imp rfModel.OOBPermutedPredictorDeltaError; %% 排序并画图 [impSorted, idxSort] sort(imp, descend); figure; barh(impSorted); set(gca, YTickLabel, idxSort); xlabel(OOB特征重要性); ylabel(特征编号);注意OOBPermutedPredictorDeltaError是一个向量每个元素对应一个特征。如果某些特征的重要性接近零甚至出现负值说明这些特征对模型大概率是没有正向贡献的可以放心删。2.3 递归消除循环删除步长与停止条件拿到特征重要性之后RFE的循环逻辑就清晰了每一轮用当前特征子集训练随机森林计算重要性删掉重要性最低的那一个特征然后进入下一轮。这里有一个工程上的取舍一次删一个特征最稳妥但如果原始特征有几百个这个方法会非常慢因为每轮都要重新训练一次随机森林。我的做法是设置一个动态步长特征数量多的时候一次删掉几个或按比例删特征数量少的时候再回到一次删一个精修。%% RFE主循环 remainingIdx 1:size(X, 2); history []; % 记录每轮剩余特征数和OOB误差 while length(remainingIdx) 1 % 训练当前特征子集下的随机森林 rfCurrent TreeBagger(numTrees, X(:, remainingIdx), Y, ... Method, regression, ... OOBPrediction, on, ... MinLeafSize, 5); % 计算当前子集的OOB误差 oobErr oobError(rfCurrent, Mode, ensemble); history [history; length(remainingIdx), oobErr(end)]; % 提取重要性并找到最不重要的特征 impCurrent rfCurrent.OOBPermutedPredictorDeltaError; [~, minIdx] min(impCurrent); remainingIdx(minIdx) []; end停止条件可以有两种思路一种是直接指定最终保留的特征个数比如我预先知道业务上最多保留10个特征就直接在while条件里限制另一种是不指定数量跑完整个循环后画“特征数-OOB误差”折线图在图上找误差开始回升的“手肘点”那个点对应的特征数就是比较合理的保留数量。第二种做法更数据驱动但需要你跑完整轮循环耗时更长。3. BP神经网络回归段结构、归一化、训练函数怎么搭配才稳3.1 网络结构从哪定输入维度与隐藏层节点特征筛选完成后保留下来的是对预测最有贡献的那部分特征。接下来把这些特征作为BP神经网络的输入输出层是1个节点对应回归目标值。隐藏层的数量我个人的建议是从单隐藏层起步。对绝大多数回归任务来说单隐藏层加足够多的节点已经能逼近任意连续函数多隐藏层反而会带来训练难度和过拟合风险的增加。如果你发现单隐藏层怎么调都拟合不好再考虑加第二层但这种情况在特征已经经过筛选的前提下很少发生。隐藏层节点数没有绝对公式工程上有几个常用的经验参考sqrt(m n) a其中m是输入节点数n是输出节点数a取1到10之间的整数2m 1即输入节点数的两倍加一ceil(m / 2)输入节点数的一半适合追求简洁模型的场景假设RFE筛完剩10个特征输出是1个节点那么用第一个公式节点数大致在sqrt(11)a也就是4到14之间的范围。我通常的做法是从8开始试每次增加2观察测试集R²和RMSE的变化取一个“再增加节点收益很小”的点停下来。3.2 归一化是回归任务的“隐形杀手”BP神经网络对输入数据的尺度高度敏感。如果某个特征数值范围是0到1另一个特征数值范围是几千到几万权重更新会被大数值的特征主导小数值特征的信息很容易被淹没。所以在把数据喂给网络之前必须做归一化。MATLAB里最常用的工具是mapminmax它会将数据线性映射到[-1, 1]或[0, 1]区间。很多人在这里踩过一个非常隐蔽的坑训练集和测试集必须使用同一套归一化参数。正确做法是先用训练集计算归一化参数然后把这个参数直接应用到测试集上%% 归一化训练集 [X_train_norm, ps_X] mapminmax(X_train, 0, 1); [Y_train_norm, ps_Y] mapminmax(Y_train, 0, 1); %% 用训练集的参数归一化测试集 X_test_norm mapminmax(apply, X_test, ps_X);注意mapminmax默认按行处理所以输入矩阵需要转置让每个特征占一行、每个样本占一列。如果你看到X_train是[样本数, 特征数]的常规布局直接调用mapminmax会得到一个按列归一化的结果而不是按特征归一化——这一点一定要留意。3.3 训练函数选择trainlm还是trainbrMATLAB的神经网络工具箱提供了多种训练函数回归任务里最常碰见的是trainlmLevenberg-Marquardt、trainbr贝叶斯正则化和trainscg量化共轭梯度。trainlm是默认选项收敛速度快适合样本量中等、数据质量比较好的场景。但它对初始权重敏感偶尔会掉进局部最优且内存占用较高。trainbr在我的使用体验里对“高维小样本”场景反而更友好。它在训练过程中引入了正则化项自动平衡拟合误差和权重大小不太容易过拟合。代价是训练速度慢一些尤其在网络结构较大的时候。trainscg适合大规模数据内存占用小但收敛精度一般我在这个流程里基本不用它。给个简单的选择参考训练函数收敛速度过拟合风险适用场景trainlm快中样本量几百以上数据质量好trainbr慢低高维小样本需要正则化trainscg中中样本量大内存受限代码里指定训练函数其实一行就够net.trainFcn trainbr;如果样本量确实很少比如不到200条我建议直接上trainbr。RF-RFE筛完特征之后即使剩下10个特征BP网络的可调参数依然不少没有合适的正则化机制很容易把训练集背下来。3.4 防止过拟合的实用手段除了换训练函数还有几个不起眼但效果明显的操作。第一数据划分方式。如果数据之间存在时间顺序比如故障预测、负荷预测不要用dividerand随机打乱应该用divideblock按顺序切分——先用前70%训练、后30%测试这样能真实反映模型对未来数据的预测能力。如果数据本身是独立同分布的样本才可以用随机划分。MATLAB里在net.divideFcn里指定即可。第二训练目标trainParam.goal别设得太极端。有些人追求训练误差无限小把goal设成1e-8最后往往只是得到一个把噪声都学进去的模型。回归任务里1e-4到1e-5已经足够。第三训练轮数epochs设大一点但配合提前停止。net.trainParam.max_fail默认是6代表验证集误差连续6轮不下降就停止训练这个机制很好用不要关掉。4. 两段链路合流从特征排序到闭环预测的完整代码4.1 数据划分回归场景下的划分原则我在前面的小节里已经提了一下divideblock这里展开说一下整体划分思路。如果你做的是普通回归预测样本不涉及时序关系可以用cvpartition做分层划分或者直接用rng加randperm随机打乱后按比例切分。但如果你手里的数据是时间序列比如每隔10分钟采集一条的传感器数据随机打乱就是灾难——模型会把未来的信息泄露到训练集里测试集上的漂亮成绩全是假的。正确的做法是严格按照时间顺序切分前60%到70%做训练剩下的做测试。如果需要调参还可以从训练集中再切出一部分做验证。这样评测出来的精度才有实际参考价值。4.2 完整代码串联下面是我实际项目里跑通过的一个完整流程从数据读取到特征选择、BP训练、预测输出一步到位数据格式前几列特征最后一列目标值%% RF-RFE-BP 完整流程 clear; clc; close all; rng(42); %% 1. 读数据 data xlsread(your_data.xlsx); X data(:, 1:end-1); Y data(:, end); [N, M] size(X); %% 2. RF-RFE特征选择 numTrees 200; remainingIdx 1:M; history []; while length(remainingIdx) 1 rfModel TreeBagger(numTrees, X(:, remainingIdx), Y, ... Method, regression, ... OOBPrediction, on, ... MinLeafSize, 5); oobErr oobError(rfModel, Mode, ensemble); history [history; length(remainingIdx), oobErr(end)]; imp rfModel.OOBPermutedPredictorDeltaError; [~, minIdx] min(imp); remainingIdx(minIdx) []; end % 查看RFE过程曲线 figure; plot(history(:, 1), history(:, 2), o-); xlabel(剩余特征数); ylabel(OOB误差); % 假定根据折线图手肘点选择保留8个特征 finalIdx remainingIdx(1:8); % 简化示意实际应从history反查手肘点 X_selected X(:, finalIdx); %% 3. 划分训练集和测试集 trainRatio 0.75; numTrain floor(N * trainRatio); X_train X_selected(1:numTrain, :); Y_train Y(1:numTrain, :); X_test X_selected(numTrain1:end, :); Y_test Y(numTrain1:end, :); %% 4. 归一化 [X_train_norm, ps_X] mapminmax(X_train, 0, 1); [Y_train_norm, ps_Y] mapminmax(Y_train, 0, 1); X_test_norm mapminmax(apply, X_test, ps_X); %% 5. 构建并训练BP神经网络 hiddenNum 10; net feedforwardnet(hiddenNum, trainbr); net.trainParam.epochs 1000; net.trainParam.goal 1e-5; net.trainParam.showWindow false; [net, tr] train(net, X_train_norm, Y_train_norm); %% 6. 预测和反归一化 Y_pred_norm net(X_test_norm); Y_pred mapminmax(reverse, Y_pred_norm, ps_Y); %% 7. 评估指标 R2 1 - sum((Y_test - Y_pred).^2) / sum((Y_test - mean(Y_test)).^2); RMSE sqrt(mean((Y_test - Y_pred).^2)); MAE mean(abs(Y_test - Y_pred)); fprintf(R2 %.4f\nRMSE %.4f\nMAE %.4f\n, R2, RMSE, MAE); %% 8. 画对比图 figure; plot(Y_test, o-); hold on; plot(Y_pred, *-); legend(真实值, 预测值); xlabel(样本序号); ylabel(目标值);注意我在第2步末尾做了简化处理。实际操作中remainingIdx最后会收敛到只剩1个特征但我建议在第2步循环里把每一轮的特征子集都存下来等循环结束后根据history绘制OOB误差随特征数变化的曲线找到误差最小或刚要反弹的位置再从history里反查出对应的remainingIdx。否则你选择“保留几个特征”时就没有数据支撑了。4.3 评估指标不是只看R²就完事了很多人跑完模型只打印一个R²看到0.9就觉得稳了。但在回归预测里R²高不代表误差小——如果目标值本身的方差很小哪怕预测误差非常小R²都可能偏低反过来如果目标值波动大R²看起来不错实际RMSE也可能大得吓人。所以我会同时看三个指标R²衡量模型对目标值方差的解释程度接近1为好RMSE强调大误差的惩罚适合评估预测值偏离真实值的整体程度MAE对大误差和小误差一视同仁反映平均绝对偏差在看预测效果图时我还会特别关注预测值在真实值峰值附近的表现。很多模型的整体R²不错但在峰值段严重滞后或偏低——如果你做的是设备故障预警或负荷预测峰值段预测不准往往才是最致命的。5. 调参经验与避坑记录特征删到几维、网络建到几层5.1 RFE折线图怎么看手肘点与特征数选择RFE跑完后你手里会有一条“剩余特征数-OOB误差”曲线。这条曲线的形状通常是从左到右先下降、后上升。特征特别多的时候模型比较冗余误差偏高随着无用特征被删掉模型逐渐变精炼误差下降但如果删过头了关键特征被误删误差又会反弹。最理想的特征数就在误差曲线的最低点附近或者曲线下降趋势由陡变缓的“手肘”位置。我一般会取曲线上OOB误差最小的那个特征数再结合业务解释性做微调。比如误差最小区间对应8到12个特征而业务上希望模型尽量简单那我会取8。有一点要提醒RFE每一轮删除的只是“当前子集下最不重要的特征”这个最优子集是基于随机森林视角得到的。它不保证对BP神经网络来说也是全局最优但实践下来RF筛出的特征子集通常能显著改善BP的预测效果因为二者都擅长捕捉非线性关系特征偏好比较一致。5.2 一次删一个还是删一批速度与精度的权衡前面代码里用的是每轮删一个特征这是最严谨的做法。但如果原始特征有100个以上这个过程会非常耗时每轮都要重新训练200棵树的随机森林我实测跑100轮大概需要十几分钟到半小时具体取决于机器配置。如果特征很多、时间紧张可以把每轮删除量设置成动态的特征数大于50时每轮删掉当前特征数的10%特征数在20到50之间时每轮删2到3个小于20时再恢复一次删一个。这样前期的粗糙筛选速度很快后期精修又能保证质量。还有一种常见做法是先用随机森林的重要性排序一次性砍掉明显无关的特征重要性接近0的那批再用RFE做精细筛选。我在处理80多个原始特征的流程工业项目时用这个方法把每轮循环次数直接压缩掉了将近一半。5.3 BP网络常见翻车现场做BP回归这几年我见过太多人对着同样的报错或者糟糕结果反复折腾这里集中列几个容易出问题的点。数据转置问题。feedforwardnet的输入要求是[特征维度, 样本个数]。如果你的矩阵是[样本个数, 特征维度]不转置直接喂进去网络会把每个样本当成一个特征训练出的模型毫无意义而且极其容易被“良好的训练误差”误导。归一化参数问题。测试集一定不能用它自己算出来的mapminmax参数必须用训练集的ps_X。否则训练集和测试集的数值尺度不一致模型在测试集上就像面对了一个完全陌生的输入空间。隐藏层节点数盲目贪多。我见过有人把隐藏层节点数直接设成输入特征数的5倍结果训练集表现好得不得了测试集一塌糊涂。隐藏层节点数不是越多越好每增加一个节点都会增加大量可调参数。小样本场景下节点数宁少勿多配合trainbr的正则化效果更稳。学习率设得过大。net.trainParam.lr默认是0.01有些人为了让训练更快改成0.1甚至更大结果损失函数振荡发散。在小样本回归任务里0.01通常已经够用了如果收敛太慢优先检查数据是否归一化而不是盲目调大学习率。5.4 关于随机种子和可复现性BP神经网络的初始权重是随机的随机森林的样本抽样也是随机的。同一个流程不固定随机种子两次跑出来的结果可能差不少。这时候不要急着怀疑代码先想想是不是没固定rng。我习惯在每个流程开头统一加一行rng(42)或任意固定种子。这样实验至少是可复现的你调的参数才谈得上有可比性。如果你发现某个网络结构在某些随机种子下忽好忽坏说明模型稳定性欠佳这时候最该做的是降低模型复杂度或增加正则化而不是反复换随机种子找一个“好看的结果”。最后说一个实操心得RF-RFE筛出来的特征子集并不是只能配BP用。同样的筛选结果拿去喂SVR、高斯过程回归或者其他树模型也常常能看到明显提升。我最近就在拿这套特征选择结果做多模型对比实验效果比直接拿全特征建模好不少。如果你也在做高维回归预测建议从这套流程跑起把特征筛选环节固定下来再慢慢试不同的回归器大概率能成为你日常建模的一条可靠路径。
返回列表