特征选择:更多特征不等于更好


文档摘要

特征选择:更多特征不等于更好 本节摘要:更多特征不等于更好,对的特征才等于更好。你有 500 个特征,模型训练慢、不停过拟合、没人能解释它学了什么;你加更多特征希望提升性能,结果更糟。这是维度灾难(Curse of Dimensionality)在行动:特征数增长,特征空间体积爆炸,数据点变稀疏,点间距离趋同,模型需要指数级更多数据才能找到真实模式,噪声特征淹没信号特征,过拟合成为默认。特征选择是解药——剥掉噪声、去除冗余、留下真正承载目标信息的特征,结果是训练更快、泛化更好、模型可解释。目标不是用上所有可得信息,而是用上对的信息。

特征选择:更多特征不等于更好

本节摘要:更多特征不等于更好,对的特征才等于更好。你有 500 个特征,模型训练慢、不停过拟合、没人能解释它学了什么;你加更多特征希望提升性能,结果更糟。这是维度灾难(Curse of Dimensionality)在行动:特征数增长,特征空间体积爆炸,数据点变稀疏,点间距离趋同,模型需要指数级更多数据才能找到真实模式,噪声特征淹没信号特征,过拟合成为默认。特征选择是解药——剥掉噪声、去除冗余、留下真正承载目标信息的特征,结果是训练更快、泛化更好、模型可解释。目标不是用上所有可得信息,而是用上对的信息。本节讲透三大类方法:过滤器(Filter,方差阈值、互信息、卡方、相关系数过滤)用统计量独立打分、快但错过交互;包装器(Wrapper,RFE、前向选择、后向消元)用模型性能评估特征子集、准但贵;嵌入法(Embedded,L1/Lasso、树重要性)在训练中选特征。我们从零实现方差阈值、互信息、RFE、L1、树重要性,讲透互信息为何能抓非线性、L1 为何产生精确零权重,并对比排列重要性这个模型无关的检验方法。

学习目标

阅读完本节,你应当能够:

  1. 从零实现过滤方法(方差阈值、互信息、卡方)和包装方法(RFE、前向选择)。
  2. 解释互信息为何能抓住相关性错过的非线性特征-目标关系。
  3. 对比 L1 正则化(嵌入选择)与 RFE(包装选择),评估它们的计算权衡。
  4. 构建一条组合多种方法的特征选择流水线,在留出数据上展示泛化改进。

一、问题与直觉

你有 500 个特征。模型训练慢、不停过拟合、没人能解释它学了什么。你加更多特征希望提升性能,结果更糟。

这是维度灾难在行动。随特征数增长,特征空间体积爆炸。数据点变稀疏。点间距离趋同。模型需要指数级更多数据才能找到真实模式。噪声特征淹没信号特征。过拟合成为默认。

特征选择是解药。剥掉噪声。去除冗余。留下真正承载目标信息的特征。结果:训练更快、泛化更好、你能解释的模型。

目标不是用上所有可得信息,而是用上对的信息。

特征选择的三大类

每个特征选择方法落入三类之一:

过滤器方法(Filter) 用统计量独立给每特征打分,不用模型。快,但错过特征交互。

包装器方法(Wrapper) 训练模型来评估特征子集,用模型性能当分数。结果更好,但贵,因为要重训模型很多次。

嵌入法(Embedded) 把特征选择作为模型训练的一部分。L1 正则化把权重压到零。决策树在最有用的特征上分裂。选择在拟合时发生,而非作为单独步骤。

方差阈值

最简单的过滤器。如果一个特征在样本间几乎不变,它几乎不带信息。

考虑一个特征,1000 样本里 999 个是 0.0。它的方差近零。没有模型能用它区分类别。删掉。

variance(x) = mean((x - mean(x))^2)

设阈值(如 0.01),丢掉方差低于它的每个特征。这在不看目标变量的情况下移除常数或近常数特征。

何时用:作为其他方法前的预处理步骤。它以近乎零的代价抓出明显无用的特征。

局限:一个特征可以方差高却仍是纯噪声。方差阈值必要但不充分。

互信息

互信息(Mutual Information)度量知道特征 X 的值能减少多少关于目标 Y 的不确定性。

