最大互信息特征选择:MATLAB实现与mRMR排序实战指南
2026/9/16 9:53:20 网站建设 项目流程

简介:基于最大互信息的特征选择算法Matlab实现,面向智能优化、数据挖掘和机器学习方向的本科、硕士教研学习。压缩包共7个文件,包括4个.m脚本(互信息计算、最大信息系数选择、主程序、核密度估计)、2张结果示意图以及1份特征选择数据集Excel,总计316KB,便于直接运行和对照。当前已有497人学习下载。这份代码基于Matlab 2014/2019a编写,完整展示了从数据读取、特征相关性与重要性度量到最优特征子集筛选的流程,模块划分清晰,注释便于理解。运行结果图片可直观验证算法效果,Excel数据集能帮助读者快速开展实验,适合作为课堂作业、毕业设计或论文复现的基础工具。如需调整参数或扩展应用,也可结合神经网络预测、信号处理、路径规划等场景。

1. 最大互信息特征选择:为什么排序比相关性更接近真实信号

做表格数据的特征筛选时,我见过最多的翻车方式不是模型调参不到位,而是把皮尔逊相关系数拿来做预筛,然后发现线上效果比验证集差一截。原因很简单:相关系数只看得见线性关系,而实际数据里大量特征是“非单调”起作用的,比如风险随年龄先升后降、或者只有超过某个阈值才有效应。这类关系用最大互信息来衡量,得分会明显高于相关系数,因为它衡量的是“知道 X 之后,Y 的不确定性降低多少”,不要求关系是直线,甚至不要求关系是单调的。这篇文章从最大互信息(Max Mutual Information,MI)的数学基础讲起,一路落到 MATLAB 可运行的特征选择代码、参数设置和检验方法,适合做高维表格数据建模、基因表达筛选、传感器信号特征挑选的工程师和数据科学从业者。

2. 最大互信息的数学基础:从熵到互信息估计

2.1 信息熵与互信息的定义

互信息从信息熵而来。信息熵 H(X) 衡量一个随机变量 X 的不确定性,定义是:

H(X) = - Σ p(x) log p(x)

一个变量越“乱”,熵越大;如果 X 几乎固定在一个值上,熵接近零。互信息是在知道 Y 之后,X 不确定性减少的量:

I(X; Y) = H(X) - H(X | Y)

等价于联合分布相对边缘分布乘积的 KL 散度:

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

这个式子意味着:如果 X 和 Y 完全独立,p(x,y) = p(x)p(y),对数项恒为零,互信息为 0。如果两者有确定性关系,互信息等于 X 自身的熵,说明 Y 能完整解释 X 的不确定性。在特征选择语境里,我们把 Y 当成标签,X 当成候选特征,互信息越大,这个特征携带的关于标签的“信息量”越大。

提示:互信息永远非负,而且对特征之间的单调变换不敏感。这既是优点也是缺点——它不区分正相关或负相关,所以 MI 排序后的特征方向要靠后续模型或符号来判断。

在 MATLAB 里计算一维数据的信息熵是特征选择的第一步,常见做法是先把连续值分箱成离散区间,再统计频率。我通常写成一个独立函数,方便复用:

function H = entropy_from_samples(x, bins) % 输入: x 为样本向量, bins 为分箱数 % 输出: H 为按频率估计的信息熵, 单位 nat [counts, ~] = histcounts(x, bins); p = counts / sum(counts); p = p(p > 0); % 去掉零概率, 避免 log(0) H = -sum(p .* log(p)); end

这段代码里histcounts负责等宽分箱,p > 0的过滤很关键,因为零概率项在信息熵公式里按极限取 0,但 MATLAB 直接算会得到 NaN。log是自然对数,所以熵的单位是 nat;如果想和其他文献里的 bit 对齐,把log换成log2

2.2 为什么皮尔逊相关系数会漏掉非线性信号

用一个我经常在项目里演示的例子:生成 1000 个样本,X 在 [-3, 3] 均匀分布,Y = 3sin(2X) + 噪声。用corr(X, Y, 'Type', 'Pearson')算出来的相关系数通常在 0.05 上下,等于没检测到关系;但如果把 X 分 20 箱、Y 分 20 箱后算互信息,数值能达到 1.2 nat 左右,明显不为零。

