做机器学习项目做到第三年的时候,我发现自己最花时间的不是调模型,而是清特征。三百多个特征丢进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.10,scikit-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, columns=data.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_size=0.2, random_state=42, stratify=y ) # 标准化 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_size=0.25, random_state=42 ) print(f"训练集: {X_train_pso.shape}, 验证集: {X_val_pso.shape}, 测试集: {X_test_scaled.shape}")这里的数据集划分做了两层。第一层把原始数据分为训练集和测试集,测试集全程不参与任何特征选择过程,只在最后评估最终效果时使用。第二层从训练集里又切了一部分验证集出来,专门给PSO算法评估粒子适应度。
这么做是因为PSO在搜索过程中会反复评估不同的特征子集,如果直接拿测试集来评估,就相当于PSO在对着测试集做优化,最后的结果会有严重的数据泄漏,测出来的准确率是不真实的。
4.2 先跑三个经典方法作为基准
为了让最后的结果有参照系,我先把三种经典特征选择方法的基线结果跑出来。这四个方法共用一个逻辑回归模型,设置max_iter=1000保证收敛。
第一个是过滤式的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, k=10) 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_iter=1000) 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_iter=1000) selector_rfe = RFE(estimator=model_lr, n_features_to_select=10) 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_iter=1000) 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正则自动把不重要的特征权重压成零。
# 嵌入式:Lasso(L1正则) from sklearn.linear_model import Lasso from sklearn.feature_selection import SelectFromModel # 用SelectFromModel+Lasso选择特征 lasso = Lasso(alpha=0.01, random_state=42) selector_lasso = SelectFromModel(lasso, max_features=10) 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_iter=1000) 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_iter=1000) 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, alpha=0.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_iter=1000) 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, linewidth=2) plt.xlabel("迭代次数") plt.ylabel("全局最优适应度") plt.title("PSO特征选择收敛过程") plt.grid(True, alpha=0.3) plt.show() # 最终结果 selected_features_pso = np.where(gbest_position == 1)[0] print(f"PSO最终选中的特征数: {len(selected_features_pso)}") print(f"PSO选中特征的索引: {selected_features_pso}") print(f"PSO最终特征的名称: {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_iter=1000) model_pso.fit(X_train_pso_final, y_train) score_pso = model_pso.score(X_test_pso_final, y_test) print(f"PSO特征选择 测试集准确率: {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的适应度函数和最终评估模型可能存在细微差别,这个"算法选择加人工确认"的流程能兼顾数据驱动和业务经验,落地效果通常更稳。