
简介本资源针对最大互信息特征选择算法提供完整的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和2019afunction 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以内时bins10到15之间比较稳样本量到5000以上可以尝试20。这里给一个快速参考表估计方式优点缺点适合场景分箱直方图速度快、实现简单对bins敏感、边界粗糙特征初筛、大样本Parzen核密度平滑、连续可导计算慢、带宽需要调小样本、精算MI2.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经验公式 h1.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×(d1)X特征矩阵n×dy标签向量n×1idx特征索引k×1err_sel选择后特征在每折上的误分率5×14. 分箱数与核带宽的调参细节从MutualInfo到p_mkde4.1 bins的选择先定范围再网格搜索分箱数是互信息估计里最容易被忽略的参数。如果你把bins从5改成15特征排序结果可能变化很大。原因很简单bins5时一个连续特征被粗粒度化两个相邻区间内样本数相差不大互信息容易被低估bins15时每个区间样本变少联合直方图里的噪声变大得分虚高。我一般会先按样本量定一个初始值然后用网格搜索确定代码类似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公式算一个初值然后乘上一个缩放系数alphan length(y); h0 1.06 * std(y) * n^(-1/5); h alpha * h0; % alpha通常取0.6~1.4alpha小于1会保留更多细节alpha大于1会让曲线更平滑。当特征值域差异很大时不同特征的h可能差几个数量级这时不要共用同一个h而是对每个特征单独估计带宽。一个快速的判断方法是算出密度后画出曲线如果曲线在无样本区域出现抖动就增大h如果两个明显分离的峰被合并就减小h。4.3 连续标签、类别型特征和缺失值三个很容易踩的坑第一个坑是标签是连续值时MutualInfo里的yb线性映射会把连续标签拉成均匀分布这没问题但bins的选择需要重新考虑。连续标签建议bins取样本量的立方根附近比如500个样本取81000个样本取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。这时候把生成的精度曲线和特征得分下降图放在一起看就能确定一个既稳定又解释性强的特征子集。本文还有配套的精品资源点击获取