
简介一份改进二元蚁群优化算法的特征选择Python实现面向机器学习与数据挖掘开发者适用于处理高维数据、剔除冗余特征、提升分类回归模型性能等场景。算法将特征选择转化为离散组合优化问题通过模拟蚂蚁觅食时的信息素更新与概率转移机制在迭代中搜索最优特征子集并针对传统蚁群方法的早熟收敛问题进行了改进。资源包以ZIP压缩格式发布共6个文件包含5个Python脚本与1个Markdown文档脚本分别承担算法主流程、适应度函数、启发式信息构造、遗传算法对比实验和结果可视化文档则说明运行方式与参数配置包体仅8KB轻量简洁。已有193人学习下载。借助该实现读者可系统理解改进蚁群优化的核心步骤直接复用或改装其中的函数模块并借助对比实验与图表分析评估特征选择效果是兼顾算法原理与工程实践的入门样例。1. 改进二元蚁群优化算法做特征选择解决的是什么问题当特征维度超过 20 个之后特征选择立刻变成一件尴尬的事滤式方法只认单特征和标签的相关性包装式方法又面临组合爆炸。基于改进二元蚁群优化算法的特征选择方法把每个特征当作一个二进制开关用信息素引导蚂蚁决定这一维留还是删再用自适应蒸发和精英更新避免早熟和震荡。在 Python 里它能直接对接 sklearn 的模型和数据集目标是用尽可能少的特征保住甚至提升分类精度。这套方法适合表格型分类任务特征数在二三十到两三百之间不想人工试特征又嫌弃递归特征消除太机械的工程师。它不算黑匣子每一步都能打开看跑完能留下收敛曲线和特征清单。下面从建模开始逐步落到能跑的 Python 工程。2. 二元蚁群优化算法的建模与改进从图寻优到二进制开关2.1 特征选择先建模把「选哪些特征」编码成二进制向量蚁群优化算法当年是为了解决旅行商这类图路径问题提出来的蚂蚁在节点之间移动边上累积的信息素决定了后续蚂蚁更愿意走哪条路。特征选择不是路径问题它是一个组合选择问题d 个特征每个只有两种状态留还是删。于是「二元蚁群优化算法」把模型改成了 d 个信息素槽位槽位 i 对应特征 i 被选中的倾向度蚂蚁构造解的时候就一个特征一个特征地决定去留。这个改造看起来只是把「边走边选」换成「逐维开关」实际上避开了把特征选择硬套成图搜索时边数量爆炸的问题——特征一多全连接图根本存不下。在 Python 里初始化这部分很直接。注意一点信息素初始值不能设成 0否则第一轮所有强度都是 0蚂蚁完全随机游走算法前期进展会非常慢。import numpy as np from sklearn.feature_selection import mutual_info_classif def init_pheromone(n_features, init_tau0.5): # 信息素初始值取 0.5 而非 00 会导致第一轮沉积失效 return np.full(n_features, init_tau, dtypefloat) def init_heuristic(X, y, methodmutual_info): # 启发式信息用互信息估计每个特征与标签的相关程度 if method mutual_info: mi mutual_info_classif(X, y, random_state42) eps 1e-8 heu (mi - mi.min()) / (mi.max() - mi.min() eps) # 压到 [0.1, 1]避免出现全 0 或全 1 的极端分布 heu 0.1 0.9 * heu return heu这段代码里init_tau 是全特征共享的初始信息素0.5 的取值让第一轮每个特征都有差不多的被选概率后续靠沉积和蒸发拉开差距。init_heuristic 里我把互信息做了 min-max 归一化再平移到 0.1~1这一步不能省如果某个数据集的互信息全部为 0连续特征分箱不合理时容易发生不处理的话启发式 η 全为 0整个转移概率就退化成纯信息素驱动算法等于少了引导。至于为什么用互信息而不是皮尔逊相关系数放到 2.3 讲。2.2 信息素、启发式信息与转移概率蚂蚁怎么决定选不选特征经典 ACO 的转移概率写成 p_ij τ_ij^α * η_ij^β 除以所有候选边的总和。BACO 里没有边常见做法是给每个特征算一个「选择强度」s_i τ_i^α * η_i^β再把它映射成概率。映射方式有两种一种是把所有特征的 s_i 做 softmax然后按这个分布抽取固定数量的特征另一种是每个特征独立按 sigmoid(s_i) 决定选或不选。我一般用第二种原因很实际——你并不知道最后该留几个特征特征子集大小应该由算法在搜索里自己定而不是预先写死。def construct_solution(tau, heu, alpha1.0, beta1.0, rngNone): # 选择强度信息素和启发式按指数加权 strength np.power(tau, alpha) * np.power(heu, beta) # 用 sigmoid 把强度压成概率中心取均值附近 prob 1.0 / (1.0 np.exp(-(strength - strength.mean()))) # 逐维独立掷骰子得到二进制特征掩码 mask (rng.random(tau.shape[0]) prob).astype(int) return maskalpha 是信息素权重beta 是启发式权重这两个参数决定了蚂蚁是「更相信前代经验」还是「更相信先验相关」。sigmoid 里的 strength - strength.mean() 是让概率中心自适应到当前信息素平均水平避免维度增多时概率整体向 1 偏移。逐维独立采样的代价是每个解的特征数不固定但这在特征选择里反而是优点——它天然覆盖了不同规模的子集而固定特征数的方法还得额外回答「到底选几个」这个问题。2.3 三个值得落地的改进自适应蒸发、精英更新、互信息启发原始 BACO 直接用会暴露三个毛病蒸发率固定前期探索不充分后期又收不拢所有蚂蚁都往信息素里沉积全局最优被平庸解稀释启发式信息如果照搬连续优化的反距离跟特征选择问题根本不匹配。改进就围绕这三条来。第一是自适应蒸发率。迭代前期把 ρ 调大让信息素快速挥发蚂蚁不容易扎堆后期把 ρ 调小让好特征的信息素能够存住。实现上我用线性退火ρ(t) ρ_start (ρ_end - ρ_start) * t / n_iter从 0.3 退到 0.05。第二是精英更新。每轮迭代只允许本代最优蚂蚁或者全局最优蚂蚁额外加一份沉积信息素其他蚂蚁的路径不参与沉积。原因很简单差解沉积得越多信息素越「和稀泥」好特征的信号反而被淹没。第三是启发式信息换成互信息。特征选择里「启发」指的是特征与标签的相关程度互信息能捕捉非线性关系比皮尔逊相关系数更适合表格数据而且 sklearn 一行就能算出来就是 2.1 里那个 init_heuristic。def update_pheromone(tau, best_mask, deposit, rho0.2, Q1.0, low0.01, high10.0): # 先蒸发所有槽位按比例衰减 tau (1.0 - rho) * tau # 精英沉积只有最优解对应的特征享受信息素增量 tau Q * deposit * best_mask # 限幅防止某几个特征的信息素无限累积把其余特征全部压死 tau np.clip(tau, low, high) return taudeposit 是沉积强度计算方式是把适应度换算成「越好沉积越多」具体在第三章的 evaluate 里做。Q 是沉积总量系数控制每一步信息素变化的幅度。低限 low 设成 0.01 是为了让未被选中的特征保留一点点信息素避免算法完全失去重新启用它们的可能高限 high 设成 10.0 是防止个别特征一家独大。这两个限幅值是我踩过坑以后加上的不加的话跑几十轮之后信息素经常膨胀到上百算法基本报废。3. 用 Python 实现改进 BACO 特征选择从评估函数到最小可复现工程先把环境准备好numpy、scikit-learn、matplotlib 三个库就够安装 sklearn 库用 pip install scikit-learn 就行Python 版本 3.8 以上都没问题。下面代码不依赖额外文件数据全部从 sklearn 自带数据集加载方便你直接复制到 Jupyter 或脚本里跑。3.1 评估函数交叉验证错误率加特征数惩罚改进 BACO 跑出来的每个二进制掩码都要打分这个分数叫适应度。我不用单次训练集准确率打分因为那样过拟合太随意交叉验证更稳。适应度定义为fitness (1 - acc_cv) λ * (n_selected / n_features)前半段是交叉验证错误率后半段是特征数惩罚。λ 越大越倾向删特征λ 越小越优先保精度。用 sklearn 的 cross_val_score 实现如下。from sklearn.model_selection import cross_val_score from sklearn.neighbors import KNeighborsClassifier def evaluate(mask, X, y, clfNone, cv5, lam0.05): if mask.sum() 0: return 1.0 # 空集直接判最差避免退化到什么都不选 selected mask.astype(bool) if clf is None: clf KNeighborsClassifier(n_neighbors5) scores cross_val_score(clf, X[:, selected], y, cvcv, n_jobs-1) acc scores.mean() ratio selected.sum() / X.shape[1] fitness (1.0 - acc) lam * ratio return fitness这个函数有两个地方值得说。一是空集判断mask 全 0 时直接返回 1.0因为空特征子集在 scikit-learn 里没法做交叉验证而且从业务上讲也不该允许——一个特征都不选意味着模型只用先验类别通常不是我们要的结果。二是 n_jobs-1这是 KNN 这类慢评估器的关键尤其是特征维度超过 50 时没有并行基本跑不动。KNN 选 k5 是分类任务最常见起步值如果你的类别很不均衡建议换分层交叉验证并把 k 调小。3.2 主循环蚂蚁游走、信息素更新、全局最优记录主循环把 2.1、2.2、2.3 的零件拼起来每一轮先按当前信息素和启发式生成 n_ants 个解逐个评估选出本代最优然后跟历史全局最优比较更新全局最优最后用本代最优解做精英信息素更新。自适应蒸发率直接在循环里算。def baco_feature_selection(X, y, n_ants30, n_iter60, alpha1.0, beta1.0, rho_start0.3, rho_end0.05, Q1.0, lam0.05, seed42): rng np.random.default_rng(seed) d X.shape[1] tau init_pheromone(d, init_tau0.5) heu init_heuristic(X, y) best_mask None best_fitness np.inf history [] for t in range(n_iter): # 自适应蒸发前期挥发快后期挥发慢 rho rho_start (rho_end - rho_start) * (t / n_iter) iter_best_f np.inf iter_best_m None for _ in range(n_ants): mask construct_solution(tau, heu, alpha, beta, rng) f evaluate(mask, X, y, lamlam) if f iter_best_f: iter_best_f, iter_best_m f, mask if iter_best_f best_fitness: best_fitness, best_mask iter_best_f, iter_best_m # 沉积量 1 / (本代最优适应度)适应度越低沉积越多 deposit 1.0 / (iter_best_f 1e-8) tau update_pheromone(tau, iter_best_m, deposit, rhorho, QQ) history.append(best_fitness) return best_mask, best_fitness, history, tau主循环的参数全部收在函数签名里方便后面做网格扫描。构造解用的随机数来自同一个 rng这是复现的关键——如果 construct_solution 内部自己调 np.random固定 seed 也没用。evaluate 里交叉验证也建议固定 cv 对象我在 5.5 还会专门说这个问题。历史记录里存的是每轮结束后的全局最优适应度画出来就是一条单调不增的收敛曲线这是判断算法有没有在优化的第一手证据。提示从第 3 章开始复制代码的话记得把第 2 章的 init_pheromone、init_heuristic、construct_solution、update_pheromone 一起复制进来主循环依赖它们。3.3 在 sklearn 自带数据集上跑通加载、标准化与收敛曲线下面用 breast_cancer 数据集做演示30 个特征二分类规模非常适合验证 BACO。KNN 对量纲敏感所以我的固定动作是先进 StandardScalerwine 数据集同理13 个特征适合后面做暴力搜索对比。from sklearn.datasets import load_breast_cancer, load_wine from sklearn.preprocessing import StandardScaler X_bc, y_bc load_breast_cancer(return_X_yTrue) X_wine, y_wine load_wine(return_X_yTrue) # KNN 基于距离特征选择前先标准化是惯用做法 sc StandardScaler() X_bc sc.fit_transform(X_bc) X_wine sc.fit_transform(X_wine) mask, fitness, history, tau baco_feature_selection(X_bc, y_bc) print(selected:, mask.sum(), /, X_bc.shape[1], fitness:, round(fitness, 4))在 breast_cancer 上用基线参数跑 60 轮、30 只蚂蚁绝大多数运行能选出 10 到 16 个特征交叉验证准确率跟全特征 30 维基本持平甚至更高。这个结论不保准因为每次运行的解可能不一样但方向是稳定的30 维特征里相当大的比例是冗余的。如果想看过程把 history 用 matplotlib 画出来这是最常用的 Python 数据分析与可视化动作之一。import matplotlib.pyplot as plt plt.plot(history, linewidth2) plt.xlabel(iteration) plt.ylabel(best fitness) plt.title(BACO convergence on breast_cancer) plt.grid(True) plt.show()收敛曲线如果是一条快速下降后完全水平的长尾说明算法还有潜力可以加大 n_iter 或扰动如果曲线抖动剧烈说明信息素限幅或蒸发率设置有问题回到第五章的避坑清单查。4. 参数怎么设五个必调参数与一组基线建议特征选择这类元启发式算法的参数设置多少带点玄学但也不至于完全靠运气。我的经验是先明白每个参数控制什么再按固定顺序调否则四个参数一起动出了问题根本不知道是谁的锅。4.1 α、β、ρ信息素、启发式与蒸发率的量级alpha 管「跟随前辈」beta 管「相信先验」rho 管「记性多久」。在 BACO 特征选择里启发式 η 用的是互信息所以 alpha/beta 的对比实际上就是「前代蚂蚁找到的好特征」和「单特征与标签的互信息」在拔河。alpha 小于 0.5 时蚂蚁几乎无视信息素结果接近随机搜索收敛很慢alpha 大于 2 时信息素强的一两轮就锁死了少数特征早熟之后很难翻盘。beta 同理但它有个特殊风险互信息只反映单特征和标签的关系不反映特征之间的冗余。beta 调太大互信息高的特征几乎必选选出来的子集可能偏大而且包含大量重复信息。所以 alpha 和 beta 的合理范围都在 0.5 到 2.0 之间基线取 1.0 通常不会出大错。rho 是蒸发率。固定 rho 太小信息素越积越厚算法反应迟钝固定 rho 太大蚂蚁记性太差跟随机搜索差不多。所以我在改进里用自适应蒸发让 rho 在前 30 轮从 0.3 退到 0.05既保证前期探索又不丢后期收敛。4.2 n_ants、n_iter、λ迭代预算与特征惩罚蚂蚁数 n_ants 和迭代数 n_iter 决定算力预算二者乘积就是评估总次数。不要贪多30 只蚂蚁乘以 60 轮等于 1800 次交叉验证评估每次评估还要跑 5 折 KNN算力已经不小。n_ants 主要管每一轮的「样本多样性」迭代数管「能优化多久」。连续 10 轮全局最优都不变时继续空转很浪费加个早停是性价比最高的改动。lambda 是最值得花时间调的参数因为它直接决定特征子集的规模倾向。我一般用一个小网格来定 lambda 的数量级先算出全特征的交叉验证准确率 acc_full再扫 lambda 在 0.01 到 0.2 之间的几个档位挑一个「准确率不低于 acc_full 减 1 个百分点且特征数最少」的档位。下面这段代码就是把早停和完整的单轮逻辑合在一起。def baco_feature_selection_with_early_stop(X, y, patience10, n_iter60, n_ants30, alpha1.0, beta1.0, rho_start0.3, rho_end0.05, Q1.0, lam0.05, seed42): rng np.random.default_rng(seed) d X.shape[1] tau init_pheromone(d) heu init_heuristic(X, y) best_fitness np.inf best_mask None stall 0 for t in range(n_iter): rho rho_start (rho_end - rho_start) * (t / n_iter) iter_best_f np.inf iter_best_m None for _ in range(n_ants): mask construct_solution(tau, heu, alpha, beta, rng) f evaluate(mask, X, y, lamlam) if f iter_best_f: iter_best_f, iter_best_m f, mask if iter_best_f best_fitness: best_fitness, best_mask iter_best_f, iter_best_m stall 0 else: stall 1 tau update_pheromone(tau, iter_best_m, 1.0 / (iter_best_f 1e-8), rhorho, QQ) # 连续 patience 轮无改善且已经跑过前 10 轮提前终止 if stall patience and t 10: break return best_mask, best_fitness, t这个版本和 3.2 的主循环差别只有三行更新最优后把 stall 清零或累加末尾判断提前退出。注意早停的 patience 不能设太小否则前期探索阶段很容易误判为「已经收敛」而提前停掉。建议 patience 大于 8并且至少让前 10 轮跑完再允许触发早停。4.3 一组可以起步的基线参数与调参顺序下面这组参数是我在多个表格分类数据集上试出来的稳妥起点特征数 20 到 200 之间都可以先拿它试跑。参数基线值建议搜索范围影响alpha1.00.5 ~ 2.0信息素权重越大收敛越快也越易早熟beta1.00.5 ~ 2.0启发式权重越大子集越偏大rho_start0.30.2 ~ 0.5前期蒸发率影响探索强度rho_end0.050.01 ~ 0.1后期蒸发率影响收敛精度n_ants3020 ~ 50每代解的多样性n_iter6040 ~ 100迭代预算配合早停使用Q1.00.5 ~ 2.0信息素沉积总量系数lam0.050.01 ~ 0.2特征数惩罚决定子集规模调参顺序我固定为三步。第一步固定 alpha1、beta1、rho 自适应只扫 lambda因为 lambda 对结果影响最大、最容易解释第二步固定 lambda扫 rho_start 和 rho_end看收敛曲线是太急还是太慢第三步才动 alpha 和 beta。这个顺序能避免参数互相干扰也方便你每一步都留下记录——我把每次运行的参数和结果存成一个 JSON翻车的时候这就是后悔药。5. 避坑指南改进 BACO 特征选择里五个高频翻车现场蚁群优化这类算法多少像个黑匣子但它不是不能排查。下面五条是我反复踩过的坑每个都按现象、原因、解决三步写照着排查比对着参数瞎试快得多。5.1 信息素爆炸五次运行出五个答案现象同一份数据同一组参数跑五次选出五个差异很大的特征子集把 tau 打印出来发现某些维度的值涨到了几十上百其他维度又被压到接近 0。原因沉积量和蒸发量失衡。适应度越接近 01 / (fitness eps) 就越大如果 Q 再取大一点单轮沉积量可以比蒸发量大一个数量级几轮下来信息素就爆了。解决给信息素加限幅并且把蒸发率设成自适应。min/max 限幅在 update_pheromone 里已经体现low0.01、high10.0 这个区间足够宽不会限制正常搜索只是禁止病态累积。这条是我最早的血泪经验加了 np.clip 之后信息素曲线立刻稳定下来。5.2 早熟停滞迭代没到一半解就不动了现象收敛曲线上 20 轮左右就完全水平最佳特征子集在剩下的 40 轮里一动不动而且明显比另一个随机种子跑出来的结果差。原因大概率是 rho_start 太小或者 alpha 太大。前期蒸发率不够信息素很快集中到少数特征上所有蚂蚁都围绕同一个局部最优打转alpha 再大一点转移概率对信息素差的特征几乎为 0算法就彻底锁死。解决把 rho_start 调回 0.3alpha 拉回 1.0 重新试同时可以加一个扰动每轮对全局最优解随机翻转 1 到 2 个特征重新评估如果更好就替换全局最优。扰动代码很简单。def local_perturb(mask, rng, flip2): perturbed mask.copy() idx rng.choice(mask.shape[0], sizeflip, replaceFalse) perturbed[idx] 1 - perturbed[idx] # 翻转选/不选状态 return perturbed这个扰动在早期几乎没用但在后期信息素固化以后是少数能把算法拉出局部最优的手段。我把它接在信息素更新之后每轮拿 best_mask 做一次扰动评估结果更好就直接覆盖全局最优。5.3 互信息启发式失效启发值全零没法用现象init_heuristic 算出来的 heu 全是 0或者集中到某一个特征上beta 调得越大跑出来的子集越奇怪。原因mutual_info_classif 对连续特征依赖估计器内部的分箱策略样本量小或者特征量纲差异极大时估计值会退化。比如某个特征取值范围是 1e-4 到 1e4分箱后几乎每个箱都只有一两个样本互信息自然失真。解决特征选择之前先做标准化这是最便宜的一步如果标准化后 heu 仍然异常就手动分箱或者换启发式——把互信息换成随机森林的特征重要性也是一种常见做法。from sklearn.ensemble import RandomForestClassifier def init_heuristic_rf(X, y): rf RandomForestClassifier(n_estimators100, random_state42) rf.fit(X, y) imp rf.feature_importances_ eps 1e-8 heu (imp - imp.min()) / (imp.max() - imp.min() eps) return 0.1 0.9 * heu随机森林特征重要性本身就是单特征加交互的混合信号在特征数几百维时计算量也还能接受。它和互信息可以各跑一轮对比哪个启发式在你的数据上效果好就用哪个。5.4 算力爆炸200 维数据跑了几小时现象特征 200 个蚂蚁 50 只、迭代 100 轮跑了一个小时才走完一半。如果用 KNN 当评估器每只蚂蚁都要重新算一次全量距离矩阵复杂度直接乘上 n_ants 乘 n_iter。原因KNN 本身在样本量大时就不便宜BACO 的评估次数又是蚂蚁数乘迭代数的量级两个放大系数叠在一起算力自然爆炸。解决三层手段。第一内层评估降采样不必用全部样本跑交叉验证抽 1500 到 2000 行足以区分候选子集的优劣最后再用全量数据验证最优解第二cv 从 5 降到 3评估速度接近减半第三加评估缓存同一个 mask 只算一次装进字典里层循环遇到重复解直接查表。cache {} def evaluate_cached(mask, X, y, lam0.05): key mask.tobytes() if key not in cache: cache[key] evaluate(mask, X, y, lamlam) return cache[key]这个缓存在迭代后期尤其有用因为蚂蚁解在收敛后高度雷同cache 命中率能到一半以上。注意 mask 是 numpy 数组字典 key 要用 mask.tobytes() 转成字节串直接拿数组当 key 会报 unhashable type。5.5 结果不可复现同一个 seed 两次结果不同现象seed 固定成 42两次运行选出的特征和适应度还是不一样跟别人对答案对不上。原因固定了 numpy 随机源不等于固定整个随机链。cross_val_score 默认的 KFold 不做 shuffle数据顺序一变结果就变此外 mutual_info_classif 内部也用自己的随机源它的 random_state 没传进去。解决把所有随机环节显式固定。交叉验证传入固定的 cv 对象互信息初始化时传 random_state再加上 numpy 随机源统一由 rng 管理。下面这段是推荐的固定方式。from sklearn.model_selection import StratifiedKFold cv_fixed StratifiedKFold(n_splits5, shuffleTrue, random_state42) def evaluate(mask, X, y, cvcv_fixed, lam0.05): if mask.sum() 0: return 1.0 selected mask.astype(bool) clf KNeighborsClassifier(n_neighbors5) scores cross_val_score(clf, X[:, selected], y, cvcv, n_jobs-1) acc scores.mean() ratio selected.sum() / X.shape[1] return (1.0 - acc) lam * ratio把 cv 对象当参数传进 evaluate比在 cross_val_score 里传 cv5 稳得多。这条同样适用于后面做网格搜索——评估标准不一致调参调出来的结果全是噪声。6. 把改进 BACO 用到自己的数据集验证流程与三个进阶技巧6.1 先跟暴力搜索对答案wine 数据集的 8192 种组合wine 数据集只有 13 个特征2 的 13 次方是 8192 种组合KNN 做 5 折交叉验证全量枚举大概一分钟就能跑完。我把这个当基准来验证改进 BACO 有没有真的起作用跑一轮算法看它的最优解是不是落在全局最优附近的组合上。我第一次跑的时候发现精英更新比所有蚂蚁都沉积的效果好不少就是在 wine 上对出来的。这种小数据集是你调试算法逻辑和参数的第一现场不要一上来就直接上几百维的数据。6.2 用 Pipeline 把标准化、特征选择、分类器串起来放进工程环境时我建议把标准化、特征选择和分类器包成一个 Pipeline这样预测和调参都不会漏掉某个预处理步骤。BACO 的特征选择结果不依赖训练集之外的信息所以可以放心把它当作 transformer 塞进 pipeline。from sklearn.pipeline import Pipeline class BacoSelector: def __init__(self, n_ants30, n_iter60, lam0.05, seed42): self.mask None self.params dict(n_antsn_ants, n_itern_iter, lamlam, seedseed) def fit(self, X, y): self.mask, _, _, _ baco_feature_selection( X, y, n_antsself.params[n_ants], n_iterself.params[n_iter], lamself.params[lam], seedself.params[seed]) return self def transform(self, X): return X[:, self.mask.astype(bool)] def fit_transform(self, X, y): return self.fit(X, y).transform(X) pipe_baco Pipeline([ (scale, StandardScaler()), (baco, BacoSelector(lam0.05, seed42)), (clf, KNeighborsClassifier(n_neighbors5)) ])这个 selector 只包了核心流程工程里要用的话再加 get_params 和 set_params否则 GridSearchCV 没法对它的参数做网格搜索。Pipeline 的好处是网格搜索时标准化和特征选择都在同一个交叉验证划分内完成不会把验证集的信息漏进特征选择。6.3 三个继续优化的方向局部搜索、多目标、按秩分配信息素如果基线版本已经跑通想继续压效果有三条路性价比比较高。一是局部搜索在全局最优解的邻居里做小幅翻转相当于给带噪的信息素更新加一层精修。二是多目标视角把「精度」和「特征数」当成两个目标维护一组非支配解而不是把特征数折进一个惩罚项里这对特征数特别敏感的业务场景更实用。三是按秩分配信息素把沉积量从「绝对适应度」改成「排名」这样不同数据集的 fitness 尺度差异就不再影响收敛代码只需要改 update_pheromone 里的 deposit 计算。我现在拿到一个新数据集的固定动作是先固定 seed 和基线参数跑一遍把收敛曲线和最优 mask 存下来再开始调参。很多时候第一步的结果就够用调参只是锦上添花比调参更重要的是先留下可复现的基线不然改了三五个参数之后你根本不知道是哪个改动让结果变好的。希望帮到你。本文还有配套的精品资源点击获取