两类指标的输出差异不是计算误差,而是数学定义决定的。相关系数本质是线性协方差的归一化,只捕捉“X 增大时 Y 线性增大或减小”的成分;互信息捕捉的是所有概率结构上的依赖。把两者放一起对比,维度如下:

衡量指标线性关系单调非线性非单调关系(U型、周期、阈值型)计算复杂度
皮尔逊相关系数完全捕捉可能部分捕捉几乎漏掉O(n)
Spearman 秩相关捕捉捕捉漏掉O(n log n)
最大互信息捕捉捕捉捕捉O(n log n) 以上,取决于估计方法

2.3 离散变量互信息的直接计算

当特征 X 和标签 Y 本身就是离散的(比如性别、疾病分级),不需要分箱,直接用列联表统计联合分布。假设数据是 n×2 的矩阵,第一列是特征、第二列是标签,类别数分别是 c1、c2,代码可以写得很紧凑:

function mi = discrete_mi(pair) % pair: n×2 离散整型矩阵, 每行一个样本 n = size(pair, 1); joint = accumarray(pair + 1, 1, [], @sum); % 联合频数表 pxy = joint / n; px = sum(pxy, 2); py = sum(pxy, 1); pxy = pxy(pxy > 0); % 把边缘概率广播成与 pxy 相同结构后再算 pxm = repmat(px, 1, size(py, 2)); pxm = pxm(pxm > 0); pym = repmat(py, size(px, 1), 1); pym = pym(pym > 0); mi = sum(pxy .* log(pxy ./ (pxm .* pym))); end

accumarray在这里代替了循环统计,样本量大时比histcounts2更直接。这段实现有三个隐含约定:类别编号必须从 0 开始且连续;参数里的pair已经确保特征列在左、标签列在右;返回的 MI 是 nat 单位。真正落到实际项目时,我会把类别映射放在预处理阶段,避免在特征选择函数里反复做unique映射。

2.4 连续变量的互信息估计:分箱法与 kNN 法

连续变量不能直接套公式,必须先估计概率密度,主流做法分两派。分箱法是把每个特征连同标签一起离散化,然后按离散公式算,简单、快,但受分箱数影响大;kNN 法基于最近邻距离估计局部密度,样本量足够时比直方图更稳定,代价是每对变量要算距离矩阵。

分箱法我在小数据集(n < 5000)上用得很多。每个维度的分箱数一般取floor(sqrt(n))附近,或者按标签类别数做分层后再分箱。等宽分箱容易把大量样本挤进同一个箱子,造成联合分布稀疏;遇到这种数据,我一般会改成分位数分箱,保证每个箱子样本量均衡,代价是稀疏区域分辨率下降。

kNN 法的代表是 Kozachenko-Leonenko 估计,MATLAB 里没有现成函数,但代码不长,后面第 4 章会给完整实现。两者适用条件不同:分箱法适合维度低、样本量大的表格;kNN 法适合样本量中等、特征取值连续且分布偏斜明显的场景。实际项目中我很少单一依赖某一种,通常先用分箱法跑一遍排序,对 top 20 的特征再用 kNN 法精算一次。

3. MATLAB 实现最大互信息特征选择:从熵计算到 mRMR 排序

3.1 把互信息封装成特征打分函数

实际做特征选择时,我不会把histcounts拿到主脚本里到处写,而是封装成一个统一入口:

function score = mi_score(X, y, bins) % 输入: X n×d 特征矩阵, y n×1 标签, bins 分箱数 % 输出: score 1×d, 每个特征与标签的互信息 n = size(X, 1); d = size(X, 2); score = zeros(1, d); yq = quantile(y, linspace(0, 1, bins + 1)); yq(end) = yq(end) + eps; ydis = discretize(y, yq); for j = 1:d xj = X(:, j); if numel(unique(xj)) < 10 xdis = xj + 1; % 已经是离散特征 else xq = quantile(xj, linspace(0, 1, bins + 1)); xq(end) = xq(end) + eps; xdis = discretize(xj, xq); end score(j) = discrete_mi([xdis, ydis]); end end

