简介:随机森林回归的MATLAB实现资源,主要面向需要完成回归预测、变量筛选与特征重要性评估的数据分析人员及机器学习初学者。资源基于集成学习原理,涵盖从数据预处理、模型构建到结果评估的完整流程,可借助TreeBagger或fitrensemble等函数完成随机森林建模,并通过出袋误差和变量重要性分数进行模型诊断与特征解释。包体为zip压缩包,共2个文件,均为.m脚本,整体仅2KB,体量精简、适合快速参考或嵌入现有项目。两个脚本分别覆盖随机森林回归与相关对比分析(如PCR),可直接运行或修改参数以适配不同数据集。资源已有3510人学习下载,通过学习,使用者可掌握MATLAB中随机森林回归的建模套路,获得可用脚本框架、参数设置思路及特征重要性的解读方法,减少从零编码与调试的时间成本,适用于教学演示、论文实验或工程预测任务。
1. RF随机森林回归:它不是默认就能用的分类器
做回归预测时,随机森林常被当成一个“默认不会错”的模型,但第一次上手的人很容易被同一个问题卡住:拿 RandomForestClassifier 改个标签去跑回归,得到一堆离散值;或者直接默认 RandomForestRegressor 跑高维稀疏特征,发现 OOB R² 是负数。随机森林回归并不是 RF 分类器的“换标签版本”,它从分裂准则、参数默认值到验证方式都跟分类版本有区别。回归树为什么用 MSE 分裂而不是 Gini,max_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_depth=3, random_state=0) tree_one.fit(X_train, y_train) print(export_text(tree_one, feature_names=list(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_score=True,回归任务会输出 R²。
OOB 指标的价值是:不用单独切验证集就能估算泛化能力,特别适合样本量小、不愿意再牺牲数据做验证集的项目。OOB R² 和测试 R² 通常接近,但不要期待完全一致:OOB 平均掉了一部分树(约0.632*B棵),预测方差略大,因此会偏保守。即便如此,它比单次切分验证集更稳定,因为每个样本的 OOB 预测都来自不同的树组合。
2.4 回归任务中先记住这几个参数
下面的参数表是随机森林回归实战里我会最先检查的 6 个位置:
| 参数 | 默认值 | 回归任务建议 | 说明 |
|---|---|---|---|
n_estimators | 100 | 300~1000 | 树越多越稳定,但收益递减,注意训练时间和内存 |
max_features | 1.0 | 0.3~1.0,或总特征数/3 | 控制随机化程度,太小欠拟合,太大树间相关性高 |
max_depth | None | None 或 10~30 | None 会生成完整树,通常靠叶子约束防过拟合 |
min_samples_leaf | 1 | 5~10 | 叶子最小样本数,回归任务里设大于 1 能明显压方差 |
min_samples_split | 2 | 5~20 | 内部节点分裂所需最少样本,配合叶子大小使用 |
oob_score | False | True | 回归任务建议开启,能拿到近似泛化分 |
有一点要单独说:sklearn 的RandomForestRegressor默认max_features=1.0,即每棵树的每次分裂都会考虑全部特征。这对低维数据问题不大,但特征数量一旦超过几十个,模型会很快变成“很多棵相似的深树”,Bagging 的降方差效果被削弱。Breiman 原始论文对回归的建议是max_features = max(1, floor(p/3)),也就是每棵树候选特征只有 1/3。用 sklearn 可以直接写max_features=0.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, columns=housing.feature_names) y = pd.Series(housing.target, name="MedHouseVal") X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) rf = RandomForestRegressor( n_estimators=300, max_features=0.33, min_samples_leaf=5, oob_score=True, n_jobs=-1, random_state=42, ) 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_features=0.33:8 个特征实际候选数按max(1, int(8*0.33))=2计算,不是“砍掉 33% 再用 67%”。每次分裂只从随机抽到的 2 个特征里挑最优切分,树与树的相关性被显著压低。min_samples_leaf=5则保证叶子节点至少有 5 个样本,避免回归树把噪声也学进去。n_jobs=-1表示使用所有 CPU 核心,RF 的每棵树都独立训练,并行提升接近线性。
3.2 rf 怎么引用关键字:特征名与重要性的映射
模型拟合时如果输入X是 DataFrame,sklearn 会把列名保存在feature_names_in_属性里。这意味着你可以直接按列名引用特征,而不需要自己维护“第2列是MedInc”这种对应关系:
if hasattr(rf, "feature_names_in_"): importances = pd.Series( rf.feature_importances_, index=rf.feature_names_in_ ) print(importances.sort_values(ascending=False))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_unknown="ignore"), ["Region"]), ] ) pre.fit(demo) X_demo_enc = pd.DataFrame( pre.transform(demo), columns=pre.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 输出喂给 RF,feature_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(n=3000, random_state=1).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_state=42), param_distributions=param_grid, n_iter=20, cv=5, scoring="neg_root_mean_squared_error", random_state=42, 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² 接近 1,OOB 或测试 R² 明显低,而且随着n_estimators增大,测试误差不再下降。
第一个控制手段是提高min_samples_leaf。叶子太小会把个别极端样本当成规律学进去,调大叶子大小能直接压方差。一个快速实验循环如下:
for leaf in [1, 5, 10, 30]: rf_tmp = RandomForestRegressor( n_estimators=300, max_features=0.33, min_samples_leaf=leaf, oob_score=True, n_jobs=-1, random_state=42, ) rf_tmp.fit(X_train, y_train) print(f"leaf={leaf:>2}, OOB R2={rf_tmp.oob_score_:.4f}")第二个手段是限制max_depth。很多人的直觉是“RF 不怕过拟合,所以不用限制深度”,实际在特征维度高、样本量小时,深树的叶子依然会落到单个样本上。设置max_depth=15或更小,往往比盲目加树更有效。
第三个手段是降低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]去取数。
如果要更规范地做 QRF,R 生态的ranger和grf都有现成实现,参数里直接开quantreg或quantile_forest,训练好的模型还能给出条件分位数;但理解上面这个轻量版本,能帮你准确判断现有 RF 模型的预测区间从哪里来。上线时建议把rf对象和tree_y叶子映射一起用joblib.dump保存,预测服务里同时暴露predict和interval两个接口,点预测和分位数预测共用同一个模型版本,避免两者因多次重训产生的漂移。
本文还有配套的精品资源,点击获取