ARTICLE DETAIL

资讯详情

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

随机森林Matlab实现:TreeBagger分类回归与调参避坑指南

随机森林Matlab实现:TreeBagger分类回归与调参避坑指南 简介随机森林RF的MATLAB实现代码包面向机器学习初学者及需要在MATLAB中完成分类与回归任务的开发者涵盖数据训练、预测与结果评估等常见环节。包内提供RFReg.m与RFClass.m两个核心函数配合Examples中的示例脚本和数据集即可直接上手随附Fortran源程序与RFClassification/RFRegression动态链接库既能看清底层实现也能加速较大规模计算。整个压缩包共14个文件以m脚本、txt数据/说明、f源码、dll库、readme及doc安装指南为主大小仅211KB结构紧凑。已有6041人浏览学习除核心算法外还可通过PrintRF.m查看模型结构与特征重要性是兼顾学习与实用性的入门资源。1. 随机森林的Matlab代码从一行TreeBagger到能跑通的分类回归很多人拿到随机森林Matlab代码的第一反应是懵代码本身不复杂几十行就能训练一个分类器或回归模型真正的问题总是出在参数和数据类型上。比如分类标签用string类型存储训练直接报错比如NumPredictorsToSample设成全部特征模型退化成了普通Bagging决策树再比如OOB误差曲线怎么画都在震荡让人分不清是参数问题还是数据问题。这篇笔记把我自己拆过的一套随机森林Matlab实现完整展开从TreeBagger的分类、回归两种场景到五个高频翻车点最后给出我对超参数调优的实操习惯。内容范围覆盖表格数据分类、遥感随机森林反演这类回归任务也适合做工业检测或科研数据分析的读者照着跑通自己的数据。2. 随机森林原理与Matlab选型为什么用TreeBagger而不是手写Bagging2.1 随机森林的本质在决策树上叠加两个随机源随机森林是决策树的Bagging集成这是理解所有代码的前提。单个决策树的问题在于无约束生长会过拟合一条从根跑到叶的路径把训练集的噪声学得很彻底。随机森林的思路是用Bootstrap抽样生成K份不同的训练集每份训练一棵决策树最后投票或取平均得到输出。样本层面的随机性解决了单棵树的方差问题但还缺一环如果特征空间里有一个强特征Bagging出来的K棵树大概率都在同一个特征上做第一次分裂树与树之间的多样性有限。第二层随机性来自特征子空间。每次分裂时不看全部M个特征只随机抽取NumPredictorsToSample个候选特征在候选集合里挑最优分裂。这两个随机源叠加之后树与树的相关性明显下降集成之后才算真正的随机森林。这也是随机森林和决策树区别最核心的一条决策树没有特征子空间机制而随机森林强制每棵树只在随机特征子集里找最优分裂。Matlab里TreeBagger的实现逻辑就是按这个思路走的对每一棵树使用bootstrap抽样在每次分裂时从特征子集里选最优。理解了这个前提后面调参就有依据了。比如把NumPredictorsToSample设成M也就是全部特征随机森林就退化成了Bagging决策树这属于最常见的方向性错误。2.2 TreeBagger、fitcensemble还是fitrensemble三条实现路径怎么选Matlab官方路线有三条TreeBagger、fitcensemble配Bag模板、fitrensemble配Bag模板。先说结论老代码和教学资料用TreeBagger居多理由是这个接口更接近Breiman原始论文的表述OOB输出直观我自己的项目里训练集在几千到几万行这个量级TreeBagger够用而且predict接口简单。fitcensemble属于集成学习统一框架如果你后面想对比AdaBoost、RUSBoost等不同集成方法用fitcensemble更省事因为它支持通过模板方法切换算法。还有一个差别在并行计算上。fitcensemble支持用statset(UseParallel, true)并行训练TreeBagger在常规用法里是串行训练的树增多之后明显慢一截。所以遇到大训练集常见做法是用fitcensemble替代TreeBagger小数据集上两者精度差异不大选哪个都行。我整理了三条路的选型对比表维度TreeBaggerfitcensemble(Method,Bag)fitrensemble(Method,Bag)适用任务分类加回归分类回归参数风格Breiman原版接口NumTrees等统一模板需要配templateTree统一模板需要配templateTreeOOB指标自带oobError、oobPredict可用oobLoss、oobPredictoobLoss可用并行训练需要自行改写支持UseParallel支持UseParallel特征重要性OOBPermutedPredictorDeltaErroroobPermutedPredictorImportanceOOBPermutedPredictorDeltaError实际选型的边界很清楚数据在万行以内、关心特征重要性和OOB曲线、希望代码直白直接TreeBagger数据量大到训练时间不可接受或者要在多种集成算法之间切换走fitcensemble或fitrensemble。我在下面的实战章节里以TreeBagger为主线展开因为它的输出结构对理解随机森林的OOB机制最直接。2.3 核心参数速查先知道每个参数在控制什么参数这块是随机森林被问得最多的。NumTrees控制树的数量先给100到200再通过OOB误差曲线决定要不要加树树太少偏差大树足够多之后误差曲线会趋于水平继续加树只增加训练时间。NumPredictorsToSample是特征子集大小分类默认取floor(sqrt(M))回归默认取floor(M/3)M是所有特征的数量。MinLeafSize是叶子最小样本数分类默认1回归默认5。它对模型复杂度的影响比直觉上更大叶子最小样本数越大模型越平滑但也牺牲了对局部细节的拟合能力。分类过拟合时优先调大它比如从1提到5到10回归里5到20之间比较常见。OOBPrediction设为on之后才能拿到OOB误差和OOB预测这个开关在训练代价上几乎可以忽略所以分类回归我都建议打开。OOBPredictorImportance设为on开启特征重要性计算代价比OOBPrediction高一些但拿到的重要性排序对特征筛选很有用。再补充一个不显眼但容易出问题的参数CategoricalPredictors。如果你的特征里有序号类变量比如设备编号、类型码可以在训练前指定哪些列是类别型否则Matlab默认把数值列全部当连续变量处理。这里先记住存在这个参数具体用法放在分类实战小节里一起演示。3. 随机森林分类实战用TreeBagger跑通一个完整流程3.1 数据准备先把标签转成categorical这个细节直接决定你后面是否报错。TreeBagger在分类模式下要求Y是categorical类型或者逻辑向量如果Y是string数组很多版本里训练会直接报错或者把字符串当成回归目标。我一般会统一用一小段脚本对数据做预处理。示例代码里我用了虚拟变量名实际使用替换成自己的特征矩阵和标签向量即可。% 读取数据假设X是m行n列的特征矩阵labels是m行1列的原始标签 % 实际执行时改成你自己的数据读取方式 load(my_dataset.mat, X, labels); % 字符串标签转categorical这一步是后面不报错的前提 Y categorical(labels); % 划分训练集与测试集比例可以根据数据量调整 cv cvpartition(Y, HoldOut, 0.2); idxTrain training(cv); idxTest test(cv); Xtrain X(idxTrain, :); Ytrain Y(idxTrain); Xtest X(idxTest, :); Ytest Y(idxTest);cvpartition是Matlab自带的分层划分函数HoldOut指定20%做测试集。用cvpartition的好处是它会按标签类别比例划分避免某个小类别全部落到训练集或测试集里。如果你有自己的划分逻辑直接替换掉这块也行但标签转categorical这一步不要省后面会有专门一节讲它造成的坑。3.2 训练分类模型TreeBagger的参数怎么给把训练集喂进去只是第一步真正有讲究的是参数怎么配。下面这段就是我项目里常用的分类配置注释写在参数行后面方便对照调。rng(42); % 固定随机种子确保结果可复现 % 训练随机森林分类器 model TreeBagger(200, Xtrain, Ytrain, ... Method, classification, ... % 指定分类任务 MinLeafSize, 1, ... % 分类默认1过拟合时往上调 NumPredictorsToSample, max(1, floor(sqrt(size(Xtrain,2)))), ... OOBPrediction, on, ... % 打开OOB误差记录 OOBPredictorImportance, on, ... % 计算特征重要性 CategoricalPredictors, []); % 没有序号型特征就留空固定随机种子很有必要。随机森林的bootstrap抽样带随机性不固定种子的话两次训练出来的模型精度和特征重要性排序会有波动排查问题时很难判断是代码改坏了还是随机性在作怪。NumPredictorsToSample我一般不用固定数值用floor(sqrt(M))算出来这样特征数量变化时不用回头改代码。训练完成后第一件事不是看测试集精度而是看OOB误差曲线。oobError返回的是每一次新增树之后的袋外误差如果曲线在树数增加后仍然持续下降说明树还没给够如果已经平坦甚至轻微回升说明200棵树对这个数据量已经足够。画曲线就三行代码oobErr oobError(model); figure; plot(oobErr, LineWidth, 1.5); xlabel(Number of Trees); ylabel(Out-of-Bag Error); grid on;观察这条曲线是判断NumTrees最直接的方法。曲线在100棵树附近就平坦的200够用到190还在下滑就把NumTrees加到300甚至400再看。我自己的习惯是先跑小树数快速出曲线再按曲线趋势定最终树数而不是一上来就给1000棵树。3.3 预测与混淆矩阵验证模型到底学成什么样测试集预测使用predict函数分类模式下predict会返回预测类别和各类别的后验概率两个输出。混淆矩阵用confusionmat一行命令就能出结果。下面是完整验证流程[Ypred, Yscore] predict(model, Xtest); % 混淆矩阵按类别顺序显示 [C, order] confusionmat(Ytest, Ypred); disp(Confusion Matrix:); disp(C); disp(Category order:); disp(order); % 总体准确率按对角线元素之和除以总样本数算 accuracy sum(diag(C)) / sum(C(:)); fprintf(Test Accuracy: %.2f%%\n, accuracy * 100);需要特别注意predict返回的Ypred是cell类型里面是字符串标签直接和categorical类型的Ytest做比较会类型不匹配。如果后续要做逐样本分析先把Ypred转成categoricalYpred_cat categorical(Ypred)。混淆矩阵的对角线是正确分类的样本数观察非对角线元素能快速定位最容易被混淆的类别对。这个信息在项目里很有价值比如某个类别被系统性误判成另一个类别说明这两类特征分布本来就靠近或者训练数据里该类别样本太少。对有倾向性的错误比如工业检测里把划伤类测成压印类常见做法是回到数据层面看这两类的特征分布是否重叠先别急着改模型参数。4. 随机森林回归实战预测场景下的参数调整4.1 回归与分类的差异评价指标和默认值都要换随机森林回归算法在Matlab里就是TreeBagger的Method,regression本质上和分类共享同一套Bagging框架但两处默认设置不同回归的NumPredictorsToSample默认是floor(M/3)而不是sqrt(M)因为回归需要更大的特征子集来保证每棵树的预测方差足够低回归的MinLeafSize默认是5比分类的1更大更不容易被噪声样本带偏。这是遥感反演这类场景——比如用光谱波段反演叶面积指数、用地形因子估算土壤属性——最常出现的两个参数坑。回归的评价指标也不同不能用准确率。我常用RMSE、MAE和R方组合起来看效果。这三个指标在Matlab里没有现成的单函数我用几行代码算放在下面的训练代码后面。4.2 训练回归模型并评估代码拆解回归训练和分类在写法上几乎一样只改Method和少数参数。我一般会在训练前对特征做一次zscore标准化尤其是当特征量纲差别很大时比如把波段反射率数值在0到1之间和地形坡度单位是度放在同一个特征矩阵里。树模型本身不直接受益于标准化但标准化之后特征重要性数值之间的可比性更好。下面代码是完整的回归训练加评估流程% 假设 X 是特征矩阵yVec 是连续数值目标向量 % 划分回归数据集回归不需要按类别分层用简单划分即可 cvR cvpartition(height(X), HoldOut, 0.2); trIdx training(cvR); teIdx test(cvR); Xtr X(trIdx, :); ytr yVec(trIdx); Xte X(teIdx, :); yte yVec(teIdx); % 训练随机森林回归模型 rfReg TreeBagger(300, Xtr, ytr, ... Method, regression, ... MinLeafSize, 5, ... % 回归默认5过拟合时往上调 NumPredictorsToSample, max(1, floor(size(Xtr,2)/3)), ... OOBPrediction, on, ... OOBPredictorImportance, on); % 测试集预测回归模式下第二个输出是预测标准差 [yhat, ysd] predict(rfReg, Xte); yhat str2double(yhat); % 回归预测返回的是字符串数组转数值 % 计算回归评价指标 resid yhat - yte; rmseVal sqrt(mean(resid.^2)); maeVal mean(abs(resid)); ssRes sum(resid.^2); ssTot sum((yte - mean(yte)).^2); r2Val 1 - ssRes / ssTot; fprintf(RMSE: %.4f, MAE: %.4f, R2: %.4f\n, rmseVal, maeVal, r2Val);这里有两个值得单独说明的点。第一回归模式下predict返回的yhat是字符串数组必须用str2double转换否则后续的resid计算全部变成字符运算直接报错这是我见过频率最高的回归翻车点。第二ysd是每棵树预测值的标准差可以近似当作不确定性度量。ysd特别大的样本说明树与树之间分歧严重常见做法是单独捞出来检查是不是特征超出了训练数据的覆盖范围。4.3 用OOB误差曲线确定树的数量回归的树数量选择逻辑和分类一致但观察对象换成袋外均方误差。oobError在回归模式下返回的是每次迭代的袋外均方误差看它什么时候平坦就能判断NumTrees。代码如下oobMSE oobError(rfReg); figure; plot(oobMSE, LineWidth, 1.5); xlabel(Number of Trees); ylabel(Out-of-Bag MSE); grid on;我遇到过不少人上来就设500甚至1000棵树其实从曲线上看100多棵就够。多余的树在精度上几乎无贡献只增加训练耗时。所以我的流程是先训练200棵画OOB曲线看平坦点在哪里再把NumTrees设成略大于平坦点的值做正式训练。这个流程在分类和回归任务里都适用。4.4 特征重要性决定特征筛选的价值排序OOBPredictorImportance开启后模型里会存一份OOBPermutedPredictorDeltaError含义是把某个特征随机打乱后OOB误差的变化量。变化量越大说明预测越依赖这个特征。我通常把它做成柱状图并且按值排序输出前几个特征名。这个结果在特征筛选阶段可以直接支撑决策重要性接近于零的特征可以考虑删掉减少训练噪声和后续数据采集成本。imp rfReg.OOBPermutedPredictorDeltaError; % 如果特征名字符串元胞数组predNames存在就一起展示 if exist(predNames, var) [sortedImp, idx] sort(imp, descend); disp(Feature importance ranking:); for i 1:min(10, length(sortedImp)) fprintf(%d. %s: %.4f\n, i, predNames{idx(i)}, sortedImp(i)); end end % 画柱状图 figure; bar(imp); xlabel(Feature Index); ylabel(OOB Permuted Predictor Importance);特征重要性的解读有一个固有坑对相关性高的特征组重要性会被分散两个强相关特征各自的重要性都会偏低不代表它们没用。做特征选择时如果看到这种成对的低重要值先检查特征间的相关系数矩阵别急着删。我在第5章避坑部分还会再提到这个关联问题。5. 随机森林Matlab实现避坑指南五条高频翻车记录5.1 OOB误差曲线剧烈震荡误差始终降不下去现象无论是分类还是回归oobError画出来不是平滑下降而是在某个区间来回抖动甚至越到后面震荡幅度越大。原因最常见的是训练集样本量太少bootstrap抽样后树与树之间差异过大单棵树的误差波动直接传导到OOB曲线上。另一个可能原因是NumPredictorsToSample设得过大树的多样性被削弱整体误差下不去。解决先确认样本量是否足够常见做法是最少保证每类有几十个样本。样本量不足时优先考虑调大MinLeafSize换取稳定参数层面把NumPredictorsToSample按默认的sqrt(M)或M/3重设一遍再画一次曲线对比。如果震荡不消失但整体趋势平坦说明树数够只是曲线本身在随机性下无法收敛成光滑直线这时候看趋势而非抖动幅度。5.2 NumPredictorsToSample设成all模型和单棵决策树没区别现象训练出来的随机森林精度比单棵决策树高不了多少特征重要性排序也集中在某一个特征上。原因把NumPredictorsToSample设成了size(X,2)也就是全部特征每次分裂都在全特征里找最优树与树之间几乎只在bootstrap样本层面有差异特征层面的多样性丢失模型退化成了Bagging决策树。这是随机森林和决策树区别被抹掉的最典型错误。解决分类回归分别按floor(sqrt(M))和floor(M/3)设置不要手动指定为全部特征。如果确实遇到特征极少的场景比如特征数不到5个尽量用Bagging模式而非强行套随机森林因为特征子空间在特征过少时本来就没什么随机性空间。5.3 fitcensemble训练特别慢CPU利用率只有单核现象用fitcensemble或fitrensemble训练Bag集成训练很久都不结束打开任务管理器发现CPU占用率只有12%左右明显只用了单核。原因Matlab默认不开并行训练集成学习框架的UseParallel默认值为false。TreeBagger本身也没有自动并行机制树多了训练自然串行变慢。解决使用fitcensemble时传入Options, statset(UseParallel, true)前提是已经开启并行池。常见做法是开头加一段% 开并行池确保UseParallel生效 if isempty(gcp(nocreate)) parpool; % 也可以指定 parpool(local, 4) end opts statset(UseParallel, true); model fitcensemble(Xtrain, Ytrain, Method, Bag, ... Options, opts, NumLearningCycles, 200);注意并行训练只在训练阶段生效predict阶段仍按串行方式做。树数量多且样本量可观时并行收益明显几百棵树加几千样本的规模开并行反而有进程通信开销不一定更快。我的建议是先对比跑一次计时再决定要不要开并行。5.4 分类标签是string类型训练直接报错现象用readtable读入数据后标签列是string或者char类型丢给TreeBagger训练时提示“Y must be a categorical vector”或者标签被当成数值目标输出一个奇怪的结果。原因Matlab对TreeBagger的分类模式有严格要求Y必须是categorical。char类型的列标签在部分旧版本里会被自动转换但string类型几乎必然触发校验报错。解决训练前统一执行Y categorical(Y)。如果标签列读入是cell数组先转string再转categorical比如categorical(string(Y))。这个习惯养成后很多莫名其妙的训练报错能少一大半。5.5 OOBPermutedPredictorDeltaError出现NaN现象OOBPredictorImportance设了on训练完查看OOBPermutedPredictorDeltaError发现某个或某几个特征的重要性是NaN。原因OOB重要性计算需要在每棵树上对特征做随机置换后重新预测。如果某棵树的OOB样本中没有用到这个特征参与分裂该特征在那棵树上的置换重要性就无法计算累积平均后出现NaN。样本量小或特征作用弱时更常见。解决出现NaN的特征不要直接当作0处理先看NaN比例。如果只有零星几个NaN可以用fillmissing把它替换成该特征在其他树上的重要性均值如果大量特征都NaN通常是训练设置有问题检查OOBPrediction是否打开以及样本量是否过少。另外一个隐蔽原因是特征矩阵里有NaN树在训练时会跳过含缺失值的样本导致OOB集合计算不稳定。先把特征矩阵的缺失值处理干净再训练。6. 把随机森林调出更好效果超参数网格搜索与OOB验证默认参数能跑通但离好用还有距离。我的做法是不要按默认参数一次训练完就交差而是做一个小的参数网格搜索用OOB作为验证集选参数。这样做的核心价值在于OOB误差来自训练过程中未被采样的样本相当于内嵌的验证集用它调参不需要额外划分验证集也避免了在测试集上反复试参数造成的评估偏差。% 定义候选参数网格按数据规模调整范围 minLeafCandidates [1 5 10]; numPredCandidates [round(sqrt(size(Xtrain,2))), round(size(Xtrain,2)/2), size(Xtrain,2)]; bestOOB inf; bestParams []; for ml minLeafCandidates for np numPredCandidates mdl TreeBagger(150, Xtrain, Ytrain, ... Method, classification, ... MinLeafSize, ml, ... NumPredictorsToSample, np, ... OOBPrediction, on); err oobError(mdl); curErr err(end); if curErr bestOOB bestOOB curErr; bestParams [ml np]; end end end fprintf(Best OOB error: %.4f, MinLeafSize: %d, NumPredictorsToSample: %d\n, ... bestOOB, bestParams(1), bestParams(2));这个循环把9组参数各跑一遍每组的树数定为150在OOB误差上选最优参数再用这个参数把树的规模按前面章节的OOB曲线原则扩到300做正式训练。最后用测试集做一次评估得到的指标才是干净的结果。网格粒度在这里给的是3乘3实际项目里特征数和数据量决定搜多细。树几百棵、参数组数几十个的组合在我的经验里计算时间完全可控不建议一上来就做大规模随机搜索。从那以后我每次训练随机森林都强制执行一遍这个流程先转标签类型再看OOB曲线定树数最后用网格搜索定MinLeafSize和NumPredictorsToSample。第5章里那个NumPredictorsToSample设成all的翻车经历让我养成一个习惯——训练前把参数打印出来核对一遍确认它没有意外变成全部特征。这套流程在遥感反演和工业数据分类上都跑过稳定省心。希望帮到你。本文还有配套的精品资源点击获取
返回列表