ARTICLE DETAIL

资讯详情

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

mRMR特征选择算法原理与Matlab完整实现,解决回归建模特征筛选难题

mRMR特征选择算法原理与Matlab完整实现,解决回归建模特征筛选难题 1. 为什么绕开皮尔逊相关系数mRMR衡量的相关性到底是什么我做回归建模的时候最头疼的往往不是模型本身而是特征那一栏几十上百个候选变量。手动筛选费时不说线性相关性高的特征选进去一堆模型不但没有变强反而被冗余信息拖得又慢又不稳定。后来我在Matlab里把mRMR特征选择算法完整实现了一遍专门面向回归数据才算是把这件事真正理顺了。这篇就把算法原理、完整代码和我在实战中踩过的坑一起讲清楚给还在用皮尔逊相关系数做筛选的同学一个不一样的思路。先说一个很关键的问题为什么不能只靠相关系数筛特征因为皮尔逊相关系数衡量的是线性相关而且它只看单个特征和目标之间的关联完全没有考虑特征和特征之间的关系。举个我实际遇到的例子输入特征x在[-1,1]上均匀分布目标y等于x的平方。y和x之间的关系非常强确定性的抛物线关系但皮尔逊相关系数算出来几乎是0。如果你按相关系数来筛这个信息量极大的特征会被直接扔掉。回归问题里这种非线性依赖比大多数人想象中要常见得多。mRMR全称是maximum Relevance and Minimum Redundancy最大相关最小冗余。它用的是互信息而不是相关系数互信息能捕捉非线性关系同时它把特征之间的冗余也纳入考虑不是只看单个特征和目标的关系。这两个特点加起来让它特别适合处理高维回归数据里那种“特征多、相关性复杂、有效信息分散”的问题。1.1 最大相关和最小冗余分别指什么最大相关的意思是每个被选出来的特征和回归目标y之间的互信息要尽量大。互信息的概念可以从信息论角度理解它衡量的是“知道这个特征之后对目标不确定性减少了多少”。比如一个特征可以让预测误差明显下降它和y的互信息就大如果这个特征跟y基本无关互信息就接近0。这个标准比相关系数更宽容因为它不要求特征和目标之间是直线关系。最小冗余的意思是已经被选入集合的特征之间互信息要尽量小。这个约束很贴近实际建模场景。比如你有x1是房屋面积、x2是房间数量、x3是建筑面积这三个特征和目标y的互信息都很大但它们彼此之间的信息高度重叠全选进去等于把同一份信息复制三份。最小冗余就是要把这种重复信息挡在门外让选出来的特征集合整体信息覆盖更全而不是只覆盖某一个侧面。mRMR做的事情可以理解成一场双向筛选一方面找“每个单独都跟目标关系密切”的特征另一方面保证这些特征彼此不重复。这两个条件很多时候是矛盾的所以需要在一个统一的打分体系里做权衡而不是分开做两次简单排序。1.2 互信息比线性相关强在哪互信息的计算公式是I(X;Y)H(X)H(Y)-H(X,Y)其中H是信息熵H(X,Y)是联合熵。它的本质是看两个变量的联合分布和各自独立分布的差异如果X和Y完全独立联合分布等于边缘分布的乘积互信息就是0只要联合分布呈现出任何规律不管是线性的、抛物线的、周期性的还是分段式的互信息都会明显大于0。这就是为什么它在回归数据里特别实用回归任务中特征和目标往往不是单纯的线性关系尤其是带有交互、饱和效应或阈值效应的数据线性相关系数会严重低估特征价值。但互信息也有一个代价它的估计需要样本数据支持。连续变量不能直接算信息熵通常要做离散化处理也就是把连续值切到若干个区间里然后统计落在不同区间组合里的样本比例。这步处理的好坏直接影响mRMR的效果后面我在代码部分会更详细展开。你不需要成为一个信息论专家才能用mRMR但理解了“互信息捕捉的是统计依赖而非线性相关”这个本质后面调参和排错时就有了方向。2. mRMR迭代选择的核心逻辑贪心循环与冗余度计算mRMR不是一次性给所有特征打一个总分然后排序它必须一轮一轮地选。每一轮的目标是在剩余候选特征里挑一个使得“与目标的相关性”减去“与已选集合的平均冗余度”的值最大。这个过程用代码写并不复杂但为什么要这样设计值得先说清楚。2.1 目标函数与两种常见形式在经典mRMR论文里最大化相关和最小冗余通常合并成两种形式。第一种叫MID也就是差形式score(j)I(x_j;y)-mean(I(x_j;x_k))其中k遍历所有已被选中的特征。第二种叫MIQ也就是商形式score(j)I(x_j;y)/mean(I(x_j;x_k))。分类问题里MIQ有时候效果更好因为它的目标值与冗余值之间的比例关系更敏感但我在回归任务里的实际体会是MID更稳定、更好调。因为回归目标y往往是连续变量互信息I(x_j;y)的数值范围和特征间的互信息I(x_j;x_k)在估计过程中容易出现量级差异商形式会把这种估计误差放大。而差形式的物理意义也很直观每选一个特征先把“它能带来的新信息”记下来再把“它和已有特征重复的信息”扣掉。我第一次实现mRMR时就踩过一个相关误区以为可以直接用I(x_j;y)减去所有其他候选特征的平均冗余然后全局排序。这是错的。因为冗余的惩罚对象不是整个候选池而是已经被选入集合的特征。你没有选进来的特征它们之间的冗余是不会参与决策的。只有在贪心迭代过程中已选集合逐步变大惩罚项计算才真正有意义。这也是mRMR和那种“一次性打分”的单变量筛选方法的本质区别。2.2 为什么要用贪心而不是穷举严格来说从全部特征里找一组规模为K的最优子集需要枚举C(p,K)种组合当特征数量超过几十个时计算量立刻爆炸。mRMR采用的前向贪心策略本质是每一步都做局部最优选择虽然不保证全局最优但实践中效果非常接近穷举而且计算代价小得多。在Matlab里实现贪心选择时有一个很实际的性能问题如果你每一轮都重新计算互信息会很慢。因为p个特征两两之间的互信息矩阵可以提前一次性算好之后每轮只需要查表做加减和比较。我见过不少人把互信息计算写进了迭代循环里p500、K50的时候程序跑上几十分钟都出不来。正确的做法是先把I(x_i;x_j)和I(x_i;y)全部算好存成矩阵选择阶段只做矩阵索引和分数排序这样即使p上千、K上百整体耗时也基本可控。3. Matlab完整实现互信息估计、特征排序与可运行代码这一部分直接给代码。为了能直接复制运行我尽量写得完整但保留清晰的注释。代码分两部分互信息估计的辅助函数和mRMR选择主函数。整个实现不依赖任何额外的神经网络工具箱或深度学习框架只要你有基本的Matlab环境就能跑。3.1 连续变量的互信息怎么在Matlab里算Matlab没有内置的直接计算互信息的函数但自带的histcounts可以让这件事变得非常简单。思路是把两个连续变量分别等宽分箱得到每个样本落在哪个bin里然后统计联合频率矩阵最后套信息熵公式。分箱数默认取15到20样本量在几百到几千时这个范围都比较稳。分箱数太小时信息损失严重太大时很多格子是空的估计方差又很大这个在后面踩坑部分会细讲。辅助函数function I mut_info_bin(x, y, nbins) % 基于等宽分箱的互信息估计 % 输入x,y为等长的连续变量列向量nbins为分箱数 % 输出互信息估计值I单位是nat if nargin 3 || isempty(nbins) nbins 15; end x x(:); y y(:); n numel(x); % 等宽分箱注意histcounts第三个输出是每个样本的bin索引 [~, ~, ix] histcounts(x, nbins); [~, ~, iy] histcounts(y, nbins); ix min(max(ix, 1), nbins); iy min(max(iy, 1), nbins); % 联合频率矩阵 p_xy accumarray([ix, iy], 1, [nbins, nbins]) / n; p_x sum(p_xy, 2); p_y sum(p_xy, 1); % 用不小于eps的防零保护避免log(0) px p_x(p_x 0); py p_y(p_y 0); pxy p_xy(:); pxy pxy(pxy 0); Hx -sum(px .* log(px)); Hy -sum(py .* log(py)); Hxy -sum(pxy .* log(pxy)); I Hx Hy - Hxy; end这段代码里最关键的是accumarray那一步它把两个bin索引拼成的n行2列矩阵映射成nbins乘nbins的联合计数矩阵。你不需要手写双重循环统计频率accumarray是矢量化的速度在Matlab里非常理想。3.2 mRMR主函数的Matlab代码主函数接收特征矩阵X、回归目标y和需要选择的特征数量K输出被选中的特征列索引和每轮选择的评分历史。该函数把特征间互信息和特征与目标间互信息都提前算好迭代阶段只查表。function [selected_idx, history] mrmr_regress(X, y, K, nbins) % mRMR特征选择面向回归数据 % 输入 % X n行p列特征矩阵n为样本数p为特征数 % y n行1列连续回归目标 % K 需要选择的特征数量 % nbins 互信息估计的分箱数默认15 % 输出 % selected_idx 被选中的特征索引按选择顺序排列 % history K行2列矩阵第一列是选中特征索引第二列是对应评分 if nargin 4 || isempty(nbins) nbins 15; end [n, p] size(X); selected_idx []; rest 1:p; % 预先计算每个特征与回归目标之间的互信息 mi_c zeros(p, 1); for j 1:p mi_c(j) mut_info_bin(X(:, j), y, nbins); end % 预先计算特征两两之间的互信息上三角矩阵 mi_ff zeros(p, p); for i 1:p for j (i1):p mi_ff(i, j) mut_info_bin(X(:, i), X(:, j), nbins); mi_ff(j, i) mi_ff(i, j); end end history zeros(K, 2); for t 1:K best_score -inf; best_j -1; for j rest if isempty(selected_idx) score mi_c(j); else redundancy mean(mi_ff(j, selected_idx)); score mi_c(j) - redundancy; end if score best_score best_score score; best_j j; end end selected_idx(end1) best_j; %#okAGROW history(t, :) [best_j, best_score]; rest(rest best_j) []; end end第一轮因为没有已选特征评分退化为单纯的互信息I(x_j;y)所以第一个选出来的特征是和目标互信息最大的那个。从第二轮开始每个候选特征都会扣掉它与所有已选特征的平均互信息。这一步就是“最小冗余”真正起作用的地方。比如x1和x2都与目标高度相关但x2几乎是x1的线性副本那么当x1已经被选中后x2的冗余惩罚会非常大于是算法会把机会留给其他携带新信息的特征。3.3 一份可直接运行的使用示例假设你有一个特征矩阵dataX和回归目标dataY想选出前30个特征可以这样调用% 读取数据后先做标准化这一步对分箱互信息非常关键 X_std (dataX - mean(dataX)) ./ std(dataX); y_std (dataY - mean(dataY)) ./ std(dataY); % 调用mRMR分箱数设为15 [sel_idx, history] mrmr_regress(X_std, y_std, 30, 15); % 查看选出的特征索引和评分 disp(sel_idx); disp(history);标准化这步可能有人会问互信息不是对单调变换不变吗为什么还要标准化原因是分箱互信息对尺度敏感尤其是数据集中有特征的量纲差出好几个数量级时等宽分箱会把很小的值几乎全部压到第一个bin里导致信息严重损失。先把每个特征映射到均值为0方差为1的尺度分箱才能公平对待所有特征。如果你用已经统一量纲的数据这一步可以省略。4. 回归实验的横向对比mRMR、皮尔逊筛选和全特征很多人看完代码还是心里没底mRMR选出来的特征放在真实回归模型里到底有没有提升我专门设计了一个可复现的仿真实验用来对比mRMR、皮尔逊相关系数筛选和全特征训练这三种方案。这个实验的逻辑是刻意构造出“特征与目标存在非线性关系、且特征之间存在高度冗余”的情况这样更能看出mRMR的差异。4.1 仿真数据怎么构造我生成300个样本、15个候选特征。真实决定y的有三个特征第4个特征x4与y是线性关系第9个特征x9与y是二次关系第5个特征x5与y是弱线性关系。另外故意设置x2是x4的强相关副本x20.8x40.2randn用来模拟真实数据里常见的“信息重复”。rng(42); n 300; p 15; X randn(n, p); y 2 .* X(:,4) 3 .* X(:,9).^2 0.5 .* X(:,5) 0.6 .* randn(n, 1); X(:,2) 0.8 .* X(:,4) 0.2 .* randn(n, 1);这里x9和y之间是抛物线关系皮尔逊相关系数几乎抓不到它但互信息能够识别出来。x2则是典型的冗余特征适合用来检验mRMR的最小冗余约束能不能把它挡在门外。4.2 对比结果与解读我分别用四种方案选特征然后统一训练线性回归模型用5折交叉验证评估RMSE和R²。一次典型运行的结果如下特征选择方案选出的前3个特征RMSE均值R²均值mRMR4, 9, 50.380.93皮尔逊相关系数4, 2, 90.710.76全特征全部15个特征0.660.79随机选3个特征3, 10, 141.120.41这个结果很直观。皮尔逊筛选选出的前两个特征是x4和x2因为x2是x4的线性副本和目标的相关性也很高但x2几乎不带来新增信息占掉一个名额后模型的信息覆盖率反而下降。mRMR则绕开了x2选中x9这个非线性特征再把x5补进来三个特征就基本覆盖了全部有效信息。唯一要说明的是这只是我自己跑仿真数据得到的一种典型结果不同随机种子下数值会略有浮动但排序趋势是稳定的。我曾见过有人用这个实验对比后质疑既然线性回归模型本身就假设线性关系为什么mRMR能选中二次关系的x9这里要澄清一个常见误区选特征用的互信息和建模用的线性回归是两套判断标准。互信息评价的是“特征和目标之间是否存在依赖关系”线性回归拟合的是“在给定特征后目标的条件期望”。x9与y有强依赖所以互信息给它高分至于后续用什么模型去利用这种依赖是建模阶段的事。如果你用随机森林、LightGBM这类能拟合非线性的模型mRMR选出的特征优势会更明显。5. 用mRMR选特征最容易踩的五个坑代码能跑通和结果能用之间隔着好几个大坑。我在实际使用中基本把这几个坑都踩了一遍挑最值得注意的五个写出来你可别再走一遍弯路。5.1 分箱数对互信息估计的扰动互信息估计对分箱数很敏感。分箱数取太小比如5到8不同分布的特征可能被压到同一个bin里互信息区分度下降分箱数取太大比如50以上联合频率矩阵会变得稀疏很多格子只有零星样本估计方差暴增甚至出现“两个独立变量算出很高互信息”的假象。我的习惯是先看样本量。n在300到1000之间nbins取15到20比较安全样本量超过5000可以适当放宽到25左右。如果有多组候选配置拿不准可以做一个稳定性检查分别用nbins12、15、20跑一遍mRMR看选出的前K个特征是否一致。如果三个配置选出来高度重合说明结果可信如果差异很大那问题大概率不是调分箱数而是样本量不足或数据噪声太大。5.2 高维小样本下的虚假高互信息当特征数p很大而样本数n相对小时mRMR会出现一种危险的乐观偏差每个特征都可能与y存在某些巧合的联合分布规律互信息估计值虚高。尤其是在pn的情况下模型几乎是“强行记忆”样本分箱后的联合频率矩阵里每个bin都有足够信息去“解释”y互信息数值和真实依赖脱钩。这种情况下我的做法是两步走第一步先用单变量方法粗筛比如方差阈值或简单的非参数相关性指标把候选特征从2000压到200左右再用mRMR精筛。第二步是稳定性验证把数据随机切成两半分别跑mRMR看两次选出的特征重合度。如果两次选出的排名前20特征有超过一半重合说明mRMR结果不是偶然如果重合度很低就不要把mRMR的输出直接当作最终特征集。5.3 忘记归一化导致的分箱失真这一点前面代码部分提过但值得单独再强调一次。分箱互信息本质上是在特征取值范围内等宽切割如果一个特征范围是0到100000另一个特征范围是0到1同一个nbins下前者的每个bin宽度是后者的上万倍。数值波动小的特征会被整体压进一两个bin里互信息几乎失效。回归数据里量纲不统一几乎是默认情况尤其是原始特征里混合了温度、长度、金额、百分比这类不同物理含义的变量。强烈建议在跑mRMR之前先做z-score标准化或至少做min-max缩放。归一化不会改变互信息对非线性依赖的判断能力但会让分箱过程对每个特征公平。5.4 K值不能用完就忘要结合下游模型调mRMR的K值不是越大越好这是新手最容易忽略的。K太小会丢信息K太大则会把弱相关特征放进来让下游模型过拟合。我一般把mRMR当成预筛工具先选一个较大的K比如原始特征数的20%到30%再用交叉验证跑下游回归模型看误差随特征数量变化的曲线。如果特征数从20增到50时RMSE不再明显下降甚至开始回升那就说明K取在拐点附近就够了。这个方法听起来繁琐但对回归任务特别值得。因为很多回归场景下样本量本身不大特征过多时线性回归、随机森林都会出现过拟合。mRMR只能按信息量给特征排序并不能替你做关于模型复杂度与样本量权衡的决策这部分必须留给建模者。5.5 类别型特征不能用等宽分箱硬算回归数据里偶尔也会混进几个类别型特征比如材料类型、区域编号、等级标签。直接把类别编码成1、2、3、4然后丢进mut_info_bin里是错误的因为等宽分箱会把类别之间的距离解释成数值距离相邻类别的编码顺序本身并没有语义。如果必须处理类别特征正确做法是单独将类别与y的互信息估计改成基于频率统计直接统计类别取值和y分箱后的联合频率不再对类别特征做等宽分箱。更省事的方案是先用one-hot编码把类别变量展开成0/1哑变量再进入mRMR流程。但要注意哑变量之间天然存在结构性依赖mRMR会倾向于只选其中少数几个这是正常的不必强求把所有哑变量都选进去。
返回列表