ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

特征选择实战:粒子群优化(PSO)算法原理与Python实现

特征选择实战:粒子群优化(PSO)算法原理与Python实现 做机器学习项目做到第三年的时候我发现自己最花时间的不是调模型而是清特征。三百多个特征丢进LightGBM一次交叉验证跑下来够泡三杯咖啡的。更要命的是特征一多模型在验证集上明明很漂亮一上线就暴露出各种毛病。后来我把特征选择这条线系统地捋了一遍才发现这个环节对项目最终效果的影响往往比换一套更强的模型参数还大。这篇不聊虚的直接把我常用的几类特征选择方法包括粒子群优化PSO算法怎么和特征选择结合起来用Python完整过一遍。我会从为什么要做特征选择讲起把过滤式、包裹式、嵌入式三种主流方法各自的设计思路和适用边界说清楚然后重点拆解PSO做特征选择的原理和代码实现。最后再把我在实际项目中踩过的坑和调参心得交代一遍。内容偏向实战有现成的代码可以直接拿去跑看完你应该能根据自己手里的数据规模和数据特点选出合适的特征选择方案。1. 特征选择这件事为什么值得单独写一篇干货1.1 一个反直觉的结论特征不是越多越好很多刚入门机器学习的朋友习惯性地认为特征越多模型掌握的信息越丰富效果自然越好。这个直觉在特征数量比较少的时候是成立的但特征一旦多起来事情就开始反向发展了。原因也不复杂首先是维度灾难特征维度越高样本在高维空间里的分布就越稀疏很多基于距离的算法几乎失效模型只能在训练集上背答案遇到新样本就垮掉。其次是冗余特征和噪声特征的存在比如电商用户画像里注册天数和活跃天数往往高度相关两个都放进去不仅没有新增信息反而放大了噪声的干扰。我遇到过最典型的一个项目特征是业务方从数据库里拉出来的所有统计指标一共876个里面大量特征是互相交叉生成的相关性极高。模型训练完线下AUC确实不错但线上效果一直不达标。后来做个最简单的方差筛选把近一半方差几乎为零的特征直接扔掉效果反而提了3个点。那一刻我才真正意识到特征选择不是可有可无的预处理步骤它本身就是模型效果的一部分。1.2 特征选择到底能解决哪些实际问题结合我做过的项目特征选择的价值可以归结为四个方面。第一是降低过拟合风险。特征越多模型越容易把训练集中的随机噪声当成规律学进去。第二是减少计算开销。无论是训练时间还是线上推理时间特征维度都直接影响延迟尤其在实时推荐这类场景下少算十个特征就能节省可观的机器资源。第三是提升可解释性。给业务方讲模型时如果最终只需要解释十几个特征比对着上百个特征讲半天要轻松得多。第四是提高模型的泛化能力筛选掉无关特征之后模型关注的是真正有因果关系的信号换一批数据效果也不会垮。有一点需要注意特征选择和降维不是一回事。PCA这类降维方法会把原始特征做线性变换生成一组新特征可解释性差特征选择则直接保留原始特征的子集业务方依然能看懂每个特征的业务含义。所以在对可解释性有要求的场景里特征选择往往是比降维更合适的手段。2. 三种主流特征选择方法先把底子打牢特征选择方法按照“选择过程和模型训练的关系”可以分成三个流派过滤式、包裹式和嵌入式。这三个流派的设计哲学各不相同分别适合不同的数据规模和业务场景。2.1 过滤式先筛选再建模便宜大碗过滤式的核心思路很直接不依赖任何机器学习模型只根据特征本身的统计性质打分排序然后按照分数选出Top K个特征。常见的打分指标有方差、皮尔逊相关系数、卡方检验、互信息等。这种方法最大的优点是快毕竟不用反复训练模型在特征数量达到数千甚至上万时依然能很快跑完。缺点是它完全不考虑特征组合之间的关系单个特征看着没用但和别的特征组合在一起反而很有价值这种模式过滤式方法发现不了。所以在实际项目里我通常把过滤式当作第一步粗筛先把明显没用的特征扔掉给后续的精细筛选减轻负担。拿互信息这个指标来说它能捕捉特征和目标之间的非线性关系比普通的相关系数适用范围更广。缺点是计算量比卡方检验大而且对连续变量需要先做离散化处理。我在使用中一般连续特征直接用卡方检验或方差筛选类别特征用互信息效果比较稳。2.2 包裹式以模型得分为准贵但准包裹式方法的思路就完全不一样了它把特征子集直接当成搜索空间里的一个候选解用模型的预测性能来评价这个特征子集好不好。经典的方法有前向搜索、后向搜索以及大家比较熟悉的递归特征消除RFE。RFE的思路是先用全部特征训练一个模型根据特征权重把最不重要的特征去掉然后再用剩余特征重新训练模型重复这个过程直到达到指定的特征数量。由于评价标准就是模型本身的性能包裹式方法选出来的特征子集通常比过滤式更能贴合模型的特性效果也普遍更好。但代价也很明显——计算量上去了。每评估一个特征子集都要完整训练一次模型如果特征空间很大、模型又比较复杂跑起来相当耗时。另外贪心策略有它的局限性RFE每次只去掉最不重要的一个特征一旦某个特征在早期被错误删掉后面没有机会再找回来容易陷入局部最优。2.3 嵌入式训练过程顺带选特征嵌入式方法把特征选择嵌入到模型训练过程中训练完模型特征的重要性也顺带算出来了。最典型的就是L1正则化Lasso和树模型的特征重要性。L1正则化的原理是在损失函数里加上特征权重的绝对值之和这会让一部分特征的权重被压缩到严格的零权重非零的特征就自然被保留下来。这种方法的优势是计算效率高特征之间的交互作用也能在一定程度上被模型捕捉。树模型的特征重要性则是通过计算每个特征在节点分裂时带来的不纯度下降量来排序训练完随机森林或者XGBoost之后直接调用feature_importances_属性就能拿到结果。嵌入式方法适合特征数量中等以上的场景尤其是树模型在表格数据上的表现本来就很好顺带做特征选择几乎不增加额外成本。缺点是嵌入式方法受模型本身的偏差影响如果模型对某个类型的数据不敏感特征重要性排序就可能偏离真实情况。2.4 三种方法怎么选一张表格看懂我用一张表把三种方法的核心差异和适用场景整理出来方便快速对照。方法类型计算代价是否依赖模型典型算法适合场景过滤式低否方差筛选、卡方检验、互信息特征非常多先做粗筛包裹式高是RFE、前向搜索、遗传算法特征数适中追求高精度嵌入式中是Lasso、树模型特征重要性大多数表格数据场景需要说明的是这三种方法不是互斥的。实际项目里常用的组合拳是过滤式粗筛 嵌入式或包裹式精筛先用便宜的过滤式把特征从一千降到一百再用贵一点的方法从一百里选出二十个核心特征效率和效果都能兼顾。3. 为什么用PSO做特征选择本质是一个组合优化问题特征选择的另一个视角是把这个问题当成组合优化问题n个特征每个特征选还是不选一共有2的n次方种组合目标是找到预测效果最好的那一种组合。当n比较大的时候枚举所有组合是不可能的任务所以需要借助各种搜索算法去寻找近似最优解。粒子群优化算法就是其中一种非常经典的群体智能搜索算法。3.1 PSO的核心思想鸟群觅食的智慧PSO算法的灵感来自鸟群觅食行为。假设有一群鸟在一片区域里找食物每只鸟都不知道食物在哪但知道自己当前位置距离食物多远同时能听到群体中其他鸟的交流。于是每只鸟在下一次飞行时方向会受两个因素影响自己历史上飞过的离食物最近的位置以及整个群体目前发现的最优位置。在PSO算法里每个候选解被称为一个粒子粒子有速度和位置两个属性。每个粒子都能记住自己找到过的最优位置叫作个体最优pbest整个群体里的最好位置叫作全局最优gbest。每一轮迭代粒子根据以下公式更新速度再根据速度更新位置速度更新公式 v w * v c1 * r1 * (pbest - x) c2 * r2 * (gbest - x)位置更新公式 x x v公式里的w是惯性权重控制粒子保持上一轮飞行方向的程度c1和c2是学习因子分别控制粒子向个体最优和全局最优学习的强度r1和r2是0到1之间的随机数用来增加搜索的随机性。3.2 把特征选择翻译成PSO能解的数学问题标准PSO算法是为连续优化问题设计的但特征选择是一个离散问题每个特征只有选或不选两种状态。解决办法是使用二进制粒子群优化算法BPSO。在二进制PSO里每个粒子的位置不再是连续值而是一个由0和1组成的向量向量的维度等于特征总数。第i维取值为1表示选择第i个特征取值为0表示不选。速度依然可以是连续值但要通过sigmoid函数把速度映射到0到1之间的概率值然后跟随机数比较决定该维度的位置取0还是取1。通过这样的编码方式一个粒子就代表了一个特征子集。评价这个粒子好不好用的是一个适应度函数。最常见的做法是用该粒子对应的特征子集去训练一个模型做交叉验证得到准确率或AUC然后减去一个和特征数量成正比的惩罚项。惩罚项的作用是控制特征数量避免粒子为了微小的精度提升而把所有特征都选进去。适应度函数可以写成这样 fitness accuracy - alpha * (n_selected_features / n_total_features)这里的alpha是特征数量惩罚系数值越大粒子越倾向于选择更少的特征。3.3 PSO比贪心搜索好在哪前面提到的RFE本质上是贪心搜索每次只做局部最优的决策一旦某一步选错后面很难补救。PSO这种群体智能算法的优势在于多个粒子同时在解空间的不同区域进行搜索信息在群体之间共享既有局部深入探索的能力又有全局跳出局部最优的能力。和遗传算法相比PSO不需要编码、交叉、变异这些复杂的遗传操作参数更少实现起来也更简单。在处理特征数在几十到几百之间的场景时PSO通常能在可接受的时间内找到一个接近最优的特征子集。当然PSO也不是万能的。粒子数量和迭代次数决定了计算的代价每评估一个粒子的适应度要完整训练一次模型所以PSO只适合特征数量适中的场景。特征达到上千甚至更高时直接用嵌入式方法会更划算这一点我在后面的实战案例里还会再展开。4. 非常详细的Python实例从数据到结果全流程理论讲再多不如跑一次实际代码印象深刻。这一节我把整个流程过一遍使用的数据集是sklearn内置的乳腺癌数据集特征维度30规模适中非常适合做特征选择实验。代码可以直接复制到Jupyter Notebook里运行。4.1 环境准备与数据选择环境方面需要安装的库包括numpy、pandas、scikit-learn和matplotlib。我用的Python版本是3.10scikit-learn版本是1.3.0这些库用pip直接装就可以。pip install numpy pandas scikit-learn matplotlib先把数据和必要的库加载进来。import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split, cross_val_score from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.feature_selection import SelectKBest, chi2, RFE from sklearn.ensemble import RandomForestClassifier # 加载数据 data load_breast_cancer() X pd.DataFrame(data.data, columnsdata.feature_names) y data.target print(f特征矩阵形状: {X.shape}) print(f类别分布: {np.bincount(y)})乳腺癌数据集中每个样本有30个特征包含肿瘤的平均半径、纹理、周长、面积、平滑度等多个维度的指标。特征是连续值并且特征之间的量纲差异比较大所以需要先做标准化。要提醒一句标准化必须在划分训练集和测试集之后进行用训练集的均值和标准差去变换测试集避免信息泄漏。# 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 标准化 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 再用一个单独的验证集来评估PSO里的粒子 X_train_pso, X_val_pso, y_train_pso, y_val_pso train_test_split( X_train_scaled, y_train, test_size0.25, random_state42 ) print(f训练集: {X_train_pso.shape}, 验证集: {X_val_pso.shape}, 测试集: {X_test_scaled.shape})这里的数据集划分做了两层。第一层把原始数据分为训练集和测试集测试集全程不参与任何特征选择过程只在最后评估最终效果时使用。第二层从训练集里又切了一部分验证集出来专门给PSO算法评估粒子适应度。这么做是因为PSO在搜索过程中会反复评估不同的特征子集如果直接拿测试集来评估就相当于PSO在对着测试集做优化最后的结果会有严重的数据泄漏测出来的准确率是不真实的。4.2 先跑三个经典方法作为基准为了让最后的结果有参照系我先把三种经典特征选择方法的基线结果跑出来。这四个方法共用一个逻辑回归模型设置max_iter1000保证收敛。第一个是过滤式的SelectKBest卡方检验作为评分函数。# 过滤式卡方检验选10个特征 from sklearn.feature_selection import SelectKBest, chi2 # 卡方检验要求特征非负用MinMaxScaler把特征转换到[0,1]区间 from sklearn.preprocessing import MinMaxScaler scaler_mm MinMaxScaler() X_train_mm scaler_mm.fit_transform(X_train) X_test_mm scaler_mm.transform(X_test) selector_chi2 SelectKBest(chi2, k10) X_train_chi2 selector_chi2.fit_transform(X_train_mm, y_train) X_test_chi2 selector_chi2.transform(X_test_mm) model_chi2 LogisticRegression(max_iter1000) model_chi2.fit(X_train_chi2, y_train) score_chi2 model_chi2.score(X_test_chi2, y_test) print(f过滤式 卡方检验 Top10 测试集准确率: {score_chi2:.4f}) print(f选中的特征: {X.columns[selector_chi2.get_support()].tolist()})卡方检验要求特征非负所以这里用MinMaxScaler把原特征转换到0到1的区间。如果直接用标准化后的负值数据卡方检验会报错。第二个是包裹式的RFE用逻辑回归作为基模型目标也是选10个特征。# 包裹式RFE逻辑回归基模型选10个特征 model_lr LogisticRegression(max_iter1000) selector_rfe RFE(estimatormodel_lr, n_features_to_select10) selector_rfe.fit(X_train_scaled, y_train) X_train_rfe selector_rfe.transform(X_train_scaled) X_test_rfe selector_rfe.transform(X_test_scaled) model_rfe LogisticRegression(max_iter1000) model_rfe.fit(X_train_rfe, y_train) score_rfe model_rfe.score(X_test_rfe, y_test) print(f包裹式 RFE Top10 测试集准确率: {score_rfe:.4f}) print(f选中的特征: {X.columns[selector_rfe.get_support()].tolist()})第三个是嵌入式的Lasso用L1正则自动把不重要的特征权重压成零。# 嵌入式LassoL1正则 from sklearn.linear_model import Lasso from sklearn.feature_selection import SelectFromModel # 用SelectFromModelLasso选择特征 lasso Lasso(alpha0.01, random_state42) selector_lasso SelectFromModel(lasso, max_features10) selector_lasso.fit(X_train_scaled, y_train) X_train_lasso selector_lasso.transform(X_train_scaled) X_test_lasso selector_lasso.transform(X_test_scaled) model_lasso LogisticRegression(max_iter1000) model_lasso.fit(X_train_lasso, y_train) score_lasso model_lasso.score(X_test_lasso, y_test) print(f嵌入式 Lasso Top10 测试集准确率: {score_lasso:.4f}) print(f选中的特征: {X.columns[selector_lasso.get_support()].tolist()})最后跑一个不筛选特征的基准也就是用全部30个特征直接训练逻辑回归作为对照。# 基准直接用全部特征 model_all LogisticRegression(max_iter1000) model_all.fit(X_train_scaled, y_train) score_all model_all.score(X_test_scaled, y_test) print(f全特征基准 测试集准确率: {score_all:.4f})4.3 PSO特征选择的完整实现PSO部分是整个实验的核心我拆成三个步骤来讲先定义适应度函数再初始化粒子群最后写主循环迭代搜索。适应度函数接收一个由0和1组成的向量提取出对应的特征子集用逻辑回归在训练集上训练在验证集上评估准确率并减去一个特征数量的惩罚项。# 适应度函数 def fitness_function(feature_mask, X_train, y_train, X_val, y_val, alpha0.01): 输入 feature_mask: 形状为(n_features,)的数组1表示选中该特征0表示不选 X_train, y_train: 训练数据 X_val, y_val: 验证数据 alpha: 特征数量惩罚系数 返回 适应度值越大越好 # 如果没选任何特征直接返回一个非常小的分数 selected_indices np.where(feature_mask 1)[0] n_selected len(selected_indices) if n_selected 0: return 0.0 # 提取选中特征 X_train_sel X_train[:, selected_indices] X_val_sel X_val[:, selected_indices] # 训练逻辑回归并评估 model LogisticRegression(max_iter1000) model.fit(X_train_sel, y_train) accuracy model.score(X_val_sel, y_val) # 特征数量惩罚 penalty alpha * (n_selected / X_train.shape[1]) return accuracy - penalty适应度函数里alpha默认取0.01意思是在30个特征的情况下每多选一个特征适应度大约扣掉0.0003。这个惩罚力度比较温和主要防止粒子把所有特征都选上。alpha可以根据业务需要调整如果希望特征数量严格控制可以增大alpha。接下来是粒子群初始化。种群大小设为30因为特征维度是30粒子数量和特征维度相当是比较合理的配置。每个粒子的位置是一个随机生成的0/1向量速度是[-v_max, v_max]之间的随机连续值。# PSO参数 n_particles 30 # 粒子数量 n_iterations 50 # 迭代次数 n_features X_train_scaled.shape[1] w 0.8 # 惯性权重 c1 1.5 # 个体学习因子 c2 1.5 # 社会学习因子 v_max 6.0 # 最大速度 alpha 0.01 # 特征数量惩罚系数 # 初始化粒子位置和速度 positions np.random.randint(2, size(n_particles, n_features)) velocities np.random.uniform(-v_max, v_max, (n_particles, n_features)) # 计算初始适应度 scores np.array([ fitness_function(p, X_train_pso, y_train_pso, X_val_pso, y_val_pso, alpha) for p in positions ]) # 初始化个体最优和全局最优 pbest_positions positions.copy() pbest_scores scores.copy() gbest_index np.argmax(scores) gbest_position positions[gbest_index].copy() gbest_score scores[gbest_index] # 记录每次迭代的最优适应度 history [gbest_score] print(f初始全局最优适应度: {gbest_score:.4f}) print(f初始全局最优特征数: {int(np.sum(gbest_position))})主循环需要反复执行四步操作更新速度、更新位置、计算新适应度、更新个体最优和全局最优。# PSO主循环 for iteration in range(n_iterations): for i in range(n_particles): # 取随机数 r1 np.random.random(n_features) r2 np.random.random(n_features) # 速度更新公式 velocities[i] ( w * velocities[i] c1 * r1 * (pbest_positions[i] - positions[i]) c2 * r2 * (gbest_position - positions[i]) ) # 速度限幅防止速度过大导致粒子震荡 velocities[i] np.clip(velocities[i], -v_max, v_max) # 二进制化sigmoid函数把速度映射到(0,1)区间 sigmoid_prob 1 / (1 np.exp(-velocities[i])) # 以sigmoid概率取1否则取0 positions[i] (np.random.random(n_features) sigmoid_prob).astype(int) # 计算全部粒子的适应度 current_scores np.array([ fitness_function(p, X_train_pso, y_train_pso, X_val_pso, y_val_pso, alpha) for p in positions ]) # 更新个体最优 improve_mask current_scores pbest_scores pbest_positions[improve_mask] positions[improve_mask] pbest_scores[improve_mask] current_scores[improve_mask] # 更新全局最优 if current_scores.max() gbest_score: best_local_idx np.argmax(current_scores) gbest_position positions[best_local_idx].copy() gbest_score current_scores[best_local_idx] history.append(gbest_score) if (iteration 1) % 10 0: print(f迭代 {iteration 1}/{n_iterations}当前最优适应度: {gbest_score:.4f}特征数: {int(np.sum(gbest_position))})位置更新的逻辑需要重点解释一下。标准PSO用连续值直接更新位置但特征选择需要0/1离散值所以这里用sigmoid函数把连续速度映射成取1的概率。速度越大sigmoid输出越接近1粒子越有可能选择该特征速度越小输出越接近0粒子越有可能丢弃该特征。引入随机数比较的目的是保持搜索的随机性避免粒子群过早收敛到同一个位置。迭代结束后把收敛曲线画出来直观地看粒子群在每一轮搜索中找到的最好解。# 绘制收敛曲线 plt.figure(figsize(8, 5)) plt.plot(history, linewidth2) plt.xlabel(迭代次数) plt.ylabel(全局最优适应度) plt.title(PSO特征选择收敛过程) plt.grid(True, alpha0.3) plt.show() # 最终结果 selected_features_pso np.where(gbest_position 1)[0] print(fPSO最终选中的特征数: {len(selected_features_pso)}) print(fPSO选中特征的索引: {selected_features_pso}) print(fPSO最终特征的名称: {X.columns[selected_features_pso].tolist()})4.4 结果对比到底谁选出来的特征更靠谱PSO搜索完之后把选出的特征子集放到测试集上用逻辑回归做一个最终评估。注意这里用的是之前完全没参与特征选择的测试集。# 用PSO选出的特征子集在测试集上评估 X_train_pso_final X_train_scaled[:, selected_features_pso] X_test_pso_final X_test_scaled[:, selected_features_pso] model_pso LogisticRegression(max_iter1000) model_pso.fit(X_train_pso_final, y_train) score_pso model_pso.score(X_test_pso_final, y_test) print(fPSO特征选择 测试集准确率: {score_pso:.4f}) # 各方法结果汇总 print(\n *50) print(特征选择方法对比结果) print(*50) print(f{方法:20} {特征数:8} {测试集准确率:12}) print(f{全特征基准:18} {30:8} {score_all:.4f}) print(f{过滤式-卡方检验:15} {10:8} {score_chi2:.4f}) print(f{包裹式-RFE:15} {10:8} {score_rfe:.4f}) print(f{嵌入式-Lasso:15} {10:8} {score_lasso:.4f}) print(f{PSO:20} {len(selected_features_pso):8} {score_pso:.4f})从我实际运行的结果来看几个方法在乳腺癌数据集上的准确率差距没那么夸张因为这个数据集本身特征区分度很高30个特征里大部分都有用。但PSO的优势主要在于它选择的特征数量更少通常只有12到15个却能达到和全特征模型相当甚至略高的准确率。这意味着更少的存储、更快的训练、更低的推理延迟在真实业务里都是实打实的收益。不同数据集上PSO的表现会不一样。特征冗余度高、噪声特征多的数据集PSO的优势会更明显如果特征本来就都很重要PSO和全特征的效果差距不大但特征数量能压下来一些。5. 我在实际项目里的经验几个容易忽略的细节代码能跑通只是第一步真正要在项目里落地还得处理一些细节问题。这些坑我基本都踩过整理出来供你参考。5.1 PSO参数怎么定种群、迭代、权重PSO参数对最终效果影响很大但没有一套参数能适配所有场景更多是靠经验加多次实验。种群大小建议设在20到50之间。太少了粒子多样性不足容易早熟太多了每轮迭代都要评估几十个模型计算时间会线性增长。特征维度在几十到几百之间时30个粒子是一个不错的起点。迭代次数建议先设50轮观察收敛曲线。如果曲线在20轮左右就平了可以把迭代次数减半省点时间如果到50轮还在上涨说明搜索还不充分要加大迭代次数或者调整其他参数。惯性权重w控制全局搜索和局部搜索的平衡常见取值在0.4到0.9之间。w越大粒子越倾向于保持当前飞行方向搜索范围更大w越小粒子越容易被群体最优吸引收敛更快但有陷入局部最优的嫌疑。实践中可以试试动态衰减的策略让w从0.9线性降到0.4前期的全局搜索能力和后期的精细搜索能力兼顾。学习因子c1和c2通常在1.0到2.0之间取值。c1偏大说明粒子更相信自己的历史经验c2偏大说明粒子更相信群体的信息。常见做法是c1和c2都取1.5或者让c1先大后小、c2先小后大模拟一种前期独立探索、后期群体收敛的节奏。这个策略在连续优化问题里效果明显在特征选择这种离散问题里提升相对有限但值得一试。5.2 最容易踩的坑泄漏、过拟合、惩罚系数第一个坑是数据泄漏。特征选择必须在训练集范围内完成绝对不能让测试集参与特征子集的选择过程。很多人用PSO时习惯直接拿全部数据去评估适应度结果测试集信息在搜索过程中就被偷看了最后报告的成绩虚高一到线上就原形毕露。我习惯的做法是先从完整数据里切出一个不少于20%的测试集锁起来剩下的训练集再切一部分做验证集验证集专门用来评估粒子适应度最后用锁起来的测试集做一次性的最终评估。第二个坑是适应度函数的过拟合。PSO在迭代过程中会反复评估很多特征组合如果验证集太小粒子必然会记住验证集中的噪声导致选出来的特征子集只在验证集上表现好。缓解的办法是使用交叉验证来评估适应度代价是多训练几轮模型。特征数量不多时推荐用3折或5折交叉验证虽然慢一点但结果更可靠。第三个坑是特征数量惩罚系数alpha的取值。alpha太小粒子倾向于把特征全选上alpha太大模型为了减少特征数量而牺牲准确率可能导致漏掉关键特征。我一般先取一个比较小的值比如0.005到0.01跑完看结果特征数量是否合理再根据需要进行调整。比较理想的效果是准确率和全特征相当特征数量能压到三分之一以下。第四个坑是PSO本身的随机性。由于初始位置和速度都是随机的每次运行得到的结果不完全一样。为了让结论稳定我通常同一个参数跑5次取最优的一次作为最终结果同时观察5次结果的特征子集重合度。如果重合度很高说明搜索比较稳定如果差异很大说明参数设置可能需要调整或者问题本身特征冗余度高导致存在多个局部最优解。5.3 怎么验证特征选择的效果最后说下效果验证的思路。不要只看准确率这一个指标还要关注特征数量减少带来的工程收益。我常用的验证方式包括在测试集上对比准确率、AUC和F1分数统计特征选择前后的训练时间和推理时间观察选择出的特征子集是否在业务逻辑上说得通。最后这一点很容易被忽略比如一个特征被PSO选中了但业务专家说这个特征理论上没有预测能力那就要排查一下是不是数据里有什么隐藏的关系被算法捕捉到了。有价值的特征往往能从业务层面得到解释这也让特征选择的结果更容易被团队其他成员接受。另一个可以做的实验是稳定性分析。对原始数据做多次bootstrap采样每次用同样的PSO参数跑一遍统计每个特征被选中的频率。频率高的特征是真正稳定有效的特征频率低的特征是可有可无的边缘特征。这个分析能帮你更理解数据而不仅仅把特征选择当成一个黑盒操作。最后再分享一个小技巧。PSO跑出来的最优特征子集不要直接当作最终特征列表交付。把它作为候选集再加上业务方之前确认过的必备特征在真实模型上做一轮完整训练和评估。因为PSO的适应度函数和最终评估模型可能存在细微差别这个算法选择加人工确认的流程能兼顾数据驱动和业务经验落地效果通常更稳。
返回列表