实战指南:用Python实现RFE与随机森林回归的特征工程革命

在数据科学项目中,我们常常面临一个经典困境:手头的数据集包含了数十甚至上百个特征,但直觉和经验都告诉我们,并非所有特征都对预测目标有同等贡献。冗余或无关的特征不仅会拖慢模型训练速度,增加计算成本,更糟糕的是,它们可能引入噪声,导致模型过拟合,最终在真实世界中的预测表现大打折扣。对于从事金融风控、销售预测或工业参数优化的数据科学家和机器学习工程师而言,如何从高维特征空间中精准“提纯”,筛选出最具预测力的核心特征子集,是构建稳健、高效模型的关键一步。

今天,我们就深入探讨一种将递归特征消除(RFE) 与强大的随机森林回归模型相结合的实战方法。这不仅仅是调用一两个Sklearn库函数那么简单,我们将从数据预处理开始,一步步构建完整的流水线,深入模型训练的内部机制,并分享一系列提升计算效率和模型性能的调参技巧。无论你是希望优化现有预测模型,还是正在处理一个全新的、特征维度爆炸的数据集,这篇文章都将为你提供一套可直接落地的解决方案。

1. 环境准备与数据基石:构建可复现的工作流

在开始任何机器学习项目之前,搭建一个清晰、可复现的工作环境至关重要。这不仅关乎代码能否运行,更决定了后续实验的可比性和结果的可信度。

首先,我们需要确保核心库已就位。除了经典的pandasnumpysklearn,我们还会用到matplotlibseaborn进行可视化,这对于理解特征选择过程非常有帮助。

pip install pandas numpy scikit-learn matplotlib seaborn

接下来,让我们模拟一个贴近现实的场景:假设我们正在为一个电商平台构建一个商品日销量预测模型。原始数据可能来自多个数据源,包含商品属性、历史销售、营销活动、季节性因素等数十个特征。

import pandas as pd
import numpy as np
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split

# 生成模拟数据集:1000个样本,50个特征,其中仅15个是真正有信息的
X, y = make_regression(n_samples=1000, n_features=50, n_informative=15,
                       noise=0.1, random_state=42)

# 转换为DataFrame,以便更直观地操作
feature_names = [f'feature_{i}' for i in range(X.shape[1])]
df = pd.DataFrame(X, columns=feature_names)
df['target_sales'] = y

# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
    df[feature_names], df['target_sales'], test_size=0.2, random_state=42
)

print(f"训练集形状: {X_train.shape}")
print(f"测试集形状: {X_test.shape}")

提示:在实际项目中,务必在特征工程和模型训练的任何步骤之前就划分好训练集和测试集,并确保测试集在整个特征选择过程中完全“不可见”,这是避免数据泄露、评估模型泛化能力的黄金法则。

数据划分后,一个常被忽略但极其重要的步骤是探索性数据分析。我们至少需要快速检查缺失值和异常值。对于回归问题,特征的尺度差异可能影响树模型的分裂,因此进行适度的标准化有时会带来好处,尽管随机森林对尺度不敏感,但后续若结合其他模型(如在集成中使用),预处理的一致性很重要。

# 快速数据检视
print("训练集缺失值统计:")
print(X_train.isnull().sum().sum())  # 理想情况下应为0

# 可选:特征标准化(使用训练集的统计量来转换训练集和测试集)
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 转换回DataFrame以保持列名
X_train_scaled = pd.DataFrame(X_train_scaled, columns=X_train.columns)
X_test_scaled = pd.DataFrame(X_test_scaled, columns=X_test.columns)

2. 理解核心武器:随机森林回归与RFE的协同原理

在深入代码之前,花几分钟理解背后的思想,能让你在调参和诊断时更有方向。随机森林回归本身就是一个强大的特征选择器。它通过构建多棵决策树,并在每棵树的每个节点上随机选取特征子集进行最优分裂,最终以所有树预测的平均值作为结果。在这个过程中,模型内部会天然地计算每个特征的重要性。

常见的特征重要性评估方式有两种:

  1. 基于不纯度减少的平均值:计算每个特征在所有树上带来的不纯度(如均方误差)减少的总和,并进行归一化。这是sklearnfeature_importances_属性的默认计算方式。
  2. 基于排列的重要性:通过随机打乱某个特征的值,观察模型性能下降的程度。下降越多,说明该特征越重要。这种方法计算成本更高,但通常更可靠。

