2026/9/20 11:40:03

随机子空间集成:原理、与随机森林的区别及scikit-learn实操

随机子空间集成:原理、与随机森林的区别及scikit-learn实操 做分类任务最怕什么不是模型不收敛也不是代码崩了而是模型给出的预测结果“飘”。之前做特征维度上百的物料分类单棵决策树效果一般试了 Bagging 之后稳了一些但总觉得基学习器彼此太像预测高度雷同集成带来的提升很有限。后来把随机子空间集成方法搬到 scikit-learn 里跑了一遍才发现问题常常出在特征维度上而不只出在样本维度。这篇文章就把随机子空间的原理、和随机森林的边界关系、以及在 scikit-learn 里的参数设置和实操细节一次性聊透不管你是刚学集成学习的新手还是已经在业务里跑随机森林的同学都应该有点参考价值。1. 随机子空间到底在解决什么问题1.1 从样本扰动到特征扰动集成学习的核心思路说白了就是“多个模型商量着来”。Bagging 做的事是让每个基学习器去不同的样本子集上训练通过有放回抽样制造样本扰动降低模型之间的相关性。但这里有个容易被忽略的前提如果特征维度本身很高或者特征之间高度冗余那么即便每个基学习器看到的样本不同它们在分裂时仍然倾向于选择那几列强特征最终学出来的结构大差不差。随机子空间方法Random Subspace Method1998 年 Ho 提出的那套思路走的是另一条路在训练每个基学习器之前先从全部特征里随机抽一个子集让这个基学习器只看这部分特征。用生活里的话说Bagging 是给一组医生看不同的病历随机子空间是让这些医生分别从心电图、血常规、影像片子等不同角度去诊断同一批病人。单看一个角度可能有偏差但把多角度的判断综合起来往往比所有人盯着同一份报告要稳。为什么这套机制在高维数据上特别管用因为集成模型的预测方差可以粗略拆成两项一项是基学习器两两之间的平均相关性带来的另一项会随着基学习器数量增加而下降。想让第一项变小光堆模型数量没用必须从源头降低相关性。Bagging 用样本扰动来降随机子空间则直接通过特征扰动来降。当特征冗余度高时这个“降相关”的效果尤其明显最终泛化误差往往能压得更低。1.2 与Bagging、随机森林的关系别搞混这是我在实际交流中见到最多混淆的地方。很多人问随机森林不就是用了随机子空间吗严格来说两者机制是不同的。纯 Bagging对样本行做有放回抽样特征全部保留。对应 sklearn 里的BaggingClassifier(max_features1.0)。经典随机子空间对特征列做随机抽样样本可以全用也可以同时抽样。每个基学习器在训练过程中固定使用这一个特征子集。对应BaggingClassifier(max_features0.5)之类的设置。随机森林既对样本抽样又在每个节点分裂时重新从全特征里随机抽一小批候选特征然后从这批候选中挑最优分裂点。注意它不是在整棵树的训练开始时固定一个特征子集而是每次分裂前都重新抽一次。我用一张表把这几个概念摆清楚方法样本维度特征维度特征采样的时机纯 Bagging有放回抽样全部特征无随机子空间可抽样也可全用每个基学习器固定一个小特征子集训练开始时采样一次随机森林有放回抽样每次分裂时抽候选特征每个节点重新采样这个区分不是抠概念而是直接关系到你怎么调参。如果你想要的是“每个基学习器从不同视角学数据”用BaggingClassifier(max_features0.5)更贴近经典随机子空间如果你想要的是“每棵树内部的分裂都充满随机性”那就直接上随机森林。两者都是有效的特征随机化但作用的粒度和对多样性的贡献方式不一样。2. scikit-learn里的核心参数与选择逻辑2.1 四个关键参数拆解在 scikit-learn 里随机子空间模型的落地主要看BaggingClassifier和BaggingRegressor的这组参数max_samples每个基学习器使用的样本比例或数量默认 1.0。想退回到经典 Bagging 的样本扰动就设成 0.5~0.8 并配合bootstrapTrue。bootstrap控制样本抽样是否放回默认 True。如果bootstrapFalse而max_samples0.8每个基学习器用的是不重复的 80% 样本这在部分场景下也能用但经典做法还是放回抽样。max_features每个基学习器使用的特征比例或数量。默认 1.0也就是所有特征都用此时就是纯 Bagging。把它降到 0.3~0.7就是我在文章里说的经典随机子空间。bootstrap_features控制特征抽样是否放回默认 False。这个参数容易被忽略它是专门为特征维度设计的。保持 False 意味着每个特征子集不重复这也是随机子空间最典型的用法如果设成 True同一个特征可能在一个基学习器内重复出现随机性更高但大多数场景下收益不稳定我一般不建议开局就开这个开关。核心逻辑就是max_samples控制样本维度的随机性max_features控制特征维度的随机性bootstrap_features决定特征抽样是有放回还是无放回。2.2 参数推荐值与版本坑先说版本。BaggingClassifier的基学习器参数在 scikit-learn 1.2 之前叫base_estimator1.2 之后改名为estimator。我下面代码里会直接用estimator如果你的环境版本偏老运行时报错提示遇到base_estimator相关的关键字问题时把它换成base_estimator就行。另外RandomForestClassifier的max_samples参数是 1.1 之后才加入的低版本需要去掉那个参数。特征比例怎么定我的经验值是这样一套特征数小于 20max_features0.8~1.0。维度本来就不高再强行砍掉一半特征单棵树的偏差会明显变大集成救不回来。特征数 20~100max_features0.5~0.8。这个区间是随机子空间最舒服的舞台既能制造足够的特征视角差异又不会让单个基学习器信息量太差。特征数上千甚至更高比如文本 TF-IDF 这类稀疏高维数据max_features0.1~0.3起步或者直接用sqrt量级的特征数重点看验证集精度的变化。注意这些是起步值不是最终答案。随机子空间的收益来自“保留足够信息”和“提升基学习器多样性”之间的平衡我后面会专门讲特征比例设错时的典型症状。3. 实操记录三组对照实验逐步跑通3.1 环境准备与实验数据安装和版本检查我习惯直接用 pip 装最新的pip install -U scikit-learn python -c import sklearn; print(sklearn.__version__)代码里我用的是 1.3 及以上版本的 API 风格。实验数据用make_classification构造一份中等偏高维度的合成数据40 个特征里25 个有信息量、10 个是冗余特征还带一点噪声。这种结构比较贴近真实业务里特征高度纠缠的情况也能看出随机子空间在特征冗余场景下的优势。3.2 代码实现与关键注释真整实现就两段。第一段是用 sklearn 封装好的接口直接跑三组对照纯 Bagging、随机子空间、随机森林。from sklearn.ensemble import BaggingClassifier, RandomForestClassifier from sklearn.tree import DecisionTreeClassifier from sklearn.datasets import make_classification from sklearn.model_selection import cross_val_score X, y make_classification( n_samples1500, n_features40, n_informative25, n_redundant10, n_clusters_per_class2, flip_y0.05, random_state42 ) models { Bagging: BaggingClassifier( estimatorDecisionTreeClassifier(), n_estimators200, max_samples0.8, max_features1.0, bootstrapTrue, bootstrap_featuresFalse, n_jobs-1, random_state42 ), RandomSubspace: BaggingClassifier( estimatorDecisionTreeClassifier(), n_estimators200, max_samples0.8, max_features0.5, bootstrapTrue, bootstrap_featuresFalse, n_jobs-1, random_state42 ), RandomForest: RandomForestClassifier( n_estimators200, max_featuressqrt, max_samples0.8, n_jobs-1, random_state42 ), } for name, model in models.items(): scores cross_val_score(model, X, y, cv5, scoringaccuracy, n_jobs-1) print(f{name}: acc {scores.mean():.4f} ± {scores.std():.4f})第二段是一个简化版的手动随机子空间实现。写这段不是为了替代封装好的接口而是为了让你看清楚训练时到底发生了什么。注意特征索引必须在fit阶段记录预测时复用千万不要在预测时重新随机抽特征否则结果完全错乱。import numpy as np from sklearn.base import clone from sklearn.tree import DecisionTreeClassifier class RandomSubspaceClassifier: def __init__(self, n_estimators50, subspace_ratio0.5, random_stateNone): self.n_estimators n_estimators self.subspace_ratio subspace_ratio self.random_state random_state def fit(self, X, y): rng np.random.RandomState(self.random_state) n_features X.shape[1] n_sub max(1, int(self.subspace_ratio * n_features)) self.estimators_ [] self.feature_idx_ [] base DecisionTreeClassifier() for _ in range(self.n_estimators): idx rng.choice(n_features, n_sub, replaceFalse) self.feature_idx_.append(idx) estimator clone(base) estimator.fit(X[:, idx], y) self.estimators_.append(estimator) return self def predict_proba(self, X): proba np.mean( [est.predict_proba(X[:, idx]) for est, idx in zip(self.estimators_, self.feature_idx_)], axis0 ) return proba def predict(self, X): return np.argmax(self.predict_proba(X), axis1)放在这里只是为了把“每个基学习器只在自己专属的特征子集上学习”这个逻辑可视化。实际项目里还是BaggingClassifier更稳因为它替你把类别对齐、样本采样、并行计算这些边界情况都处理好了。3.3 结果解读和调参方向下面是我本地一次运行时的结果不同随机种子和机器上会有波动但相对趋势比较有代表性模型accuracy5折标准差纯 Bagging0.87400.0310随机子空间特征比例 0.50.88670.0246随机森林0.89130.0255在这个数据上随机子空间比纯 Bagging 高了约 1.3 个点标准差也更低说明特征扰动确实带来了更稳定的集成效果。随机森林略好一点这是因为节点级特征采样比整棵树固定特征子集的随机性更充分在特征冗余较高的合成数据上更容易逼近更好的分裂点。换成真实业务数据后差别可能没有这么整齐尤其是当特征信息有重叠但单特征区分能力都很弱时随机子空间经常能跑出更稳的成绩。调参方向上我会从max_features0.5起步如果单棵树的训练精度明显下降且测试精度也下滑就把比例往上调到 0.7反之如果测试精度一直提升但没有过拟合迹象可以继续往下压到 0.3寻找方差下降的红利。4. 常见问题与避坑实录4.1 特征重要性“忽高忽低”是怎么回事随机子空间模型给出的特征重要性天然会比普通随机森林更不稳定。原因不复杂每个基学习器只看到一部分特征某个特征可能只在少数几棵树里出现它在这几棵树里表现再强求平均后也可能被其他特征“稀释”。所以如果你用随机子空间跑完直接看feature_importances_做特征筛选很容易被带偏。我的做法是先用permutation importance代替原生重要性按“打乱某个特征后模型精度下降多少”来排序这个指标对特征出现频次没那么敏感。如果一定要用原生重要性建议多跑几次不同随机种子把排序结果做并集或取均值而不是只看一次结果。4.2 max_features设错时的典型症状max_features设得太大基学习器彼此太像集成效果等同于把 Bagging 又跑了一遍特征重要性也会向少数强特征集中。设得太小单个基学习器的信息量不够偏差一下子抬起来典型表现是训练精度和测试精度同步偏低而且无论怎么加树都救不回来。判断方法很简单打印一两棵单棵树的深度和训练精度如果单棵树训练精度明显比全特征树差很多说明特征子集里有效信息不足。此时把max_features往上提而不是硬加n_estimators。另外数据规模很小但特征很多时max_features也别压太低否则每棵树都在极其有限的信息里打转随机性带来的好处会被高偏差吞掉。4.3 别盲目套用随机子空间的场景随机子空间不是万金油。特征维度极低时比如不到 10 个特征再砍一半等于自废武功这种场景直接上 Bagging 或普通随机森林就好。特征之间高度独立、互相没有冗余时特征随机的收益也不明显因为每个特征本来就带独立信息缺了谁都是一种损失。还有一种情况是你要给业务方一个稳定的特征重要性解释特征子集的固定随机性会让解释成本变高这时候宁可牺牲一点精度也要用更稳定的模型。当遇到回归任务时BaggingRegressor同样可以设置max_features1.0但我的体感是回归问题对特征连续性信息更敏感特征比例一般要比分类场景高一些从 0.7~0.9 起步比较稳。问题可能原因解决办法测试精度上不去单棵树也弱max_features 太小调大比例比如 0.5 提到 0.7基学习器彼此太像集成提升有限max_features 太大特征冗余高调小比例同时增加 n_estimators特征重要性排序不稳定每个基学习器的特征子集不同导致频次不均多次训练取均值或用 permutation importance高维数据训练慢每棵树的特征集偏大、树过深降低 max_features限制 max_depth / 最小叶子样本数最后说点个人体会。我之前总觉得随机森林就是随机子空间的代名词直到把 BaggingClassifier 的 max_features 调成 0.4 之后才真正意识到固定特征子集的随机子空间和节点级特征采样的随机森林在高维业务数据上的表现差异其实很有意思。遇到几百维、样本不多、特征之间又互相纠缠的任务我现在的流程是先跑一个 BaggingClassifier(max_features0.4, max_samples0.8, n_estimators200) 做基线再拿随机森林对比效果好就留效果不好就再调。你也不妨把这两者的差异当成一个对照实验跑跑看比光看理论印象要深得多。