尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

MATLAB随机森林预测实战:从数据清洗到模型交付的完整指南

MATLAB随机森林预测实战:从数据清洗到模型交付的完整指南 简介这是一个面向机器学习入门及以上水平研究者、数据分析师和MATLAB用户的随机森林预测项目完整示例。资源基于波士顿房价数据集系统演示了使用遗传算法、粒子群算法及贝叶斯优化对随机森林回归模型进行超参数寻优的完整流程涵盖数据划分、适应度函数设计、代码实现、结果验证与未来改进方向等关键环节。包内附有可直接运行的MATLAB代码有助于理解集成学习与自动化调参机制。资源以1个docx文档形式提供压缩包大小约32KB内容集中、便于快速查阅。已有80人学习浏览适合用于数据科学竞赛备赛、回归类项目实践或作为相关课程实验参考。1. 为什么选随机森林做 MATLAB 预测项目三行命令之外的完整交付链做预测项目的人最容易卡住的往往不是模型而是数据根本喂不进去。之前我接过一批来自某工厂的设备振动数据特征列几十个目标是要预测未来一周的故障概率。当时把随机森林模型放到 MATLAB 里跑本以为一个训练函数就能结束结果从缺失值清洗、类别变量编码到验证集划分连着一个周末都在处理边界问题。后来我把这套流程沉淀下来发现用随机森林模型优化的完整 MATLAB 预测项目真正值钱的部分并不是几十行训练代码而是程序与数据如何组织、参数如何取舍、验证怎么做才算数。这篇笔记的读者是手里有表格数据、想把预测真正交付出去的工程师和研究生。你能得到的不是一句“调 fitrensemble 就行”而是一条从原始 CSV 到干净训练集、再到可解释模型的完整路径外加我踩过的几个坑。2. 数据先喂对从原始表格到随机森林可以吃的输入2.1 清洗顺序缺失值、离群值和变量类型的处理习惯随机森林不需要特征归一化因为树模型按阈值做分裂特征数值的绝对大小不影响分裂结果。这是它在 MATLAB 预测项目里比神经网络省事的地方但也带来两个前提特征列不能满是缺失值类别变量必须有正确的类型。如果直接把一个字符串列当成数值传给训练函数MATLAB 的报错信息会让人怀疑人生。我一般先读数据、看概览再决定清洗方案。用readtable读入 CSV 后第一件事是看每一列的类型和缺失情况% 读取原始 CSV并快速查看类型与缺失情况 T readtable(sensor_data.csv); T.Properties.VariableNames % 打印所有列名 summary(T) % 每列类型、缺失数量、取值分布readtable会自动判断列类型但经常把时间戳读成datetime、把类别列读成字符串元胞数组。summary能直接输出每列的缺失值数量和取值分布清洗前必须先跑这一步否则后面会反复报错。确认缺失情况后我按变量类型分别填充。数值列我习惯用中位数而不是均值中位数对离群值更稳类别列缺了就直接补一个missing水平或者删除这个样本% 统计每列缺失数量 missCount sum(ismissing(T), 1); for i 1:width(T) if isnumeric(T.(i)) missCount(i) 0 medVal median(T.(i), omitnan); % 用中位数填充数值列 T.(i) fillmissing(T.(i), constant, medVal); elseif iscategorical(T.(i)) missCount(i) 0 % 类别列若有未定义的空水平先清理掉 T.(i) removecats(T.(i)); end end这段代码里median(..., omitnan)很关键它让中位数计算忽略掉已有的缺失值避免中位数本身变成 NaN。fillmissing用常量填充而不是用前后值插值因为表格数据没有天然的先后顺序。这里要提醒一点填充用的统计量应该只来自训练集测试集要做同样填充但必须沿用训练集算好的值。这个原则第 5 章的避坑部分还会专门展开。离群值处理要更克制。树模型对离群值比线性模型稳健过度删除反而会丢失样本。我只对特征列处理不对目标列动手而且阈值非常保守% 离群值标记后置为 NaN交给上一轮中位数填充 for i 1:width(T) if isnumeric(T.(i)) i ~ targetCol flag isoutlier(T.(i), percentiles, [1 99]); if sum(flag) 0.01 * height(T) % 离群比例不超过 1% 才处理 T.(flag, i) NaN; end end end这里用percentiles而不是默认的中位数绝对偏差因为 [1 99] 分位数更符合工程直觉只有最极端的 1% 才算离群。处理后的 NaN 会被上一段代码用中位数覆盖所以整个清洗流程的顺序是标记离群 - 置 NaN - 统一填充。2.2 类别变量编码直接指定 CategoricalPredictors 而不是盲目转数值随机森林处理类别变量不需要像线性模型那样做 one-hot树分裂时可以直接按类别子集二分。在 MATLAB 中正确做法是把列转成categorical然后在训练时用CategoricalPredictors告诉训练函数哪些列是类别。如果硬把类别转成 1、2、3 这样的整数当数值用树会错误地把类别当成有序连续量分裂出来的节点毫无业务含义这是新手最容易翻车的地方。读入的表格里类别列可能是字符串元胞数组或字符矩阵先统一转换% 把字符串/字符型类别列统一转成 categorical T.shift categorical(T.shift); % 班次早班/中班/晚班 T.group categorical(T.group); % 设备分组 T.sensor categorical(T.sensor); % 传感器型号 % 检查每个类别变量的水平数 for i 1:width(T) if iscategorical(T.(i)) fprintf(%s : %d levels\n, T.Properties.VariableNames{i}, ... numel(categories(T.(i)))); end endcategorical自带一组离散水平树分裂时会尝试把所有水平分成两个子集水平越多枚举次数越多。如果某个类别列有上千个水平训练时间会指数级上升还容易生成只覆盖单条样本的叶节点。我的警戒线是水平数超过样本数的 10% 就要处理。比如训练集有 5000 条某列超过 500 个水平就该考虑删除或合并。合并低频水平我常用mergecats% 统计水平频次保留 top 20其余合并成 one 个新水平 tbl table(categories(T.group), countcats(T.group), ... VariableNames, {level, count}); tbl sortrows(tbl, count, descend); keepLevels tbl.level(1:20); dropLevels tbl.level(21:end); T.group mergecats(T.group, dropLevels, other);这段代码把出现次数最低的那些水平统一合并成other训练时模型只会见到 21 个水平20 个高频水平加other。这样既保留了主要信息又给未来可能出现的冷门水平留了去处。注意mergecats的第三参数是目标新水平名dropLevels会被整体合并到other里。2.3 划分数据集先划分后清洗用 cvpartition 固定训练/测试边界很多人在数据清洗完成后才做训练/测试划分这是流程上的大坑。我的习惯是先把样本分成训练集和测试集再对训练集做清洗统计。测试集从划分那一刻起就彻底隔离后面所有统计量都不能再碰它。rng(2024); % 固定随机数种子让划分结果可复现 cv cvpartition(height(T), Holdout, 0.3); trainIdx training(cv); testIdx test(cv); Ttrain T(trainIdx, :); Ttest T(testIdx, :);Holdout比例 0.3 意味着 30% 样本不参与训练中小数据集这个比例够用数据量大时可以下调到 0.2。cvpartition的好处是划分本身自带随机种子控制同一段代码在任何机器上跑出的划分结果一致。这里的training(cv)和test(cv)返回的是逻辑索引直接用它们切表。划分完成后前面 2.1 的填充逻辑要改成只从训练集取统计量% 用训练集的中位数填充测试集同列避免数据泄漏 for i 1:width(Ttrain) if isnumeric(Ttrain.(i)) medVal median(Ttrain.(i), omitnan); Ttrain.(i) fillmissing(Ttrain.(i), constant, medVal); Ttest.(i) fillmissing(Ttest.(i), constant, medVal); end end测试集填充时用的medVal来自训练集测试集自身的中位数没有被使用。这个细节直接决定交叉验证分数是否可信。测试集在你调完所有参数之前应该一次都不碰。3. 训练随机森林的 MATLAB 写法入口函数选型与三个必调参数3.1 三种建模入口怎么选fitrensemble、TreeBagger 与单棵树MATLAB 里做随机森林常见有三个入口选错会直接影响后续工作流。我按使用频率排如下入口函数回归/分类特点适用场景fitrensemble/fitcensemble两者都支持支持表格输入、内置交叉验证、自带 OOB 指标配合templateTree可精确控制树结构首选适合绝大多数预测项目TreeBagger两者都支持面向对象风格oobError和oobPermutedPredictorDeltaError调用直观但要求输入全为数值接手旧代码或需要逐指标检查时fitrtree/fitctree两者都支持单棵决策树训练极快便于检查单特征分裂效果调templateTree参数时快速试错fitrensemble里要显式写Method, Bag这表示用 bootstrap 聚合的方式训练。严格说随机森林和袋装树的区别在于每棵树分裂时还要随机抽特征子集所以在templateTree里设置NumVariablesToSample后这套组合才符合教科书里的随机森林定义。我在项目里都是按这个标准配置来写的。3.2 树数量、叶节点和特征抽样三个必调参数及其顺序三个参数决定随机森林的绝大部分行为参数含义参考范围调参优先级NumLearningCycles集成中树的数量回归 200~500分类 100~300第一优先MinLeafSize叶节点最少样本数回归从 5 试起分类从 2 试起最后调NumVariablesToSample每棵树分裂时随机候选特征数回归约 p/3分类约 sqrt(p)p 为特征总数第二优先调参顺序有讲究先确认树数量够不够再看特征抽样最后调叶节点。树数量通常最先固定因为 OOB 误差曲线能直接告诉我们 100 棵还是 300 棵够用特征抽样控制树与树之间的相关性相关性太高则集成退化成单棵大树的平均叶节点大小是正则化手段放在最后处理是因为它和过拟合之间的平衡最依赖数据本身的噪声水平。判断树数量是否够用我一般画 OOB 累积误差曲线% 训练一个不带交叉验证的袋装回归集合用于观察 OOB 收敛 mdl fitrensemble(Ttrain(:, predictors), Ttrain.target, ... Method, Bag, ... NumLearningCycles, 300, ... Learners, templateTree(MinLeafSize, 5, ... NumVariablesToSample, floor(p/3))); oobCurve oobLoss(mdl, Mode, cumulative); plot(1:300, oobCurve);oobLoss的cumulative模式返回从第 1 棵树到当前树的袋外误差曲线先快速下降然后进入平台期。如果曲线在 250 棵时还在明显下降说明树不够如果 50 棵以后就平了加到 300 棵只是浪费时间。这里的p是特征总数floor(p/3)是回归任务常用的特征抽样数。3.3 最小可运行代码交叉验证与独立测试集预测准备好Ttrain、Ttest和特征名列表后最小可运行的回归代码如下% 特征列表和类别特征索引 p numel(predictors); catIdx find(contains(string(predictors), {shift, group})); rng(42); % 固定种子训练过程可复现 mdl fitrensemble(Ttrain(:, predictors), Ttrain.target, ... Method, Bag, ... NumLearningCycles, 200, ... Learners, templateTree(MinLeafSize, 5, ... NumVariablesToSample, floor(p/3)), ... KFold, 5, ... CategoricalPredictors, catIdx); cvLoss kfoldLoss(mdl); % 五折交叉验证的均方误差 predCV kfoldPredict(mdl); % 每一折袋外样本的预测值KFold设为 5训练函数内部会做五折划分每折轮流留出 20% 做验证。kfoldLoss返回的是均方误差kfoldPredict给出每个样本在所在折外的预测结果。注意带KFold的对象是交叉验证对象不能直接拿去做独立测试集预测。最终上线前要用相同参数重训一个完整模型mdlFinal fitrensemble(Ttrain(:, predictors), Ttrain.target, ... Method, Bag, ... NumLearningCycles, 200, ... Learners, templateTree(MinLeafSize, 5, ... NumVariablesToSample, floor(p/3)), ... CategoricalPredictors, catIdx); predTest predict(mdlFinal, Ttest(:, predictors));这里有个容易漏的步骤交叉验证对象评估的是“这个参数下的模型大概什么水平”mdlFinal才是真正预测用的模型。两者参数必须完全一致否则交叉验证分数和线上表现对不上。分类任务把fitrensemble换成fitcensemble目标列换成分类标签即可其余写法相同。4. 评估与解释随机森林不是黑匣子OOB 与特征重要性要会用4.1 回归指标怎么算RMSE、MAE 和 R² 要跟基准对比训练完成后第一步不是看准确率而是算清楚模型在交叉验证上的误差。回归任务我固定算三个指标RMSE、MAE、R²。代码如下Y Ttrain.target; predCV kfoldPredict(mdl); rmse sqrt(mean((Y - predCV).^2)); mae mean(abs(Y - predCV)); ssRes sum((Y - predCV).^2); ssTot sum((Y - mean(Y)).^2); r2 1 - ssRes / ssTot; fprintf(RMSE%.3f MAE%.3f R2%.3f\n, rmse, mae, r2);RMSE 对大误差敏感预测里偶尔出现离谱偏差时 RMSE 会明显变大MAE 更直观表示平均偏离多少单位。R² 越接近 1 说明模型比“用均值预测”好得越多但这个指标单独看会骗人。我每次都额外算一个基准模型% 基准模型用训练集均值预测全部交叉验证样本 basePred ones(size(predCV)) * mean(Y); baseRmse sqrt(mean((Y - basePred).^2));把baseRmse和rmse放在一起看能快速判断模型有没有真正在学习。如果 RMSE 只比均值基线低 5%说明模型没捕捉到有效规律再调参意义不大如果低 30% 以上基本可以继续往下做。这个对比在向业务方汇报时也很有说服力。4.2 特征重要性与偏依赖定位模型靠什么做预测随机森林最容易给人“黑匣子”的错觉但 MATLAB 里两个工具能让它变得可解释特征重要性和偏依赖图。特征重要性我用oobPermutedPredictorDeltaError而不是简单的predictorImportance。它的原理是把某一列随机打乱观察袋外误差上升多少上升越多说明模型越依赖这个特征imp oobPermutedPredictorDeltaError(mdlFinal); bar(imp); set(gca, XTickLabel, predictors, XTickLabelRotation, 45);这个指标直接和预测误差挂钩比基于节点分裂增益的重要性更贴近业务判断。注意两个强相关特征会分摊重要性比如温度和压力高度相关时两者重要性都会被压平不代表它们没用。看到重要性分布明显不合理要想到相关性和特征冗余而不要急着删特征。偏依赖图是给业务方讲模型时最好用的工具。它显示当某个特征取不同值时模型预测的平均变化趋势figure; plotPartialDependence(mdlFinal, predictors{1});比如观察振动特征从 50 升到 80预测故障概率是否单调上升如果趋势和业务常识相反就要回头查特征构造或者数据泄漏。plotPartialDependence也支持传两个特征的索引画出二维交互热力图用来发现“温度和压力同时高时故障率激增”这类非线性关系。5. 随机森林预测避坑五个我在 MATLAB 里踩过的坑5.1 预测值像复读机全在均值附近现象模型跑完对测试集预测出来的值几乎围绕训练集均值波动画出曲线是几条横线交叉验证 RMSE 比基准好不了多少。原因常见有两个。一是树的数量太少默认设置下只有十几棵树每棵树之间的差异又被很大的NumVariablesToSample抹平集成后预测趋向平均二是MinLeafSize设得太大叶节点输出太粗整棵树的预测粒度已经跟不上目标的波动。解决先把树数量提高到 300 以上画 OOB 曲线确认收敛然后把NumVariablesToSample从 p/3 往 sqrt(p) 方向调小增加树间随机性最后把MinLeafSize从默认值往 2~5 方向调小。每次只动一个参数记录一次损失。这个现象很容易被误判成特征没选好实际是集成度不够。5.2 类别变量水平太多报错或者内存爆炸现象某列有几百上千个水平用dummyvar一展开内存直接爆掉即使没爆训练时间也涨到让人无法接受。原因把设备编号、单号、时间戳这类几乎每条记录都不重复的列当成普通类别特征塞进去了。树模型虽然能处理类别但水平越多每次分裂时枚举子集的开销越大。解决先把纯编号类列删掉业务上真正有关的类别列水平数超过样本数 10% 就合并低频水平。常见做法是保留 top 20 个水平其余并入 other% 统计水平频次保留 top 20其余合并为 other tbl table(categories(T.group), countcats(T.group), ... VariableNames, {level, count}); tbl sortrows(tbl, count, descend); keepLevels tbl.level(1:20); dropLevels tbl.level(21:end); T.group mergecats(T.group, dropLevels, other);这里mergecats把所有低频水平合并成一个other训练时模型只会见到 21 个水平。保留一个other还有一个好处等模型部署后如果遇到新的冷门类别可以直接归入other而不至于报错。5.3 交叉验证分数虚高多半是数据泄漏现象交叉验证 RMSE 很漂亮可一上线用测试集实测误差直接翻倍。原因数据泄漏。最常见的是先用全体数据做缺失值填充、离群值剔除或特征选择再做训练/测试划分。填充用的全体均值已经包含了测试集的信息模型等于在考试前偷看了答案。解决把“划分”提到所有统计步骤之前。缺失值只用训练集统计量填充离群值阈值只在训练集上判断特征选择只用训练集。测试集从划分那一刻起就彻底隔离。还有一个容易忽略的点如果项目里同时做了降维、PCA 这类转换同样只能用训练集拟合转换器再套到测试集上。树模型里这些操作不常见但一旦用了泄漏路径一模一样。5.4 特征重要性第一名是时间戳删掉原始时间列现象oobPermutedPredictorDeltaError排序结果里最靠前的竟然是一个时间戳列模型预测几乎完全依赖时间顺序。原因把datetime当成数值传进树模型或者把精确到秒的时间戳作为特征。树会按时间戳数值切出很多只覆盖单条样本的叶节点训练时误差极低一旦时间分布变了模型立刻失效。解决删除原始时间戳列按业务需要构造时间衍生特征比如小时、星期、是否节假日、距上一次事件的间隔。这样的特征既保留周期性又不会让树把时间当连续数值去硬切tt T.time_col; % datetime 列 T.hourOfDay hour(tt); % 小时 T.dayOfWeek weekday(tt); % 星期几 T removevars(T, time_col); % 删除原始时间戳如果是真正的时间序列预测还要额外注意样本顺序先用sortrows(T, time_col)按时间排序再按时间顺序划分训练和测试不能用随机划分。5.5 加载保存的模型后预测报错类别水平不一致现象训练好的模型用save保存换到现场用load加载后做predict报错说类别水平对不上或者预测结果和原来不一致。原因模型里记录了训练集见过的所有类别水平集合现场数据里出现了训练时没见过的水平predict无法处理。另一个原因是保存了整个大对象包含训练数组等冗余信息体积几百 MB加载慢容易让人误以为文件损坏。解决训练前把低频类别合并成other保证水平集合稳定保存前用compact压缩模型预测脚本里对现场数据做同样的水平对齐mdlFinal compact(mdlFinal); save(rf_pred_model.mat, mdlFinal, predictors, catIdx, keepLevels); % 现场加载后重新编码类别列 load(rf_pred_model.mat); Tnew.group categorical(Tnew.group, keepLevels); Tnew.group(isundefined(Tnew.group)) other; % 未知名归入 other pred predict(mdlFinal, Tnew(:, predictors));categorical(x, keepLevels)会把不在keepLevels里的值变成undefined然后手动赋值为other。前提是训练时确实保留了这个水平keepLevels必须包含other否则赋值会失败。6. 收尾的优化技巧小网格搜索、模型压缩与结果复现6.1 网格搜索固定树数量一次只搜两个参数随机森林的超参组合数不小但真正值得搜的只有MinLeafSize和NumVariablesToSample。我习惯把树数量固定为 300然后用小网格搜索这两个参数minLeafCandidates [2 5 10]; numVarCandidates [ceil(sqrt(p)), floor(p/3)]; best struct(loss, inf, minLeaf, [], numVar, [], mdl, []); for minLeaf minLeafCandidates for numVar numVarCandidates cvMdl fitrensemble(Ttrain(:, predictors), Ttrain.target, ... Method, Bag, NumLearningCycles, 300, ... Learners, templateTree(MinLeafSize, minLeaf, ... NumVariablesToSample, numVar), ... KFold, 5, CategoricalPredictors, catIdx); loss kfoldLoss(cvMdl); if loss best.loss best.loss loss; best.minLeaf minLeaf; best.numVar numVar; end end end六个组合在中小数据集上几分钟就能跑完。不要一上来就五个参数全搜组合数量爆炸式增长而且小数据集上的“最优”换一批数据很容易失效。数据量大时可以考虑用bayesopt做贝叶斯优化但中小项目手工网格已经够用。6.2 模型压缩与随机种子交付现场最容易被忽略的两件事最终模型训练好后务必compact再保存finalMdl fitrensemble(Ttrain(:, predictors), Ttrain.target, ... Method, Bag, NumLearningCycles, 300, ... Learners, templateTree(MinLeafSize, best.minLeaf, ... NumVariablesToSample, best.numVar), ... CategoricalPredictors, catIdx); finalMdl compact(finalMdl); save(rf_model.mat, finalMdl, predictors, catIdx, -v7.3);compact只保留预测所需的结构体积通常能缩到十分之一以内-v7.3是为大数组准备的存储格式模型较大时能避免保存失败。复现方面训练前用rng(2024)固定随机种子并且不要用并行池训练同一个模型否则每棵树的随机数流不一致再次运行结果会对不上。我现在交付随机森林项目的习惯是先跑一遍这个小网格把best参数、训练数据、最终模型和 OOB 曲线存档然后连同特征重要性图、偏依赖图一起交给现场。模型本身不是黑匣子OOB 收敛曲线、特征重要性和偏依赖图三样东西拿出去业务方基本能信服。至于那些“预测值全在均值附近”“特征重要性第一名是时间戳”的坑多数是因为数据和验证流程不干净而不是随机森林这个算法不够强。希望这篇笔记能帮你在 MATLAB 上少走几趟弯路把时间花在特征理解和业务沟通上希望帮到你。本文还有配套的精品资源点击获取
返回列表