MATLAB实现最大互信息特征选择:从分箱估计到交叉验证调参
2026/9/16 8:58:45 网站建设 项目流程

简介:本资源针对最大互信息特征选择算法,提供完整的Matlab实现代码与配套说明,适合本科、硕士阶段的教研学习,以及从事智能优化、神经网络预测、信号处理、图像处理等方向的仿真开发者参考使用。压缩包共7个文件,主要包含4个m源码文件、2个png示意图和1个xlsx特征选择数据集,其中m文件覆盖互信息估计、主程序与核心选择流程,便于读者理解算法实现路径并直接运行验证。资源整体大小仅316KB,轻量易用,目前已吸引497人学习下载。通过代码注释、运行结果与配套数据集的组合,读者可以快速掌握基于最大互信息进行特征筛选的基本思路,并迁移到自身实验或课题中,是兼顾算法原理与实践操作的入门参考资料。

1. 最大互信息为什么值得用来做特征选择

做特征选择时,最大互信息(Mutual Information, MI)是我最常用的一种过滤式指标。它不假设特征与标签之间存在线性关系,能捕捉到类似正弦波、分段的非线性依赖,这一点比皮尔逊相关系数更实用。拿到一份“特征选择数据集.xlsx”,我会先用互信息给所有特征打分,再让模型只在得分最高的几个特征上训练,这一步往往能同时降低过拟合和训练时间。

这套MATLAB实现包含MutualInfo.m、mic_select.m、p_mkde.m、main.m,我会把互信息在连续变量上如何被估计出来、参数怎么调、怎样用交叉验证确认保留几个特征讲清楚。适合正在做特征工程、数据挖掘课程设计或论文实验的本科和硕士生,也适合想用过滤式方法快速跑通基线的人。

2. 互信息的计算原理与MutualInfo.m中的估计方式

2.1 从熵到互信息:为什么能度量非线性依赖

互信息的定义是 I(X;Y)=H(X)+H(Y)-H(X,Y),等价于联合分布与边缘分布乘积的KL散度。当X和Y独立时,p(x,y)=p(x)p(y),对数项为0;依赖越强,互信息越大。它的核心优势在于:只要两个变量存在统计依赖,不管这个依赖是线性的、环形的、还是类似阶梯函数的,互信息都能反映出来。这也是特征选择算法里它比相关系数更稳的原因。

在离散情况下,I(X;Y)=∑∑ p(x,y) log(p(x,y)/(p(x)p(y)))。这里的难点在于概率分布未知,需要从样本中估计。对分类标签,可以直接统计每个类别频次;对连续特征,不能直接套公式,必须先把连续取值映射到离散区间,或者用连续密度估计。MutualInfo.m这里选择的是“分箱+联合直方图”的路线,这也是MATLAB实现中最直观的做法。如果你只想单独看某个特征的信息熵,把边缘分布Px带进 -sum(Px.*log(Px)) 就行,MutualInfo.m的中间结果里已经包含了这一步。

2.2 MutualInfo.m的分箱互信息实现

常见做法是把特征和标签各自分成bins个区间,统计落在每个格子里的样本数,再除以总样本数得到联合概率。下面这段代码对应MutualInfo.m的核心逻辑,兼容MATLAB 2014a和2019a:

function mi = MutualInfo(x, y, bins) % x, y: 长度相同的列向量 % bins: 每个维度的分箱数,默认10 if nargin < 3 || isempty(bins) bins = 10; end n = length(x); % 线性映射到1~bins区间 xb = floor((x - min(x)) / (max(x) - min(x)) * bins) + 1; xb(xb > bins) = bins; % 处理边界值 yb = floor((y - min(y)) / (max(y) - min(y)) * bins) + 1; yb(yb > bins) = bins; % 联合直方图:Pxy(i,j) = 落在第i个x分箱和第j个y分箱的样本比例 Pxy = accumarray([xb(:), yb(:)], 1, [bins bins]) / n; Px = sum(Pxy, 2); % 边缘分布 p(x) Py = sum(Pxy, 1); % 边缘分布 p(y),注意是行向量 % 把0替换成极小值,避免log(0) Pxy(Pxy == 0) = eps; Px(Px == 0) = eps; Py(Py == 0) = eps; % 互信息 = 联合分布与边缘分布乘积的KL散度 mi = sum(sum(Pxy .* log(Pxy ./ (Px * Py)))); end