递归特征消除是一种包装式特征选择方法。它的核心思想非常直观:不是一次性评估所有特征,而是通过一个迭代的“训练-评估-淘汰”循环,逐步剔除最不重要的特征。其与随机森林的结合,可以形象地理解为:让随机森林这个“评委”在每一轮比赛中,给所有“选手”(特征)打分,然后淘汰得分最低的那一位,剩下的选手进入下一轮,直到剩下我们指定数量的优胜者。

这个过程的关键优势在于,它考虑了特征之间的相互作用。某个特征单独看可能不重要,但与另一个特征组合时却至关重要,RFE的迭代过程能在一定程度上捕捉到这种关系。

为了更直观地对比不同特征选择策略,我们可以看下面这个简单的对比表:

特征选择方法 类型 核心思想 优点 缺点 与随机森林的适配性
方差阈值 过滤式 移除方差低于阈值的特征 计算快,无需模型 无法评估与目标的相关性 低,作为预处理
单变量统计检验 过滤式 逐个评估特征与目标的相关性 简单,可解释性强 忽略特征间交互 一般,可作为初筛
L1正则化 嵌入式 在模型训练中通过惩罚项使部分系数为零 将选择过程嵌入训练 对高度相关特征不稳定 中,需用线性模型
递归特征消除 包装式 递归地训练模型并移除最弱特征 考虑特征交互,基于模型性能 计算成本高,可能过拟合到特定模型 ,天然适配

3. 核心实战:手把手实现RFE+随机森林回归

理论清晰后,我们进入实战环节。我们将使用sklearn提供的RFECV类,它封装了RFE过程并集成了交叉验证,能自动帮助我们确定最优的特征数量。

3.1 基础实现与模型训练

首先,初始化我们的随机森林回归模型。这里有几个关键参数需要关注:

  • n_estimators: 森林中树的数量。通常越多越好,但会增加计算时间,且收益会递减。
  • max_depth: 树的最大深度。控制模型的复杂度,防止过拟合。
  • min_samples_split: 内部节点再划分所需最小样本数。
  • random_state: 固定随机种子,确保结果可复现。
from sklearn.ensemble import RandomForestRegressor
from sklearn.feature_selection import RFECV
from sklearn.model_selection import KFold

# 初始化随机森林回归器
# 先使用一组相对保守的默认参数
base_rf = RandomForestRegressor(
    n_estimators=100,
    max_depth=None,  # 不限制深度,让树完全生长,依靠袋外样本来评估
    min_samples_split=2,
    min_samples_leaf=1,
    random_state=42,
    n_jobs=-1  # 使用所有CPU核心加速
)

# 初始化RFECV对象
# step=1表示每次迭代移除一个特征
# cv=5表示使用5折交叉验证评估每个特征子集的性能
# scoring='neg_mean_squared_error' 评估指标为负均方误差(sklearn约定,越大越好)
rfecv = RFECV(
    estimator=base_rf,
    step=1,
    cv=KFold(5, shuffle=True, random_state=42),
    scoring='neg_mean_squared_error',
    n_jobs=-1  # 并行化交叉验证过程
)

# 执行特征选择(在标准化后的数据上进行)
print("开始递归特征消除与交叉验证...")
rfecv.fit(X_train_scaled, y_train)

print(f"最优特征数量: {rfecv.n_features_}")
print(f"所有特征排名 (1为最好): {rfecv.ranking_}")
print(f"被选中的特征掩码: {rfecv.support_}")

# 获取被选中的特征名
selected_features = X_train_scaled.columns[rfecv.support_].tolist()
print(f"\n最终选中的特征 ({len(selected_features)}个):")
for feat in selected_features:
    print(f"  - {feat}")

运行上述代码后,RFECV会完成所有繁重的工作。它不仅会输出最优的特征子集,还会在内部记录下交叉验证的得分曲线,这是我们进行诊断和调优的重要依据。

3.2 可视化诊断:理解选择过程

模型训练完成后,我们不能只关心结果,更要理解过程。可视化交叉验证得分随特征数量变化的曲线,能告诉我们特征选择是否稳定,以及最优点是否明确。

import matplotlib.pyplot as plt
import seaborn as sns

plt.figure(figsize=(10, 6))
# 获取网格分数(负MSE),我们将其转换为正数并取平方根得到RMSE,更易解释
grid_scores = -rfecv.grid_scores_  # 转换为正MSE
rmse_scores = np.sqrt(grid_scores)  # 转换为RMSE
n_features = np.arange(1, len(rfecv.grid_scores_) + 1)

