1. 数据准备与环境搭建迈出第一步嘿朋友们今天咱们来聊聊在Matlab里用TreeBagger玩转随机森林回归。我知道一听到“机器学习”、“随机森林”这些词很多刚入门的朋友可能就有点发怵觉得门槛太高。别担心我刚开始接触的时候也一样感觉一堆参数和概念砸过来头都大了。但实际用下来特别是Matlab的TreeBagger它其实把很多复杂的东西都封装好了咱们要做的就是理解核心流程然后动手去调。今天我就把自己踩过坑、试过有效的经验用最直白的话分享给你保证你跟着做一遍就能上手。首先咱们得把“战场”准备好。用Matlab做机器学习第一步永远是把数据和环境弄利索。我强烈建议你使用Matlab较新的版本比如R2019b之后的因为TreeBagger函数和相关工具箱Statistics and Machine Learning Toolbox在这些版本里更稳定功能也更全。怎么检查呢你可以在Matlab命令窗口输入ver看看有没有‘Statistics and Machine Learning Toolbox’这一项。没有的话你得通过Matlab的附加功能管理器安装一下这是基础没得商量。数据从哪里来对于学习来说最好用的就是经典数据集。原始文章里提到了波士顿房价数据集这确实是回归问题的“Hello World”。不过直接去网站下载文件再导入对新手来说可能多了几步。我更喜欢用Matlab自带的数据集或者用一行代码就能获取的。这里给你分享两个我常用的方法一是使用load命令加载Matlab内置的示例数据比如load carsmall汽车数据二是用webread配合一些公开的数据集API。但为了完全复现波士顿房价这个经典案例我们可以用更直接的方式。实际上在Matlab的统计与机器学习工具箱示例中就包含了类似的数据。我们可以用load boston吗很遗憾Matlab并没有直接命名为‘boston’的内置数据。但别急我们可以用readtable或webread从网络资源获取。这里我演示一个更稳妥、无需手动下载的方法——使用UCI机器学习仓库的一个镜像。你可以在脚本开头运行这几行代码% 方法从网络URL读取波士顿房价数据CSV格式 url ‘https://archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.data‘; data webread(url); % 注意webread读回的是文本需要解析 % 更简单的方式是使用‘readtable’直接读取某些托管的文件但格式可能需要调整 % 为了绝对的可复现性我建议先将数据文件保存到本地工作目录说实话直接从网络读数据有时会受网络或格式影响。我最推荐的做法是第一次运行时手动从一个可靠来源如Kaggle下载‘housing.data’文件放到你的Matlab当前工作文件夹。然后使用readtable加载并指定列名。波士顿数据集有14列前13列是特征最后一列是目标房价。列名没有在原始数据中我们需要自己指定这样后面看着也清楚。% 假设 housing.data 文件已在当前文件夹 data readtable(‘housing.data‘, ‘FileType‘, ‘text‘); % 给数据表添加变量名 data.Properties.VariableNames {‘CRIM‘, ‘ZN‘, ‘INDUS‘, ‘CHAS‘, ‘NOX‘, ‘RM‘, ‘AGE‘, ‘DIS‘, ‘RAD‘, ‘TAX‘, ‘PTRATIO‘, ‘B‘, ‘LSTAT‘, ‘MEDV‘}; % 查看前几行 head(data)运行head(data)你就能看到数据的样子了。MEDV就是我们要预测的房价中位数其他都是特征比如RM是平均房间数LSTAT是低收入人口比例等等。数据准备好了我们通常需要做一个简单的数据探查比如看看有没有缺失值sum(ismissing(data))以及各特征的统计摘要summary(data)。波士顿数据集比较干净一般没有缺失值这步可以快速过。接下来我们要把数据拆成特征矩阵X和目标向量Y这是模型能理解的格式。% 将表格数据转换为矩阵 X table2array(data(:, 1:end-1)); % 所有行第1到倒数第2列作为特征 Y table2array(data(:, end)); % 最后一列作为目标变量好了数据已经躺在X和Y变量里了。在正式训练模型前还有一个重要的习惯性操作设置随机数种子。机器学习算法中涉及随机抽样比如随机森林的Bootstrap采样和特征随机选择设置种子可以保证你每次运行的结果是一致的这对于调试和比较不同参数的效果至关重要。不然你这次跑出来准确率是0.88下次变成0.85你就不知道是参数改动的效果还是随机性导致的。很简单加一行rng(1234, ‘twister‘); % 1234可以换成任何你喜欢的数字环境、数据、可重复性这三板斧准备好咱们就可以放心地召唤TreeBagger了。记住磨刀不误砍柴工前面这些步骤看似琐碎但能帮你避开很多“为什么我的结果和教程不一样”的坑。2. 模型训练与核心参数初探数据在手天下我有。现在咱们来训练第一个随机森林回归模型。Matlab里的TreeBagger函数就是干这个的它的名字很有意思“Bagging”“Tree”直译就是“装袋的树”非常形象地描述了随机森林的核心用Bootstrap抽样造出很多袋数据每袋数据训练一棵决策树最后把结果汇总。最基本的调用格式是Mdl TreeBagger(NumTrees, X, Y, ‘Method‘, ‘regression‘)。这里NumTrees是森林里树的数量X是特征矩阵Y是目标值。关键点来了你必须显式指定‘Method‘, ‘regression‘因为TreeBagger默认是用于分类的我刚开始就忘了加这个参数训练出来的模型怎么看都不对劲查了半天文档才恍然大悟。所以咱们的第一个模型可以这样建% 训练一个包含100棵树的随机森林回归模型 initialModel TreeBagger(100, X, Y, ‘Method‘, ‘regression‘); disp(‘模型训练完成‘)就这么简单一行代码模型就训练好了。你可以把initialModel变量看作是一个黑盒子里面装着100棵决策树以及它们如何做决策的所有规则。训练完成后我们最关心两件事一是模型在训练数据上的预测能力如何但不能完全信它容易过拟合二是模型有没有在训练过程中提供一些“自我诊断”的工具。幸运的是TreeBagger提供了非常方便的袋外误差估计。什么是袋外误差这算是随机森林的一个“天才”设计。在Bootstrap抽样时大概有37%的数据不会被抽中这些数据就叫袋外数据。对于每一棵树来说它没见过自己的袋外数据那么就可以用这些数据来验证这棵树的预测能力。把所有树的袋外预测误差平均起来就是整个森林的袋外误差。这个误差是对模型泛化能力的一个无偏估计非常有用我们不需要额外预留验证集在训练时就能得到一个可靠的性能参考。查看袋外误差曲线是了解模型训练进程的窗口。我们可以用oobError函数% 绘制袋外误差随树数量增加的变化曲线 figure; plot(oobError(initialModel), ‘LineWidth‘, 2); xlabel(‘已生长的树的数量‘); ylabel(‘袋外均方误差 (OOB MSE)‘); title(‘随机森林训练过程袋外误差变化‘); grid on;运行这段代码你会看到一条曲线。通常这条曲线会随着树的数量增加而快速下降然后逐渐趋于平缓。曲线变得平坦的点大致就是森林“学够了”的地方。如果曲线一直剧烈震荡下降说明可能还需要更多的树如果很早就平了也许你一开始设置的树数量就有点多可以适当减少以节省训练时间。通过这个图我们能直观地感受到模型是否收敛。除了树的数量另一个新手必须知道的参数是‘MinLeafSize‘即最小叶子大小。这是什么意思呢决策树在生长时会不断分裂节点直到某个节点里的样本数少于这个设定值它就不再分裂成为一个叶子节点。这个参数是控制树复杂度的关键。MinLeafSize设得越小树就会长得越深、越复杂更容易捕捉数据中的细节但也更容易过拟合记住训练数据中的噪声。设得越大树就越简单、越浅可能欠拟合但泛化能力可能更强。原始文章里做了一个实验比较了不同叶子大小5, 10, 20, 50, 100下的袋外误差曲线。我强烈建议你也亲手试一下代码结构类似leafSizes [5, 10, 20, 50, 100]; colors ‘rbcmy‘; figure; hold on; for i 1:length(leafSizes) model_temp TreeBagger(50, X, Y, ‘Method‘, ‘regression‘, ... ‘MinLeafSize‘, leafSizes(i), ... ‘OOBPrediction‘, ‘on‘); plot(oobError(model_temp), colors(i), ‘LineWidth‘, 1.5); end hold off; xlabel(‘树的数量‘); ylabel(‘袋外均方误差‘); legend({‘5‘, ‘10‘, ‘20‘, ‘50‘, ‘100‘}, ‘Location‘, ‘best‘); title(‘不同最小叶子大小对模型误差的影响‘); grid on;跑完这个代码你会看到几条不同颜色的曲线。通常你会发现叶子越小比如5初始误差下降可能更快最终误差可能也略低但曲线后期可能更不稳定。叶子越大曲线越平滑但最终误差平台可能稍高。这里没有绝对的“最佳值”对于回归问题从5开始尝试是一个不错的经验法则。你可以根据这个图在模型复杂度和稳定性之间做一个权衡。我的经验是对于波士顿房价这种几百个样本的中小数据集MinLeafSize设在5到20之间通常效果都不错。先有个感性认识后面我们还会结合其他参数一起调优。3. 特征重要性分析与模型优化模型训练出来了误差曲线也看了是不是就结束了当然不是一个好的机器学习实践者不仅要会“炼丹”还得会“看丹”知道模型为什么有效以及哪里还能改进。随机森林有一个特别棒的特性就是它能天然地评估每个特征的重要性。这就像是一个团队项目做完之后复盘看看每个成员的贡献度有多大。在TreeBagger中特征重要性是通过计算袋外数据误差的排列重要性来得到的。原理很巧妙对于某个特征我们把它在袋外数据中的值随机打乱破坏这个特征和标签之间的真实关系然后用打乱后的数据再次进行预测并计算新的误差。如果这个特征很重要打乱它会导致预测误差显著上升如果不重要误差变化就不大。这个误差上升的量就是该特征的重要性得分。在训练模型时我们只需要加上一个参数‘OOBPredictorImportance‘,‘on‘模型就会自动计算这个值。训练完成后结果存储在模型对象的OOBPermutedPredictorDeltaError属性里。% 训练一个计算特征重要性的模型 model_with_importance TreeBagger(100, X, Y, ‘Method‘, ‘regression‘, ... ‘OOBPredictorImportance‘, ‘on‘, ... ‘MinLeafSize‘, 5); % 获取重要性分数 importance_scores model_with_importance.OOBPermutedPredictorDeltaError; % 绘制条形图 figure; bar(importance_scores); xlabel(‘特征编号‘); ylabel(‘特征重要性得分‘); title(‘波士顿房价数据集特征重要性分析‘); grid on;运行后你会看到一个条形图每个柱子代表一个特征对应X矩阵的列。柱子越高说明这个特征对预测房价越重要。在波士顿数据集中你可能会发现LSTAT低收入人口比例和RM平均房间数的重要性得分遥遥领先这非常符合常识一个地区的贫困程度和房屋本身的房间数确实是影响房价的核心因素。知道特征重要性有什么用用处可大了第一特征筛选。你可以设定一个阈值比如重要性大于平均值的特征只保留重要的特征用它们重新训练一个模型。这样做的好处是1. 可能提升模型性能去除了噪声特征2. 加快预测速度3. 让模型更易于解释。我们来试一下% 设定阈值例如选择重要性大于均值1.5倍的特征 threshold 1.5 * mean(importance_scores); idx_important find(importance_scores threshold); fprintf(‘筛选出 %d 个重要特征编号为: ‘, length(idx_important)); disp(idx_important‘); % 使用重要特征重新训练模型 X_important X(:, idx_important); model_reduced TreeBagger(100, X_important, Y, ‘Method‘, ‘regression‘, ... ‘OOBPredictiction‘, ‘on‘, ‘MinLeafSize‘, 5); % 比较全特征模型和筛选后模型的袋外误差 figure; plot(oobError(model_with_importance), ‘b-‘, ‘LineWidth‘, 2); hold on; plot(oobError(model_reduced), ‘r--‘, ‘LineWidth‘, 2); hold off; xlabel(‘树的数量‘); ylabel(‘袋外均方误差‘); legend({‘全特征模型‘, ‘特征筛选后模型‘}, ‘Location‘, ‘best‘); title(‘特征筛选前后模型性能对比‘); grid on;很多时候你会发现用更少的特征模型误差并没有明显上升甚至可能下降这就是特征筛选的魔力。第二指导数据收集。在实际项目中收集数据是有成本的。通过特征重要性分析你可以知道应该把资源和精力集中在收集哪些关键数据上哪些无关紧要的数据可以忽略这能极大提高项目效率。除了特征重要性我们还可以利用袋外预测来评估模型的最终预测效果。我们可以用oobPredict函数得到每个样本的袋外预测值然后和真实值做对比计算R²等指标。% 获取袋外预测值 Y_oob_pred oobPredict(model_with_importance); % 计算R平方 SS_res sum((Y - str2double(Y_oob_pred)).^2); % 注意oobPredict返回的是字符串元胞数组 SS_tot sum((Y - mean(Y)).^2); R2_oob 1 - (SS_res / SS_tot); fprintf(‘模型的袋外预测R平方为: %.4f\n‘, R2_oob); % 绘制真实值 vs 预测值的散点图 figure; scatter(Y, str2double(Y_oob_pred), ‘filled‘); hold on; plot([min(Y), max(Y)], [min(Y), max(Y)], ‘r--‘, ‘LineWidth‘, 2); % 绘制yx参考线 hold off; xlabel(‘真实房价 (MEDV)‘); ylabel(‘袋外预测房价‘); title(sprintf(‘真实值 vs 预测值 (OOB R^2 %.3f)‘, R2_oob)); grid on; axis equal;这个散点图能直观地告诉你模型的预测效果。点越紧密地分布在红色虚线理想预测线附近说明模型预测越准。如果出现明显的系统性偏离比如点都落在线上方或下方可能说明模型存在偏差。通过这张图你对模型的性能会有一个非常扎实的感性认识。4. 高级调优与实战技巧前面我们聊了基础训练、看误差曲线、分析特征重要性这已经能解决80%的问题了。但如果你想把模型性能再往上提一提或者应对更复杂的数据就需要了解一些高级参数和技巧。别怕我挑几个最实用、效果最明显的跟你分享。第一个技巧是关于树的数量。我们之前都是拍脑袋定一个数比如100或200。那到底多少棵树才够呢理论上随机森林的误差会随着树的数量增加而收敛但边际效益递减。树太多会显著增加训练和预测时间却带不来精度的明显提升。一个实用的方法是观察袋外误差曲线当曲线基本走平不再有明显下降时对应的树数量就是一个合理的值。你可以写个循环来自动寻找这个“拐点”。maxTrees 500; oobErr zeros(maxTrees, 1); % 训练一个包含maxTrees棵树的模型并记录每增加一棵树时的袋外误差 model_for_tuning TreeBagger(maxTrees, X, Y, ‘Method‘, ‘regression‘, ... ‘OOBPrediction‘, ‘on‘); for i 1:maxTrees oobErr(i) oobError(model_for_tuning, ‘Mode‘, ‘cumulative‘); end % 找到误差下降变得平缓的点例如连续50棵树误差变化小于0.1% threshold 1e-4 * oobErr(1); for i 50:maxTrees if abs(oobErr(i) - oobErr(i-49)) threshold fprintf(‘建议的树数量约为: %d\n‘, i); break; end end % 绘制误差曲线 figure; plot(oobErr, ‘LineWidth‘, 2); xlabel(‘树的数量‘); ylabel(‘累积袋外均方误差‘); title(‘寻找最优树数量‘); grid on; hold on; plot([i, i], [min(oobErr), max(oobErr)], ‘r--‘); hold off;第二个高级参数是‘NumPredictorsToSample‘。这是随机森林“随机性”的另一个来源在每棵树分裂节点时不是考虑所有特征而是随机抽取一部分特征比如总特征数的平方根作为候选然后从中选最好的进行分裂。这能确保树之间的差异性是防止过拟合的关键。默认情况下对于回归问题TreeBagger会抽取全部特征数的三分之一。但你可以手动调整它。有时候减少这个数量增加随机性可以进一步提升模型的泛化能力尤其是在特征很多的时候。你可以把它当作一个超参数来尝试。numFeatures size(X, 2); sampleRatios [round(numFeatures/2), round(sqrt(numFeatures)), round(numFeatures/3)]; for i 1:length(sampleRatios) model_tmp TreeBagger(100, X, Y, ‘Method‘, ‘regression‘, ... ‘NumPredictorsToSample‘, sampleRatios(i), ... ‘OOBPrediction‘, ‘on‘); fprintf(‘随机抽取特征数: %d, 最终OOB MSE: %.4f\n‘, ... sampleRatios(i), oobError(model_tmp, ‘Mode‘, ‘ensemble‘)); end第三个实战技巧是处理类别特征。波士顿数据集中都是数值特征但现实中很多数据包含类别比如房屋类型别墅、公寓、所在区域等。TreeBagger通过‘CategoricalPredictors‘参数来处理。你需要告诉模型哪些列是类别变量。模型内部会采用适合类别变量的分裂方式。假设我们的数据第4列CHAS查尔斯河虚拟变量是二分类的类别特征我们可以这样指定% 创建一个逻辑向量标记类别特征列例如第4列 categorical_idx false(1, size(X, 2)); categorical_idx(4) true; % 假设第4列是类别特征 model_cat TreeBagger(100, X, Y, ‘Method‘, ‘regression‘, ... ‘CategoricalPredictors‘, find(categorical_idx), ... ‘OOBPredictorImportance‘, ‘on‘);第四个技巧是使用并行计算加速。如果你的数据集很大或者树的数量很多训练会非常慢。TreeBagger支持并行计算可以充分利用多核CPU。你只需要在训练前打开并行池并在调用函数时加上‘Options‘参数。% 检查并开启并行池如果需要 if isempty(gcp(‘nocreate‘)) parpool; % 开启并行池 end options statset(‘UseParallel‘, true); model_parallel TreeBagger(200, X, Y, ‘Method‘, ‘regression‘, ... ‘Options‘, options); disp(‘使用并行计算完成训练。‘);最后我想提一下模型保存与部署。辛辛苦苦调好的模型不能每次都用脚本重新训练。我们可以把它保存下来下次直接加载使用。% 保存模型 save(‘my_trained_boston_model.mat‘, ‘model_with_importance‘); % 清除工作区中的模型变量 clear model_with_importance % 加载模型 load(‘my_trained_boston_model.mat‘); % 对新数据进行预测 (假设new_X是新数据矩阵) % predicted_prices predict(model_with_importance, new_X);调优的过程有点像给汽车做改装没有唯一的最优解只有最适合当前数据和任务的平衡点。我的建议是从一个合理的默认配置开始比如100棵树MinLeafSize5然后优先调整MinLeafSize和树的数量观察袋外误差的变化。如果效果还不满意再考虑调整NumPredictorsToSample。每次只调整一个参数并记录结果这样才能清楚地知道每个参数的影响。记住袋外误差是你最忠实、最方便的向导。多动手试几次你就能对随机森林在Matlab里的“脾气”摸得一清二楚了。