逻辑说明:先把连续值线性缩放到[1,bins],用floor得到整数分箱号。accumarray是MATLAB里构建直方图矩阵最高效的方式之一,比双重循环快很多。Px是bins×1的列向量,Py是1×bins的行向量,Px*Py自动得到外积矩阵,也就是独立假设下的联合分布。最后一行sum(sum(...))把所有的KL散度项相加,得到互信息。

参数说明:bins是唯一需要手工指定的参数。bins太小,分布被粗糙化,互信息会被低估;bins太大,每个格子内样本很少,统计噪声会抬高互信息。经验值:样本量在1000以内时,bins=10到15之间比较稳;样本量到5000以上,可以尝试20。这里给一个快速参考表:

估计方式优点缺点适合场景
分箱直方图速度快、实现简单对bins敏感、边界粗糙特征初筛、大样本
Parzen核密度平滑、连续可导计算慢、带宽需要调小样本、精算MI

2.3 连续密度估计与p_mkde.m:分箱之外的另一种选择

分箱直方图实现简单,但对分箱边界很敏感。如果特征在某个小区间内密集、其他区间稀疏,直方图会把密度拉平。更精细的做法是用Parzen窗核密度估计,也就是项目里p_mkde.m在做的事。它的思想是:对每个查询点x,用所有样本点xi的高斯核叠加出一个平滑密度:

function p = p_mkde(x, xi, h) % x: 待估计的查询点列向量 % xi: 已知样本点列向量 % h: 核带宽 n = length(xi); p = zeros(size(x)); for j = 1:n p = p + exp(-(x - xi(j)).^2 / (2 * h^2)); end p = p / (n * h * sqrt(2*pi)); end

这个函数对每个样本点都计算一次高斯核,复杂度O(nm),适合小样本。带宽h决定了平滑程度:h太小,密度曲线出现锯齿;h太大,会把两个本来分离的峰合并成一个。一个常见做法是用Silverman经验公式 h=1.06std(xi)*n^(-1/5)。在MutualInfo.m和p_mkde.m之间选择时,我的原则是:特征选择初扫用分箱法,因为它快;最终需要精算互信息值或者样本量明显不平衡时,再切到核密度估计,然后数值积分求互信息。注意p_mkde.m的输入xi必须是列向量,否则循环里的减法会得到矩阵,导致p的维度错乱。

3. 从mic_select.m到main.m的完整特征选择流程

3.1 读取xlsx并分离特征与标签

拿到“特征选择数据集.xlsx”后,先用xlsread读入,假设最后一列是标签,其余列是特征。在main.m里我是这样写的:

clear; clc; data = xlsread('特征选择数据集.xlsx'); X = data(:, 1:end-1); % 特征矩阵 y = data(:, end); % 标签列 fprintf('数据维度:%d 个样本,%d 个特征\n', size(data,1), size(X,2));

如果xlsx中第一行是表头,xlsread返回数值矩阵时会把文本变成NaN。这时可以改用原始单元格读取:

[x_num, x_txt, x_raw] = xlsread('特征选择数据集.xlsx'); data = x_raw(2:end, :); X = cell2mat(data(:, 1:end-1)); y = cell2mat(data(:, end));

注意,xlsread在2014a和2019a中都能运行,但如果你手头是2019a,也可以改用readtable,不过readtable读入的是table,需要再table2array。考虑到兼容性,我一般用xlsread加原始单元格读取,避免表头污染。这一步的常见错误是忽略NaN行,后面计算min/max时会把整个分箱逻辑打乱,所以读完数据后最好先清理一下。

3.2 mic_select.m如何给所有特征打分并排序

mic_select的作用是对每个特征计算它和标签的互信息,然后返回得分最高的前k个特征索引。这里我给出一个常见实现:

function selected = mic_select(X, y, k, bins) % X: n×d 特征矩阵 % y: n×1 标签列向量 % k: 需要保留的特征个数 % bins: 分箱数,传给MutualInfo nFeat = size(X,2); scores = zeros(nFeat,1); for j = 1:nFeat scores(j) = MutualInfo(X(:,j), y, bins); end [~, idx] = sort(scores, 'descend'); selected = idx(1:k); % 画一个简单条形图看得分分布 figure; bar(scores(idx)); xlabel('特征序号(按得分降序)'); ylabel('互信息值'); end

