尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

随机森林回归实战:从MSE分裂到P10/P90预测区间

随机森林回归实战:从MSE分裂到P10/P90预测区间 简介随机森林回归的MATLAB实现资源主要面向需要完成回归预测、变量筛选与特征重要性评估的数据分析人员及机器学习初学者。资源基于集成学习原理涵盖从数据预处理、模型构建到结果评估的完整流程可借助TreeBagger或fitrensemble等函数完成随机森林建模并通过出袋误差和变量重要性分数进行模型诊断与特征解释。包体为zip压缩包共2个文件均为.m脚本整体仅2KB体量精简、适合快速参考或嵌入现有项目。两个脚本分别覆盖随机森林回归与相关对比分析如PCR可直接运行或修改参数以适配不同数据集。资源已有3510人学习下载通过学习使用者可掌握MATLAB中随机森林回归的建模套路获得可用脚本框架、参数设置思路及特征重要性的解读方法减少从零编码与调试的时间成本适用于教学演示、论文实验或工程预测任务。1. RF随机森林回归它不是默认就能用的分类器做回归预测时随机森林常被当成一个“默认不会错”的模型但第一次上手的人很容易被同一个问题卡住拿 RandomForestClassifier 改个标签去跑回归得到一堆离散值或者直接默认 RandomForestRegressor 跑高维稀疏特征发现 OOB R² 是负数。随机森林回归并不是 RF 分类器的“换标签版本”它从分裂准则、参数默认值到验证方式都跟分类版本有区别。回归树为什么用 MSE 分裂而不是 Ginimax_features默认 1.0 会带来什么后果以及怎么把点预测扩展成 P10/P90 区间下面的内容按实际建模顺序展开配套代码直接用加州房价数据验证。2. 随机森林回归的核心机制回归树、Bagging 与 OOB 评分2.1 回归树怎么分裂MSE 就是随机森林回归算法的算法基础随机森林回归算法不是独立算法它是很多棵回归树的平均。要理解 RF先看单棵回归树CART做什么给定一组样本每步选一个特征j和一个阈值t把当前节点样本切成左右两个集合希望切开后两个集合里的目标值分别更“齐”。回归任务里衡量不齐的指标是均方误差 MSE落在左边的子节点均值为y_left右边为y_right分割得分是左右子节点 MSE 按样本量加权后的和。分裂器会穷举每个特征和阈值找加权 MSE 下降最多的那个切分点。分类树用 Gini 不纯度回归树不能用 Gini这是刚接触 RF 时最容易踩的坑。下面用一棵限制深度的回归树展示分裂结果from sklearn.tree import DecisionTreeRegressor, export_text tree_one DecisionTreeRegressor(max_depth3, random_state0) tree_one.fit(X_train, y_train) print(export_text(tree_one, feature_nameslist(X_train.columns)))export_text输出的每一行是一个分裂规则比如MedInc 4.71对应某个收入水平附近的样本。叶子节点里的value是落入该区间样本的目标均值。随机森林回归做预测时新样本沿每棵树走到叶子最后把所有叶子的value取算术平均。这也是为什么 RF 回归的输出是连续值而不是分类任务里那个投票得分。2.2 Bagging 为什么能压低预测波动单棵回归树很灵活深树几乎能记住样本导致方差很大换一组训练数据同一个叶子边界会剧烈变化。Bagging 的做法是对原始数据集做B次有放回抽样每次抽 N 个样本构成一个自助样本集然后在每个自助集上训练一棵树预测时对B棵树的输出做平均。对回归任务算术平均是唯一的聚合方式没有多数投票。方差层面的解释是如果B棵树互相独立平均预测的方差是单棵树的1/B如果树之间完全相关平均不会带来任何改善。RF 的关键改进是每次分裂时只随机选一部分特征max_features参与候选让树之间相关性降下来。这个随机化比单纯的 bagged trees 效果好得多也是“随机”二字的含义。这里有个常见误解n_estimators越大模型一定不会过拟合。从方差角度看增加 B 只让平均更稳定偏差基本不变但如果单棵树过拟合严重Bagging 只能减少一部分方差不能消除错误边界。调参时要先限制树的复杂度再考虑加树的数量。2.3 OOB 评分RF 自带的一张验证集自助采样时每棵树大约有 36.8% 的样本没被抽到。对第i个样本来说只有它没参与训练的树会参与预测把这些树的预测结果平均就得到 OOB 预测。每个样本都能拿 OOB 预测和真实值算误差sklearn 中设置oob_scoreTrue回归任务会输出 R²。OOB 指标的价值是不用单独切验证集就能估算泛化能力特别适合样本量小、不愿意再牺牲数据做验证集的项目。OOB R² 和测试 R² 通常接近但不要期待完全一致OOB 平均掉了一部分树约0.632*B棵预测方差略大因此会偏保守。即便如此它比单次切分验证集更稳定因为每个样本的 OOB 预测都来自不同的树组合。2.4 回归任务中先记住这几个参数下面的参数表是随机森林回归实战里我会最先检查的 6 个位置参数默认值回归任务建议说明n_estimators100300~1000树越多越稳定但收益递减注意训练时间和内存max_features1.00.3~1.0或总特征数/3控制随机化程度太小欠拟合太大树间相关性高max_depthNoneNone 或 10~30None 会生成完整树通常靠叶子约束防过拟合min_samples_leaf15~10叶子最小样本数回归任务里设大于 1 能明显压方差min_samples_split25~20内部节点分裂所需最少样本配合叶子大小使用oob_scoreFalseTrue回归任务建议开启能拿到近似泛化分有一点要单独说sklearn 的RandomForestRegressor默认max_features1.0即每棵树的每次分裂都会考虑全部特征。这对低维数据问题不大但特征数量一旦超过几十个模型会很快变成“很多棵相似的深树”Bagging 的降方差效果被削弱。Breiman 原始论文对回归的建议是max_features max(1, floor(p/3))也就是每棵树候选特征只有 1/3。用 sklearn 可以直接写max_features0.33。调参时优先动这一项它对最终 R² 的影响往往比n_estimators更明显。3. 用随机森林回归预测模型跑通最小预测流程数据准备与特征引用3.1 用加州房价数据跑一遍随机森林回归预测模型最稳妥的上手路径是先用一份公开表格数据把模型跑通再替换成自己的数据。这里用 sklearn 自带的加州房价数据包含 8 个连续特征和 20640 个样本import pandas as pd import numpy as np from sklearn.datasets import fetch_california_housing from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score housing fetch_california_housing() X pd.DataFrame(housing.data, columnshousing.feature_names) y pd.Series(housing.target, nameMedHouseVal) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) rf RandomForestRegressor( n_estimators300, max_features0.33, min_samples_leaf5, oob_scoreTrue, n_jobs-1, random_state42, ) rf.fit(X_train, y_train) y_pred rf.predict(X_test) mse mean_squared_error(y_test, y_pred) print(test R2:, r2_score(y_test, y_pred)) print(test MSE:, mse) print(test RMSE:, mse ** 0.5) print(OOB R2:, rf.oob_score_)这里的核心参数是max_features0.338 个特征实际候选数按max(1, int(8*0.33))2计算不是“砍掉 33% 再用 67%”。每次分裂只从随机抽到的 2 个特征里挑最优切分树与树的相关性被显著压低。min_samples_leaf5则保证叶子节点至少有 5 个样本避免回归树把噪声也学进去。n_jobs-1表示使用所有 CPU 核心RF 的每棵树都独立训练并行提升接近线性。3.2 rf 怎么引用关键字特征名与重要性的映射模型拟合时如果输入X是 DataFramesklearn 会把列名保存在feature_names_in_属性里。这意味着你可以直接按列名引用特征而不需要自己维护“第2列是MedInc”这种对应关系if hasattr(rf, feature_names_in_): importances pd.Series( rf.feature_importances_, indexrf.feature_names_in_ ) print(importances.sort_values(ascendingFalse))feature_importances_是 sklearn 对 MDI平均不纯度减少的归一化结果每棵树分裂时按当前节点的 MSE 下降量加权累加到对应特征上最后在全部树上取平均。它的意义是“这个特征对拟合树结构的总贡献”适合用来做特征粗筛但不要直接解释成因果重要性。高基数的离散特征和连续特征会天然偏高因为它们有更多候选切分点。如果还想看单棵树实际用了哪些特征分裂可以通过rf.estimators_[0].tree_.feature拿到索引数组再用feature_names_in_还原成名字tree rf.estimators_[0] feat_idx tree.tree_.feature split_names [rf.feature_names_in_[i] if i 0 else leaf for i in feat_idx] print(split_names[:10])叶子节点在tree_.feature里的标记是-2所以上面用条件表达式做了一次过滤。这个技巧在排查“为什么某棵树用了不该用的特征”时很实用也能验证编码后的列名有没有被 Pipeline 弄丢。3.3 rf data convert类别特征和列顺序转换RF 回归不能直接吃object类型或缺失值。最常见的报错是ValueError: could not convert string to float原因通常是 DataFrame 里还留着一列category类型。常见做法是用ColumnTransformer把数值列透传、把类别列做 OneHot 编码同时保留输出列名from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder num_cols [MedInc, HouseAge, AveRooms, AveBedrms, Population, AveOccup, Latitude, Longitude] demo X_train.head(500).copy() demo[Region] np.where(demo[MedInc] 3, high, low) pre ColumnTransformer( transformers[ (num, passthrough, num_cols), (cat, OneHotEncoder(handle_unknownignore), [Region]), ] ) pre.fit(demo) X_demo_enc pd.DataFrame( pre.transform(demo), columnspre.get_feature_names_out() ) print(X_demo_enc.head())pre.get_feature_names_out()返回的列名形如num__MedInc、cat__Region_high。把这个 DataFrame 喂给 RF 后feature_names_in_就会保存这些完整列名。要重点关注的是如果直接把ColumnTransformer的 NumPy 输出喂给 RFfeature_names_in_不会生成后面引用特征名就只能靠外部维护的列名数组特征一变顺序就容易错位。rf data convert还有一个常见方向是压缩内存把只参与数值计算的列统一转成float32能让大 DataFrame 在复制和训练时内存占用明显下降。但要注意OneHot 后的稀疏矩阵不要强转成稠密ndarray否则内存可能扩大几个量级应该直接以稀疏格式参与训练。列顺序也要盯死ColumnTransformer默认按 transformers 列表的顺序拼接列模型不关心顺序但get_feature_names_out()的顺序必须和实际输出保持一致否则后面的特征名引用全是错的。4. 随机森林回归的调参与过拟合排查从随机搜索到 OOB 信号4.1 用 RandomizedSearchCV 找参数组合而不是硬刚网格随机森林回归的参数空间里有连续分布比如max_features、min_samples_leaf全网格搜索会指数级爆炸。调参时我不会用GridSearchCV硬刚而是先用RandomizedSearchCV固定预算做粗筛再对少数参数精调。一个可裁剪的做法是先抽样 3000 行数据把搜索时间控制在分钟级from scipy.stats import randint, uniform from sklearn.model_selection import RandomizedSearchCV sample_idx X_train.sample(n3000, random_state1).index X_search X_train.loc[sample_idx] y_search y_train.loc[sample_idx] param_grid { n_estimators: randint(100, 600), max_features: uniform(0.2, 0.8), max_depth: randint(10, 40), min_samples_leaf: randint(1, 12), min_samples_split: randint(2, 25), } search RandomizedSearchCV( RandomForestRegressor(random_state42), param_distributionsparam_grid, n_iter20, cv5, scoringneg_root_mean_squared_error, random_state42, n_jobs-1, ) search.fit(X_search, y_search) print(search.best_params_) print(-search.best_score_)neg_root_mean_squared_error是 RMSE 的负数形式因为 sklearn 的评分约定是“越大越好”取负号之后变成 RMSE-search.best_score_就是验证集 RMSE。搜索目标建议用 RMSE 而不是 R²因为 R² 对离群值不敏感RMSE 能直接反映预测错误量级。这里把n_iter压到 20 是为了让示例能跑完真实项目我会把预算提高到 50~100 组并在全量数据上重新拟合最优参数。4.2 OOB 和 K 折交叉验证怎么分工很多人在调参时纠结到底用 OOB 还是交叉验证。两者不是二选一而是不同阶段的工具。OOB 不需要额外训练fit时顺手就出来了交叉验证需要把 K 折数据各训练一次但给出的是多次重复后的稳定估计。它们的取舍可以参考下表维度OOB 评分K 折交叉验证训练成本无额外成本需要 K 次重训练数据利用率每个样本约 63% 的树参与预测每次训练只用 80%~90% 数据稳定性波动略大设置固定随机种子后更稳定适用场景快速初筛、观察超参趋势最终评分、特征选择对比、模型上线前验收调参初筛我会优先看 OOB把n_estimators和min_samples_leaf各设几组画出 OOB R² 的曲线基本能判断模型复杂度方向。最终验收时再用cross_val_score或RandomizedSearchCV的交叉验证结果。如果 OOB R² 和 K 折 R² 差距特别大常见原因是max_features设得太大导致树间相关性过高或者是样本量太小、OOB 预测的树集合过于残缺导致的波动。4.3 三个能立竿见影的过拟合控制手段随机森林回归的过拟合不像深度模型那么剧烈但依然会出现在特征噪声大、样本量小的场景里。判断信号是训练集 R² 接近 1OOB 或测试 R² 明显低而且随着n_estimators增大测试误差不再下降。第一个控制手段是提高min_samples_leaf。叶子太小会把个别极端样本当成规律学进去调大叶子大小能直接压方差。一个快速实验循环如下for leaf in [1, 5, 10, 30]: rf_tmp RandomForestRegressor( n_estimators300, max_features0.33, min_samples_leafleaf, oob_scoreTrue, n_jobs-1, random_state42, ) rf_tmp.fit(X_train, y_train) print(fleaf{leaf:2}, OOB R2{rf_tmp.oob_score_:.4f})第二个手段是限制max_depth。很多人的直觉是“RF 不怕过拟合所以不用限制深度”实际在特征维度高、样本量小时深树的叶子依然会落到单个样本上。设置max_depth15或更小往往比盲目加树更有效。第三个手段是降低max_features。这跟限制树深度配合使用max_features越小树越“弱”Bagging 平均后的偏差也越小max_depth限制的是单棵树的学习空间两者一起调才不会把树压得太死。还有一个容易忽略的点RF 的特征重要性单次训练排名会波动尤其是特征间相关性高时。想用“哪个特征最重要”做决策我会用多个随机种子跑 3~5 次取平均排名或者改用 permutation importance而不是直接打印一次feature_importances_就下结论。5. 用随机森林回归预测模型输出 P10/P90分位数森林的轻量实现5.1 点预测之外的预测区间业务上只有点预测往往不够备货要算安全库存电力调度要留出峰值余量供应链排产更关心“最差情况在哪”。随机森林回归的predict返回的是所有叶子均值的平均天然丢掉了叶子内样本的分布信息。直接拿每棵树的预测值取分位数也不行因为那个分布只反映“树与树之间的不一致”不包含样本噪声算出来的区间会偏窄。正确思路是分位数回归森林Quantile Regression Forests训练完成后对每个新样本在每棵树上定位到它落入的叶子把该叶子内训练样本的目标值全部收集起来合成一个大数组最后对这个数组取分位数。这个思路的工程实现不复杂RandomForestRegressor.apply()正好能拿到样本在每棵树上的叶子索引。5.2 用 rf.apply() 把叶子样本收集成分位数下面的函数把叶子样本映射保存下来再用新样本的叶子索引分段取分位数是一个轻量可复用的实现import numpy as np def rf_interval(rf, X_train, X_test, y_train, q(0.1, 0.5, 0.9)): leaf_train rf.apply(X_train) leaf_test rf.apply(X_test) # 记录每棵树的叶子 - 该叶子内训练样本的目标值列表 tree_y [] for t in range(rf.n_estimators): leaf_to_y {} for leaf, yv in zip(leaf_train[:, t], y_train): leaf_to_y.setdefault(leaf, []).append(yv) tree_y.append(leaf_to_y) # 对测试样本收集所有树对应叶子的 y取分位数 preds [] for row in leaf_test: vals [] for t, leaf in enumerate(row): vals.extend(tree_y[t].get(leaf, [])) preds.append(np.quantile(vals, q)) return np.array(preds)用前面训练好的rf调用一次intervals rf_interval( rf, X_train.iloc[:1000], X_test.iloc[:5], y_train.iloc[:1000], q(0.1, 0.5, 0.9), ) print(intervals)输出数组的每一行对应一个测试样本的三列分别是 P10、P50、P90。P50 通常会接近rf.predict的结果但不完全相等因为分位数来自原始目标值的经验分布而不是树均值再做平均。这个方法的代价是tree_y会把训练样本的目标值复制到多个叶子桶里内存量大约是n_estimators * n_samples * 每个叶子的样本数。数据量小无所谓数据量大了以后更常见的工程做法是只存叶子索引到训练样本序号的映射不复制y值然后再用y_train.iloc[idx]去取数。如果要更规范地做 QRFR 生态的ranger和grf都有现成实现参数里直接开quantreg或quantile_forest训练好的模型还能给出条件分位数但理解上面这个轻量版本能帮你准确判断现有 RF 模型的预测区间从哪里来。上线时建议把rf对象和tree_y叶子映射一起用joblib.dump保存预测服务里同时暴露predict和interval两个接口点预测和分位数预测共用同一个模型版本避免两者因多次重训产生的漂移。本文还有配套的精品资源点击获取
返回列表