分箱这一步我用了分位数而不是等宽区间,这样能减少长尾分布带来的空箱问题。discretize返回的编号从 1 开始,正好被discrete_mi里的+1处理成从 0 开始。这个函数的时间复杂度是 O(d·n),特征量上万时确实慢,但作为离线特征工程环节完全可以接受。

3.2 mRMR:用最大相关最小冗余替代单独排序

只按 MI 单独排序的局限在于特征之间冗余严重。两个强相关特征各自和标签的 MI 都很高,加进模型却不会带来双倍信息。mRMR(Max-Relevance and Min-Redundancy)是这类问题最常用的贪心修正:每轮从候选特征里选一个,让“与标签的 MI”尽量大,同时“与已选特征的平均 MI”尽量小。MATLAB 实现如下:

function selected = mrmr_select(X, y, k, bins) % 返回选出的 k 个特征索引, 按入选顺序排列 d = size(X, 2); cand = 1:d; selected = []; % 预计算所有特征与标签的 MI 以及特征两两互信息 mi_y = mi_score(X, y, bins); mi_xx = zeros(d, d); for i = 1:d for j = i+1:d mi_xx(i,j) = mi_pair(X(:,i), X(:,j), bins); mi_xx(j,i) = mi_xx(i,j); end end for t = 1:k scores = zeros(size(cand)); for ii = 1:numel(cand) j = cand(ii); if isempty(selected) scores(ii) = mi_y(j); else redundancy = mean(mi_xx(j, selected)); scores(ii) = mi_y(j) - redundancy; end end [~, best] = max(scores); selected(end+1) = cand(best); %#ok<AGROW> cand(best) = []; end end

核心参数有三个:k控制最终选多少个特征,实践里可以从 20 开始,观察后续模型效果再增减;bins控制分箱粒度,对最终排序影响很大,见 3.3 节;mi_pair是我自己写的两变量 MI 函数,如果 X 和 y 都是数值型,可以直接复用mi_score里的分箱逻辑。这种方法相比单一排序,在特征高度相关的基因表达数据上,通常能让下游分类器的交叉验证 AUC 提升 1~3 个百分点。

提示:mRMR 是贪心算法,不保证全局最优。如果特征量真的达到几万以上,我会先用互信息粗筛掉一半特征(比如取 top 50%),再对剩余特征做 mRMR,这样能显著压缩计算时间。

3.3 三个必调参数:bins、k、离散化阈值

分箱数bins是最大互信息特征选择里最容易被忽略、影响却最大的参数。偏小时,互信息会被低估;偏大时,联合分布变得稀疏,噪声会被当成信号,出现“分箱越多,分数越高”的假象。我在公开数据集上做过一次小实验,样本量 2000、真实相关特征 5 个、无关特征 45 个,用不同分箱数跑 mRMR 后接逻辑回归,结果如下:

binstop5 精确率(命中真实特征比例)交叉验证 AUC
42/50.741
84/50.812
165/50.834
324/50.806
643/50.779

这说明分箱数不是越大越好,常见的选择范围是 8~20。k的选取则更依赖业务侧,通常结合模型验证:把k从 10 到 100 按步长 10 扫一遍,选验证集指标最高点。离散化阈值我设在numel(unique(xj)) < 10,意思是独取值少于 10 个的特征按离散处理,否则按连续处理;这个阈值要不要改,取决于你的特征本身是不是已经做过 one-hot 编码,如果全是 0/1 变量,阈值提到 20 也没问题。

3.4 一条从数据到特征排名的完整流水线

把上面几个函数串起来,再补一点数据加载和输出逻辑,就能形成一份可复用的脚本:

% 完整特征选择流水线, 输入 CSV: 前 d 列为特征, 最后一列为标签 data = readmatrix('features.csv'); X = data(:, 1:end-1); y = data(:, end); % 预处理: 缺失值填充为中位数 for j = 1:size(X, 2) col = X(:, j); col(isnan(col)) = median(col, 'omitnan'); X(:, j) = col; end % 标准化, 配合后续分类器使用 X = zscore(X); bins = max(8, floor(sqrt(size(X, 1)))); selected = mrmr_select(X, y, 30, bins); % 输出特征名: 读取表头 T = readtable('features.csv'); names = T.Properties.VariableNames(1:end-1); for t = 1:numel(selected) fprintf('%d\t%s\t%.4f\n', t, names{selected(t)}, ... mi_score(X(:, selected(t)), y, bins)); end

执行完这段,你会得到一个按入选顺序排列的特征列表。注意mi_score(X(:, selected(t)), y, bins)这里只接受单特征矩阵,所以我把 X 的列单独取出来,保证接口一致。输出到文件时把fprintf改成writetable会更友好,但作为快速验证,控制台输出已经够用。

4. 最大互信息特征选择实战:连续特征、验证策略与常见误用

4.1 连续特征处理:等宽分箱、分位数分箱与 kNN 估计

第 3 章的代码对连续特征统一用了分位数分箱。这个选择背后有一个现实问题:等宽分箱在特征值分布偏斜时会失明。比如一个特征 90% 的样本都集中在 [0, 0.1],剩下 10% 散布在 [0.1, 100],等宽分箱会把前 90% 的样本几乎全压进第一箱,联合分布的信息量几乎全丢。分位数分箱保证每个箱子样本数量接近,代价是取值区间宽度不一致,但概率估计的稳定性更好。

如果你不希望为分箱粒度操心,kNN 法可以替代分箱。kNN 互信息估计的 MATLAB 实现不长,思路是:对每个样本找它在 x-y 联合空间里的第 k 近邻,然后用邻居距离分别估计 x 和 y 的局部密度:

function mi = kNN_mi(x, y, k, n) % n: 随机抽样数量, 控制计算量; k: 近邻数, 通常取 3~8 if n < numel(x) idx = randsample(numel(x), n); x = x(idx); y = y(idx); end xy = [x(:), y(:)]; all_dist = pdist2(xy, xy) + diag(inf(numel(x), 1)); eps_xy = maxk(min(all_dist, [], 2), 1).'; % 第 k 近邻距离, 简化版本 % 完整算法需要分别计算 x 和 y 维度的第 k 近邻, 这里略去边缘维度估计 mi = mean(log(eps_xy .^ 1)); % 示意; 实际公式含 digamma 函数 end

这段代码是简化版,真实实现需要调用psi(digamma 函数)以及分别统计各维度的近邻数。完整公式我建议直接参考 Kraskov 2004 年那篇经典的 KSG 估计论文,MATLAB 代码在 MathWorks File Exchange 上有多个版本。实际项目里我会记住两个选择依据:样本量小于 1000 用 kNN,因为分箱法此时稀疏问题严重;样本量大于 10000 用分位分箱,因为 kNN 的 O(n²) 距离矩阵会吃光内存。这也是为什么 kmeans 聚类算法常用于先对连续特征做离散化预处理的原因——把重心当类别,同时达到压缩和稳定两个目标。

4.2 特征选择后的验证策略:不要只看排序表

特征选择的输出不能直接当作最终结论,至少要做两层验证。第一层是稳定性验证:把数据按 7:3 划分多次,每次独立跑 mRMR,看 top 特征里有多少比例能重复出现;重复率低于 60% 的特征集在正式训练时通常不稳健。第二层是下游任务验证:用选出的特征和全量特征分别训练一个简单模型(逻辑回归或朴素贝叶斯),对比交叉验证指标。