逻辑说明:循环逐个特征调用MutualInfo,把得分存成向量,然后sort降序。sort返回的idx是原始列号,所以selected就是按重要性排序的特征索引。之后在main里就可以用X(:, selected)构建新特征矩阵。条形图的横轴是排序后的特征序号,不是原始列号,这样能直观看到得分衰减。

参数说明:k的选择不一定要固定。如果你不确定保留多少特征,可以让mic_select返回完整排序,例如把函数改成输出全部idx,然后在main里用特征个数曲线去决定。这个技巧我放到最后一章展开。另外,如果特征维度很高,循环调用MutualInfo会有一定开销,可以考虑先用相同值剔除或相关性粗筛,把特征数降到几百以内再跑互信息。

3.3 在main.m中把选择结果接到后续建模

选择出特征只是第一步,真正要验证的是“这些特征有没有用”。main.m里通常会接一个简单的分类器或回归器,比如用fitctree或fitcsvm,对比全特征和筛后特征的交叉验证精度。下面是一段可运行的对比逻辑:

idx = mic_select(X, y, 10, 10); % 先选出10个特征 X_sel = X(:, idx); rng(2024); % 5折交叉验证,用决策树作为基准模型 cv_all = cvpartition(y, 'KFold', 5); err_all = zeros(cv_all.NumTestSets,1); err_sel = zeros(cv_all.NumTestSets,1); for i = 1:cv_all.NumTestSets tr = training(cv_all, i); te = test(cv_all, i); mdl_all = fitctree(X(tr,:), y(tr)); mdl_sel = fitctree(X_sel(tr,:), y(tr)); err_all(i) = loss(mdl_all, X(te,:), y(te)); err_sel(i) = loss(mdl_sel, X_sel(te,:), y(te)); end fprintf('全特征误分率:%.3f ± %.3f\n', mean(err_all), std(err_all)); fprintf('MI选择特征误分率:%.3f ± %.3f\n', mean(err_sel), std(err_sel));

逻辑说明:cvpartition生成5折训练/测试索引,循环中分别在全特征和选择特征上训练决策树,loss计算测试集的误分率。注意cvpartition是按标签分布分层的,类别不平衡时比随机拆分更可靠。

参数说明:fitctree是分类树,如果标签是连续的,应该换成fitrtree。rng(2024)固定随机种子,保证实验结果可复现。项目里附的4.png就是跑完这一步之后生成的结果图,你可以在自己的实验里把每次折叠的误差画成箱线图,或者直接打印均值和标准差。对于中间变量,建议在main里加一个表格导出,方便后续引用:

变量含义形状
data原始数值矩阵n×(d+1)
X特征矩阵n×d
y标签向量n×1
idx特征索引k×1
err_sel选择后特征在每折上的误分率5×1

4. 分箱数与核带宽的调参细节:从MutualInfo到p_mkde

4.1 bins的选择:先定范围,再网格搜索

分箱数是互信息估计里最容易被忽略的参数。如果你把bins从5改成15,特征排序结果可能变化很大。原因很简单:bins=5时,一个连续特征被粗粒度化,两个相邻区间内样本数相差不大,互信息容易被低估;bins=15时,每个区间样本变少,联合直方图里的噪声变大,得分虚高。我一般会先按样本量定一个初始值,然后用网格搜索确定,代码类似:

binList = [5 10 15 20]; cvErr = zeros(length(binList), 1); for b = 1:length(binList) idx_b = mic_select(X, y, 10, binList(b)); mdl = fitctree(X(:, idx_b), y); cvErr(b) = crossval('mcr', X(:, idx_b), y, 'KFold', 5, 'Tree', mdl); end [minErr, bestB] = min(cvErr); fprintf('最优bins = %d, 误分率 = %.3f\n', binList(bestB), minErr);

这里把分箱数当成超参,用交叉验证来选,而不是只看互信息值。注意crossval的'Tree'参数需要传入一个fitctree返回的模型,'mcr'表示误分率。如果标签是连续回归任务,可以把'Tree'换成'Reg'并改用'mse'。下面是我常用的参考范围:

bins取值5101520
互信息表现容易低估折中噪声上升高估明显
适用样本量100以下200~10001000~50005000以上

