查看全课大纲(第 21 / 35 节)

特征选择方法

约 8 分钟

小象实战讲义 · 特征工程精讲与实践

在上一节我们了解了特征选择的意义与基本流程后,本节将系统学习特征选择的三大主流方法:过滤式、包裹式和嵌入式。我们将深入理解每种方法的核心思想、典型算法及其优缺点,并通过实际代码演示,掌握如何根据具体任务和数据特点选择合适的方法,为后续的实战项目打下坚实基础。

💡 核心导读

  • 过滤式(Filtering):基于单特征统计量(如方差、相关性)进行快速筛选,计算高效但忽略特征间交互。
  • 包裹式(Wrapper):将特征选择视为子集搜索问题,通过迭代训练模型来评估特征子集性能,效果更好但计算成本高。
  • 嵌入式(Embedded):在模型训练过程中自动进行特征选择(如L1正则化),平衡了效率与效果。
  • 方法对比与选择:理解不同方法的适用场景,学会在实际项目中权衡计算资源与模型性能。

一、 特征选择的基本思考

在进行特征选择之前,我们需要明确两个核心的评判标准,这也是所有方法的基础逻辑。

1. 特征是否发散? 如果一个特征的取值几乎没有变化(即不发散),那么它对样本的区分就没有任何帮助。例如:

  • 一个数值型特征,其方差接近于0(如所有用户的年龄都是30岁)。
  • 一个类别型特征,其取值几乎全部为同一个类别(占比超过90%甚至100%)。

这样的特征无法提供任何差异化信息,任何模型都无法从中学习到有效规律,因此应当被优先剔除。在过滤式方法中,方差筛选就是基于这一原则。

2. 特征与目标的相关性如何? 特征工程的最终目的是为了更好地预测目标变量。因此,与目标变量(target)相关性高的特征,通常包含更多对预测有用的信息,应当优先保留。衡量相关性的方法有很多,例如:

  • 对于连续型特征与连续型目标:可以使用皮尔逊相关系数。
  • 对于类别型特征与类别型目标:可以使用卡方检验或互信息。

过滤式方法中的相关系数法、卡方检验和互信息法,都是基于这一相关性原则对特征进行排序和筛选。

二、 过滤式(Filtering)方法

过滤式方法是一种逐个特征进行评估的策略。它独立地评估每个特征与目标变量之间的相关性或特征本身的统计特性,然后根据设定的阈值或排名,选择出最相关的特征子集。

核心思想与流程

  1. 单特征评估:对数据集中的每个特征单独计算一个评价指标(如方差、与目标的相关系数)。
  2. 排序与筛选:根据评价指标对所有特征进行排序。
  3. 选择子集:保留排名靠前的特征(如Top K个),或剔除低于阈值的特征。

常用方法示例

下面我们通过一个综合示例,演示几种常见的过滤式方法。我们将构造一个微型数据集,其中包含一个低方差特征、一个与目标强相关特征和一个弱相关特征。

import pandas as pd
import numpy as np
from sklearn.feature_selection import VarianceThreshold, SelectKBest, f_classif

# 1. 构造微型示例数据(硬编码,确保输出确定)
data = {
    'f1': [1.0, 2.0, 3.0, 4.0, 5.0, 1.1, 2.1, 3.1, 4.1, 5.1],  # 与目标有一定关系
    'f2': [10, 10, 10, 10, 10, 10, 10, 10, 10, 10],           # 低方差特征(常数)
    'f3': [0.1, 0.5, 0.9, 1.3, 1.7, 0.2, 0.6, 1.0, 1.4, 1.8], # 与目标强相关
    'target': [0, 0, 0, 1, 1, 0, 0, 1, 1, 1]                  # 二分类目标
}
df = pd.DataFrame(data)
X = df[['f1', 'f2', 'f3']]
y = df['target']