I(X; Y) = sum_x sum_y p(x, y) * log(p(x, y) / (p(x) * p(y)))

若 X 与 Y 独立,p(x, y) = p(x) * p(y),对数项为零,I(X; Y) = 0。X 告诉你越多关于 Y,互信息越高。

相对相关性的关键优势:互信息抓住非线性关系。一个特征可能与目标零相关,却因关系是二次或周期的而有高互信息。

对连续特征,先离散化到箱(基于直方图的估计)。箱数影响估计——太少箱丢信息,太多箱加噪声。常见选择:sqrt(n) 箱或 Sturges 法则(1 + log2(n))。

递归特征消元(RFE)

RFE 是包装方法。它用模型自身的特征重要性迭代剪枝:

  1. 用所有特征训练模型。
  2. 按重要性排特征(线性模型看系数,树看不纯度下降)。
  3. 移除最不重要的特征。
  4. 重复直到达到目标特征数。

RFE 考虑特征交互,因为模型一起看到所有剩余特征。移除一个特征改变其他特征的重要性。这使其比过滤器方法更彻底。

代价:你训练模型 N - 目标数 次。500 特征、目标 10,就是 490 次训练。对贵模型,这慢。你可以每步移除多个特征加速(如每轮移除底部 10%)。

L1(Lasso)正则化

L1 正则化把权重的绝对值加到损失函数:

loss = prediction_error + alpha * sum(|w_i|)

alpha 参数控制剪特征强度。alpha 越高,更多权重精确为零。

为何精确为零?L1 惩罚在权重空间创造一个菱形约束区。最优解倾向于落在这个菱形的角上,那里一个或多个权重为零。L2 正则化(Ridge)创造圆形约束,权重收缩但很少精确为零。

这是嵌入特征选择:模型在训练中学会忽略哪些特征。权重为零的特征实际被移除。

优点:单次训练、处理相关特征(选一个把其他压零)、内置于多数线性模型实现。

局限:只对线性模型有效。不能抓非线性特征重要性。

树特征重要性

决策树及其集成(随机森林、梯度提升)天然给特征排序。每次分裂降低不纯度(分类用基尼或熵,回归用方差)。产生更大不纯度下降的特征更重要。

对一个 T 棵树的随机森林:

importance(feature_j) = (1/T) * 对所有树求和 of 对所有在 feature_j 上分裂的节点求和 of (n_samples * impurity_decrease)

这给每个特征一个归一化的重要性分数。它自动处理非线性关系和特征交互。

警告:树重要性偏向高基数(很多唯一值)特征。一个随机 ID 列会显得重要,因为它能完美分裂每个样本。用排列重要性做交叉检验。

排列重要性

一个模型无关的方法:

  1. 训练模型,记录验证数据上的基线性能。
  2. 对每特征:随机打乱它的值,测性能下降。
  3. 下降越大,特征越重要。

若打乱一个特征不损性能,模型不依赖它。若性能崩,那个特征关键。

排列重要性(Permutation Importance)避免了树重要性的基数偏置。但慢:每特征一次完整评估,为稳定性重复多次。

对比表

方法 类型 速度 非线性 特征交互
方差阈值 过滤 极快
互信息 过滤
相关系数过滤 过滤
RFE 包装 取决于模型
L1 / Lasso 嵌入 否(线性)
树重要性 嵌入
排列重要性 模型无关

决策流程图

二、从零实现

第 1 步:生成含已知特征结构的合成数据

import numpy as np def make_feature_selection_data(n_samples=500, seed=42): rng = np.random.RandomState(seed) x1 = rng.randn(n_samples) x2 = rng.randn(n_samples) x3 = rng.randn(n_samples) x4 = x1 + 0.1 * rng.randn(n_samples) x5 = x2 + 0.1 * rng.randn(n_samples) informative = np.column_stack([x1, x2, x3, x4, x5]) correlated = np.column_stack([ x1 * 0.9 + 0.1 * rng.randn(n_samples), x2 * 0.8 + 0.2 * rng.randn(n_samples), x3 * 0.7 + 0.3 * rng.randn(n_samples), x1 * 0.5 + x2 * 0.5 + 0.1 * rng.randn(n_samples), x2 * 0.6 + x3 * 0.4 + 0.1 * rng.randn(n_samples), ]) noise = rng.randn(n_samples, 10) * 0.5 X = np.hstack([informative, correlated, noise]) y = (2 * x1 - 1.5 * x2 + x3 + 0.5 * rng.randn(n_samples) > 0).astype(int) feature_names = ( [f"info_{i}" for i in range(5)] + [f"corr_{i}" for i in range(5)] + [f"noise_{i}" for i in range(10)] ) return X, y, feature_names

我们知道真相:特征 0-4 有信息(其中 3、4 是 0、1 的相关副本),特征 5-9 与有信息特征相关,特征 10-19 是纯噪声。好的选择方法应把 0-4 排最高、10-19 排最低。

第 2 步:方差阈值

def variance_threshold(X, threshold=0.01): variances = np.var(X, axis=0) mask = variances > threshold return mask, variances

第 3 步:互信息(离散)

def discretize(x, n_bins=10): min_val, max_val = x.min(), x.max() if max_val == min_val: return np.zeros_like(x, dtype=int) bin_edges = np.linspace(min_val, max_val, n_bins + 1) binned = np.digitize(x, bin_edges[1:-1]) return binned def mutual_information(X, y, n_bins=10): n_samples, n_features = X.shape mi_scores = np.zeros(n_features) y_vals, y_counts = np.unique(y, return_counts=True) p_y = y_counts / n_samples for f in range(n_features): x_binned = discretize(X[:, f], n_bins) x_vals, x_counts = np.unique(x_binned, return_counts=True) p_x = dict(zip(x_vals, x_counts / n_samples)) mi = 0.0 for xv in x_vals: for yi, yv in enumerate(y_vals): joint_mask = (x_binned == xv) & (y == yv) p_xy = np.sum(joint_mask) / n_samples if p_xy > 0: mi += p_xy * np.log(p_xy / (p_x[xv] * p_y[yi])) mi_scores[f] = mi return mi_scores

第 4 步:递归特征消元

def simple_logistic_importance(X, y, lr=0.1, epochs=100): n_samples, n_features = X.shape w = np.zeros(n_features) b = 0.0 for _ in range(epochs): z = X @ w + b pred = 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500))) error = pred - y w -= lr * (X.T @ error) / n_samples b -= lr * np.mean(error) return w, b def rfe(X, y, n_features_to_select=5, lr=0.1, epochs=100): n_total = X.shape[1] remaining = list(range(n_total)) rankings = np.ones(n_total, dtype=int) rank = n_total while len(remaining) > n_features_to_select: X_subset = X[:, remaining] w, _ = simple_logistic_importance(X_subset, y, lr, epochs) importances = np.abs(w) least_idx = np.argmin(importances) original_idx = remaining[least_idx] rankings[original_idx] = rank rank -= 1 remaining.pop(least_idx) for idx in remaining: rankings[idx] = 1 selected_mask = rankings == 1 return selected_mask, rankings

第 5 步:L1 特征选择

def soft_threshold(w, alpha): return np.sign(w) * np.maximum(np.abs(w) - alpha, 0) def l1_feature_selection(X, y, alpha=0.1, lr=0.01, epochs=500): n_samples, n_features = X.shape w = np.zeros(n_features) b = 0.0 for _ in range(epochs): z = X @ w + b pred = 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500))) error = pred - y gradient_w = (X.T @ error) / n_samples gradient_b = np.mean(error) w -= lr * gradient_w w = soft_threshold(w, lr * alpha) b -= lr * gradient_b selected_mask = np.abs(w) > 1e-6 return selected_mask, w

第 6 步:树重要性(简单决策树)

def gini_impurity(y): if len(y) == 0: return 0.0 classes, counts = np.unique(y, return_counts=True) probs = counts / len(y) return 1.0 - np.sum(probs ** 2) def best_split(X, y, feature_idx): values = np.unique(X[:, feature_idx]) if len(values) <= 1: return None, -1.0 best_threshold = None best_gain = -1.0 parent_gini = gini_impurity(y) n = len(y) for i in range(len(values) - 1): threshold = (values[i] + values[i + 1]) / 2.0 left_mask = X[:, feature_idx] <= threshold right_mask = ~left_mask n_left = np.sum(left_mask) n_right = np.sum(right_mask) if n_left == 0 or n_right == 0: continue gain = parent_gini - (n_left / n) * gini_impurity(y[left_mask]) - (n_right / n) * gini_impurity(y[right_mask]) if gain > best_gain: best_gain = gain best_threshold = threshold return best_threshold, best_gain def tree_importance(X, y, n_trees=50, max_depth=5, seed=42): rng = np.random.RandomState(seed) n_samples, n_features = X.shape importances = np.zeros(n_features) for _ in range(n_trees): sample_idx = rng.choice(n_samples, size=n_samples, replace=True) feature_subset = rng.choice(n_features, size=max(1, int(np.sqrt(n_features))), replace=False) X_boot = X[sample_idx] y_boot = y[sample_idx] tree_imp = _build_tree_importance(X_boot, y_boot, feature_subset, max_depth) importances += tree_imp total = importances.sum() if total > 0: importances /= total return importances def _build_tree_importance(X, y, feature_subset, max_depth, depth=0): n_features = X.shape[1] importances = np.zeros(n_features) if depth >= max_depth or len(np.unique(y)) <= 1 or len(y) < 4: return importances best_feature = None best_threshold = None best_gain = -1.0 for f in feature_subset: threshold, gain = best_split(X, y, f) if gain > best_gain: best_gain = gain best_feature = f best_threshold = threshold if best_feature is None or best_gain <= 0: return importances importances[best_feature] += best_gain * len(y) left_mask = X[:, best_feature] <= best_threshold right_mask = ~left_mask importances += _build_tree_importance(X[left_mask], y[left_mask], feature_subset, max_depth, depth + 1) importances += _build_tree_importance(X[right_mask], y[right_mask], feature_subset, max_depth, depth + 1) return importances

第 7 步:跑所有方法并对比

代码文件在同一合成数据集上跑所有五种方法,打印对比表,显示每个方法选中了哪些特征。

三、框架对比

用 scikit-learn,特征选择内置在流水线里:

from sklearn.feature_selection import ( VarianceThreshold, mutual_info_classif, RFE, SelectFromModel, ) from sklearn.linear_model import Lasso, LogisticRegression from sklearn.ensemble import RandomForestClassifier vt = VarianceThreshold(threshold=0.01) X_filtered = vt.fit_transform(X) mi_scores = mutual_info_classif(X, y) top_k = np.argsort(mi_scores)[-10:] rfe_selector = RFE(LogisticRegression(), n_features_to_select=10) rfe_selector.fit(X, y) X_rfe = rfe_selector.transform(X) lasso_selector = SelectFromModel(Lasso(alpha=0.01)) lasso_selector.fit(X, y) X_lasso = lasso_selector.transform(X) rf = RandomForestClassifier(n_estimators=100) rf.fit(X, y) importances = rf.feature_importances_

从零实现精确展示每个方法内部发生什么。方差阈值就是算 var(X, axis=0) 加个掩码。互信息就是在列联表里数联合和边际频率。RFE 就是「训练、排序、剪枝」的循环。L1 是带软阈值步骤的梯度下降。树重要性是跨分裂累加不纯度下降。无魔法——只有统计和循环。

sklearn 版本加了鲁棒性(如 mutual_info_classif 用 k 近邻密度估计而非装箱)、速度(C 实现)、流水线集成。

把特征选择放进流水线

from sklearn.pipeline import Pipeline from sklearn.feature_selection import SelectKBest, mutual_info_classif pipe = Pipeline([ ("variance", VarianceThreshold(threshold=0.01)), ("mi_select", SelectKBest(mutual_info_classif, k=20)), ("model", LogisticRegression(C=0.1, penalty="l1", solver="liblinear")), ]) pipe.fit(X_train, y_train)

特征选择作为流水线步骤时,fit 只在训练折上算统计量/重要性,transform 在验证/测试折上套用——彻底防泄漏,与第 13 节的预处理变换同理。

排列重要性(scikit-learn)

from sklearn.inspection import permutation_importance result = permutation_importance( rf, X_val, y_val, n_repeats=10, random_state=42, scoring="f1" ) for i in result.importances_mean.argsort()[::-1]: print(f"{feature_names[i]:<20} {result.importances_mean[i]:.4f}")

排列重要性在已训练模型上做,不重训,适合做树重要性的交叉检验——尤其当存在高基数特征或强相关特征时。

维度 从零实现 scikit-learn
互信息估计 装箱直方图 k 近邻密度估计,更鲁棒
选择器集成 手动 SelectKBest / SelectFromModel / RFE 都进 Pipeline
适用 理解原理 生产

💡 特征选择必须只基于训练折,把它放进 Pipeline 即可自动正确;在全集上算重要性再切分,会泄漏验证折信息。

四、可复用产物

本节产出 outputs/skill-feature-selector.md——一个选择正确特征选择方法的快速参考决策树,根据特征数、模型类型、非线性需求给出方法推荐。

五、练习

  1. 前向选择:实现 RFE 的反面。从零特征开始,每步加入提升模型性能最多的特征,加入不再有帮助时停。对比 RFE 选中的特征。哪个更快?哪个结果更好?

  2. 稳定性选择:跑 50 次 L1 特征选择,每次在数据 80% 随机子样本上、alpha 略有不同。数每个特征被选中的频率。被选中 > 80% 次的特征是「稳定的」。对比稳定特征与单次 L1 选择。哪个更可靠?

  3. 多重共线性检测:计算所有特征的相关矩阵。实现一个函数,给定相关阈值(如 0.9),从每个高相关对里移除一个(保留与目标互信息更高的那个)。在合成数据上测试并验证它移除了冗余的相关特征。

  4. 特征选择流水线:把方差阈值、互信息过滤、RFE 链成一条流水线。先移除近零方差特征,再按互信息保留前 50%,再对幸存者跑 RFE。对比这条流水线与单跑 RFE 全部特征。流水线更快吗?同样准吗?

  5. 从零实现排列重要性:实现排列重要性。对每特征,打乱其值 10 次,测 F1 平均下降。对比排序与树重要性。找出它们不一致的情况并解释原因(提示:相关特征)。

本节要点回顾

  1. 更多特征不等于更好:维度灾难让特征空间体积爆炸、数据点稀疏、距离趋同、过拟合成为默认;特征选择是解药。
  2. 三大类方法:过滤器(独立打分、快、错过交互)、包装器(用模型评子集、准、贵)、嵌入法(训练中选)。
  3. 方差阈值是必要不充分的预处理:删常数/近常数特征,近零成本;但高方差特征仍可能是纯噪声。
  4. 互信息抓非线性:I(X;Y)=sum p(x,y)*log(p(x,y)/(p(x)p(y))),独立性时为零;一个零相关的特征可能因二次或周期关系而有高互信息;连续特征先离散化。
  5. RFE 考虑交互:训练-排序-剪枝循环,移除一个特征改变其他特征重要性,比过滤器彻底;但要训练 N-目标数 次,贵时可每步移除多个。
  6. L1 产生精确零权重:菱形约束让最优解落在角上,权重精确为零——这是嵌入特征选择;L2 圆形约束只收缩不压零;只对线性模型有效。
  7. 树重要性自动抓非线性与交互:累加跨分裂的不纯度下降;但偏向高基数特征(随机 ID 列会显得重要)。
  8. 排列重要性是模型无关交叉检验:打乱特征值测性能下降,避免基数偏置;但慢,每特征一次完整评估重复多次。
  9. 方法选择按特征数和模型类型:<50 用方差+互信息、50~500 用方差+L1/树、>500 用方差+互信息+RFE 级联;线性模型配 L1、树配树重要性+排列重要性、其他用 RFE。
  10. 特征选择必须只在训练折上:放进 Pipeline 自动正确;在全集算重要性再切分会泄漏验证折;sklearn 的 SelectKBest/SelectFromModel/RFE 都原生支持 Pipeline 集成。

本章到此结束。回看 18 节,我们从「什么是机器学习」出发,吃透了线性/逻辑回归、决策树与随机森林、SVM、k 近邻、无监督学习、特征工程、模型评估、偏差方差权衡、集成方法、超参数调优、流水线、朴素贝叶斯、时间序列、异常检测、不平衡数据、特征选择——这些构成了从数据中学习的完整地基。下一章,我们将进入神经网络,把「从数据学规律」升级为「用层层非线性变换学表征」。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U