plt.plot(n_features, rmse_scores, 'o-', linewidth=2, markersize=8, color='darkblue')
plt.xlabel('Number of Features Selected', fontsize=12)
plt.ylabel('Cross-Validation RMSE', fontsize=12)
plt.title('RFECV Performance vs. Number of Features', fontsize=14, pad=15)
plt.grid(True, linestyle='--', alpha=0.7)

# 标记最优特征数点
optimal_idx = np.argmin(rmse_scores)
plt.axvline(x=n_features[optimal_idx], color='red', linestyle='--', label=f'Optimal ({n_features[optimal_idx]} features)')
plt.scatter(n_features[optimal_idx], rmse_scores[optimal_idx], color='red', s=100, zorder=5)

plt.legend()
plt.tight_layout()
plt.show()

一个理想的曲线通常呈现“L”形或“U”形:随着无关特征的移除,模型性能(RMSE)会先快速提升或保持稳定,到达某一点后,继续移除特征会开始损害模型性能,导致RMSE急剧上升。最优特征数就在这个“拐点”附近。如果你的曲线非常平缓,没有明显的拐点,可能意味着:

  • 许多特征都提供相似的信息(共线性高)。
  • 或者,随机森林模型本身对冗余特征不敏感,此时特征选择的价值可能有限。

4. 高级调参与性能优化技巧

基础流程跑通只是第一步。要让RFE+随机森林回归在实际项目中发挥最大威力,我们需要深入调参和优化。这部分是区分普通应用和高手实践的关键。

4.1 随机森林参数的精调

随机森林本身的参数对特征重要性的计算有直接影响,进而影响RFE的淘汰顺序。

  • n_estimators(树的数量): 更多的树会使特征重要性的估计更稳定。对于RFE,建议使用一个足够大的值(如500),以确保重要性排名可靠。你可以观察袋外误差随树数量增加的变化,当其基本稳定时即可。

    # 寻找稳定的n_estimators
    from sklearn.model_selection import cross_val_score
    
    estimators_range = [50, 100, 200, 300, 400, 500]
    oob_scores = []
    for n in estimators_range:
        rf = RandomForestRegressor(n_estimators=n, oob_score=True, random_state=42, n_jobs=-1)
        rf.fit(X_train_scaled, y_train)
        oob_scores.append(rf.oob_score_)
    # 绘制oob_score随n_estimators变化的曲线,选择增长平缓的点
    
  • max_features:这是随机森林中一个至关重要但常被忽视的参数。它控制每棵树分裂时考虑的最大特征数。默认值(auto,即sqrt(n_features))适用于很多情况,但在特征选择场景下,尝试log2或更小的比例(如0.3)有时能产生更具判别力的特征重要性,因为树之间的差异性更大。

    # 在RFECV中尝试不同的max_features
    param_grid = {'estimator__max_features': ['sqrt', 'log2', 0.3]}
    # 可以将RFECV放入GridSearchCV中进行超参数搜索,但计算量巨大
    
  • 基于排列的重要性:如前所述,sklearnpermutation_importance函数可以提供更可靠的重要性度量。我们可以自定义一个评分函数,在RFE的每一步使用排列重要性来淘汰特征。这虽然慢,但结果更稳健。

    from sklearn.inspection import permutation_importance
    from sklearn.base import clone
    
    def custom_rfe_with_permutation(X, y, estimator, n_features_to_select):
        """自定义RFE,使用排列重要性"""
        support = np.ones(X.shape[1], dtype=bool)
        ranking = np.ones(X.shape[1], dtype=int)
        
        for i in range(X.shape[1], n_features_to_select, -1):
            estimator_clone = clone(estimator)
            estimator_clone.fit(X.loc[:, support], y)
            
            # 计算排列重要性
            perm_result = permutation_importance(
                estimator_clone, X.loc[:, support], y,
                n_repeats=10, random_state=42, n_jobs=-1
            )
            importances = perm_result.importances_mean
            
            # 找到当前支持集中最不重要的特征
            least_important_idx = np.argmin(importances)
            # 映射回原始特征索引
            original_idx = np.where(support)[0][least_important_idx]
            
            support[original_idx] = False
            ranking[original_idx] = i
            
        return support, ranking
    

4.2 提升RFE的计算效率

