互信息与特征选择:从原理到代码实现MRMR算法(Python示例)

在机器学习项目中,特征选择往往是决定模型性能的关键步骤。面对成百上千的特征,如何高效筛选出最具价值的子集?MRMR(Maximum Relevance Minimum Redundancy)算法提供了一种基于信息论的优雅解决方案。本文将带您深入理解互信息原理,并手把手实现MRMR算法——从数学公式推导到NumPy/Pandas实战,最后探讨与scikit-learn生态的集成技巧。

1. 互信息:信息论的度量语言

互信息(Mutual Information)是理解MRMR算法的基石。它衡量两个随机变量之间的非线性依赖关系,比传统的皮尔逊相关系数更能捕捉复杂关联。想象两个变量X和Y,它们的互信息I(X;Y)可以理解为:知道X的值后,Y的不确定性减少了多少。

数学上,互信息定义为:

I(X;Y) = Σ Σ p(x,y) * log(p(x,y)/(p(x)p(y)))

这个公式看似复杂,实则揭示了变量间的本质联系。让我们用Python实现一个基础版本:

import numpy as np
from sklearn.metrics import mutual_info_score

def manual_mutual_info(X, Y, bins=10):
    # 联合概率分布
    joint_prob, _, _ = np.histogram2d(X, Y, bins=bins)
    joint_prob /= joint_prob.sum()
    
    # 边缘概率分布
    p_x = joint_prob.sum(axis=1)
    p_y = joint_prob.sum(axis=0)
    
    # 计算互信息
    mi = 0
    for i in range(joint_prob.shape[0]):
        for j in range(joint_prob.shape[1]):
            if joint_prob[i,j] > 0:
                mi += joint_prob[i,j] * np.log(joint_prob[i,j] / (p_x[i] * p_y[j]))
    return mi

# 对比验证
X = np.random.rand(1000)
Y = X**2 + np.random.normal(0, 0.1, 1000)
print(f"手动实现: {manual_mutual_info(X, Y):.4f}")
print(f"scikit-learn: {mutual_info_score(np.digitize(X, np.linspace(0,1,10)), 
                                      np.digitize(Y, np.linspace(0,2,10))):.4f}")

注意:实际应用中推荐使用scikit-learn的mutual_info_score,它经过优化且支持更多数据类型。我们的手动实现主要用于教学演示。

2. MRMR算法原理拆解

MRMR算法的核心思想非常直观:选择的特征应该与目标变量高度相关(最大相关),同时特征之间尽可能不相似(最小冗余)。这就像组建团队——我们需要能力强的成员(相关),但成员技能最好互补而非重叠(冗余)。

算法步骤可以形式化为:

  1. 初始化已选特征集合S=∅,候选特征集合F=所有特征
  2. 计算每个特征f∈F与目标y的互信息I(f;y),选择最大值加入S
  3. 迭代选择后续特征:在剩余特征中寻找最大化下式的特征
    argmax_{f∈F-S} [I(f;y) - 1/|S| Σ_{s∈S} I(f;s)]
    
  4. 重复步骤3直到达到预定特征数量

这个优化目标函数中,第一项I(f;y)保证相关性,第二项(1/|S|)ΣI(f;s)惩罚冗余性。平衡这两者就是MRMR的精妙之处。

3. 从零实现MRMR算法

让我们用Python完整实现MRMR算法。假设输入数据是Pandas DataFrame,目标变量是单独列:

import pandas as pd
from sklearn.feature_selection import mutual_info_classif

class MRMRFeatureSelector:
    def __init__(self, n_features=10, discrete_features='auto'):
        self.n_features = n_features
        self.discrete_features = discrete_features
        self.selected_features = []
    
    def fit(self, X, y):
        features = X.columns.tolist()
        remaining_features = set(features)
        
        # 第一步:选择与目标互信息最大的单个特征
        mi_scores = mutual_info_classif(X, y, discrete_features=self.discrete_features)
        first_feature = features[np.argmax(mi_scores)]
        self.selected_features.append(first_feature)
        remaining_features.remove(first_feature)
        
        # 迭代选择后续特征
        while len(self.selected_features) < min(self.n_features, len(features)):
            scores = {}
            redundancy_penalty = np.zeros(len(remaining_features))
            
            # 计算现有已选特征的冗余项
            for i, f in enumerate(remaining_features):
                redundancy = 0
                for s in self.selected_features:
                    mi_fs = mutual_info_classif(
                        X[[f]], X[s], discrete_features=self.discrete_features
                    )[0]
                    redundancy += mi_fs
                redundancy_penalty[i] = redundancy / len(self.selected_features)
            
            # 计算相关性得分
            relevance_scores = mutual_info_classif(
                X[list(remaining_features)], y, 
                discrete_features=self.discrete_features
            )
            
            # MRMR得分 = 相关性 - 冗余性
            for i, f in enumerate(remaining_features):
                scores[f] = relevance_scores[i] - redundancy_penalty[i]
            
            # 选择得分最高的特征
            next_feature = max(scores.items(), key=lambda x: x[1])[0]
            self.selected_features.append(next_feature)
            remaining_features.remove(next_feature)
        
        return self
    
    def transform(self, X):
        return X[self.selected_features]
    
    def fit_transform(self, X, y):
        self.fit(X, y)
        return self.transform(X)