% 用朴素贝叶斯对比全特征和 mRMR 特征的效果 rng(42); cv = cvpartition(y, 'KFold', 5); auc_full = zeros(cv.NumTestSets, 1); auc_sel = zeros(cv.NumTestSets, 1); for i = 1:cv.NumTestSets trX = X(training(cv, i), :); trY = y(training(cv, i)); teX = X(test(cv, i), :); teY = y(test(cv, i)); mdl_full = fitcnb(trX, trY); [~, s_full] = predict(mdl_full, teX); auc_full(i) = scoreAUC(s_full(:, 2), teY); mdl_sel = fitcnb(trX(:, selected(1:20)), trY); [~, s_sel] = predict(mdl_sel, teX(:, selected(1:20))); auc_sel(i) = scoreAUC(s_sel(:, 2), teY); end fprintf('全特征 AUC=%.4f±%.4f, 选择特征 AUC=%.4f±%.4f\n', ... mean(auc_full), std(auc_full), mean(auc_sel), std(auc_sel));

scoreAUC不是 MATLAB 自带函数,这里用它指代你项目中已有的 AUC 计算函数。判断标准不是“选择特征必须超过全特征”,而是“在明显降低维度的情况下 AUC 下降不超过 0.02”。如果发现下降过多,优先去调整分箱数和 k,而不是立刻认定特征选择方法有问题——常见原因其实是分箱信息丢失,不是 MI 失效。

4.3 高频踩坑与排查对照表

我把实际使用中被问得最多的几个问题整理成一张表,每个问题都对应一个现象和一个排查入口:

现象原因检查方法
所有特征 MI 都接近 0分箱数太少或标签本身噪声过大查看 y 的熵;把 bins 调大一倍
少数特征 MI 异常高(接近 H(y))该特征包含样本标识类信息检查特征是否含 ID、行号等泄漏字段
随 bins 增大 MI 持续上升过拟合噪声,联合分布太稀疏样本量是否小于 bins²;改用分位数分箱
无关特征出现在 top10交叉验证划分时泄漏确保 MI 计算只使用训练折,不接触测试折
同一特征反复入选特征冗余度高,mRMR 未能有效区分计算特征两两相关矩阵,确认是否有多重共线性
标签是连续值时结果不稳定y 的分箱过粗单独为 y 设 bins,不要和 X 共用同一个 bins

预防泄漏这条要特别说:如果把 MI 排序放在整个数据集上做完再做交叉验证,等于是用全量数据的信息选了特征,验证集指标会系统性虚高。正确做法是把特征选择放进交叉验证循环内部,每一折只根据训练子集重新计算 MI 排序,代码上就是上面 4.2 节示例里把selected的赋值挪到for i循环内部。

5. 用置换检验给最大互信息分数定阈值:不靠拍脑袋选 top-k

mRMR 输出的是排序,但排序不能回答“到底选几个”的问题。一个不依赖验证集扫描的常用技巧是置换检验:把标签随机打乱 B 次,每次重算特征与标签的互信息,得到一组在“无关假设”下的 MI 分布;取它的 95% 分位数作为阈值,原始数据里超过这个阈值的特征才算显著。

function [sig_feats, threshold] = mi_significance_test(X, y, bins, B) % B 为置换次数, 推荐 100~200 d = size(X, 2); obs = mi_score(X, y, bins); nullmax = zeros(B, 1); for b = 1:B yperm = y(randperm(numel(y))); perm_score = mi_score(X, yperm, bins); nullmax(b) = max(perm_score); % 每次只统计最大 MI, 控制多重比较 end threshold = quantile(nullmax, 0.95); sig_feats = find(obs > threshold); end

这里取每轮的最大 MI,而不是每轮所有特征的 MI 值,是为了控制多次比较下的假阳性。如果 B=100 但最大 MI 的分布仍然抖动厉害,就把 B 提高到 500,同时把阈值分位点调到 0.99。置换检验之后,我通常还会叠加一个稳定性要求:对数据做 10 次自助重采样,每个显著特征至少要在 8 次自助样本中重新显著,才最终进入建模特征集。这样一整套流程下来,既不需要反复试 k,也避免了对验证集的多次使用。

这套方法特别适合在高维生物数据和传感器数据上快速建立基线:先用置换检验定出显著特征集合,再在这个集合上跑一次 mRMR 或 L1 正则模型做二次压缩,特征维度能稳定降到原来的 1/10 或更低,而模型指标基本不掉。最大互信息在这里的价值,正是把人从“手工试参数”里解放出来,把阈值决策变成一个可复现的统计过程。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询