RFE需要反复训练模型,当特征数很多时,计算可能非常缓慢。以下策略可以显著加速:

  1. 增大step参数RFECV中的step参数默认为1,即每次移除一个特征。如果特征维度很高(如>100),可以设置为每次移除一定比例(如5%)或固定数量的特征,在初期快速缩减规模。

    rfecv_fast = RFECV(estimator=base_rf, step=5, cv=5, scoring='neg_mean_squared_error', n_jobs=-1)
    
  2. 并行化一切:确保RandomForestRegressorRFECV都设置了n_jobs=-1以利用多核CPU。

  3. 使用更小的cv折数:5折交叉验证是平衡偏差和方差的好选择。在初期探索或数据量很大时,可以暂时使用3折(cv=3)来快速获得趋势。

  4. 先进行过滤式预选:在运行RFE之前,先用一种快速的方法(如基于方差或与目标的相关性)剔除掉明显无用的特征,可以极大减少RFE需要处理的维度。

4.3 稳定性评估与最终模型验证

特征选择的结果不应该是一次性的。由于随机森林和交叉验证的随机性,每次运行RFE选出的特征集合可能有细微差别。评估特征选择的稳定性是工业级应用的必要步骤。

一种简单的方法是多次运行RFE(例如10次),每次使用不同的随机种子,然后统计每个特征被选中的频率。

n_runs = 10
feature_selection_counts = {col: 0 for col in X_train_scaled.columns}

for run in range(n_runs):
    rf_temp = RandomForestRegressor(n_estimators=100, random_state=run)
    rfecv_temp = RFECV(estimator=rf_temp, step=1, cv=5, scoring='neg_mean_squared_error', n_jobs=-1)
    rfecv_temp.fit(X_train_scaled, y_train)
    selected = X_train_scaled.columns[rfecv_temp.support_]
    for feat in selected:
        feature_selection_counts[feat] += 1

# 打印特征稳定性
print("特征被选中的次数(共10次运行):")
for feat, count in sorted(feature_selection_counts.items(), key=lambda x: x[1], reverse=True):
    print(f"{feat}: {count}/10")

那些在多次运行中都被稳定选中的特征,才是我们真正可以信赖的核心特征。最后,务必在从未参与过任何训练或特征选择过程的独立测试集上,评估使用最优特征子集训练的最终模型的性能,这是检验我们整个流程是否成功的唯一标准。

# 使用选定的特征训练最终模型
X_train_final = X_train_scaled[selected_features]
X_test_final = X_test_scaled[selected_features]

final_rf = RandomForestRegressor(n_estimators=500, random_state=42, n_jobs=-1)
final_rf.fit(X_train_final, y_train)

from sklearn.metrics import mean_squared_error, r2_score
y_pred = final_rf.predict(X_test_final)

test_rmse = np.sqrt(mean_squared_error(y_test, y_pred))
test_r2 = r2_score(y_test, y_pred)

print(f"\n=== 最终模型在测试集上的表现 ===")
print(f"测试集RMSE: {test_rmse:.4f}")
print(f"测试集R²: {test_r2:.4f}")

# 与使用全部特征的基准模型对比
base_rf_all = RandomForestRegressor(n_estimators=500, random_state=42, n_jobs=-1)
base_rf_all.fit(X_train_scaled, y_train)
y_pred_all = base_rf_all.predict(X_test_scaled)
test_rmse_all = np.sqrt(mean_squared_error(y_test, y_pred_all))
test_r2_all = r2_score(y_test, y_pred_all)

print(f"\n=== 使用全部特征的基准模型表现 ===")
print(f"测试集RMSE: {test_rmse_all:.4f}")
print(f"测试集R²: {test_r2_all:.4f}")

print(f"\n=== 对比总结 ===")
print(f"RMSE降低: {(test_rmse_all - test_rmse):.4f} ({(test_rmse_all - test_rmse)/test_rmse_all*100:.1f}%)")
print(f"特征数减少: {X_train_scaled.shape[1]} -> {len(selected_features)} (减少{((X_train_scaled.shape[1] - len(selected_features))/X_train_scaled.shape[1])*100:.1f}%)")

如果特征选择是有效的,我们期望看到:在模型性能(RMSE, R²)基本持平甚至略有提升的同时,使用的特征数量大幅减少。这意味着我们得到了一个更简洁、更高效、可能泛化能力更强的模型。在实际项目中,我遇到过特征数从200个精简到30个,而模型R²反而从0.85提升到0.87的情况,这主要归功于移除了大量引入噪声的无关特征。记住,机器学习不仅是让模型学习信号,更是要帮助它避开噪声。RFE与随机森林的组合,正是我们实现这一目标的利器。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