关键实现细节:

  • 使用scikit-learn的mutual_info_classif高效计算互信息
  • 动态更新冗余惩罚项,确保每次选择都考虑现有特征集
  • 支持离散特征自动检测(通过discrete_features参数)

4. 实战应用与性能优化

在实际数据集上应用我们的MRMR实现。以经典的乳腺癌数据集为例:

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

# 加载数据
data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = data.target

# 特征选择
selector = MRMRFeatureSelector(n_features=10)
X_selected = selector.fit_transform(X, y)

print(f"Selected features: {selector.selected_features}")

# 对比全特征和选择后的模型性能
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 全特征模型
clf_full = RandomForestClassifier(random_state=42)
clf_full.fit(X_train, y_train)
full_acc = accuracy_score(y_test, clf_full.predict(X_test))

# MRMR选择特征模型
clf_mrmr = RandomForestClassifier(random_state=42)
clf_mrmr.fit(X_train[selector.selected_features], y_train)
mrmr_acc = accuracy_score(y_test, clf_mrmr.predict(X_test[selector.selected_features]))

print(f"全特征准确率: {full_acc:.4f}")
print(f"MRMR选择后准确率: {mrmr_acc:.4f}")
print(f"特征数量从{X.shape[1]}减少到{len(selector.selected_features)}")

性能优化技巧:

  • 并行计算:使用joblib并行化互信息计算
  • 增量更新:缓存已计算的互信息值,避免重复计算
  • 近似算法:对于高维数据,可采用基于k近邻的快速互信息估计
# 并行计算优化示例
from joblib import Parallel, delayed

def parallel_mi(features, X, y, n_jobs=-1):
    def compute_mi(f):
        return mutual_info_classif(X[[f]], y)[0]
    
    mi_scores = Parallel(n_jobs=n_jobs)(
        delayed(compute_mi)(f) for f in features
    )
    return dict(zip(features, mi_scores))

5. 与scikit-learn生态集成

为了让我们的MRMR实现能与scikit-learn的Pipeline无缝协作,需要实现标准的Transformer接口:

from sklearn.base import BaseEstimator, TransformerMixin

class MRMRTransformer(BaseEstimator, TransformerMixin):
    def __init__(self, n_features=10, discrete_features='auto'):
        self.n_features = n_features
        self.discrete_features = discrete_features
        self.selector = None
    
    def fit(self, X, y):
        self.selector = MRMRFeatureSelector(
            n_features=self.n_features,
            discrete_features=self.discrete_features
        )
        self.selector.fit(X, y)
        return self
    
    def transform(self, X):
        return self.selector.transform(X)
    
    def fit_transform(self, X, y):
        return self.fit(X, y).transform(X)

# 在Pipeline中使用示例
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('feature_selector', MRMRTransformer(n_features=15)),
    ('classifier', RandomForestClassifier(random_state=42))
])

pipeline.fit(X_train, y_train)
print(f"Pipeline准确率: {accuracy_score(y_test, pipeline.predict(X_test)):.4f}")

高级应用场景:

  • 分类与回归:通过替换mutual_info_classifmutual_info_regression支持回归任务
  • 自定义评分:扩展支持其他相关性度量方法
  • 动态特征数量:根据互信息分数自动确定最优特征数量

6. 可视化分析与解释

理解特征选择过程同样重要。我们可以通过可视化来展示MRMR的选择逻辑:

import matplotlib.pyplot as plt
import seaborn as sns