4.2 核带宽h的调整:p_mkde.m中的关键系数

p_mkde.m使用高斯核,带宽h直接决定密度估计的平滑度。我在实际使用中会先用Silverman公式算一个初值,然后乘上一个缩放系数alpha:

n = length(y); h0 = 1.06 * std(y) * n^(-1/5); h = alpha * h0; % alpha通常取0.6~1.4

alpha小于1会保留更多细节,alpha大于1会让曲线更平滑。当特征值域差异很大时,不同特征的h可能差几个数量级,这时不要共用同一个h,而是对每个特征单独估计带宽。一个快速的判断方法是:算出密度后画出曲线,如果曲线在无样本区域出现抖动,就增大h;如果两个明显分离的峰被合并,就减小h。

4.3 连续标签、类别型特征和缺失值:三个很容易踩的坑

第一个坑是标签是连续值时,MutualInfo里的yb线性映射会把连续标签拉成均匀分布,这没问题,但bins的选择需要重新考虑。连续标签建议bins取样本量的立方根附近,比如500个样本取8,1000个样本取10。

第二个坑是类别型特征。如果特征列是0/1/2这样的整数,直接用MutualInfo没问题,但如果是字符串,matlab读入xlsx后就是NaN,必须先grp2idx变成数值编码。注意这种编码是任意的,但互信息对离散编号的单调变换不敏感,因为分箱本质是等宽划分,只要编号的顺序改变了,等宽分箱的结果就会变。所以对类别特征最好用独热编码后再算互信息,避免编码顺序带来的伪梯度。

第三个坑是缺失值。xlsread读入缺失单元格得到NaN,而NaN参与min/max和floor时会产生NaN,导致accumarray报错。我在main里会在开头加一句:

data(any(isnan(data), 2), :) = []; X = data(:, 1:end-1); y = data(:, end);

直接删除含NaN的行。如果你不想删行,也可以用knnimpute补全,但特征选择阶段我倾向删除,因为互信息对缺失值没有天然容忍能力。删除后记得重新打印数据维度,确认样本量没有损失太多。

5. 用交叉验证画特征个数-精度曲线,定位拐点

5.1 让mic_select返回完整排序

固定k值的做法在真实项目里不够用。我们更想知道“如果逐步增加特征,精度什么时候饱和”。所以我会把mic_select改成返回完整排序:

function [scores, idx] = mic_select(X, y, bins) nFeat = size(X,2); scores = zeros(nFeat,1); for j = 1:nFeat scores(j) = MutualInfo(X(:,j), y, bins); end [~, idx] = sort(scores, 'descend'); end

这样调用一次就得到排序,后面可以在main里逐个累加特征。

5.2 在main.m中画出特征个数-误分率曲线

下面这段代码依次取前1到前maxFeat个特征,做5折交叉验证,画曲线:

[scores, idx] = mic_select(X, y, 10); maxFeat = min(size(X,2), 20); % 最多看前20个 errSeq = zeros(maxFeat,1); for f = 1:maxFeat Xsub = X(:, idx(1:f)); mdl = fitctree(Xsub, y); errSeq(f) = crossval('mcr', Xsub, y, 'KFold', 5, 'Tree', mdl); end figure; plot(1:maxFeat, errSeq, 'o-', 'LineWidth', 1.2); xlabel('使用的特征个数'); ylabel('5折交叉验证误分率'); grid on;

运行后你会看到曲线先快速下降,到达某个点后进入平台期,甚至略微上升。上升的那一段就是特征冗余和过拟合的信号。选择平台期开始的位置,比选择曲线最低点更稳妥,因为最低点往往对应一个过拟合的小波动。

5.3 结合互信息值看排序质量

除了画误差曲线,我还会把前几个特征的互信息值打印出来:

for f = 1:maxFeat fprintf('Top%2d: feature %3d, MI=%.4f\n', f, idx(f), scores(idx(f))); end

如果发现前5个特征的MI值明显高于后面,后面的特征基本可以放掉;如果MI值下降得很平缓,说明特征之间信息重叠多,需要再考虑mRMR那种带冗余惩罚的改进,不能只看单变量MI。这时候把生成的精度曲线和特征得分下降图放在一起看,就能确定一个既稳定又解释性强的特征子集。

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

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

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

立即咨询