print("=== 原始数据与特征 ===")
print(df)
print(f"\n各特征方差:\n{X.var()}")
=== 原始数据与特征 ===
    f1  f2   f3  target
0  1.0  10  0.1       0
1  2.0  10  0.5       0
2  3.0  10  0.9       0
3  4.0  10  1.3       1
4  5.0  10  1.7       1
5  1.1  10  0.2       0
6  2.1  10  0.6       0
7  3.1  10  1.0       1
8  4.1  10  1.4       1
9  5.1  10  1.8       1

各特征方差:
f1    2.225000
f2    0.000000
f3    0.358333
dtype: float64
from sklearn.feature_selection import f_classif
from sklearn.feature_selection import SelectKBest
import pandas as pd
import numpy as np

# 微型示例数据(与本节首块一致,硬编码确保输出确定)
data = {
    'f1': [1.0, 2.0, 3.0, 4.0, 5.0, 1.1, 2.1, 3.1, 4.1, 5.1],
    'f2': [10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
    'f3': [0.1, 0.5, 0.9, 1.3, 1.7, 0.2, 0.6, 1.0, 1.4, 1.8],
    'target': [0, 0, 0, 1, 1, 0, 0, 1, 1, 1]
}
df = pd.DataFrame(data)
X = df[['f1', 'f2', 'f3']]
y = df['target']

from sklearn.feature_selection import VarianceThreshold
# 2. 方差筛选 (移除低方差特征)
print("\n=== 方差筛选 (threshold=0.01) ===")
selector_var = VarianceThreshold(threshold=0.01)
X_var_selected = selector_var.fit_transform(X)
selected_mask = selector_var.get_support()
selected_features = X.columns[selected_mask]
print(f"原始特征数: {X.shape[1]}")
print(f"方差筛选后特征数: {X_var_selected.shape[1]}")
print(f"保留的特征: {list(selected_features)}")
print(f"被移除的特征 'f2' 的方差: {X['f2'].var():.6f}")

# 3. 单变量特征选择 (例如,基于F检验的ANOVA,适用于分类问题)
print("\n=== 单变量特征选择 (SelectKBest with f_classif, k=2) ===")
selector_kbest = SelectKBest(score_func=f_classif, k=2) # 选择得分最高的2个特征
X_kbest = selector_kbest.fit_transform(X, y)
scores = selector_kbest.scores_
print("各特征的F检验得分:")
for feat, score in zip(X.columns, scores):
    print(f"  {feat}: {score:.4f}")
selected_features_kbest = X.columns[selector_kbest.get_support()]
print(f"选择的前2个特征: {list(selected_features_kbest)}")
=== 方差筛选 (threshold=0.01) ===
原始特征数: 3
方差筛选后特征数: 2
保留的特征: ['f1', 'f3']
被移除的特征 'f2' 的方差: 0.000000

=== 单变量特征选择 (SelectKBest with f_classif, k=2) ===
各特征的F检验得分:
  f1: 21.7548
  f2: nan
  f3: 23.3107
选择的前2个特征: ['f1', 'f3']

结果分析:

  • 方差筛选:成功移除了方差为0的常数特征 f2。
  • 单变量特征选择(F检验):f3 得分最高,与目标相关性最强;f1 次之;f2 得分为 nan(常数特征组内方差为 0,F 统计量无法计算),同样被识别为无用特征。最终选择了 f1 和 f3。

优缺点

  • 优点:计算速度快,不依赖于具体的机器学习模型,是一种高效的预处理步骤。
  • 缺点:没有考虑特征之间的关联作用。例如,在决策树中,两个单独与目标相关性都不高的特征,组合起来可能形成有效的决策路径。过滤式方法可能会误删这类特征。

三、 包裹式(Wrapper)方法

包裹式方法将特征选择本身视为一个特征子集搜索问题。它使用一个预定的机器学习模型,通过迭代评估不同特征子集的性能(如准确率、AUC),来找到最优的特征组合。

核心思想与流程

  1. 定义搜索策略:如前向搜索(逐步添加特征)、后向搜索(逐步删除特征)等。
  2. 评估子集:对于每个候选特征子集,训练一个机器学习模型,并在验证集上评估其性能。
  3. 选择最优子集:根据评估指标(如AUC最高、准确率最高)选择性能最好的特征子集。

递归特征消除(RFE) 是包裹式方法的典型代表。其步骤如下:

  1. 用全部特征训练一个模型(如逻辑回归、随机森林)。
  2. 根据模型提供的特征重要性进行排序(如逻辑回归的系数绝对值、树模型的特征重要性)。
  3. 移除重要性最低的一个或一部分特征。
  4. 用剩余的特征重复步骤1-3,直到特征数量达到预设值,或模型性能出现显著下降。

下面我们用一个简化的例子来模拟RFE的思想:

import pandas as pd
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score

# 构造一个微型数据集,模拟特征重要性差异
# 特征:x1(重要), x2(次要), x3(噪声)
np.random.seed(42)
n_samples = 20
x1 = np.random.randn(n_samples)  # 重要特征
x2 = np.random.randn(n_samples)  # 次要特征
x3 = np.random.randn(n_samples) * 0.1  # 噪声特征,方差小
# 目标由x1和x2决定,加入噪声
y = (x1 * 0.8 + x2 * 0.2 + np.random.randn(n_samples)*0.2 > 0).astype(int)

X_sim = pd.DataFrame({'x1': x1, 'x2': x2, 'x3': x3})
y_sim = pd.Series(y)

print("=== 模拟数据集(前5行)===")
print(pd.concat([X_sim.head(), y_sim.head().rename('target')], axis=1))
print(f"\n特征方差:\n{X_sim.var()}")

# 模拟递归特征消除(手动步骤演示)
print("\n=== 模拟递归特征消除 (RFE) 过程 ===")
features = list(X_sim.columns)
current_features = features.copy()
model = LogisticRegression(max_iter=1000, random_state=42)

for step in range(len(features)):
    if len(current_features) == 0:
        break
    # 用当前特征子集训练模型
    model.fit(X_sim[current_features], y_sim)
    # 获取特征系数绝对值作为重要性度量(对于逻辑回归)
    importances = np.abs(model.coef_[0])
    # 找到最不重要的特征
    least_important_idx = np.argmin(importances)
    least_important_feat = current_features[least_important_idx]
    
    print(f"步骤 {step+1}: 当前特征 {current_features}")
    print(f"  模型系数(绝对值): {dict(zip(current_features, importances.round(4)))}")
    print(f"  移除最不重要特征: '{least_important_feat}'")
    print(f"  当前特征集({current_features})训练的模型准确率: {accuracy_score(y_sim, model.predict(X_sim[current_features])):.4f}")
    print("-" * 50)
    
    # 移除该特征
    current_features.pop(least_important_idx)
=== 模拟数据集(前5行)===
         x1        x2        x3  target
0  0.496714  1.465649  0.073847       1
1 -0.138264 -0.225776  0.017137       0
2  0.647689  0.067528 -0.011565       1
3  1.523030 -1.424748 -0.030110       1
4 -0.234153 -0.544383 -0.147852       0

特征方差:
x1    0.921655
x2    0.937099
x3    0.006738
dtype: float64

=== 模拟递归特征消除 (RFE) 过程 ===
步骤 1: 当前特征 ['x1', 'x2', 'x3']
  模型系数(绝对值): {'x1': np.float64(1.7817), 'x2': np.float64(1.0722), 'x3': np.float64(0.1019)}
  移除最不重要特征: 'x3'
  当前特征集(['x1', 'x2', 'x3'])训练的模型准确率: 0.9500
--------------------------------------------------
步骤 2: 当前特征 ['x1', 'x2']
  模型系数(绝对值): {'x1': np.float64(1.7839), 'x2': np.float64(1.0718)}
  移除最不重要特征: 'x2'
  当前特征集(['x1', 'x2'])训练的模型准确率: 0.9500
--------------------------------------------------
步骤 3: 当前特征 ['x1']
  模型系数(绝对值): {'x1': np.float64(1.6086)}
  移除最不重要特征: 'x1'
  当前特征集(['x1'])训练的模型准确率: 0.8500
--------------------------------------------------

过程解读:

  1. 初始模型使用全部三个特征(系数绝对值显示 x1 最重要,x2 次之,x3 几乎为0),训练集准确率为 0.95。
  2. 第一轮移除噪声特征 x3,用剩余的 x1、x2 训练,准确率仍为 0.95——去掉无信息特征通常无害。
  3. 第二轮移除次要特征 x2,仅用 x1 训练,准确率降至 0.85,开始出现明显下降。
  4. 第三轮(仅剩 x1)之后若继续删除将没有任何特征可用。 在实际的RFE中,我们会在一个独立的验证集或测试集上监控性能,当性能出现“较大程度下滑”时停止删除。本例中,移除 x2 后准确率已从 0.95 降至 0.85,合理的停止点是保留 x1 和 x2。

优缺点

  • 优点:由于直接使用后续要用的机器学习模型来评估特征子集,所以选出的特征子集通常对该模型性能更优。
  • 缺点:计算成本非常高,需要反复训练模型。特征数量多时,搜索空间巨大,可能无法遍历。

四、 嵌入式(Embedded)方法

嵌入式方法将特征选择过程与模型训练过程融为一体。在模型训练的同时,通过某种机制自动地进行特征选择。

核心思想

最常见的嵌入式特征选择方法是使用带有L1正则化(Lasso)的线性模型。

  • L1正则化:在模型的损失函数中,增加一项模型权重(系数)绝对值的和乘以一个正则化系数 λ\lambda。
  • 作用:L1正则化会限制模型参数的搜索空间,倾向于产生稀疏(sparse)的解,即将不重要特征的权重压缩至0。这些权重为0的特征,自然就被模型“选择”掉了。

以逻辑回归为例,其带有L1正则化的代价函数如下:

J(w)=−1m∑i=1m[y(i)log⁡(hw(x(i)))+(1−y(i))log⁡(1−hw(x(i)))]+λ∥w∥1J(\mathbf{w}) = -\frac{1}{m} \sum_{i=1}^{m} [y^{(i)} \log(h_{\mathbf{w}}(\mathbf{x}^{(i)})) + (1-y^{(i)}) \log(1-h_{\mathbf{w}}(\mathbf{x}^{(i)}))] + \lambda \Vert \mathbf{w}\Vert _1

其中 ∥w∥1=∑j=1n∣wj∣\Vert \mathbf{w}\Vert _1 = \sum_{j=1}^{n} |w_j| 就是L1正则化项。

import pandas as pd
import numpy as np
from sklearn.linear_model import LogisticRegression

# 构造一个微型数据集,包含相关特征和无关特征
data_emb = {
    'relevant_1': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
    'relevant_2': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
    'noise_1': [0.1, -0.1, 0.2, -0.2, 0.3, -0.3, 0.4, -0.4, 0.5, -0.5],
    'noise_2': [100, 100, 100, 100, 100, 100, 100, 100, 100, 100], # 常数特征
}
X_emb = pd.DataFrame(data_emb)
# 目标由两个相关特征决定
y_emb = pd.Series([0, 0, 0, 0, 0, 1, 1, 1, 1, 1])

print("=== 嵌入式特征选择示例数据 ===")
print(pd.concat([X_emb, y_emb.rename('target')], axis=1))

# 使用L1正则化的逻辑回归(嵌入式选择)
print("\n=== 使用L1正则化的逻辑回归 (C=0.1, 强正则化) ===")
# 注意:sklearn 1.8+ 中 penalty 参数已逐渐弃用,推荐使用 l1_ratio 和 C 参数控制。
# 为了演示L1效果,我们仍使用 penalty='l1',并设置较小的C值(C是正则化强度的倒数,C小则正则化强)。
lr_l1 = LogisticRegression(penalty='l1', solver='liblinear', C=0.1, random_state=42, max_iter=1000)
lr_l1.fit(X_emb, y_emb)

print("模型系数:")
for feat, coef in zip(X_emb.columns, lr_l1.coef_[0]):
    print(f"  {feat}: {coef:.6f}")

# 识别被稀疏化的特征(系数为0)
selected_features_emb = X_emb.columns[lr_l1.coef_[0] != 0]
print(f"\n非零系数特征(被模型选中): {list(selected_features_emb)}")
print(f"零系数特征(被模型剔除): {list(X_emb.columns[lr_l1.coef_[0] == 0])}")
=== 嵌入式特征选择示例数据 ===
   relevant_1  relevant_2  noise_1  noise_2  target
0           1           2      0.1      100       0
1           2           3     -0.1      100       0
2           3           4      0.2      100       0
3           4           5     -0.2      100       0
4           5           6      0.3      100       0
5           6           7     -0.3      100       1
6           7           8      0.4      100       1
7           8           9     -0.4      100       1
8           9          10      0.5      100       1
9          10          11     -0.5      100       1

=== 使用L1正则化的逻辑回归 (C=0.1, 强正则化) ===
模型系数:
  relevant_1: 0.095624
  relevant_2: 0.000000
  noise_1: 0.000000
  noise_2: -0.004852

非零系数特征(被模型选中): ['relevant_1', 'noise_2']
零系数特征(被模型剔除): ['relevant_2', 'noise_1']

结果分析: 在 C=0.1 的强正则化下,relevant_2 与 noise_1 的系数被完全压缩为 0,只有 relevant_1(0.0956)保留了非零系数——L1 正则化确实按重要性「筛掉了」大部分特征。有个值得注意的细节:常数特征 noise_2 也分到了一个极小的非零系数(-0.0049),这说明常数列与截距存在共线关系,最好在预处理阶段(如方差筛选)就先把它移除。在实际应用中,我们需要调整正则化强度(C 参数),或者使用正则化路径,观察随着正则化强度变化,哪些特征最先被剔除(系数变为0),哪些特征最稳定。通常,我们会选择一个合适的 C 值,使得模型在验证集上性能最好,此时对应的非零系数特征就是被选中的特征子集。

优缺点

  • 优点:在模型训练中自动完成特征选择,计算效率比包裹式方法高,同时由于考虑了特征之间的相互作用(通过模型),效果通常比过滤式方法好。
  • 缺点:依赖于特定的模型。例如,L1正则化适用于线性模型,其选出的特征子集可能对树模型并非最优。

五、 方法对比与小结

方法类型核心思想典型算法优点缺点适用场景
过滤式基于单特征统计量筛选方差筛选、相关系数、卡方、互信息计算快,独立于模型忽略特征间交互数据预处理,特征初筛
包裹式将特征选择视为子集搜索问题递归特征消除(RFE)针对特定模型性能优计算成本极高特征数不多,对性能要求高
嵌入式在模型训练中自动选择L1正则化(Lasso)、树模型特征重要性平衡效率与效果,考虑特征交互依赖特定模型与模型训练结合,通用性强

在实际工业流程中,我们常常会混合使用这些方法:

  1. 先用过滤式方法进行快速初筛,移除明显无用的特征(如常数、方差极低的特征),降低维度。
  2. 再用嵌入式或包裹式方法进行精细筛选。例如,可以先使用L1正则化线性模型做一次选择,再用树模型或更复杂的模型进行包裹式搜索。

📝 动手练一练

  1. 概念辨析:过滤式方法中的“方差筛选”和“相关系数法”分别基于哪种“基本思考”?如果有一个特征方差很大但与目标完全无关,方差筛选会剔除它吗?相关系数法呢?
👉 点击查看参考答案
  • 方差筛选基于“特征是否发散”的基本思考。它只关心特征自身的取值变化,不关心与目标的关系。因此,一个方差很大但与目标无关的特征,不会被方差筛选剔除。
  • 相关系数法基于“特征与目标的相关性”的基本思考。它会计算该特征与目标向量的相关系数。如果相关性很低(接近0),相关系数法可能会将其剔除(取决于设定的阈值)。
  • 这个例子也说明了为什么需要多种特征选择方法:方差筛选能剔除无信息的常数特征,但无法剔除有信息但无用的特征;相关性方法则能弥补这一点。
  1. 代码实践:在“嵌入式方法”的代码示例中,我们将正则化强度 C 设为0.1(强正则化),大部分特征的系数被压缩为 0,只有 relevant_1 和常数列 noise_2 保留非零系数。请尝试增大 C 值(例如 C=10 或 C=100),重新运行模型,观察哪些特征的系数恢复为非零?这说明了什么?
👉 点击查看参考答案
import pandas as pd
import numpy as np
from sklearn.linear_model import LogisticRegression

# 嵌入式示例数据(与上一块一致)
data_emb = {
    'relevant_1': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
    'relevant_2': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
    'noise_1': [0.1, -0.1, 0.2, -0.2, 0.3, -0.3, 0.4, -0.4, 0.5, -0.5],
    'noise_2': [100, 100, 100, 100, 100, 100, 100, 100, 100, 100],
}
X_emb = pd.DataFrame(data_emb)
y_emb = pd.Series([0, 0, 0, 0, 0, 1, 1, 1, 1, 1])

# 尝试增大C值,减弱正则化
lr_l1_weak = LogisticRegression(penalty='l1', solver='liblinear', C=10, random_state=42, max_iter=1000)
lr_l1_weak.fit(X_emb, y_emb)
print("C=10 时模型系数:")
for feat, coef in zip(X_emb.columns, lr_l1_weak.coef_[0]):
    print(f"  {feat}: {coef:.6f}")
C=10 时模型系数:
  relevant_1: 2.941373
  relevant_2: 1.408811
  noise_1: 0.000000
  noise_2: -0.253294

运行结果印证了 L1 正则化的「筛选」机制:减弱正则化(C=10)后,与目标真正相关的 relevant_1 和 relevant_2 的系数率先恢复为显著的非零值(2.94 和 1.41),而噪声特征 noise_1 仍被压制为 0。常数特征 noise_2 虽然也拿到了一个非零系数(-0.25),但它对预测没有任何贡献——这也提醒我们,常数特征最好在预处理阶段(方差筛选)就移除,不要留给正则化去处理。 说明:L1正则化就像一个“特征过滤器”,正则化强度(C的倒数)就像过滤网的“网眼大小”。网眼很小时(C小,正则化强),大部分特征都被过滤掉(系数为0)。逐渐调大网眼(增大C,减弱正则化),对模型预测最重要的特征(relevant_*)会最先“漏过去”(系数非零),而不重要的噪声特征则被持续压制。这体现了嵌入式方法如何自动区分特征重要性。

本章小结

本节我们系统学习了特征选择的三大类方法,理解了从快速初筛到精细搜索的完整技术图谱。

📋 行动清单

  • 理解方法本质:能清晰阐述过滤式、包裹式、嵌入式三种方法的核心思想与优缺点。
  • 掌握基础操作:能够使用 VarianceThreshold 和 SelectKBest 对数据进行快速的过滤式特征选择。
  • 建立流程意识:在接下来的实战中,尝试设计一个混合的特征选择流程(如先过滤,再嵌入)。
  • 思考模型关联:认识到特征选择方法与后续使用的机器学习模型密切相关,树模型自带重要性评估,线性模型可通过正则化选择,为模型调优提供更多维度。

—— 小象教研组