ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Matlab随机森林分类实战:从原理到代码调参与评估

Matlab随机森林分类实战:从原理到代码调参与评估 简介面向机器学习初学者与MATLAB使用者资源包完整演示随机森林在分类任务中的实现流程覆盖数据预处理、Bootstrap抽样、特征随机选取、决策树构建与多数投票等关键环节可帮助理解集成学习原理并快速迁移至实际问题。压缩包共5个文件以4个带详细注释的M脚本为主另含1个MAT格式数据集主程序、训练函数与投票逻辑分模块编写便于逐段研读、断点调试和参数修改。已有13075人学习下载适合课程设计、毕业设计及算法对比实验使用。通过运行代码可直观观察随机森林的预测效果、特征重要性排序以及树数量等参数对分类性能的影响为后续将模型嵌入真实项目打下扎实基础。 随机森林在分类任务里算是那种“平时不声不响关键时刻特别靠谱”的模型。尤其在Matlab里做数据处理和实验验证好多同学一上来就调fitctree或者fitcknn遇到一点噪声就过拟合换一组数据准确率掉得没法看。后来换成随机森林效果一下就稳了——不夸张地说它是我在Matlab里做分类用得最顺手的模型之一没有复杂的调参技巧也能拿到一个挺能打的结果。这篇就围绕“随机森林用于分类的Matlab代码”这件事展开把我自己在实际项目里怎么组织代码、怎么调参数、怎么评估模型、踩过哪些坑都梳理一遍。无论你是课程作业需要还是论文实验里想快速出个稳定基线这篇都能给你一个可以直接跑的框架。1. 随机森林凭什么适合做分类先花点篇幅把随机森林的原理捋一遍因为只有理解了它为什么“稳”后面写代码才知道那些参数在干嘛。1.1 三句话讲清随机森林随机森林的核心思想是“三个臭皮匠赛过诸葛亮”。它由一大堆决策树组成每一棵树都用训练集的随机子集进行训练同时还随机挑选一部分特征来寻找最优划分。等到要预测一个新样本时森林里所有树分别投票票数多的类别就是最终输出。这里有三个关键点Bootstrap采样每棵树的训练数据是从原始样本里有放回地抽出来的也就是说有些样本会被重复抽到有些样本可能一直没被抽到。没被抽到的那些样本叫“袋外数据”OOB它们天然适合用来评估模型的泛化能力不需要额外划分验证集。特征随机化每次节点分裂时不是从全部特征里挑最优的而是随机抽一个特征子集再从子集里找最优划分。这么做的好处是让每棵树长得都不太一样避免所有树都陷入同一个局部模式。投票决策分类时采用的是多数投票。单棵树可能不靠谱但几十上百棵树一起投票个别树的随机误差会被相互抵消所以整体在测试集上的表现很稳。我已经把随机森林用在好几个不同领域的项目里从二分类到多分类都有。给人的感觉是它对数据尺度、特征分布、缺失值这类问题没那么敏感很多数据清洗在常规模型上要做半天在随机森林这里不做也能跑还不容易崩。1.2 随机森林和GBDT这类集成模型差在哪热词里也出现了GBM、GBDT这类集成模型很多人会纠结随机森林和梯度提升树该怎么选。我的理解是这样的随机森林是并行训练每棵树独立生成谁也不依赖谁所以训练时间可以压得很小GBDT是串行训练每一棵新树都在拟合前面所有树的残差或梯度整体精度上限通常更高但对参数更敏感也更容易过拟合随机森林对噪声和异常值相对稳健因为投票机制会在一定程度上稀释掉个别树的异常输出GBDT更擅长挖掘细粒度特征交互但一旦数据有比较脏的噪声它的迭代优化会把噪声也学进去。所以如果我的首要目标是快速得到一个稳定、可解释、不容易翻车的基线结果我会先跑随机森林。如果后续发现随机森林已经到瓶颈、又确实需要进一步提精度再上GBDT不迟。2. Matlab里做随机森林分类的工具怎么选Matlab里实现随机森林有几个路径我挨个用过简单说下区别避免你走弯路。2.1 三条实现路线对比方案核心函数适用场景说明新版框架fitcensembleTemplateTree通用分类、交叉验证、自动超参优化推荐首选代码规范跟其他模型接口统一老牌函数TreeBagger教学代码、旧项目维护、跟统计教材对应输出结果里能看到OOB误差等经典字段但接口偏老手写实现循环调用fitctree学习原理、自定义集成逻辑不建议用在正式项目里纯为了理解原理可以写一写fitcensemble是Matlab里集成学习的统一入口指定Method, AdaBoostM2就是提升树指定Method, Bag配合决策树模板就是随机森林。虽然Matlab文档里对Bagging的表述和“随机森林”有所区分但从实际效果上看树模板里对特征子抽样做了限制后它就是随机森林。2.2 环境准备和工具箱检查跑下面的代码前先在命令行里确认一下你的环境有没有装统计和机器学习工具箱% 检查工具箱是否可用 if license(test, Statistics_Toolbox) disp(Statistics and Machine Learning Toolbox 可用); ver(stats); else error(缺少 Statistics and Machine Learning Toolbox请先安装); end % 查看fitcensemble是否可用 which fitcensemble如果提示找不到函数或没有许可需要先安装Statistics and Machine Learning Toolbox。这个是Matlab里做数据分类最常见的工具箱之一学生版和正式版都包含在内。后面的所有代码都基于这个工具箱。2.3 选fitcensemble而不是TreeBagger的原因有同学会问网上很多老代码都在用TreeBagger怎么到了我这里推荐fitcensemble原因有三点fitcensemble配合TemplateTree能直接在分类器上调用kfoldLoss、predict、oobLoss等方法整个建模流程非常统一在后续做超参数优化时fitcensemble可以直接用OptimizeHyperparameters参数省去手工写网格搜索的麻烦TreeBagger的输入格式、输出类型和分类器对象很不一样跟新版工具链的交互没有fitcensemble顺滑。不过如果你是想跑老代码或者看教程里用TreeBagger也完全没问题。只要数据格式对得上它的分类效果和fitcensemble基本没有差别。3. 随机森林分类的完整Matlab代码框架下面这份代码我在多个数据集上都跑通过你把自己手里的数据整理成表格替换掉加载部分就行。整个过程包括数据准备、划分训练集测试集、训练模型、评估模型、绘制混淆矩阵这几个标准环节。3.1 数据准备与训练测试集划分假设你的数据是一个表格最后一列是类别标签前面的列是特征。代码里我直接用readtable读取想快速验证的话也能直接用内置数据集fisheriris% 设置随机种子保证实验可复现 rng(42); % 如果使用内置鸢尾花数据集做演示 % [X, Y] fisheriris(); % 旧接口 % 新版推荐用table load fisheriris.mat % 构建表格前四列是特征最后一列是类别 data table(meas(:,1), meas(:,2), meas(:,3), meas(:,4), species, ... VariableNames, {SepalLength, SepalWidth, PetalLength, PetalWidth, Species}); % 观察类别分布 summary(data); % 划分训练集和测试集70%训练30%测试 cv cvpartition(data.Species, HoldOut, 0.3); trainIdx training(cv); testIdx test(cv); trainData data(trainIdx, :); testData data(testIdx, :); fprintf(训练集样本数: %d\n, height(trainData)); fprintf(测试集样本数: %d\n, height(testData));这里我用的是cvpartition而不是randperm手动划分原因是它支持按类别分层采样能保证划分后的训练集和测试集里每一类的比例和原始数据大致一致。如果你的数据本身就是不平衡的这一步对后面评估模型尤其重要。3.2 核心训练代码基于fitcensemble这是整个流程的核心部分也是最容易出问题的地方% 定义决策树模板限制每次分裂随机选择的特征数 % NumVariablesToSample 对应随机森林中的特征子集大小 t templateTree(... NumVariablesToSample, all ... % 这里先用全部特征后面调参再改 ); % 训练随机森林模型Bagging 决策树 随机森林 rng(42); model fitcensemble(... trainData, ... % 表格数据 Species, ... % 类别标签列名 Method, Bag, ...% Bagging集成方式 NumLearningCycles, 100, ... % 树的数量 Learners, t, ... % 决策树模板 ClassNames, {setosa, versicolor, virginica} ... % 显式指定类别 ); % 查看模型摘要 disp(model);跑完之后model是一个ClassificationEnsemble对象。如果你在fitcensemble中指定了ClassNames后面预测的类别顺序就会按照这个顺序来画混淆矩阵的时候对应关系一目了然。这里有个细节NumVariablesToSample这个参数在templateTree里控制的是每棵决策树在做节点分裂时随机选取的特征个数取值可以是正整数、all或sqrt等。真正意义的随机森林一般选平方根级别的特征数比如总特征9个就选3个但Matlab里设为all也依然能跑出可用的结果只是树与树之间的差异性会小一些大家根据特征数量决定。3.3 在测试集上预测并计算准确率训练完了最重要的就是看模型在新数据上的表现% 在测试集上进行预测 [predLabels, predScores] predict(model, testData); % 真实标签 trueLabels testData.Species; % 计算准确率 accuracy sum(predLabels trueLabels) / numel(trueLabels); fprintf(测试集准确率: %.2f%%\n, accuracy * 100); % 查看每个样本的预测概率前5行 disp(前5个样本的预测类别和得分); disp(table(predLabels(1:5), predScores(1:5,:), ... VariableNames, {PredictedLabel, PredictedScore}));predict返回的predScores是每个样本属于各个类别的概率估计在多分类问题里非常有用。后续如果要画ROC曲线或者做成本敏感分析这个分数就是原料。需要提醒的是概率分数是取平均得到的不是投票比例换算出来的。Matlab文档里写得明白集成模型输出的是各棵树后验概率的平均和简单少数服从多数还有点区别。这个细节在做概率校准的时候要格外小心。3.4 绘制混淆矩阵只说准确率不够直观混淆矩阵才是看清分类器哪里犯错的关键% 绘制混淆矩阵 figure; cm confusionchart(trueLabels, predLabels); cm.Title 随机森林分类结果混淆矩阵; cm.RowSummary row-normalized; % 显示每一类的召回率 cm.ColumnSummary column-normalized; % 显示每一类的精确率 % 计算每类精确率和召回率如果不画图也可以用公式手算 [C, order] confusionmat(trueLabels, predLabels); fprintf(类别顺序: %s, %s, %s\n, order{1}, order{2}, order{3}); disp(混淆矩阵); disp(C);亲手跑几次分类任务之后你就知道只看准确率很容易把模型的缺陷藏起来。比如手写数字识别里“7”经常被认成“1”如果不看混淆矩阵根本不知道问题出在哪个类别上。而且confusionchart能直接以百分比形式展示每一类的召回率比单独算指标方便很多。3.5 完整可跑的整合代码把上面的代码拼在一起就是一个完整的分类流程。我平时做项目时通常还会把模型保存下来% 保存模型方便后续部署或复用 save(random_forest_model.mat, model); % 加载模型做预测 % S load(random_forest_model.mat); % predictions predict(S.model, newData);关于保存模型这块提醒一下Matlab的模型对象强依赖工具箱版本高版本保存的模型低版本可能打不开。如果是要交付给别人的代码建议同时提供训练脚本让对方自己重新训练一遍或者改用save时指定-v7.3并告知对方对应的Matlab版本。4. 随机森林调参的实战策略模型跑通只是第一步。真正想要把效果用好调参是绕不开的。随机森林的参数不算多但每个参数怎么调、调整顺序是什么是有门道的。4.1 核心参数清单参数默认值作用调参建议NumLearningCycles100集成中树的数量从100开始看OOB误差曲线决定要不要增加NumVariablesToSampleallBag下为all其实应为sqrt型逻辑每次分裂选择的特征数特征多时设小一点增加随机性MaxNumSplitsn-1不限制单棵树的最大分裂次数控制单棵树复杂度太小欠拟合太大过拟合MinLeafSize1叶子节点最小样本数增大可以让模型更平滑抑制过拟合NPrint0训练过程打印间隔调试用正式训练不用开单棵树越复杂、数量越多模型对训练数据拟合得越彻底但泛化能力不一定随之提升。关键是要靠验证集或袋外数据来判断当前参数下模型是欠拟合还是过拟合。4.2 用袋外误差曲线确定树的个数fitcensemble在训练完成后可以直接画袋外误差曲线这是决定树的数量最直接的工具% 绘制袋外误差随树数量变化的曲线 figure; oobError oobLoss(model, Mode, cumulative); plot(1:numel(oobError), oobError, LineWidth, 1.5); xlabel(树的数量); ylabel(袋外误差); title(随机森林OOB误差曲线); grid on; % 找到误差开始平台期的位置给出推荐的树个数 % 这里简单取 OOB 误差达到最小值 95% 处的方式做参考 minErr min(oobError); idx find(oobError minErr * 1.05, 1, first); fprintf(建议树的数量至少为: %d\n, idx);画出来的曲线通常一开始迅速下降到某个值之后趋于平缓。如果你观察到曲线在上升说明树的数量太多或者单棵树太复杂模型开始出现过拟合。多数情况下100棵左右已经足够200棵以上收益就很小了。4.3 网格搜索与自动优化手工试参数毕竟累fitcensemble自带了贝叶斯自动优化适合在不知道参数调到多少时快速探索% 使用自动超参数优化 rng(42); modelOpt fitcensemble(... trainData, ... Species, ... Method, Bag, ... Learners, t, ... OptimizeHyperparameters, {... NumLearningCycles, ... MinLeafSize}, ... HyperparameterOptimizationOptions, struct(... AcquisitionFunctionName, expected-improvement-plus, ... MaxObjectiveEvaluations, 30, ... Kfold, 5, ... ShowPlots, true)); % 查看最优参数 disp(modelOpt.HyperparameterOptimizationResults.XAtMinObjective);自动优化的时间通常比较长数据集大一点可能要跑十几分钟。我的经验是先在小样本上快速确定参数大致范围再用全量数据训练一次这样比直接在全量数据上优化高效得多。4.4 调参的先后顺序心得我调随机森林的顺序基本是固定的先把NumLearningCycles调到100画OOB误差曲线确认树的数量够用再调MinLeafSize从1逐步增加到5、10甚至更大观察OOB误差和测试集误差的变化然后调NumVariablesToSample特征多时用sqrt或更小值特征少时保持全部或取一半最后看一眼MaxNumSplits是否需要限制通常数据量不大时不用动。这套顺序能帮你避免同时动五六个参数然后完全不知道该怪谁的问题。随机森林在千维以下特征上效果都很稳定MinLeafSize带来的变化往往比树的数量更明显。5. 特征重要性分析与模型解释随机森林能直接输出特征重要性这一点在学术论文和实际业务里都非常实用。决策树本身不可解释但通过累积每个特征在分裂时带来的不纯度下降可以知道哪些特征对分类贡献最大。5.1 Matlab里计算特征重要性的方法fitcensemble训练完的模型可以直接调用predictorImportance得到每个特征的相对重要性分数% 计算特征重要性 imp predictorImportance(model); % 获取特征名称 featureNames trainData.Properties.VariableNames(1:end-1); % 展示排序结果 [impSorted, idxSorted] sort(imp, descend); fprintf(特征重要性排序\n); for i 1:length(impSorted) fprintf(%d. %s: %.4f\n, i, featureNames{idxSorted(i)}, impSorted(i)); end % 绘制特征重要性条形图 figure; bar(imp(idxSorted)); set(gca, XTickLabel, featureNames(idxSorted), XTick, 1:length(featureNames)); xtickangle(45); ylabel(特征重要性得分); title(随机森林特征重要性);注意一点Matlab里返回的重要性分数是相对值并非概率。分数之和也不保证等于1所以用来做排序参考没问题但不要直接解释成“某个特征贡献了百分之多少”。5.2 特征重要性怎么辅助降维和特征选择拿到重要性排序之后你可以尝试从得分最低的特征开始逐个删掉重新训练模型看验证集精度是否下降。如果删除后精度几乎不变说明这些特征就是冗余的删掉还能提升训练速度。我之前在处理高维表格数据时就是靠特征重要性把原始维度砍掉将近一半测试集精度反而还略微上升了一点。原因很简单无关特征在随机森林里会引入额外的随机噪声删除之后每棵树的投票结果反而更一致。5.3 效果不好时先用特征重要性做诊断如果模型测试集准确率就是不达标不要急着调参数先看一下特征重要性。如果所有特征的重要性都差不多且都很低说明特征整体和标签的相关性不强这时候想靠调参把效果拉上去是不现实的要么换特征工程方案要么换模型。如果只有一两个特征重要性特别高其他几乎为0说明模型就靠那一两个特征撑着。这时候你可以单独拿那一两个特征跑一个简单的树模型对比一下如果差距不大说明问题出在训练数据本身而不是集成模型没发挥出优势。6. 随机森林分类常见问题与排查实录代码在跑的时候总会出现一些共性问题下面这几条是我被问过最多、也在不同数据集上反复踩过的。整理成速查表方便你对照处理。6.1 常见错误对照表错误现象可能原因解决办法fitcensemble报错“未找到变量”类别标签列名写错检查data.Properties.VariableNames确认标签列名准确率只有40%~60%但训练集100%过拟合调大MinLeafSize限制MaxNumSplits增加训练数据OOB误差持续上升树太多或单棵树太复杂减少NumLearningCycles或限制每棵树深度predict报错维度不一致测试集特征列数与训练集不一致确保训练和测试用的表格结构一致类别不平衡导致少数类准确率极低数据本身不平衡且未做处理尝试用Prior, empirical或者采样法重平衡模型训练时间过长树数量或特征数偏大用并行池加速或减少树的数量6.2 关于随机数种子一定要设置我见过不少同学跑同一份代码两次结果不一样还以为是自己哪里写错了。其实是随机森林里的Bootstrap采样和特征随机选择都依赖随机数生成器如果不设置种子每次运行结果肯定不同。建议在训练和划分数据之前都加上rng(42);只要固定了种子任何一次运行结果都能复现。这也是写论文实验的基本底线——审稿人如果不小心重跑一次发现结果对不上那可就说不清了。6.3 树的数量越多越好吗不是。树的数量增加到一定程度之后误差会趋于平台期再增加只会徒增训练时间和内存占用。一个常见误区是看到默认100棵树就一路加到1000实际上对于几千条样本的小数据集300棵树已经是天花板了再往上加几乎不会改变预测结果。用前面提到的袋外误差曲线判断当前树数下误差是否已经稳定是最省事的办法。我个人的习惯是先从100开始画一次OOB曲线如果到了100还没稳定就加到300如果50就稳了那就把100保留没必要多算。6.4 如何加速大规模数据训练如果你的样本量到了十万级以上训练随机森林会明显变慢。这里有几个可行办法使用Options, statset(UseParallel, true)开启并行训练前提是要先启动并行池减少树的数量优先保证OOB误差在可接受范围内限制MaxNumSplits控制单棵树深度用NumVariablesToSample减小特征子集规模考虑先做特征筛选把重要性极低的特征删掉再训练。% 开启并行池并行训练随机森林 if isempty(gcp(nocreate)) parpool; end t templateTree(NumVariablesToSample, sqrt, MaxNumSplits, 50); model fitcensemble(trainData, Species, ... Method, Bag, ... NumLearningCycles, 200, ... Learners, t, ... Options, statset(UseParallel, true));实测下来并行训练在大数据量上提速非常明显但在小数据量上反而可能因为进程通信开销变得更慢。所以数据量不到万级别不建议开并行。7. 一个完整的实战案例基于UCI威斯康星乳腺癌数据最后放一个可以直接复现的完整案例。这个数据集是经典的二分类问题特征少、样本量小非常适合用来验证随机森林分类的流程。7.1 数据加载与预处理不额外下载数据文件用Matlab内置的readtable配合网络上的UCI数据可以不过更省事的方式是直接在Matlab里构造示例数据。为了让你能直接跑我这里用随机生成的数据模拟特征矩阵并构造一个二分类标签来演示完整流程rng(42); % 模拟500个样本、10个特征 nSamples 500; nFeatures 10; X randn(nSamples, nFeatures); % 构造标签逻辑是前3个特征线性组合大于0为正类 Y (X(:,1)*1.5 X(:,2)*1.2 X(:,3)*0.8 randn(nSamples,1)*0.5 0); Y categorical(Y, [0, 1], {阴性, 阳性}); data array2table(X, VariableNames, ... {特征1,特征2,特征3,特征4,特征5,特征6,特征7,特征8,特征9,特征10}); data.标签 Y; % 划分训练测试集 cv cvpartition(data.标签, HoldOut, 0.3); trainData data(training(cv), :); testData data(test(cv), :);7.2 训练与调参先跑一个100棵树的模型再用oobLoss看误差曲线t templateTree(NumVariablesToSample, 4, MaxNumSplits, 20); rng(42); model fitcensemble(trainData, 标签, ... Method, Bag, NumLearningCycles, 100, Learners, t); figure; oobErr oobLoss(model, Mode, cumulative); plot(oobErr); xlabel(树的数量); ylabel(袋外误差);从曲线上基本能看出这个模拟数据大概40棵树以后误差就稳定了。接下来再看一下测试集表现。7.3 评估与结果解读[predLabel, score] predict(model, testData); accuracy mean(predLabel testData.标签); fprintf(测试集准确率: %.2f%%\n, accuracy * 100); % 绘制混淆矩阵 figure; confusionchart(testData.标签, predLabel);跑下来准确率大概在85%~90%之间对于这个模拟数据来说效果还是可以的。你可以尝试调大MaxNumSplits或者改变NumVariablesToSample看看准确率怎么变化这样对随机森林的“随机性”会理解得更深。7.4 和单一决策树对比为了验证随机森林的提升可以顺便训练一个单独的决策树作为对照treeModel fitctree(trainData, 标签, MaxNumSplits, 20); treePred predict(treeModel, testData); treeAcc mean(treePred testData.标签); fprintf(单决策树准确率: %.2f%%\n, treeAcc * 100);在我跑这个模拟数据的时候随机森林通常比单棵决策树高5到10个百分点。这就是集成学习的价值——单棵树方差大随机森林通过平均大幅降低了这个方差泛化能力自然更强。最后分享一点小经验从第一次在Matlab里跑通随机森林到现在我的一个很明显的感觉是随机森林是一个“付出少、回报稳”的模型它不需要像深度学习那样精细地调学习率、不需要像SVM那样折腾核函数只要你把树的数量和特征子集大小设得合理它就能给出一个相当可靠的结果。在绝大多数表格型分类任务里它可以作为第一个尝试的模型也是最合适的基线模型。几个顺手养成的好习惯再提一下训练前固定随机种子、训练后看OOB曲线、评估时画混淆矩阵、遇到效果不好先看特征重要性。把这四步走完你对当前数据集的判断就已经超过大多数只跑一遍准确率就完事的做法了。后续如果你觉得随机森林的精度到瓶颈了可以考虑在同样的数据上试一下带早停的梯度提升树。但那是另一个话题了需要的时候再单独写一篇来聊。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进