def plot_mrmr_process(selector, X, y):
    # 准备数据
    mi_with_target = mutual_info_classif(X, y)
    mi_df = pd.DataFrame({
        'feature': X.columns,
        'mi_target': mi_with_target,
        'selected': [f in selector.selected_features for f in X.columns]
    })
    
    # 绘制与目标的互信息
    plt.figure(figsize=(12, 6))
    sns.barplot(data=mi_df.sort_values('mi_target', ascending=False),
                x='mi_target', y='feature', hue='selected', dodge=False)
    plt.title('Mutual Information with Target')
    plt.xlabel('Mutual Information Score')
    plt.ylabel('Features')
    plt.show()
    
    # 绘制特征间互信息矩阵(仅展示已选特征)
    if len(selector.selected_features) > 1:
        selected_data = X[selector.selected_features]
        mi_matrix = np.zeros((len(selector.selected_features), 
                             len(selector.selected_features)))
        
        for i, f1 in enumerate(selector.selected_features):
            for j, f2 in enumerate(selector.selected_features):
                if i != j:
                    mi_matrix[i,j] = mutual_info_classif(
                        selected_data[[f1]], selected_data[f2]
                    )[0]
        
        plt.figure(figsize=(10, 8))
        sns.heatmap(mi_matrix, annot=True, 
                    xticklabels=selector.selected_features,
                    yticklabels=selector.selected_features,
                    cmap='coolwarm')
        plt.title('Mutual Information Between Selected Features')
        plt.show()

# 使用可视化函数
selector = MRMRFeatureSelector(n_features=8)
selector.fit(X, y)
plot_mrmr_process(selector, X, y)

这些可视化帮助我们:

  • 确认所选特征确实具有高相关性
  • 验证特征间的冗余度较低
  • 理解算法在不同阶段的选择逻辑

7. 处理特殊场景的技巧

实际应用中常会遇到一些特殊场景,需要调整基础算法:

连续型特征处理 默认的互信息估计对连续特征可能不够稳定。我们可以考虑:

from sklearn.preprocessing import KBinsDiscretizer

def continuous_mi(X, y, n_bins=10):
    discretizer = KBinsDiscretizer(n_bins=n_bins, encode='ordinal', strategy='quantile')
    X_disc = discretizer.fit_transform(X)
    return mutual_info_classif(X_disc, y, discrete_features=True)

高维数据加速 当特征数量超过1000时,可以考虑:

def fast_mrmr(X, y, n_features=20, top_k=100):
    # 先用方差筛选减少候选特征数量
    variances = X.var(axis=0)
    top_features = X.columns[np.argsort(variances)[-top_k:]]
    
    # 在子集上应用MRMR
    selector = MRMRFeatureSelector(n_features=n_features)
    return selector.fit(X[top_features], y)

类别不平衡调整 对于不平衡数据集,可以加权计算互信息:

from sklearn.utils import class_weight

def weighted_mi(X, y):
    weights = class_weight.compute_sample_weight('balanced', y)
    return mutual_info_classif(X, y, sample_weight=weights)

流式特征选择 对于在线学习场景,可以实现增量式MRMR:

class IncrementalMRMR:
    def __init__(self, n_features=10):
        self.n_features = n_features
        self.selected_features = []
        self.mi_cache = {}  # 缓存已计算的互信息值
    
    def partial_fit(self, X_new, y_new):
        # 更新与目标的互信息
        for f in X_new.columns:
            if f not in self.mi_cache:
                self.mi_cache[f] = {}
            self.mi_cache[f]['target'] = mutual_info_classif(
                X_new[[f]], y_new
            )[0]
        
        # 更新特征间互信息
        for f1 in X_new.columns:
            for f2 in self.selected_features:
                mi = mutual_info_classif(X_new[[f1]], X_new[f2])[0]
                self.mi_cache[f1][f2] = mi
                self.mi_cache[f2][f1] = mi
        
        # 重新计算MRMR分数
        if not self.selected_features:
            # 初始选择
            next_f = max(self.mi_cache.items(), 
                        key=lambda x: x[1]['target'])[0]
            self.selected_features.append(next_f)
        else:
            # 增量选择
            candidates = [f for f in self.mi_cache 
                         if f not in self.selected_features]
            scores = {}
            for f in candidates:
                relevance = self.mi_cache[f]['target']
                redundancy = sum(
                    self.mi_cache[f][s] for s in self.selected_features
                ) / len(self.selected_features)
                scores[f] = relevance - redundancy
            
            if scores:
                next_f = max(scores.items(), key=lambda x: x[1])[0]
                self.selected_features.append(next_f)
        
        # 保持特征数量不超过设定值
        if len(self.selected_features) > self.n_features:
            self.selected_features = self.selected_features[:self.n_features]
        
        return self
Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