使用Scikit-learn包的随机森林嵌入方法处理数据
引言
在机器学习中,特征工程往往比算法选择更能决定模型的上限。当数据呈现非线性结构时,传统的线性模型常常力不从心。本文将介绍Scikit-learn中一个相对冷门却非常强大的工具——随机森林嵌入(Random Forest Embedding),它能够将原始数据映射到高维稀疏空间,从而让线性模型也能捕捉非线性关系。
什么是随机森林嵌入?
随机森林嵌入的核心思想来自Scikit-learn的RandomTreesEmbedding模块。它的工作流程如下:
- 首先在训练集上拟合一个树的集成模型(可以是完全随机树、随机森林或梯度提升树)
- 集成中的每棵树的每个叶子节点都被分配一个固定的特征索引
- 每个样本经过每棵树的决策路径后,会落入一个叶子节点
- 最终采用独热编码(one-hot) 的方式,将样本编码为一个高维稀疏向量——被激活的叶子位置为1,其余为0
最终每个样本的编码中,1的个数等于森林中树的数量。编码后的特征维度约为
n_estimators * 2 ** max_depth。
这种嵌入本质上是将原始数据通过树的结构转化为一个有监督的、稀疏的、高维的分类编码。映射过程完全无监督且非常高效。
为什么需要嵌入?
原始数据可能是低维但非线性的(比如同心圆分布),线性分类器难以直接处理。通过随机森林嵌入,数据被映射到高维空间后,原本线性不可分的问题变得可分。在高维稀疏空间中,线性分类器往往能取得优异的精度。
实战案例一:无监督嵌入 + 朴素贝叶斯
首先来看一个无监督的嵌入案例。我们使用Scikit-learn官方示例中的同心圆数据集:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_circles
from sklearn.ensemble import RandomTreesEmbedding, ExtraTreesClassifier
from sklearn.decomposition import TruncatedSVD
from sklearn.naive_bayes import BernoulliNB
# 生成同心圆数据集(非线性可分)
X, y = make_circles(factor=0.5, random_state=0, noise=0.05)
# 使用RandomTreesEmbedding进行无监督嵌入
hasher = RandomTreesEmbedding(
n_estimators=10, # 树的数量
random_state=0,
max_depth=3 # 每棵树的最大深度
)
X_transformed = hasher.fit_transform(X)
# 使用截断SVD降维到2维以便可视化
svd = TruncatedSVD(n_components=2)
X_reduced = svd.fit_transform(X_transformed)
# 在嵌入后的数据上训练朴素贝叶斯分类器
nb = BernoulliNB()
nb.fit(X_transformed, y)
# 作为对比,直接在原始数据上训练ExtraTreesClassifier
trees = ExtraTreesClassifier(max_depth=3, n_estimators=10, random_state=0)
trees.fit(X, y)
上述代码中,RandomTreesEmbedding将原始2维数据映射到了高维稀疏空间,TruncatedSVD再将高维数据降维回2维以便观察嵌入效果。可以看到,原本同心圆分布的数据在嵌入后变得线性可分。
RandomTreesEmbedding的主要参数包括:
| 参数 | 说明 | 默认值 |
|---|---|---|
n_estimators |
森林中树的数量 | 10 |
max_depth |
每棵树的最大深度 | 5 |
min_samples_split |
内部节点再划分所需最小样本数 | 2 |
min_samples_leaf |
叶子节点最少样本数 | 1 |
random_state |
随机种子 | None |
实战案例二:有监督嵌入 + 逻辑回归(Pipeline方式)
Scikit-learn的RandomTreesEmbedding是一个标准的transformer,可以无缝集成到Pipeline中。以下示例展示了如何将随机森林嵌入与逻辑回归组合使用:
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomTreesEmbedding
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.metrics import roc_curve
# 生成分类数据集
X, y = make_classification(n_samples=80000)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5)
# 【重要】为避免过拟合,将训练集再拆分为两部分:
# 一部分用于训练嵌入(树模型),另一部分用于训练线性模型
X_train, X_train_lr, y_train, y_train_lr = train_test_split(
X_train, y_train, test_size=0.5
)
# 构建Pipeline:随机树嵌入 → 逻辑回归
rt = RandomTreesEmbedding(max_depth=3, n_estimators=10, random_state=0)
rt_lm = LogisticRegression(solver='lbfgs', max_iter=1000)
pipeline = make_pipeline(rt, rt_lm)
# 训练并预测
pipeline.fit(X_train, y_train)
y_pred_rt = pipeline.predict_proba(X_test)[:, 1]
fpr_rt_lm, tpr_rt_lm, _ = roc_curve(y_test, y_pred_rt)
实战案例三:有监督嵌入(手动方式,使用随机森林)
如果你想使用有监督的随机森林(而非完全随机树)来做嵌入,可以借助RandomForestClassifier的apply()方法获取每个样本的叶子索引,再配合OneHotEncoder进行独热编码:
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import OneHotEncoder
from sklearn.linear_model import LogisticRegression
# 训练随机森林(有监督)
rf = RandomForestClassifier(max_depth=3, n_estimators=10)
rf.fit(X_train, y_train)
# 获取每个样本在每棵树中的叶子索引
X_train_leaves = rf.apply(X_train) # shape: (n_samples, n_estimators)
X_train_lr_leaves = rf.apply(X_train_lr)
X_test_leaves = rf.apply(X_test)
# 对叶子索引进行独热编码
rf_enc = OneHotEncoder(categories='auto')
rf_enc.fit(X_train_leaves)
# 在编码后的特征上训练逻辑回归
rf_lm = LogisticRegression(solver='lbfgs', max_iter=1000)
rf_lm.fit(rf_enc.transform(X_train_lr_leaves), y_train_lr)
# 预测
y_pred_rf_lm = rf_lm.predict_proba(rf_enc.transform(X_test_leaves))[:, 1]
rf.apply(X)返回一个形状为(n_samples, n_estimators)的数组,表示每个样本在每棵树中落入的叶子节点索引。
注意事项
1. 避免过拟合
这是最关键的一点:训练嵌入的树模型和使用嵌入特征的线性模型,应该使用不同的数据子集。如果叶子总数接近训练样本数,树模型可能会过拟合,导致嵌入后的特征缺乏泛化能力。上述代码中通过将训练集拆分为两部分来解决这个问题。
2. 适用场景
- 数据呈现非线性结构,但你想使用线性模型(如逻辑回归、线性SVM)
- 需要处理高维稀疏数据,
BernoulliNB等模型在嵌入后表现优异 - 作为特征工程的一种手段,提升基线模型的表现
3. 计算开销
嵌入后的特征维度为 n_estimators * 2 ** max_depth。当树的数量和深度较大时,特征维度会急剧膨胀,需要注意内存和计算开销。
- 点赞
- 收藏
- 关注作者
评论(0)