ARTICLE DETAIL

资讯详情

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

动态加权条件互信息特征选择算法WMRI:原理与Python实现

动态加权条件互信息特征选择算法WMRI:原理与Python实现 简介针对高维数据中无关与冗余特征带来的建模难题这份文档系统阐述了一种新的过滤式特征选择算法——动态加权条件互信息的特征选择算法WMRI。内容从信息论框架入手梳理了传统过滤式方法如MRMR、JMI等的局限重点推导WMRI如何利用均值和标准差动态调节“新分类信息”与“保留类别信息”之间的权重从而避免固定参数对无关特征和冗余特征的忽略或误判。全文结构清晰适合机器学习、数据挖掘方向的研究者、学生以及从事高维数据分析的工程师用于算法理解与复现参考。资源为单个Word文档共1个docx文件压缩包大小454KB携带方便、便于批注阅读。文档包含算法定义、候选特征评估公式、完整伪代码以及对10个基准数据集的实验对比结论可帮助读者掌握条件互信息、特征冗余度量等核心知识点并借鉴其实验设计与结果分析方法。该资源已有80人浏览学习对关注特征选择前沿方法的人来说是一份紧凑且实用的参考资料。1. 动态加权条件互信息为什么固定权重的特征选择会翻车特征选择做到后期很多人会卡在同一个问题上MRMR、JMI、CMIM 这些经典算法表现都还行但 β、λ 一旦设死换数据集就翻车。动态加权条件互信息的特征选择算法 WMRI 把固定参数改成用均值和标准差动态计算让新分类信息和保留类别信息的重要程度随数据分布自动调整这是它和同系列算法最本质的区别。这份资源是一篇完整论文包含 WMRI 的推导、可照抄的伪代码、10 个基准数据集的实验设置以及与 IG-RFE、CFR、JMIM、DCSF、MRI 五个基线的 fmi 对比。适合正在做高维特征选择、想在不引入额外分类器的情况下压缩特征空间的工程师和研究生。下文先拆 Brown 框架和 MRI 的局限再给出可运行的 Python 实现最后列五个复现时踩过的坑。2. Brown 框架到 WMRI两个动态权重是怎么算出来的2.1 Brown 统一框架MRMR、JMI、CMIM 只是参数不同Brown 等人提出的信息论特征选择框架把一大类过滤式算法统一成了一个式子J(fk) I(fk; C) − β · Σ I(fk; fsel) λ · Σ I(fk; fsel | C)这里的 S 是已选特征集合fk 是候选特征C 是类标签。第一项 I(fk; C) 衡量候选特征和标签的直接相关性第二项减去的是候选特征和已选特征之间的冗余第三项加上的是在已选特征条件下候选特征和标签的条件互信息也就是已选特征没能解释、fk 还能补充的那部分分类信息。之所以说选不同参数就是选不同算法是因为 β 和 λ 取不同值会退化成不同的经典方法。β0 且 λ0 时是最朴素的 max-Relevance 贪心MRMR 是设定 β 为 1/|S| 量级、λ 为 0JMI 和 CMIM 则把 λ 调成和 β 同一个量级让条件互信息项参与竞争。具体数值因实现略有差异但它们在统一框架里的参数位置是固定的。实现层面这三个算法共用同一个计算骨架区别只在 β、λ 的取值所以调参的玄学就在这两个系数上。2.2 MRI 的局限两个信息项被当成同等重要MRI 算法在 Brown 框架上做了一步改写把目标函数整理成J_MRI(fk) I(C; fk) Σ [ I(C; fk | fsel) I(C; fsel | fk) ] ∝ I(fk; C) − (2 / (|S| 1)) · Σ [ I(fk; fsel) − I(fk; fsel | C) ]后者的等价形式意味着 β λ 2/(|S|1)也就是每个已选特征的权重随 |S| 增长而递减但两个方向的条件互信息始终等权。问题就在这I(C; fk | fsel) 表示已选特征已经告诉我的情况下fk 还能新增多少分类信息I(C; fsel | fk) 表示已知 fk 后已选特征里有多少类别信息值得保留。这两者在实际数据里几乎不可能相等——fk 如果是强特征前者会远大于后者如果 fk 是弱特征但和已选特征高度耦合后者反而占优。MRI 默认它们五五开换到噪声多的数据集上选出来的特征子集经常偏向冗余侧。2.3 WMRI 的核心改动用标准差当权重WMRI 的评估标准是J_WMRI(fk) I(C; fk) (1 − α) · Σ I(C; fk | fsel) (1 − β) · Σ I(C; fsel | fk)其中 α 是 I(C; fk | fsel) 这一组值在全部已选特征上的标准差β 是 I(C; fsel | fk) 这一组值的标准差。标准差大说明这个信息项在不同已选特征之间波动剧烈也就是它和已选特征的交互不稳定应该降权标准差小说明该项在不同已选特征上表现一致是稳定贡献应该加回去。均值在这里有两个作用一是作为归一化的参照让标准差相对均值有意义二是如果两组信息项的量纲差异大均值可以帮助识别哪一组整体偏低、需要压缩。(1−α) 这个形式要留意当标准差 α 大于 1 时权重会变成负数。论文实验里的数据特征经过离散化和分箱互信息值大多落在 0 到 1 之间所以 α 超 1 的情况不常出现但自己复现时如果遇到负权重我会先把互信息做 min-max 归一化而不是直接改公式。2.4 伪代码逐行拆解前向贪心选择的三阶段论文表 1 的伪代码可以分成三个阶段。第一阶段是初始化S 置空计算每个特征和标签的互信息 I(C; fk)把最大值对应的特征作为第一个入选特征从 F 移到 S。第二阶段是循环迭代对每个候选 fk分别计算它和每一个已选特征的 I(C; fk | fsel) 与 I(C; fsel | fk)得到两组长度等于 |S| 的数组再按式(4)-(7) 求各自的均值 μ 和标准差 α/β。第三阶段用式(3) 更新评估值选出最大 J 对应的特征加入 S直到达到阈值 K。用 Python 写这个循环的骨架时我一般这样组织S [] F list(range(X.shape[1])) mi mutual_info_classif(X, y, random_state42) first int(np.argmax(mi)) S.append(first); F.remove(first) while len(S) K: best_j, best_f -np.inf, None for fk in F: cmi_new, cmi_keep [], [] for fsel in S: cmi_new.append(conditional_mutual_info(X[:, fk], y, X[:, fsel])) cmi_keep.append(conditional_mutual_info(X[:, fsel], y, X[:, fk])) alpha np.std(cmi_new); beta np.std(cmi_keep) j mi[fk] (1 - alpha) * np.sum(cmi_new) (1 - beta) * np.sum(cmi_keep) if j best_j: best_j, best_f j, fk S.append(best_f); F.remove(best_f)这段代码里 mi 数组提前算好每次循环直接取 mi[fk]避免重复调用互信息估计器。cmi_new 是「已知已选特征后fk 新增的分类信息」cmi_keep 是「已知 fk 后已选特征保留的分类信息」两组分别求标准差后各自加权这和式(3) 完全对应。论文说时间复杂度是 O(Kmn)和 MRI、CFR 是一个量级但多了均值和标准差的计算实际跑下来在特征数 600 的数据上会明显慢一截优化手段放在第 5 章讲。3. 复现 WMRIPython 实现、参数设置与 sklearn 管线接入3.1 互信息和条件互信息的估计先决定离散化还是连续估计复现 WMRI 第一件要决定的事是怎么算 I(C; fk) 和条件互信息。论文实验数据来自 UCI 和 ASU混合了连续特征和离散特征但论文没有详细说明预处理阶段是否做了分箱。常见的做法有两种一是把连续特征等宽分箱后用联立直方图估计互信息二是直接用 sklearn 的 mutual_info_classif它内部用 k 近邻估计连续变量的互信息对连续特征更友好。我一般建议主流程用 mutual_info_classif 算基础互信息因为它在 sklearn 1.x 里已经调得比较稳不需要手动分箱条件互信息由于 sklearn 没现成接口只能自己实现。这里给一个最小实现用等宽分箱 条件概率加权import numpy as np from sklearn.metrics import mutual_info_score def _digitize(x, bins10): edges np.histogram_bin_edges(x, binsbins) return np.clip(np.digitize(x, edges[:-1]), 0, bins - 1) def conditional_mutual_info(x, y, z, bins10): if np.ptp(x) 0 or np.ptp(y) 0 or np.ptp(z) 0: return 0.0 xd _digitize(x, bins) yd _digitize(y, bins) zd _digitize(z, bins) cmi 0.0 for z_val in np.unique(zd): mask zd z_val pz mask.mean() if pz 0: continue cmi pz * mutual_info_score(xd[mask], yd[mask]) return max(cmi, 0.0)逻辑说明I(X; Y | Z) 的定义就是按 Z 取值的条件加权求和所以这里对每个 z_val 取出对应样本子集在子集上算 X 和 Y 的互信息再乘上 p(Zz_val) 累加。开头对零方差特征的判断是防御性写法常量特征算不出分箱边界直接返回 0 比报错更符合特征选择场景。bins 控制离散化粒度论文里特征维度从几十到上千都有bin 数取 10 到 20 之间比较安全bins 太小会把连续特征压成几个粗糙档位丢失信息bins 太大则每个格子里样本太少估计方差变大。末尾的 max(cmi, 0.0) 是我个人加的约束直方图估计在小样本上很容易出现轻微的负偏差论文公式不允许负权重参与后续计算。3.2 WMRI 的完整实现类封装与前向搜索把 2.4 的骨架补全封装成一个 sklearn 风格的转换器import numpy as np from sklearn.feature_selection import mutual_info_classif class WMRI: def __init__(self, n_features30, bins10, epsilon1e-12): self.n_features n_features self.bins bins self.epsilon epsilon self.selected_ [] self.mi_ None def fit(self, X, y): n, d X.shape self.mi_ mutual_info_classif(X, y, random_state42) first int(np.argmax(self.mi_)) self.selected_ [first] remaining [i for i in range(d) if i ! first] while len(self.selected_) min(self.n_features, d): best_j, best_f -np.inf, None for fk in remaining: cmi_new, cmi_keep [], [] for fsel in self.selected_: cmi_new.append(conditional_mutual_info( X[:, fk], y, X[:, fsel], self.bins)) cmi_keep.append(conditional_mutual_info( X[:, fsel], y, X[:, fk], self.bins)) cmi_new np.asarray(cmi_new) self.epsilon cmi_keep np.asarray(cmi_keep) self.epsilon alpha np.std(cmi_new) beta np.std(cmi_keep) j (self.mi_[fk] (1 - alpha) * cmi_new.sum() (1 - beta) * cmi_keep.sum()) if j best_j: best_j, best_f j, fk self.selected_.append(best_f) remaining.remove(best_f) return self def transform(self, X): return X[:, self.selected_]说明几个设计点。epsilon 加在两组条件互信息数组上既避免标准差为 0 时权重失效也避免 alpha 恰好等于 1 时该项被完全抹掉这是一个可以调的稳定性参数。mi_ 在 fit 里一次性算完循环里直接索引能省掉大量重复计算。mutual_info_classif 对每个特征返回标量互信息所以 mi_[fk] 就是式(3) 里的 I(C; fk)。n_features 是停止条件对应论文里预先指定的特征子集规模 K论文实验取 30但对小数据集我建议设成 min(30, d)否则 while 循环会在特征选完前越界。3.3 阈值 K 怎么定从论文的 30 到实际项目的经验值论文统一把特征子集规模设成 30这是为了在 10 个数据集上做横向对比保证可比性。实际项目中我一般先用 30 跑一遍记录 fmi 随特征数的变化曲线看拐点在哪。特征数在 100 以下的低维数据K 取总特征数的 20%30% 起步特征数 1000 以上的高维数据K 取 30100 观察曲线。如果分类器是随机森林这种对冗余不敏感的方法K 可以偏小因为 RF 自带特征重采样如果是 KNN 或线性模型冗余特征对距离度量的干扰更大K 要偏保守。3.4 接进 sklearn 管线注意 fit 和 transform 的两次调用WMRI 和 sklearn 的 SelectKBest 用法一致可以包进 Pipelinefrom sklearn.pipeline import Pipeline from sklearn.neighbors import KNeighborsClassifier pipe Pipeline([ (wmri, WMRI(n_features30, bins15)), (clf, KNeighborsClassifier(n_neighbors5)), ]) scores cross_val_score(pipe, X, y, cv6, scoringf1_macro)Pipeline 会自动在每一折的训练数据上调 WMRI.fit再对验证数据调 transform。这里要强调一个容易翻车的点特征选择必须在交叉验证循环内部执行不能在全集上先筛完特征再交叉验证否则会数据泄漏。WMRI 的 fit 只用训练折的统计量transform 只做列切片不引入测试折信息这是它能安全放进 Pipeline 的前提。4. 实验复现10 个数据集上的评价指标与结果解读4.1 数据集清单与预处理论文用了 10 个公开数据集覆盖手写数字、文本、语音、图像和生物数据。下表是我整理的复现清单数据集特征数样本数类别数来源musk21664762UCImadelon50026002ASUALLAML7129722ASUCNAE-985710809UCImfeat-kar64200010UCIUSPS256929810ASUsemeion256159310ASUCOIL201024144020ASUwpbc341982UCIIsolet617156026ASU预处理环节论文只说了 6 折交叉验证没有细说标准化。我的做法是连续特征做 z-score 标准化再分箱类别特征直接编码成 0 到 C−1 的整数。要注意 ALLAML 只有 72 个样本、7129 个特征属于典型的 pn 数据6 折交叉验证时每折训练集只有 60 个样本条件互信息的直方图估计会非常稀疏这种数据上最好把 bins 降到 8 以下或者先用方差过滤砍掉大部分零方差特征再跑 WMRI。4.2 评价指标sen、prc 和 fmi 的计算论文用宏平均的 sen、prc 和 fmi 三个指标其中 fmi 是 sen 和 prc 的调和平均对应机器学习里常见的 macro-F1sen (1/|C|) · Σ TP_i / (TP_i FN_i) prc (1/|C|) · Σ TP_i / (TP_i FP_i) fmi 2 · prc · sen / (prc sen)复现时直接用 sklearn 的 f1_score 就能拿到宏平均 F1不需要手写混淆矩阵。注意 sklearn 的 macro-F1 默认对每个类分别算 F1 再平均和论文的先宏平均 sen/prc 再算调和平均在类别不均衡时有细微差异为了对齐论文结果建议用 averageNone 取出每类的 precision/recall 手动算。from sklearn.metrics import precision_recall_fscore_support def paper_fmi(y_true, y_pred): prec, rec, _, _ precision_recall_fscore_support( y_true, y_pred, averageNone) prec_macro prec.mean() rec_macro rec.mean() return 2 * prec_macro * rec_macro / (prec_macro rec_macro 1e-12)这个函数在类别均衡的数据集上结果和 macro-F1 基本一致但在 wpbc34 特征、198 样本、2 类但不均衡这种数据上会略有差别。复现对比实验时统一用这个函数避免算法没实现对齐先输在评估口径上。4.3 W/T/L 统计论文结果里那些加减号怎么读论文表 3-5 的每一行里WMRI 和其他算法的差值后面标了 、、−分别表示 WMRI 高于、等于、低于该基线。W/T/L 行是汇总胜平负次数。我自己复现时会写一小段统计代码def wtl(wmri_scores, base_scores, tol1e-6): datasets wmri_scores.keys() w sum(1 for d in datasets if wmri_scores[d] - base_scores[d] tol) t sum(1 for d in datasets if abs(wmri_scores[d] - base_scores[d]) tol) l len(datasets) - w - t return w, t, ltol 取 1e-6 是为了避免浮点比较的误判。论文里的等于比较少见只在 CNAE-9 和 mfeat-kar 这类数据集上出现因为这两个数据集上某些算法收敛到了相同的特征子集差值在数值精度内为 0。4.4 高维数据集上的性能曲线为什么选 3 个特定规模点画图论文的图 1-3 展示了部分高维数据集在不同特征子集规模下的 fmi 变化用来证明动态加权在高维时更有效。复现时不需要画 10 条曲线选特征数最多的 3 个数据集madelon、COIL20、Isolet就够。每个数据集上取 K 5, 10, 15, 20, 25, 30 六个节点跑完画折线图。你会发现 WMRI 和 MRI 的差距在 K 增大后逐渐拉开这正是因为 |S| 增大后标准差 α/β 有了足够的样本量去估计动态权重的优势才体现出来。5. 避坑指南条件互信息估计、负值与复杂度五个坑5.1 连续特征直接算互信息结果全是零现象拿原始 CSV 跑 WMRI所有特征的基础互信息都接近 0第一个特征选完基本等于随机。原因条件互信息实现里用 np.histogram_bin_edges 分箱但连续特征如果没有先做标准化量纲差异大比如一个特征范围 0-1另一个 0-10000同一组 bin 数量下有的特征被压成 1-2 个档位信息全部丢失。解决进 WMRI 前先做 min-max 标准化或 z-score 标准化如果特征本身是稀疏的先跑一遍 VarianceThreshold 把零方差特征清掉。我在 3.1 代码里加的那行零方差防御就是为这个坑兜底。5.2 条件互信息算出负值现象cmi_new 或 cmi_keep 里出现负数导致 (1−α) 权重被撑大或压小选出的特征和直觉不符。原因直方图估计在样本量不足时是有偏的个别格子样本太少联立概率估计出现负偏差连续特征分箱后还会引入量化误差。解决在 3.1 的实现末尾加 max(cmi, 0.0) 截断同时把 bins 从 10 调小到 8 或 6 看结果是否稳定。如果截断后仍然明显异常怀疑是特征本身和已选特征完全独立条件互信息理论值确实接近 0此时截断不影响排序。5.3 标准差为零选第一个特征后的权重失效现象|S|1 时cmi_new 和 cmi_keep 各只有一个元素标准差恒为 0αβ0式(3) 退化成 I(C; fk) 两组条件互信息之和动态加权失去意义。原因这是公式本身的边界情况——单个样本点上的标准差没有统计意义。解决我一般在 |S| 3 时让 α 和 β 退化为等权也就是直接用 MRI 的公式走前两步等 S 里积累了 3 个以上特征再启用标准差加权。这是一个工程上的 hack论文没提这个边界复现时必须有。5.4 计算复杂度 O(Kmn)Isolet 上跑到怀疑人生现象特征数 617 的 IsoletK30每个候选特征要对每个已选特征算两次条件互信息单次拟合跑了 20 多分钟。原因条件互信息内部要遍历 z 的所有取值做子集切片每切一次都要重新算 mutual_info_score外层循环是 K×n整体复杂度是 O(Kmn) 且常数很大。解决三个手段叠加。第一先算 mi 排序取前 200 个特征做候选集其余直接丢弃第二conditional_mutual_info 里用 np.unique(zd) 的循环改成按组索引预切好避免在 Python 里重复建 mask第三对 K 从 10 开始调确认曲线趋势后再拉高到 30。高维数据上这个预处理和循环优化通常能把时间压到原来的 1/4。5.5 交叉验证里的数据泄漏现象在全部数据上先跑 WMRI 选出 30 个特征再做 6 折交叉验证fmi 虚高 5-8 个百分点换成真实部署数据效果立刻崩。原因特征选择在全集上看到了测试折的分布信息等价于把测试集的信息泄漏进了训练流程。解决把 WMRI 包进 Pipeline让每一折的交叉验证内部重新 fit。这条是老生常谈但在复现论文算法时特别容易犯——论文里给的是先在全集上选特征再评估子集的简写流程工程上必须把特征选择当作模型训练的一部分而不是数据预处理的一部分。6. 验证 WMRI从合成数据到 MRMR 对比的检查清单6.1 合成数据上的冒烟测试动手之前先把文档里表 2 的数据集清单和表 1 的伪代码放在手边下面所有代码都按这个口径实现。复现论文算法第一步我建议先不碰真实数据集用 make_classification 造一批特征验证 WMRI 的排序是否符合预期from sklearn.datasets import make_classification X, y make_classification( n_samples500, n_features30, n_informative8, n_redundant5, n_repeated2, n_classes3, random_state0) wmri WMRI(n_features15) wmri.fit(X, y) print(wmri.selected_)informative 特征在 make_classification 里固定排在最前面也就是索引 0-7。如果 WMRI 的前 15 个选中特征里包含了 0-7 的大部分说明动态加权没有把核心相关特征丢掉如果前几个偏偏都是 10-14 的冗余特征n_redundant 部分说明条件互信息估计有问题要回头查分箱参数。6.2 和 MRMR 的定向对比MRMR 是我最常拿来和 WMRI 对比的基线语义接近、实现简单、参数只有一个 K。手写一个只做等权去冗余的 MRMR和 WMRI 对比同样数据下的 Top-K 差异def mrmr_select(X, y, K): mi mutual_info_classif(X, y, random_state42) sel [int(np.argmax(mi))] while len(sel) K: best None; best_v -np.inf for f in range(X.shape[1]): if f in sel: continue red np.mean([conditional_mutual_info(X[:, f], X[:, s], y) for s in sel]) v mi[f] - red if v best_v: best_v, best v, f sel.append(best) return sel这个函数里 conditional_mutual_info(x, y, z) 第三个参数传的是 y算的是在标签条件下两个特征的冗余这正好对应 Brown 框架里的 I(fk; fsel | C) 项。和 WMRI 对比时重点看两类特征和标签强相关但彼此也强冗余的特征对MRMR 会尽量只留一个WMRI 则通过保留类别信息项的加权保留对已选特征有补充价值的特征。两类算法的分歧点往往就是动态权重发挥价值的地方。6.3 上线前的检查清单在真实数据集上跑之前我一般会过一遍这张表检查项通过标准连续特征标准化所有特征量纲一致稀疏特征已过滤bins 取值815验证相邻取值结果稳定小样本数据集ALLAML 这类 pn 的数据 bins 降到 8 以下S条件互信息负值已做 max(0, ·) 截断K 值曲线跑 3 个 K 值确认 fmi 趋势单调交叉验证嵌套WMRI 在 Pipeline 内无泄漏评估口径用 paper_fmi 而非默认 macro-F1时间预算每折 WMRI 拟合时间可控和 MRMR 差异合成数据上先确认排序合理这份资源里的表 2-5 可以直接当实验设计模板用伪代码对应上面的 WMRI 类数据集清单对应 4.1 的表拿到手后建议先按第 2 章的公式把式(3) 到式(7) 在草稿上演算一遍再跑代码。这张表也是我复现任何信息论特征选择算法的通用流程。最后说一个我的习惯从那以后我每次拿到新的特征选择论文都会先造一组 synthetic 数据把算法的核心方程跑通再上真实数据集而不是直接拿 UCI 数据一跑了之——因为特征选择算法的问题往往不在数学推导而在估计器和边界条件的处理上。希望帮到你。本文还有配套的精品资源点击获取
返回列表