ARTICLE DETAIL

资讯详情

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

动态加权条件互信息:高维特征选择的Python实现与调优

动态加权条件互信息:高维特征选择的Python实现与调优 简介这是一份关于动态加权条件互信息的特征选择算法的技术文档面向机器学习、数据挖掘领域的研究人员、高维数据分析学习者以及需要开展特征选择实验的开发者重点解决传统过滤式特征选择方法中参数预先设置、冗余特征误判以及分类信息权重失衡等问题。该算法WMRI基于信息论框架利用条件互信息衡量特征与类标签的相关性及特征间冗余性通过均值和标准差动态调节新分类信息与保留类别信息的权重在不显著增加计算量的前提下提升最优特征子集的整体质量。资源包包含1个docx格式文档压缩包大小454KB内容涵盖算法研究背景、相关算法对比分析如MRI、DCSF、JMIM等、完整公式推导、伪代码实现以及在10个基准数据集上的实验对比结构清晰既可作为学术论文写作的参考素材也便于读者按步骤复现算法。目前已有80人学习下载适合具备一定信息论基础、希望深入理解条件互信息机制并改进特征选择方法的读者。1. 为什么特征选择要引入“动态加权”的条件互信息高维数据里做特征选择互信息MI是入门首选但它在存在冗余特征时会给出虚高的相关性打分。条件互信息CMI把“已选特征集合”作为条件纳入了计算能剥离掉这部分干扰。可一旦条件集合变大CMI 的估计值就会系统性偏低特征排序时高维特征被持续低估选出来的子集往往不够稳定。动态加权解决的就是这个问题——权重随条件集合的规模动态变化而不是用固定系数去矫正。这套思路在基因表达数据、文本分类、工业传感数据的特征筛选中都有应用场景。适合已经在用过滤式特征选择、但发现排序结果不稳定或模型效果不升反降的工程师。读完可以落地一个可运行的 Python 版本并知道参数怎么调、坑在哪儿。2. 条件互信息的估计路径从互信息到动态权重2.1 从互信息到条件互信息的演进逻辑互信息衡量两个变量之间的共同信息量公式是 I(X;Y) H(X) H(Y) − H(X,Y)。它在特征选择里有个致命缺陷如果两个特征强相关它们各自与标签的 MI 都很高但加在一起并没有带来双倍的信息增益。条件互信息把已选特征 S 放进条件里计算 I(X;Y|S)得到的是“在已知 S 的情况下X 还能为 Y 提供多少新信息”。这正好用来做增量式特征选择——每次从候选池里挑一个能让 CMI 最大的特征加入 S。但 CMI 不是免费的午餐。当 S 的维度增长条件空间呈指数膨胀有限样本下每个条件单元格里的样本数急剧减少概率密度估计的方差随之飙升。实际表现就是候选特征的 CMI 值普遍偏低且不同特征之间的区分度变小。另一个更隐蔽的问题是不同候选特征所面对的“条件集合大小”不同——先被选进 S 的特征在计算时条件较少估值相对可靠后参与竞争的特征要面对更大的条件集合估值被压缩得更狠。这等于让先来者占了便宜排序结果偏离了真实的特征重要性。2.2 动态权重为什么能缓解高维条件集合带来的偏差动态加权的核心思路对 CMI 的值乘上一个随 |S| 变化的权重函数。常见做法是 w(|S|) |S|^γ 或 w(|S|) log(1 |S|)其中 γ 是一个可调的超参数。权重会补偿因为条件空间增大而导致的 CMI 低估让在不同阶段参与竞争的特征处于一个相对公平的比较基准上。这个设计的动机来自非参数估计的收敛速度。基于 k 近邻或核密度估计的 CMI 估计量其偏差与条件维度 d 相关典型形式是 O(n^−1/(d4)) 这样的量级。当 d 从 1 涨到 10收敛速度会急剧下降。动态权重本质上是在做方差-偏差的折中条件集合越大权重给得越高把被压缩的信号放大回来但不是无脑放大——γ 太大会让高维特征被过度补偿γ 太小又回到没有补偿的原始状态。实际选型时我一般会先用 γ 1 跑一轮观察所选特征在验证集上的表现再在 0.5 到 1.5 之间做网格搜索。需要注意权重只对参与排序的 CMI 值生效不参与特征本身的条件概率计算所以不会引入新的估计偏差。2.3 动态权重与传统加权特征选择的本质区别传统加权特征选择比如用互信息乘以一个固定系数调整的是“所有特征的整体偏好”相当于全局性地提高或压低某一类特征的分值。动态加权的本质是“按选择进度调整竞争环境”——在选择过程的前期条件集合小权重接近 1CMI 基本是原始值随着 S 变大权重逐渐升高对后参与竞争的特征给出补偿。这样做的收益在特征维度超过 50 时开始明显。维度较低时CMI 的估计方差本来就小加权与否差别不大固定权重反而好调参。正因为这个特性动态加权的实现必须先有一个可靠的 CMI 基座——基座估计都偏的话权重补偿只是在放大噪声。3. 用 Python 实现动态加权 CMI 特征选择3.1 数据集准备与基准方法对比实现上需要一个能算 CMI 的库。scikit-learn的mutual_info_classif支持conditional参数吗不支持它只算 MI。可以用f_pvalues或者chi2做基准对比但核心 CMI 计算得自己写或另找实现。常见方案是用skfeature库的cmim函数做基座或者用numpy自己写基于分箱的估计器。前者方便但只能处理离散特征后者可控能接连续特征。先用skfeature跑通流程再替换成自实现。数据集用公开的 UCI 乳腺癌数据集做演示。这个数据集 30 个特征、569 个样本维度不算高但足够看出动态加权在排序稳定性上的差异。加载后用train_test_split划分特征做标准化。注意分箱之前不要标准化因为分箱依赖原始分布标准化留到分箱之后做或者不做都行取决于分类器需求。import pandas as pd from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split data load_breast_cancer() X pd.DataFrame(data.data, columnsdata.feature_names) y pd.Series(data.target, nametarget) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) print(fTrain shape: {X_train.shape}, Test shape: {X_test.shape})逻辑说明stratifyy保证训练集和测试集的标签分布一致避免类别不平衡影响 CMI 的估计——CMI 对边缘分布非常敏感训练集和测试集分布差距大排序结果就不稳定。标准化在这个阶段不做因为分箱需要原始特征的天然阈值。3.2 核心实现kNN 熵估计 动态权重写一个基于 k 近邻的 CMI 估计器。思路不复杂CMI H(X|S) H(Y|S) − H(X,Y|S)。有了熵CMI 就是加减法。k 近邻熵估计用 Kozachenko-Leonenko 估计量公式是 H ≈ ψ(k) log(N) − ψ(N) − mean(log(2 * distance_to_kth_neighbor))其中 ψ 是 digamma 函数。这里给出一个可直接运行的实现用scipy.spatial里的cKDTree做加速。import numpy as np from scipy.special import digamma from scipy.spatial import cKDTree def _knn_entropy(X, k3): Kozachenko-Leonenko k近邻熵估计 n X.shape[0] if n k 1: return 0.0 tree cKDTree(X) dist, _ tree.query(X, kk 1) # 第一个是自身距离为0 rho dist[:, -1] # 第k近邻距离 # 避免距离为0导致log(0) rho np.maximum(rho, 1e-12) h digamma(k) np.log(n) - digamma(n) np.mean(np.log(2 * rho)) return h def conditional_mutual_information(X, y, S_idx, k3): 计算 I(X; Y | S)S_idx是已选特征的下标列表 # 把X、y和S拼成条件空间 if len(S_idx) 0: S_data X[:, S_idx] cond_data np.hstack([S_data, y.reshape(-1, 1)]) else: cond_data y.reshape(-1, 1) X_cond np.hstack([X.reshape(-1, 1), y.reshape(-1, 1)]) h_xs _knn_entropy(X_cond, k) h_ys _knn_entropy(cond_data, k) h_xys _knn_entropy(np.hstack([X.reshape(-1, 1), S_data, y.reshape(-1, 1)]), k) # CMI H(X,S) H(Y,S) - H(X,Y,S) - H(S) # 由链式法则推导I(X;Y|S) H(X|S) H(Y|S) - H(X,Y|S) h_x _knn_entropy(X.reshape(-1, 1), k) cmi h_xs h_ys - h_xys - _knn_entropy(S_data, k) if len(S_idx) 0 else h_x h_ys - h_xys return max(cmi, 0.0)逻辑说明_knn_entropy用了距离的 log 均值作为熵的估计量这是 kNN 估计的核心。conditional_mutual_information里的几个熵分量分别对应条件空间和联合空间。cKDTree.query返回距离和索引k1 取一个是因为第一个点是自身。代码里留了max(cmi, 0.0)是因为数值误差可能让 CMI 变成微小的负数。3.3 前向搜索 动态权重选择特征有了 CMI 基座接下来做前向贪心搜索并加入动态权重。def dynamic_weighted_cmi_selection(X, y, num_features, k3, gamma1.0): 动态加权条件互信息的特征选择 Parameters: - X: 特征矩阵 (n_samples, n_features) - y: 标签向量 - num_features: 要选择的特征数 - k: kNN参数 - gamma: 动态权重指数 n_samples, n_features X.shape selected [] remaining list(range(n_features)) for step in range(num_features): scores [] for idx in remaining: # 原始CMI cmi_val conditional_mutual_information(X[:, idx], y, selected, k) # 动态权重|S|^gamma weight (len(selected) 1) ** gamma scores.append((cmi_val * weight, idx)) # 选得分最高的 scores.sort(reverseTrue, keylambda t: t[0]) best_score, best_idx scores[0] selected.append(best_idx) remaining.remove(best_idx) print(fStep {step 1}: selected feature {best_idx}, fCMI{best_score:.4f}, weight{len(selected) ** gamma:.2f}) return selected参数说明gamma1.0是线性权重len(selected) 1的乘方保证了第一步权重为 1、之后逐步递增。k3是 kNN 估计的默认参数后续会说明怎么调。实际运行时建议把打印改成日志输出特征多的时候remaining.remove是 O(n) 操作可以考虑用布尔掩码优化。4. 参数调优与排错k 值、γ 与特征分箱4.1 k 近邻参数与 CMI 估计方差的权衡k 值的大小直接影响熵估计的偏差-方差取舍。k 太小1 或 2距离最近邻很近估计方差大容易出现 CMI 值虚高k 太大超过 10估计会过度平滑弱特征和强特征之间的差距被抹平。我常用的范围是 35。一个更实操的判断方法把同一个 CMI 计算跑 5 次加不同的随机种子重采样看结果的方差。如果方差占到均值的 30% 以上k 加大一档如果两个候选特征的 CMI 差值小于方差这个差值本身没有统计意义说明样本量不够或者需要降维预处理。# 用重采样验证k值的稳定性 python - EOF import numpy as np from your_module import conditional_mutual_information X np.random.rand(200, 5) y (X[:, 0] 0.3 * np.random.randn(200) 0.5).astype(int) for k in [1, 3, 5, 10]: cmi_values [] for seed in range(5): idx np.random.RandomState(seed).choice(200, 150, replaceFalse) cmi_values.append(conditional_mutual_information(X[idx, 0], y[idx], [], k)) print(fk{k}, mean{np.mean(cmi_values):.4f}, std{np.std(cmi_values):.4f}) EOF运行后会看到 k1 时标准差明显高于 k5。这个判断方法比死记参数值实用得多——数据量不同、特征分布不同最优 k 都不一样。4.2 动态权重 γ 的灵敏度验证γ 是动态加权里最需要小心调的超参数。γ 太大后选择的特征会被严重高估可能把噪声特征推上来γ 太小就退化成普通 CMI失去了动态加权的意义。验证 γ 的方式是看“选择顺序的稳定性”。做法把数据集随机分成 10 份跑 10 次前向搜索对比不同 γ 下选出的前 10 个特征的 Jaccard 相似度。import numpy as np from sklearn.model_selection import StratifiedKFold def stability_score(X, y, gamma, num_features10, folds10): 计算给定gamma下特征选择的稳定性 skf StratifiedKFold(n_splitsfolds, shuffleTrue, random_state42) feature_sets [] for train_idx, _ in skf.split(X, y): X_train X.iloc[train_idx].values y_train y.iloc[train_idx].values selected dynamic_weighted_cmi_selection( X_train, y_train, num_featuresnum_features, gammagamma ) feature_sets.append(set(selected)) # 两两计算Jaccard相似度 sims [] for i in range(len(feature_sets)): for j in range(i 1, len(feature_sets)): inter len(feature_sets[i] feature_sets[j]) union len(feature_sets[i] | feature_sets[j]) sims.append(inter / union) return np.mean(sims) for gamma in [0.0, 0.5, 1.0, 1.5, 2.0]: score stability_score(X_train, y_train, gamma) print(fgamma{gamma}, stability{score:.3f})逻辑说明gamma0.0时权重恒为 1等价于普通 CMI。观察不同 γ 下的稳定性曲线通常 γ1 附近会出现一个平台期这就是建议的工作区间。如果曲线一直上升没有下降趋势说明特征之间存在强相关结构需要加特征聚类作预处理。4.3 连续特征分箱的三个关键设置kNN 估计器理论上不需要分箱但实际数据里经常混着离散特征和连续特征。cKDTree 的度量要求所有维度数值尺度一致直接用会出问题。常见做法是先给离散特征做 one-hot再跟连续特征一起标准化。但这样 CMI 计算时 one-hot 维度会被当成连续空间的一部分产生度量失真。替代方案是分箱后统一处理。分箱参数有三个关键点箱数bin count、分箱方式等宽/等频、缺失值处理。等宽分箱对长尾分布不友好np.linspace切出来的区间可能在尾部没有样本等频分箱则要处理重复值——大量重复值都在同一个箱里箱数会变少。def equal_freq_bin(x, bins10): 等频分箱处理重复值 quantiles np.percentile(x, np.linspace(0, 100, bins 1)) # 去重并保证边界单调 quantiles np.unique(quantiles) if len(quantiles) 3: quantiles np.array([np.min(x), np.median(x), np.max(x)]) return np.digitize(x, quantiles[1:-1])注意np.digitize返回的索引从 0 开始刚好可以直接送给cKDTree当整数点用。但需要说明分箱后的 CMI 估计值会小于连续空间的真实 CMI因为它丢失了箱内的分布信息。所以分箱后的 CMI 值只能用于特征之间的相对比较不能当作信息量的绝对估计。这一点在跨数据集对比时尤其要小心——同一个特征在不同数据集上分箱后算出的 CMI不能直接比较大小。提示分箱数一般 812。箱数过少区分度下降箱数过多每个箱内样本太少CMI 方差变大。缺失值在分箱前用中位数填充否则np.percentile会报错。5. 把算法接进实际数据流水线两个完整案例5.1 案例一高维生物数据上的特征降维生物信息学场景里常见的是几千到几万个特征、几百个样本的矩阵。这种数据直接跑前向搜索不现实——每轮都要对所有候选特征算 CMI复杂度是 O(F²) 量级。先把动态加权 CMI 和过滤法结合第一轮用 MI 或方差过滤砍掉 80% 的低信息特征再在剩余特征上跑动态加权 CMI。from sklearn.feature_selection import VarianceThreshold from sklearn.preprocessing import StandardScaler # 1. 方差过滤删除低方差特征 scaler StandardScaler() X_scaled scaler.fit_transform(X) selector VarianceThreshold(threshold0.01) X_filtered selector.fit_transform(X_scaled) kept_idx selector.get_support(indicesTrue) # 2. 在过滤后的特征上跑动态加权CMI X_filtered_df pd.DataFrame(X_filtered, columns[ff{i} for i in kept_idx]) selected_local dynamic_weighted_cmi_selection( X_filtered_df.values, y_train.values, num_features50, k3, gamma1.0 ) selected_global [kept_idx[i] for i in selected_local]这里VarianceThreshold(0.01)的参数需要根据特征量纲调——如果特征已经标准化0.01 意味着方差低于 0.01 的特征基本是常数或近常数。但注意方差过滤会删掉一些方差低但区分度高的特征比如某个基因只在少数样本中高表达方差不大但对稀有类别的区分很有价值。所以过滤阈值不要设太激进0.01 或 0.05 都是安全范围。5.2 案例二工业传感数据的在线特征选择工业场景里特征选择往往要做成在线模式——数据流式到达特征重要性随时间漂移。动态加权在这里的用法不是跑一遍前向搜索而是维护一个滑动窗口每个窗口结束后重算 CMI用时间衰减代替条件集合规模的加权。class StreamingCMISelector: def __init__(self, window_size1000, k3, gamma1.0, top_k20): self.window [] self.window_size window_size self.k k self.gamma gamma self.top_k top_k self.selected_features None def add_sample(self, x, y): self.window.append((x, y)) if len(self.window) self.window_size: self.window.pop(0) def recompute(self): if len(self.window) 50: return X np.array([s[0] for s in self.window]) y np.array([s[1] for s in self.window]) # 时间衰减权重越近的样本权重越大 n len(self.window) time_weights np.exp(-np.arange(n) / n) self.selected_features dynamic_weighted_cmi_selection( X, y, num_featuresself.top_k, kself.k, gammaself.gamma ) return self.selected_features这个结构里dynamic_weighted_cmi_selection没有显式用time_weights——要真正接入需要对 CMI 估计器做样本加权。常见做法是把样本权重传给cKDTree的距离计算或者在重采样时按权重采样。轻量级的替代方案是每次重算时从窗口内按权重采样固定数量的样本再用采样后的子集去跑 CMI。牺牲一点精度换来实现复杂度大幅下降。5.3 一个容易踩的坑特征尺度差异造成的距离失真最后说一个最容易踩的坑。cKDTree 用欧氏距离找近邻如果某个特征量纲是 01另一个是 010000后者会主导距离计算导致 k 近邻几乎都集中在那个高量纲特征的取值范围内其他维度的信息被当噪声忽略。解决方案不是标准化而是对每个特征单独做分位数变换。标准化只保证均值为 0、方差为 1但长尾分布仍然会让距离集中在少数异常值附近。分位数变换会把数据映射到均匀分布上让每个特征对距离的贡献大致相等CMI 估计才会稳定。from sklearn.preprocessing import QuantileTransformer qt QuantileTransformer(output_distributionuniform, n_quantiles100, random_state42) X_transformed qt.fit_transform(X)这里n_quantiles100意味着每个特征被映射到 100 个取值上分箱效应已经存在后续不必再分箱。output_distributionuniform会消除特征的边缘分布差异CMI 估计器里假设的条件分布会更接近均匀方差也会小一些。如果特征已经做过分箱这一步可以跳过避免双重分箱带来的信息损失。本文还有配套的精品资源点击获取
返回列表