资讯详情

资讯详情

随机森林MATLAB代码:TreeBagger调参与排错

简介随机森林MATLAB代码压缩包面向希望在MATLAB环境中实现随机森林分类与回归的学习者和开发者适合机器学习课程实验、算法复现及小规模数据建模练习。包内共14个文件约211KB包含m函数脚本、txt示例数据、Fortran源码、dll动态库以及readme和doc说明文档其中m文件对应分类、回归与模型打印入口txt提供训练测试样本dll与Fortran部分承担底层计算便于理解从接口调用到核心实现的完整结构。该资源已有6072人学习配套示例覆盖分类与回归两条使用路径读者可借此观察随机森林的样本与特征采样、决策树集成输出及特征重要性查看方式并对照说明文档梳理目录组织、依赖配置与模型评估思路作为算法学习和代码研读的参考资料。1. 一段跑不通的脚本暴露了随机森林 MATLAB 代码的三个前提很多人找随机森林 MATLAB 代码拿到的第一份东西通常是三五行load 一份数据TreeBagger 建三十棵树predict 完打印一个准确率。这段脚本在自带示例数据上能刷到 0.9 以上换到自己的表上就翻车——要么类别变量直接报类型错误要么准确率死死卡在多数类占比上不动看着像模型没学实际上是数据和接口没对齐。随机森林在 MATLAB 里上手门槛很低但 TreeBagger 与 fitcensemble 两条路线各有硬性输入约束树数、叶子大小、每次分裂的特征数这三个需要调整的参数又互相牵制调错一个结果就不可信。这篇按「选哪条路 → 数据怎么喂 → 参数怎么调 → 报错怎么查」的顺序给一份能改能跑、踩过坑再写出来的实现说明适合手里已经有一张特征表、打算把 RF 当基线模型的从业者也适合课设阶段需要一份既能运行又能解释清楚的同学。2. 三条实现路线的选型TreeBagger、fitcensemble 与手写脚本MATLAB 里做随机森林本质上是三种东西一个封装好的老牌类、一个能接进交叉验证流水线的建模函数、一份可以完全控制分裂准则的自写脚本。选错路线不是精度问题而是后面每一步都别扭——想算 OOB 误差却发现没开预测、想接超参搜索却发现接口不兼容、想把模型导出到别的语言却只能对着对象干瞪眼。所以先把选型摆清楚。2.1 TreeBagger分类回归一套接口的老牌实现TreeBagger 是最直接的入口一行构造自动完成自助采样bootstrap、随机特征子集、多树汇总投票还内置了袋外样本out-of-bagOOB的评估能力。它的价值在于「不用自己写循环」每棵树的训练样本由有放回抽样产生约 36.8% 的样本不进袋这些样本天然构成一个不花钱的验证集能在不切分数据的前提下给出误差估计。对样本量小、切出去一块验证集就心疼的场景这一点很实用。% X: n×p 数值特征矩阵Y: n×1 的 categorical 标签 rng(2024); % 固定随机种子保证结果可复现 rf TreeBagger(200, X, Y, ... Method, classification, ... % 分类任务回归改成 regression OOBPrediction, on, ... % 开启袋外预测后面才能算 OOB 误差 OOBPredictorImportance, on, ... % 同时计算置换重要性 MinLeafSize, 5, ... % 叶节点最小样本数控制单棵树深度 NumPredictorsToSample, all, ... % 每次分裂考察的特征数 PredictorSelection, allsplits, ... % 用标准 CART 穷举分裂而非曲率检验 InBagFraction, 1, ... % 自助采样比例1 表示抽 n 个 UseParallel, true); % 需要并行工具箱否则会报错 Yfit predict(rf, X); % 返回 cell 数组元素是字符标签 Yfit categorical(Yfit); % 转回 categorical 才能直接比标签 acc mean(Yfit Y); fprintf(训练集准确率 %.4f\n, acc);代码里几个参数值得单独说清楚。NumTrees取 200 是经验起点不是定值后面 4.1 会用 OOB 曲线判断到底该停在哪。MinLeafSize从默认的 1 提到 5是给噪声留一点缓冲叶子太纯的树在训练集上几乎满分测试集上掉得厉害。NumPredictorsToSample设成all意味着每次分裂都用全部特征这其实弱化了「随机」这一半分类任务常见做法是取特征数的平方根取整回归取三分之一。PredictorSelection默认是curvature曲率检验偏向能产生强交互的分裂想做教科书式的标准 CART 就改成allsplits两者在小样本上的结果差异可能达到好几个百分点别默认它俩等价。需要留意的是predict的返回类型。分类模型返回的是 cell 数组里面装的是字符直接和数值标签比较会得到一个全 false 的逻辑向量然后你会以为模型废了。转成 categorical 或者用str2double走一遍是必须补的一步。2.2 fitcensemble能挂进交叉验证和超参搜索的版本如果后面打算做 K 折交叉验证、网格搜索、或者把模型塞进一条预处理加建模的流水线fitcensemble更顺。它把「集成方法」和「基学习器」拆开了Method选Bag就是袋装基学习器用templateTree描述树的参数写在模板里而不是散在函数参数上。t templateTree( ... MinLeafSize, 5, ... NumVariablesToSample, all, ... % 等价于 TreeBagger 的 NumPredictorsToSample Surrogate, off); % 关闭代理分裂缺失值需提前处理 mdl fitcensemble(X, Y, ... Method, Bag, ... % 袋装AdaBoostM2 等是提升法 NumLearningCycles, 200, ... % 基学习器个数对应 NumTrees Learners, t, ... Prior, uniform); % 类别先验处理不平衡时改成 empirical cvmdl crossval(mdl, KFold, 5); % 直接得到交叉验证模型 fprintf(5 折误差 %.4f\n, kfoldLoss(cvmdl));这套写法的好处是crossval、kfoldLoss、oobLoss、plotPartialDependence全都认这个对象不用自己写循环去拼混淆矩阵。Prior参数也是 TreeBagger 没有直接对应物的东西类别不平衡时把先验改成uniform或者传入自定义向量往往比调阈值更省事。代价是参数名和 TreeBagger 不完全一样NumLearningCycles对应树数、NumVariablesToSample对应每次分裂的特征数来回切换时容易写错。两条路线的取舍可以压成一张表维度TreeBaggerfitcensembleMethodBag分类/回归统一接口靠Method一个参数切换同一个函数靠标签类型自动判断OOB 误差oobError/oobLossoobLoss(mdl,Mode,cumulative)交叉验证需自己写 K 折循环直接crossvalkfoldLoss超参自动搜索不支持需外层包 bayesopt支持OptimizeHyperparameters特征重要性OOBPermutedPredictorDeltaErrorpredictorImportance 置换法偏依赖图需手动计算plotPartialDependence直接出图数据量大、只想要一个能跑的基线TreeBagger 更省事要做实验对比、要出可复现的交叉验证数字fitcensemble的流水线更稳。2.3 手写版只在这三种情况下值得自己写递归建树不是炫技绝大多数场景纯属浪费时间。真正值得动手的情况只有三类一是分裂准则要改比如你需要代价敏感的分裂或者自定义的不纯度二是模型要导出到 C 或嵌入式环境MATLAB 的对象没法直接落地得自己序列化成数组三是教学或答辩场景需要指着每一层节点解释「这棵树为什么在这里切」。给一份最小骨架能说明 bagging 内部到底在干什么function node growTree(X, y, depth, maxDepth, minLeaf, mtry) % 单棵 CART 分类树的最小实现省略缺失值处理与剪枝 if depth maxDepth || numel(unique(y)) 1 || size(X,1) 2*minLeaf node struct(leaf, true, label, mode(y)); return; end p size(X,2); featIdx randperm(p, min(mtry,p)); % 随机挑 mtry 个特征这是随机森林的一半 bestGain -inf; bestFeat 0; bestThr 0; parentImp giniImp(y); for f featIdx vals unique(X(:,f)); if numel(vals) 2, continue; end thrCand vals(1:end-1) diff(vals)/2; % 相邻取值中点作候选阈值 for t thrCand left X(:,f) t; right ~left; if nnz(left) minLeaf || nnz(right) minLeaf, continue; end gain parentImp - (nnz(left)*giniImp(y(left)) ... nnz(right)*giniImp(y(right))) / numel(y); if gain bestGain bestGain gain; bestFeat f; bestThr t; end end end if bestFeat 0 node struct(leaf, true, label, mode(y)); return; end left X(:,bestFeat) bestThr; node struct(leaf, false, feat, bestFeat, thr, bestThr, ... left, growTree(X(left,:), y(left), depth1, maxDepth, minLeaf, mtry), ... right, growTree(X(~left,:), y(~left), depth1, maxDepth, minLeaf, mtry)); end function g giniImp(y) % 基尼不纯度分类树最常用的分裂指标 [~,~,idx] unique(y); p accumarray(idx, 1) / numel(y); g 1 - sum(p.^2); end外层再套一层 bootstrap 采样和投票就是完整森林。这段代码的性能比内置实现差一到两个数量级因为它没有向量化候选阈值、没有做排序优化也完全没有剪枝和缺失值处理只建议用来理解结构或者做定制化改造不要拿它去跑十万行数据。3. 预处理链路把表、缺失值和类别变量对齐到 RF 的输入约定随机森林对特征缩放不敏感这容易让人误以为它对数据形态也没要求。实际上 MATLAB 的实现有一套明确的输入约定矩阵还是 table、标签是 categorical 还是字符 cell、缺失值走哪条路径任何一条不对报错信息都含糊得让人抓瞎。3.1 table 输入与类别变量的三种处理最省事的做法是把整张表连同标签一起喂进去让实现自己去识别哪一列是类别变量。table 的优势是保留了变量名后面看特征重要性时直接输出名字不用回头对列号。T readtable(features.csv, TextType, string); labelName label; predictorNames setdiff(T.Properties.VariableNames, labelName, stable); % 字符串列转 categorical避免被当成 p 个哑变量处理 for k 1:numel(predictorNames) if isstring(T.(predictorNames{k})) || iscellstr(T.(predictorNames{k})) T.(predictorNames{k}) categorical(T.(predictorNames{k})); end end X T(:, predictorNames); Y categorical(T.(labelName)); rf TreeBagger(200, X, Y, Method, classification, ... CategoricalPredictors, all); % 显式声明非数值列按类别变量处理TextType,string这一步容易被忽略。默认读进来是 cell 字符数组某些版本里categorical转换会给出警告甚至失败统一成 string 再转 categorical 就干净了。CategoricalPredictors写成all是让实现自己对非数值列做处理但如果表里混进了 ID 列这种高基数变量几千个不同取值随机森林会把它当成一个极强的分裂特征训练集精度飙高、测试集惨不忍睹——这类列在建表阶段就该删掉别指望模型自己判断。3.2 缺失值填补、代理分裂、还是直接删老版本的 TreeBagger 遇到 NaN 会直接抛错新版本和fitcensemble的templateTree支持Surrogate,all做代理分裂也就是在最优分裂特征缺失时用相关的替代特征顶上。代理分裂在缺失比例低于 5% 时效果不错缺失比例一高代理分裂本身也没得选还是老老实实填补靠谱。% 数值列中位数填补类别列众数填补同时打缺失标记 Xnum varfun(median, X, InputVariables, isnumeric, ... OutputFormat, uniform); for k 1:width(X) col X{:,k}; if isnumeric(col) miss isnan(col); if any(miss) X{miss,k} median(col(~miss)); % 中位数对偏态分布更稳 end else miss isundefined(col); if any(miss) X{miss,k} mode(col(~miss)); % 类别列用众数 end end if any(miss) X.([miss_ X.Properties.VariableNames{k}]) double(miss); % 缺失本身就是信息 end end补一个缺失指示列的做法在小样本上很值缺失往往不是随机的某个字段空着本身就带信号把它当特征喂进去比单纯填补保留的信息更多。反过来如果某一列缺失超过 30%更常见的处理是直接删列因为填补出来的值基本上等于在主要分裂上人为制造了一个众数堆积会系统性地带偏重要性排序。3.3 划分分层 Hold-Out 与 K 折的对象设计类别不平衡时最怕的就是普通随机划分某次抽样某个少数类一个都没进训练集模型直接学不会。cvpartition的第一参数传分类标签时默认做分层抽样这一条能挡掉不少意外。cp cvpartition(Y, HoldOut, 0.3); % 传标签向量自动分层 idxTrain training(cp); idxTest test(cp); rf TreeBagger(200, X(idxTrain,:), Y(idxTrain), ... Method, classification, OOBPrediction, on); Ytest categorical(predict(rf, X(idxTest,:))); C confusionmat(Y(idxTest), Ytest); acc sum(diag(C)) / sum(C(:)); fprintf(测试集准确率 %.4f\n, acc);分层之后的第二个坑是顺序cvpartition给出的是逻辑索引如果先对整张表做了标准化、特征筛选或者缺失值填补再划分那么测试集的信息已经漏进训练过程OOB 误差和测试误差会出现明显背离。正确顺序永远是「先切分、再在训练集上拟合任何变换、最后用训练集的统计量去处理测试集」。4. 参数调整树数、叶子大小、特征子集与自动调参随机森林被叫「几乎不用调参的模型」这句话只在「能跑出个结果」的前提下成立。真实项目里树数决定训练成本叶子大小决定过不过拟合每次分裂的特征数决定树之间的相关性三个参数串起来看才有效。4.1 NumTrees看 OOB 曲线什么时候走平树数不是越多越好而是「加到 OOB 误差不再明显下降」为止。判断方法很直接开OOBPrediction,on用oobError取随树数累积的误差序列画出来看拐点。rf TreeBagger(300, X, Y, Method, classification, ... OOBPrediction, on, MinLeafSize, 5); err oobError(rf); % 长度 300第 k 个是前 k 棵树的 OOB 误差 [~, kBest] min(err); fprintf(OOB 最优树数 %d误差 %.4f\n, kBest, err(kBest)); fprintf(300 棵树时误差 %.4f进一步增加树数收益有限\n, err(end)); figure; plot(err, LineWidth, 1.2); grid on; xlabel(树的数量); ylabel(OOB 分类误差);曲线在 100 到 150 棵之间通常已经形成平台之后再加树只是线性增加训练时间。需要注意的是oobError返回的是累积误差第 k 个元素对应前 k 棵树的投票结果所以它天然单调性很差、会抖动——抖动幅度大的时候说明数据本身噪声重这时候盯着最小值去取树数反而过拟合了 OOB取平台起点更稳。300 棵是个保守上限演示够用生产环境按数据量自己压。4.2 MinLeafSize偏差与方差最直接的旋钮叶子最小样本数是控制单棵树复杂度的主要参数。值越小树越深训练集误差越低方差越大值越大树越浅偏差上升但对噪声的容忍度更好。分类任务上1 到 10 是常见搜索区间回归任务建议从 5 起步。leafList [1 3 5 10 20 50]; oob zeros(size(leafList)); for i 1:numel(leafList) rf TreeBagger(150, X, Y, Method, classification, ... OOBPrediction, on, MinLeafSize, leafList(i)); oob(i) oobError(rf, Mode, last); % 只看最终树的 OOB 误差 end [bestErr, bestIdx] min(oob); fprintf(最优 MinLeafSize %dOOB 误差 %.4f\n, leafList(bestIdx), bestErr);oobError(rf,Mode,last)跟默认的累积序列不是一回事加Mode,last拿到的是最终集成的单个误差值做网格循环时用它更省事。如果这条曲线一路往下走到 1 才停通常说明数据里的噪声不多、或者样本量够大如果曲线在 10 附近触底再往上走就要考虑特征里是不是混进了大量弱相关列。4.3 NumPredictorsToSamplemtry 的经验值与实测这是随机森林「随机」的核心参数。每次分裂只从 p 个特征里抽 m 个候选m 越小树之间越不相关集成的方差降得越多但单棵树的精度也越低。经验值是分类取 √p、回归取 p/3这个起点在大部分数据上不会太离谱但不是最优。p size(X, 2); mtryList unique(round([sqrt(p), p/3, p/2, p])); % 经验值加两个扩展点 for i 1:numel(mtryList) rf TreeBagger(150, X, Y, Method, classification, ... OOBPrediction, on, NumPredictorsToSample, mtryList(i)); fprintf(mtry%3d OOB误差%.4f\n, mtryList(i), oobError(rf,Mode,last)); end一个常见误用是把 mtry 设成 p也就是每次分裂都用全部特征然后把模型叫作随机森林。这时候它退化成了纯 bagging树之间的相关性明显提高集成降方差的效果打折在特征高度相关的数据上尤其明显。实测中如果 mtryp 和 mtry√p 的 OOB 误差差不多选 √p 更划算因为训练快、树之间更独立。4.4 自动调参与并行把网格交给 bayesopt参数超过三个之后手写循环的组合爆炸会吞掉大量时间。fitcensemble支持内置的贝叶斯优化把搜索空间一次性描述清楚让它自己找。rng(2024); t templateTree(Surrogate, off); mdl fitcensemble(X, Y, Method, Bag, Learners, t, ... OptimizeHyperparameters, {NumLearningCycles, MinLeafSize, ... NumVariablesToSample}, ... HyperparameterOptimizationOptions, struct( ... MaxObjectiveEvaluations, 30, ... % 目标函数评估次数30 起步 UseParallel, true, ... % 并行评估需要并行工具箱 Repartition, true, ... % 每轮重新划分降低划分偏差 Verbose, 1));Repartition,true值得开着它让每轮评估用不同的交叉验证划分避免某组参数恰好抽到一份「好切分」而虚高。MaxObjectiveEvaluations给 30 次是在时间和收益之间的折中数据量大的时候可以先在小样本上搜一轮拿到大致范围再回到全量数据上手工微调。并行开启前最好先手动起一次本地池否则第一次调用会额外花时间初始化。5. 常见排查随机森林 MATLAB 代码跑不动、跑不准的五个现场调参投入再多也挡不住输入层面的一些低级错误。总结下来有五类高频现场每次排查都从这几条过一遍能省掉大量猜测。5.1 准确率恰好等于多数类占比现象测试集准确率 0.83而训练集里正类占比正好 83%混淆矩阵显示模型把所有样本都判成了多数类。原因类别严重不平衡随机森林的投票机制下少数类的票数被压过去树在分裂时也倾向于先把多数类分干净因为它对整体不纯度的贡献更大。这时准确率这个指标本身已经失效。解决三条一起上更稳——cvpartition传标签做分层抽样保证每折里少数类都有fitcensemble把Prior设成uniform压制先验带来的偏置评估指标换成confusionmat之外看召回率或者 F1别只看准确率。如果少数类样本少于几百条还要考虑过采样但过采样必须在训练折内部做不能在划分前对全量数据做。5.2 标签类型报错或者预测结果全 false现象训练阶段报Y must be a vector或者代码没报错但Yfit Y全是 false。原因标签是字符串 cell 或者表格里的 string 列predict返回的是字符 cell 数组直接和数值或 categorical 标签比较类型不匹配比较结果全是 false看起来像模型完全没用。解决训练前统一Y categorical(T.label)预测后立刻Yfit categorical(predict(rf, Xtest))再做比较。回归任务同理TreeBagger的回归模型predict返回的是数值矩阵但如果是多输出或者用了 cell 输入也要检查维度对齐。这类问题不会给明确报错只能靠打印前几行class(Y)和class(Yfit)定位。5.3 内存爆掉或者训练慢到无法接受现象树数加到 500 之后训练要跑半小时内存占用飙到十几 G机器开始交换。原因每棵树都把完整训练集副本存进对象里MinLeafSize太小时单棵树节点数暴涨再加上OOBPrediction会额外保存袋外索引内存是乘法级增长的。解决先把MinLeafSize从 1 提到 5 到 10节点数能降一个量级UseParallel打开用多核分摊时间但要确认并行池的进程数不要超过物理核数超了反而更慢树数按 4.1 的 OOB 曲线取平台起点别一上来就 1000 棵。特征维度超过几百列时先做一轮基于重要性的粗筛再重新训练比硬扛着跑全量特征划算得多。5.4 OOB 误差很低但测试集一塌糊涂现象训练时 OOB 误差 0.06交叉验证或者独立测试集误差 0.25差距大得不正常。原因数据泄漏。最常见的是在划分之前对整张表做了标准化、缺失值填补、特征筛选或者 SMOTE 过采样测试集的分布信息通过这些步骤渗进了训练过程。另一种是同一实体有多条记录被分到了训练集和测试集两侧模型实际上在背答案。解决把整条流水线重排成「切分 → 训练集上拟合变换 → 应用到测试集」。如果有分组结构同一个体多次测量、同一批次多个样本用cvpartition的分组形式保证整组进同一侧。改完之后 OOB 和测试误差的差距通常会收敛到 3 到 5 个百分点以内还收不拢就要怀疑特征里混了标签派生的列。5.5 特征重要性全是零或者与业务直觉相反现象OOBPermutedPredictorDeltaError返回一列数前几个全是 0或者排第一的是一个业务上明显无关的字段。原因三个可能——一是特征之间高度相关重要性被平摊到几个相关列上单独看每个都不高二是标签编码或者 ID 列被当成了有效特征模型找到了捷径三是树太浅某些特征根本没被用到重要性自然为 0。解决先把 ID 类、时间戳类、索引类列手工剔掉再重跑一次。相关特征的问题用分组置换重要性处理把一组相关特征同时打乱再算精度下降而不是逐个打乱。树太浅就按 4.2 把MinLeafSize降回来。排查时用predictorImportance和置换重要性各算一遍两个结果差异很大的那几列往往是需要单独看一眼的。6. 进阶把 OOB 误差、置换重要性和偏依赖用成决策依据模型跑通只是起点真正决定这份代码有没有用的是后面三件事误差估计可不可信、特征排序能不能指导下一步采集、单特征的影响方向说不说得清。误差估计上我习惯同时看三个数OOB 误差、5 折交叉验证误差、独立测试集误差。三个数互相之间的差距比绝对值更有信息量——OOB 和交叉验证差得多说明样本顺序有结构交叉验证和测试集差得多说明分布漂移或者泄漏。这三个数都稳定在同一个量级模型才算立住。特征重要性不要只信内置那一列。手写一遍置换重要性逻辑简单但可控把某一列打乱重新预测看精度掉多少掉得越多越重要。重复多次取均值能压掉随机性。baseAcc mean(categorical(predict(rf, Xtest)) Ytest); imp zeros(1, size(Xtest,2)); nRepeat 10; for j 1:size(Xtest,2) drop zeros(nRepeat,1); for r 1:nRepeat Xp Xtest; Xp{:,j} Xp(randperm(height(Xp)), j); % 打乱第 j 列其余不动 drop(r) baseAcc - mean(categorical(predict(rf, Xp)) Ytest); end imp(j) mean(drop); end [~, order] sort(imp, descend); disp(Xtest.Properties.VariableNames(order(1:10)));randperm打乱整列是关键不能只交换相邻行否则相关性结构还在重要性会被严重低估。重复 10 次的成本可以接受如果预测很慢降到 5 次也够看趋势。单特征影响方向用偏依赖看fitcensemble训练出的模型直接plotPartialDependence(mdl, j)就能出图看的是「把第 j 列固定在不同取值上其余列保持原样时预测概率的平均变化」。图上出现明显的台阶或者非单调折线通常意味着树在这个特征上切出了硬阈值这时候回头去看MinLeafSize是不是设得太小。TreeBagger 对象没有这个接口需要自己构造网格、替换列、批量预测再取均值代码不长但容易写错列索引建议封装成小函数复用。血泪经验是这三件事里最容易被跳过的是置换重要性因为内置的OOBPermutedPredictorDeltaError一行就能出结果看起来已经够了。但在相关特征多的表上内置结果和置换结果能差出完全不同的排序而排序错了后面所有的特征采集和工程投入都会跟着错。从那以后我每次拿到新的特征表都强制把分组置换重要性跑一遍再下结论。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →