多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

特征选择实战:子集选择法原理、Python实现与模型评估指南

特征选择实战:子集选择法原理、Python实现与模型评估指南 1. 从“全模型”到“最优子集”一个经典的数据科学难题在数据科学和统计建模的日常工作中我们常常面临一个看似简单、实则棘手的抉择手头有几十个、甚至上百个候选特征变量究竟该把哪些放进最终的回归模型里新手最容易犯的错误就是一股脑儿把所有变量都塞进去构建一个“全模型”。这听起来很“全面”但实际效果往往适得其反。过多的变量会引入噪声导致模型过于复杂、难以解释并且极易在训练数据上表现完美过拟合而在新数据上一塌糊涂。反过来如果变量选得太少模型又可能遗漏关键信息导致拟合不足预测能力孱弱。“模型选择”的核心就是在模型的复杂度和预测精度之间找到一个最佳的平衡点。而“子集选择法”正是解决这一经典难题的一整套方法论工具箱。它不像某些“黑箱”算法那样一次性处理所有变量而是通过系统性地评估不同特征组合的优劣来筛选出那个预测能力最强、也最简洁明了的变量子集。理解并掌握子集选择法是构建稳健、可解释线性模型的基本功其思想也深远影响着更现代的机器学习特征工程。2. 子集选择法的三大核心流派原理、流程与适用场景子集选择法并非单一方法而是一个方法家族。根据搜索策略和计算复杂度的不同主要分为三大流派最优子集选择、逐步选择包括向前和向后以及混合策略。每种方法都有其独特的运作逻辑和适用边界。2.1 最优子集选择穷举的“理想”与现实的“骨感”最优子集选择在概念上最为直接和“完美”。它的目标是从p个候选特征中找出所有可能的k个特征组合k从1到p并从中挑选出在某种评价标准下如RSS最小、调整R²最高、AIC/BIC最小最优的那个子集。2.1.1 核心算法步骤定义空模型M0即不包含任何预测变量的模型通常仅包含截距项。对于每个k (k 1, 2, ..., p)拟合所有包含恰好k个预测变量的线性回归模型。对于p个变量选择k个的组合数为 C(p, k)。例如当p10k5时需要拟合C(10,5)252个模型。从这C(p, k)个模型中根据预设的评价准则如最小的残差平方和RSS选出一个最优模型记为Mk。跨k值比较在得到了M1, M2, ..., Mp这一系列“分项冠军”后我们需要一个能够惩罚模型复杂度的准则来从中选出最终的“总冠军”。常用的准则包括调整R²在R²的基础上对自变量个数进行惩罚。调整R²越大越好。赤池信息准则 (AIC)AIC n log(RSS/n) 2k其中n是样本量。AIC越小越好它权衡了模型的拟合优度和复杂度。贝叶斯信息准则 (BIC)BIC n log(RSS/n) k log(n)。BIC对模型复杂度的惩罚比AIC更重尤其当n较大时因此倾向于选择更简洁的模型。BIC也是越小越好。Mallows‘ Cp统计量Cp ≈ RSS / σ̂² 2k - n其中σ̂²是全模型包含所有p个变量的误差方差估计。一个良好模型的Cp值应接近于k。2.1.2 优势与致命缺陷优势理论上它遍历了所有可能性确保了找到的确实是给定评价准则下的全局最优解。致命缺陷计算量呈组合级数爆炸。当变量数p稍大时例如p40需要拟合的模型数量将是一个天文数字在计算上完全不可行。因此最优子集选择在实践中通常只用于变量数量较少p20的场景更多是作为一种理论上的黄金标准存在。2.2 逐步选择法在计算可行性与最优性之间的折衷为了应对最优子集选择的计算灾难逐步选择法应运而生。它们采用一种贪婪的、迭代的搜索策略每次只添加或删除一个变量从而大幅减少需要评估的模型数量。2.2.1 向前逐步选择向前选择从一个空模型只有截距开始然后依次添加对模型改进最大的变量。初始化从空模型M0开始。添加变量对于所有不在当前模型中的变量考虑将其加入模型并拟合相应的回归模型。选择那个能带来最大模型改进例如使RSS下降最多或F统计量的p值最小的变量将其加入模型。迭代重复步骤2直到满足某个停止准则。例如所有不在模型中的变量的p值都大于某个阈值如0.05或者模型的评价指标如AIC不再显著改善。输出得到一系列嵌套模型M0 ⊂ M1 ⊂ M2 ⊂ ...最终根据交叉验证或信息准则选择一个。注意向前选择的一个主要问题是一旦某个变量被加入模型后续步骤就不会再将其移除。这意味着如果两个变量高度相关先加入的那个可能会“独占”解释力导致后一个更有价值的变量无法被加入。2.2.2 向后逐步选择向后选择与向前相反它从一个包含所有p个变量的全模型开始然后依次移除对模型贡献最小的变量。初始化从全模型Mp开始。移除变量对于当前模型中的所有变量计算如果将其移除后模型拟合优度的下降程度或该变量t检验的p值。移除那个对模型贡献最小p值最大的变量。迭代重复步骤2直到满足停止准则。例如模型中所有变量的p值都小于某个阈值或者移除任何变量都会导致AIC显著增加。输出得到一系列嵌套模型Mp ⊃ M(p-1) ⊃ ...最终选择一个。注意向后选择要求样本量n必须大于变量数p否则无法拟合初始的全模型。这在高维数据p n场景下是行不通的。2.2.3 双向逐步选择混合策略这是向前和向后策略的结合体旨在克服它们各自的缺点。在每一步算法既考虑添加一个新变量像向前选择也考虑从当前模型中移除一个已存在的变量像向后选择。具体步骤通常基于统计检验如F检验的p值来决策添加步骤检查所有不在模型中的变量如果某个变量的p值低于“进入”阈值如0.05则将其加入。删除步骤检查模型中已有的所有变量如果某个变量的p值高于“移除”阈值如0.10则将其删除。 这个过程反复进行直到没有变量符合加入或移除的条件为止。双向策略更加灵活但计算量也稍大。2.3 方法对比与选型心得为了更直观地对比这几种方法我整理了下表这源于我多次项目中的实际选型经验特性最优子集选择向前逐步选择向后逐步选择双向逐步选择搜索策略穷举所有组合贪婪只加不减贪婪只减不加贪婪可加可减计算复杂度极高 (O(2^p))较低 (O(p^2))较低 (O(p^2))中等 (略高于向前/向后)全局最优性是理论上否可能陷入局部最优否可能陷入局部最优否但比单纯向前/向后更优起始模型无需遍历所有k空模型全模型通常为空模型或全模型高维数据适应性不适用 (p20即困难)适用(pn也可)不适用 (需np)适用(通常从空模型开始)主要缺点计算不可行无法剔除已加入的冗余变量无法重新加入已剔除的有用变量计算和规则设置稍复杂实操选型建议变量少求最优如果特征数p在15个以内且计算资源允许可以尝试最优子集选择作为基准。变量多起点低这是最常见的情况。我个人的首选是向前逐步选择。因为它从空模型开始计算快且能处理pn的情况。虽然可能不是全局最优但在大多数实际项目中其效果已经足够好且具有很好的可解释性。有先验全模型如果你的领域知识强烈建议所有变量都可能相关且样本量充足(np)可以从向后选择开始这相当于一个“精细化修剪”的过程。追求更优解当你不确定变量间复杂的共线性关系时使用双向逐步选择通常是更稳妥的选择它提供了更多的调整灵活性。3. 超越RSS如何科学评估与选择最终模型通过上述方法我们得到了一系列候选模型例如向前选择产生的M1, M2, ... Mk。现在面临第二个关键问题如何从这些候选模型中选出那个“最好”的仅仅看训练集上的残差平方和(RSS)或R²是绝对不够的因为它们会随着变量增加而单调改善必然导致选择最复杂的模型。3.1 基于训练误差的惩罚准则AIC、BIC与调整R²这些准则在训练集上计算但通过引入与模型复杂度变量数k成正比的惩罚项来抵消过拟合。AIC (Akaike Information Criterion)其目标是找到一个能最好地描述数据生成过程的模型侧重于预测精度。公式为AIC 2k - 2ln(L)在线性回归的正态假设下可简化为AIC n ln(RSS/n) 2k 常数。选择AIC最小的模型。AIC的惩罚项(2k)相对较轻。BIC (Bayesian Information Criterion)从贝叶斯观点出发旨在找到后验概率最大的模型。公式为BIC ln(n) * k - 2ln(L)简化后BIC n ln(RSS/n) k ln(n) 常数。选择BIC最小的模型。由于ln(n)通常大于2BIC对复杂模型的惩罚更重因此BIC倾向于选择比AIC更简洁的模型。调整R²调整R² 1 - [ (RSS/(n-k-1)) / (TSS/(n-1)) ]。其中TSS是总平方和。它引入了自由度进行校正。选择调整R²最大的模型。使用心得在项目报告中我通常会同时计算AIC和BIC。如果两者选出的模型一致那结论就很强。如果不一致需要结合业务理解如果模型解释性和简洁性优先如用于推断我可能倾向于BIC的选择如果预测精度优先且担心遗漏变量则可能参考AIC。3.2 金标准交叉验证尽管AIC/BIC很有用但在现代机器学习工作流中交叉验证CV被认为是模型选择更可靠的金标准。它通过直接估计模型在新数据上的预测误差来评估性能思想非常直观。3.2.1 K折交叉验证流程以最常用的10折交叉验证为例将整个训练数据集随机分成大小大致相等的10个子集折。对于每一个候选模型例如包含不同变量数的子集模型进行10轮训练和验证。在第i轮使用第i折作为验证集其余9折作为训练集拟合模型并在验证集上计算预测误差如均方误差MSE。将10轮得到的10个MSE取平均得到该候选模型的交叉验证误差CV Error。选择CV Error最小的那个候选模型。3.2.2 交叉验证的实操细节与坑折数K的选择K5或10是最常见的选择。K太小如2估计偏差大K太大如n即留一法LOOCV计算成本高且方差可能较大。10折是一个很好的平衡点。随机性每次划分数据都会导致略有不同的CV误差估计。为了结果更稳定可以进行多次重复的K折CV例如重复5次10折CV取平均误差但这会显著增加计算量。一个关键陷阱必须在交叉验证的每一折内重新进行特征选择这是一个极易被忽略但至关重要的步骤。错误的做法是先用全部数据做一遍特征选择得到变量子集再用这个固定的子集去做交叉验证。这会导致严重的数据泄露因为验证集的信息通过参与特征选择已经泄露到了训练过程中使得CV误差被严重低估过于乐观。正确的做法是在每一折只用该折的训练集数据来运行整套特征选择流程如向前选择确定该折的变量子集然后用这个子集模型去预测该折的验证集。这样得到的CV误差才是对真实泛化误差的无偏估计。3.3 验证集法简单场景下的实用选择如果数据量非常大最直接的方法是随机划分出单独的验证集或测试集。在训练集上进行特征选择和模型训练在验证集上评估不同子集模型的性能如MSE选择验证集误差最小的模型。这种方法计算一次简单快捷但缺点是需要牺牲一部分数据不用于训练且评估结果对单次划分比较敏感。4. 从理论到实践一个完整的Python案例演练让我们用一个真实的案例将上述所有概念串联起来。我们将使用经典的Boston房价数据集可通过sklearn或statsmodels获取目标是预测房价。4.1 数据准备与探索import pandas as pd import numpy as np from sklearn.datasets import fetch_openml from sklearn.model_selection import train_test_split, cross_val_score import statsmodels.api as sm from statsmodels.formula.api import ols import matplotlib.pyplot as plt import seaborn as sns # 加载数据 boston fetch_openml(nameboston, version1, as_frameTrue) df boston.frame print(f数据集形状: {df.shape}) print(df.columns) # 划分训练集和测试集最终评估用 train_df, test_df train_test_split(df, test_size0.2, random_state42) print(f训练集: {train_df.shape}, 测试集: {test_df.shape})首先我们检查数据的相关性对特征选择有个初步印象。# 计算相关性矩阵并查看与目标变量‘MEDV’房价中位数相关性最高的特征 corr_matrix train_df.corr() print(corr_matrix[MEDV].sort_values(ascendingFalse))假设我们发现LSTAT低收入人口比例、RM房间数、PTRATIO师生比等与房价有较强的相关性。4.2 实现向前逐步选择我们将手动实现一个基于AIC的向前逐步选择算法以便理解其内部机制。def forward_selection_aic(data, target_name, max_varsNone): 基于AIC的向前逐步选择 data: 包含特征和目标变量的DataFrame target_name: 目标变量列名 max_vars: 最大选择变量数默认为所有特征 remaining_vars [col for col in data.columns if col ! target_name] selected_vars [] current_aic np.inf history [] # 记录每一步的选择和AIC if max_vars is None: max_vars len(remaining_vars) for i in range(max_vars): aic_with_candidates [] for candidate in remaining_vars: # 构造当前候选变量列表 formula target_name ~ .join(selected_vars [candidate]) model ols(formulaformula, datadata).fit() aic_with_candidates.append((candidate, model.aic)) # 选择能使AIC降低最多的变量 best_candidate, best_aic min(aic_with_candidates, keylambda x: x[1]) # 如果AIC不再降低则停止 if best_aic current_aic: print(fStep {i1}: AIC不再改善停止选择。) break # 更新状态 remaining_vars.remove(best_candidate) selected_vars.append(best_candidate) current_aic best_aic history.append((selected_vars.copy(), current_aic)) print(fStep {i1}: 添加 {best_candidate}, AIC {current_aic:.2f}) return selected_vars, history # 在训练集上运行向前选择 selected_vars, history forward_selection_aic(train_df, MEDV, max_vars10) print(f\n最终选择的变量: {selected_vars})运行后你会看到类似如下的输出展示了变量被加入的顺序以及每一步的AIC值Step 1: 添加 LSTAT, AIC 3000.50 Step 2: 添加 RM, AIC 2800.25 Step 3: 添加 PTRATIO, AIC 2750.10 Step 4: 添加 DIS, AIC 2735.80 Step 5: 添加 NOX, AIC 2729.50 Step 6: AIC不再改善停止选择。 最终选择的变量: [LSTAT, RM, PTRATIO, DIS, NOX]4.3 使用交叉验证确定最佳子集大小现在我们有了一个变量加入的顺序。但我们需要确定到底前几个变量是最佳的。我们将使用交叉验证来评估不同大小子集的性能。from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error from sklearn.model_selection import KFold # 准备数据 X_train train_df.drop(MEDV, axis1) y_train train_df[MEDV] X_test test_df.drop(MEDV, axis1) y_test test_df[MEDV] # 根据向前选择的历史定义不同大小的子集 subset_sizes range(1, len(selected_vars)1) cv_scores [] # 存储每个子集大小的CV误差 kf KFold(n_splits5, shuffleTrue, random_state42) for k in subset_sizes: current_vars selected_vars[:k] # 取前k个变量 X_subset X_train[current_vars] mse_scores [] for train_idx, val_idx in kf.split(X_subset): X_tr, X_val X_subset.iloc[train_idx], X_subset.iloc[val_idx] y_tr, y_val y_train.iloc[train_idx], y_train.iloc[val_idx] model LinearRegression().fit(X_tr, y_tr) y_pred model.predict(X_val) mse_scores.append(mean_squared_error(y_val, y_pred)) cv_error np.mean(mse_scores) cv_scores.append(cv_error) print(f子集大小 {k} ({current_vars}): 5折CV MSE {cv_error:.4f}) # 找到CV误差最小的子集大小 best_size subset_sizes[np.argmin(cv_scores)] best_vars selected_vars[:best_size] print(f\n根据交叉验证最佳子集大小为 {best_size}变量为: {best_vars})4.4 在测试集上进行最终评估我们使用交叉验证选出的最佳变量子集在完整的训练集上重新训练一个最终模型并在从未使用过的测试集上进行最终评估。# 使用最佳变量子集训练最终模型 final_model LinearRegression().fit(X_train[best_vars], y_train) # 在测试集上评估 y_test_pred final_model.predict(X_test[best_vars]) test_mse mean_squared_error(y_test, y_test_pred) test_rmse np.sqrt(test_mse) print(f最终模型在测试集上的表现:) print(f MSE: {test_mse:.4f}) print(f RMSE: {test_rmse:.4f}) print(f R^2: {final_model.score(X_test[best_vars], y_test):.4f}) # 对比全模型所有变量作为参照 full_model LinearRegression().fit(X_train, y_train) y_test_pred_full full_model.predict(X_test) test_mse_full mean_squared_error(y_test, y_test_pred_full) print(f\n全模型{X_train.shape[1]}个变量在测试集上的MSE: {test_mse_full:.4f})通常情况下你会发现使用子集选择法得到的模型其测试集误差MSE会低于或等于全模型而模型却简单得多。这正体现了子集选择的价值用更少的变量获得可比甚至更好的预测性能同时模型更易于理解和部署。5. 高级议题、常见陷阱与最佳实践掌握了基本流程后在实际项目中应用子集选择法还需要注意以下更深层次的问题和技巧。5.1 共线性对子集选择的影响共线性是指预测变量之间高度相关。它对子集选择有重大影响向前选择在高度相关的变量群中哪个变量最先被选中具有很大的随机性。一旦其中一个被选中其他高度相关的变量可能因为无法提供额外的解释力而永远不会被加入即使它们单独看也很有用。最优子集选择共线性会导致模型不稳定。数据微小的变动就可能导致选出的最优子集发生巨大变化。因为几个高度相关的变量在解释因变量时作用几乎可以相互替代。应对策略业务理解优先在高度相关的变量中根据业务意义选择一个最具代表性的。主成分分析PCA在特征选择之前可以先对高度相关的变量组进行PCA用主成分作为新的特征。但这会损失变量的可解释性。正则化方法Lasso这是处理共线性更现代、更强大的工具。Lasso回归本身内置了特征选择功能且对共线性相对稳健。当遇到严重的共线性问题时我通常会建议直接使用Lasso而非经典子集选择法。5.2 与正则化方法Lasso Ridge的对比子集选择法特别是最优子集是一种“离散”的过程——一个变量要么在模型中系数被自由估计要么不在模型中系数为0。而Lasso和Ridge等正则化方法是“连续”的收缩过程。Lasso (L1正则化)可以将某些变量的系数精确地压缩至0从而实现特征选择。与子集选择相比Lasso的计算效率极高尤其对于高维数据并且由于是凸优化问题总能找到唯一解稳定性更好。在现代实践中Lasso通常被认为是替代传统逐步回归的首选方法。Ridge (L2正则化)将所有系数向零收缩但不会将任何系数恰好设为0。它主要用于处理共线性、防止过拟合但不做特征选择。如何选择如果你的主要目标是特征选择和模型可解释性且变量数不算极端多p在几百以内子集选择特别是配合交叉验证和Lasso都是好选择可以同时尝试并对比结果。如果变量数成千上万基因组学、文本数据Lasso在计算和效果上几乎总是更优。5.3 分类问题中的子集选择上述讨论主要围绕线性回归。对于逻辑回归等分类模型子集选择的思想完全适用只是评价准则需要更换评价指标不再使用RSS或MSE而是使用偏差Deviance类似于逻辑回归中的残差平方和、AIC、BIC或者分类任务特有的指标如交叉验证准确率、AUC等。算法步骤向前/向后/双向选择的流程不变只是在每一步用于比较模型好坏的统计量变成了似然比检验、分数检验或信息准则。5.4 最佳实践与避坑指南根据我多年的项目经验以下是一些至关重要的实践建议永远进行交叉验证不要仅仅依赖训练集上的AIC或调整R²来做最终决定。使用交叉验证误差是评估模型泛化能力更可靠的方法。警惕p值操纵在逐步回归中反复地添加/删除变量相当于进行了多次假设检验这会大大增加犯第一类错误选出无关变量的概率。最终模型中变量的p值会失去其传统的解释意义。因此报告逐步回归结果时应着重关注预测性能和变量系数的方向与大小而非其p值。业务逻辑是最终裁判统计方法选出的变量必须经过业务常识的检验。如果一个在业务上毫无意义的变量被选中需要深入排查是否是数据巧合或共线性导致。反之一个业务上极其重要的变量如果没被选中也需要反思模型设定或数据本身是否有问题。标准化数据如果特征量纲差异巨大如年龄和收入在运行某些实现或使用Lasso/Ridge前应对连续型特征进行标准化减去均值除以标准差。这可以保证系数大小具有可比性并且基于系数的选择过程不受量纲影响。子集选择是起点不是终点选出的模型应该作为一个强有力的基线模型。你仍然需要检查其残差是否符合假设独立性、正态性、同方差性是否存在异常点或高杠杆点。模型诊断与模型选择同等重要。子集选择法作为模型选择领域的经典方法其核心思想——在复杂度与精度间权衡、系统性地搜索特征空间——至今依然深刻影响着机器学习实践。尽管有Lasso等更高效的现代方法但理解子集选择法的工作机制能让你更扎实地掌握模型构建的底层逻辑在面对复杂数据时做出更明智的决策。
返回列表