☰
MATLAB实现MRMR与ReliefF特征选择方法详解与实战
2026/10/30 1:42:12 网站建设 项目流程

简介:一套MATLAB实现的特征选择工具包,面向机器学习和数据挖掘学习者,解决高维数据中冗余特征筛选问题。内含MRMR与ReliefF两种经典算法:前者基于互信息最大化相关并最小化冗余,适合处理大规模特征集;后者通过近邻距离评估特征权重,计算效率较高,两者均以函数形式封装,可直接调用。包内共二十一个文件,主体为m脚本,便于阅读和修改;同时附带C++源码及头文件,以及适用于Windows、Linux、Mac等平台的多种MEX编译文件,确保跨环境运行,压缩包整体仅约一百零一千字节。已有二百六十五人参与学习下载。通过源码可深入理解两种特征的评分机制,并在分类、回归任务中动手实践特征筛选;同时可作为毕设或竞赛的基础工具,帮助提升模型性能与编程能力。

1. 特征选择先于建模:MRMR 和 ReliefF 到底在解决什么问题

做分类建模时,最怕的不是模型选错,而是特征太多。几百上千维的数据直接丢进分类器,训练慢是一回事,冗余特征还会把模型带偏,测试集上的泛化能力直线下降。特征选择就是在训练前把真正有用的维度挑出来,这一步做对了,后面模型调参都能省一半功夫。这份「matlab MRMR和relieff特征选择方法」资源包,把两种主流特征选择算法封装成了可直接运行的 MATLAB 函数:MRMR 用互信息平衡相关性与冗余,ReliefF 用近邻差异给特征打分,选特征的同时还自带加速用的 mex 文件。适合正在做毕设、竞赛特征工程,或者论文里需要对比多种特征选择方法的从业者。下面我会从原理、调用、参数设置一路拆到踩坑记录,照着跑就能用。

2. 拆 MRMR 实现:互信息计算、增量选择与 fsMRMR 调用

2.1 先认识一下 MRMR 在算什么

MRMR(Maximal Relevance and Minimal Redundancy)的核心目标一句话就能说清:选出来的特征既要跟目标变量 y 高度相关,彼此之间又别太像。两个「相关性」都用互信息(Mutual Information, MI)来度量,定义是:

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

互信息衡量的是一个变量的取值在多大程度上携带了另一个变量的信息。它比线性相关系数强的地方在于:线性相关系数只捕捉线性关系,而互信息对非线性关系同样敏感。比如一个特征跟标签之间是正弦曲线的关系,皮尔逊系数可能算出来接近 0,但互信息能给出明显的非零值。所以 MRMR 在处理真实表格数据时,比单纯按方差或卡方筛选要可靠不少。

MRMR 的搜索策略是贪心增量,不是暴力枚举所有特征子集。第一步先选与 y 互信息最大的那个特征,之后每一步从剩余特征里,挑「与 y 互信息大、与已选特征平均互信息小」的特征。这里面有两种折中方式:

  • MID(Mutual Information Difference):score = rel - red,直接相减
  • MIQ(Mutual Information Quotient):score = rel / red,直接相除

两种打分各有边界。MID 实现简单、数值稳定,大多数场景下够用;MIQ 对相关性弱的特征更友好,但 red 接近 0 时得分会爆炸,实际使用要加个极小常数保护。资源包里的 fsMRMR.m 核心就是这个增量循环,后面我会把循环逻辑拆开看。

2.2 文件包里哪些文件是 MRMR 的

解压后fs_sup_mrmr目录下的文件可以分成三组。第一组是算法入口:

文件作用
fsMRMR.m增量选择主入口,调用底层互信息函数完成任务
fsMRMR_info.m用信息熵直接估计互信息的变体
fsMRMR_parson.m用 Parzen 窗估计概率密度的变体,对分布不规整的特征更稳

第二组是互信息计算的底层支撑:

文件作用
mutualinfo.m互信息的 MATLAB 封装入口
estpab.cpp联合概率分布表估计的 C++ 源码
estmutualinfo.cpp互信息最终计算的 C++ 源码
miinclude.h、elementmexheader.h编译所需的头文件
makeosmex.m重新编译 mex 二进制的脚本

第三组是已经编译好的 mex 二进制,后缀区分平台:.mexw64是 Windows 64 位,.mexa64是 Linux 64 位,.mexglx是老版 Linux 32 位,.mexmac是老版 Mac,.dll是老版 Windows。看到这么一堆后缀就该有心理准备:这个包跨了多个平台,拿到手后第一步要确认当前 MATLAB 平台跟 mex 后缀匹配,不匹配就得重新编译。这个坑我在第 4 章专门展开。

2.3 最小可跑通的调用代码

先写一个最简单能跑的调用,用随机数据验证流程:

addpath('fs_sup_mrmr'); % 把 MRMR 目录加进搜索路径 X = randn(120, 60); % 120 个样本、60 维特征 y = randi([0 1], 120, 1); % 二分类标签,注意保持列向量 k = 15; % 期望选出的特征数量 [selected, score] = fsMRMR(X, y, k); disp(selected); % 按增量选入顺序排列的特征下标 disp(score); % 每个特征被选中时的 MRMR 得分

这里有个容易混的点:X 必须是每行一个样本,每列一个特征,和 sklearn 的惯例一致。y 必须是列向量,如果传成行向量,函数内部按列处理时维度就对不上。selected里的顺序不代表特征重要性排序,而是增量选择被选入的先后顺序——第一个元素是单变量互信息最大的特征,后面的元素是在冗余惩罚下选出来的。想看最终的优先级排名,需要自己根据业务需求重新排。

如果调用fsMRMR_info或fsMRMR_parson,输入输出格式和fsMRMR保持一致,只是互信息估计方式不同。我一般先把三个都跑一遍,对比选定特征集合的重合度,重合度高的说明结果稳定,重合度低就要想想数据里的噪声是不是太大。

2.4 从 mex 到 MATLAB 层:互信息是怎么算出来的

互信息计算是整个 MRMR 的性能瓶颈,也是这个包选择用 C++ 写底层的原因。纯 MATLAB 循环遍历所有样本做直方图统计,特征一多就慢得没法用。mutualinfo.m是封装入口,内部会调用编译好的estpab先估计联合概率分布表,再由estmutualinfo汇总出互信息值。

单独验证一对特征的互信息可以这样调:

% 手动计算两个特征之间的互信息 mi = mutualinfo(X(:, 3), X(:, 7), 10); % 10 是直方图分箱数

第三个参数n_bins控制直方图的分箱数量,直接影响互信息估计的精度。样本量在 100 到 500 之间时,我一般取 8 到 12;样本量超过 1000 可以放宽到 16 到 20。分箱数太小,概率分布被压得太粗,互信息会被低估;分箱数太大,每个箱子里样本太少,统计噪声又会上来。这个参数值得用交叉验证试一轮,它对 MRMR 选出的特征集影响不小。

2.5 增量选择的循环逻辑:MID 与 MIQ 两种打分

fsMRMR.m 的核心是一个双重循环。外层循环要选 k 个特征,内层循环遍历所有剩余特征,计算每个候选特征的 rel 和 red 值。核心逻辑示意如下:

% 增量选择的示意,fsMRMR.m 内部做的就是这个事 selected = []; for t = 1:k bestscore = -Inf; best = 0; for i = 1:size(X, 2) if any(selected == i) continue; % 已选特征跳过 end rel = mutualinfo(X(:, i), y); % 与目标的互信息 if isempty(selected) red = 0; % 第一个特征没有冗余 else red = 0; for j = selected red = red + mutualinfo(X(:, i), X(:, j)); end red = red / numel(selected); % 与已选特征的平均冗余 end s = rel - red; % MID 打分;MIQ 用 rel / (red + eps) if s > bestscore bestscore = s; best = i; end end selected(end + 1) = best; end

这段代码的复杂度特征很明显:每选一个新特征,都要重新计算剩余特征与目标、与所有已选特征的互信息。特征数 p 越大,内层循环越长,总开销接近 O(k·p²)。这也是为什么高维数据上 MRMR 跑得慢——不是 MATLAB 慢,是算法本身就是这个量级。如果想用 MIQ,把s = rel - red换成s = rel / (red + eps)就行,eps是浮点极小值,防止除以零。

3. 拆 ReliefF 实现:近邻遍历、权值更新与交叉验证版

3.1 ReliefF 的权值更新逻辑

ReliefF 的思路和 MRMR 完全不同,它不看信息论,而是从样本的局部结构出发。核心假设是:一个好的特征,应该让同类样本在这个特征上取值相近,让异类样本在这个特征上取值差异明显。基于这个假设,算法通过反复抽样样本、查找近邻、累加权值来给每个特征打分。

形式化的更新规则是:重复 m 次,每次随机抽一个样本 R_i,在样本集中找它的 k 个同类近邻 H_j 和 k 个异类近邻 M_j。对每个特征 A,按下面的规则更新权值:

W(A) = W(A) - Σ_j diff(A, R_i, H_j) / (m·k) + Σ_j diff(A, R_i, M_j) / (m·k)

diff 是特征 A 在两个样本之间的归一化差异。连续特征直接做|x1 - x2| / (max - min),离散特征则取值相同记 0、不同记 1。累加到最后,W(A) 为正且越大,说明这个特征在区分不同类别上越有贡献;W(A) 为负,说明这个特征在同类别样本内部跳动得厉害,反而干扰分类。

这个算法的好处是直观,输出天然就是每个特征的权值排名,不像 MRMR 要自己解析增量顺序。但它有一个隐性约束:依赖「同类近邻」和「异类近邻」的概念,所以它是为分类任务设计的。回归任务没有类别标签,ReliefF 直接不适用,这一点在选型时要记牢。

3.2 调用 fsReliefF:拿到排名与权值

fs_sup_relieff目录下的主函数是fsReliefF.m,最小调用代码如下:

addpath('fs_sup_relieff'); % 加入 ReliefF 目录 [ranked, weight] = fsReliefF(X, y, 10); % 第三个参数是近邻数

输出两个变量:ranked是按权值降序排列的特征下标,ranked(1)就是最有区分度的特征;weight是每个特征对应的权值,与ranked一一对应。第三个参数 10 是近邻数 k,不是要选的特征数——这个跟 fsMRMR 的第三个参数含义完全不一样,用的时候千万别混。选多少个特征由你自己根据ranked截取,比如取前 15 个:top_features = ranked(1:15);

ReliefF 对连续特征和离散特征都可以处理,但连续特征不同量纲对 diff 的影响很大。如果特征里既有年龄(0-100)又有收入(0-100000),收入维度会主导近邻距离计算,选出来的特征排名基本被量纲大的特征霸占。我一般喂数据之前先做一遍 zscore 标准化,让每个连续特征都在同一尺度上再跑 ReliefF。

3.3 近邻数 k 与迭代次数 m 怎么调

fsReliefF的调用参数看起来简单,真正要调的参数藏在函数内部。近邻数 k 和迭代次数 m 是影响结果的两个关键量:

参数位置建议值
近邻数 k调用时的第三个参数默认 10;样本量小于 200 时降到 3~5;特征噪声大时可以试 15~20
迭代次数 m函数内部实现原始实现默认遍历全体样本;样本量几千时建议改到 300~500 次随机抽样

k 太小,近邻对噪声敏感,单次抽样带来的权值波动大;k 太大,近邻里混入距离较远的样本,同类近邻和异类近邻的区别被模糊化。m 的值直接影响稳定性和耗时的平衡。样本量大时跑全量重采样很慢,我一般会把 m 改成固定次数,比如 500,结果和全量采样差别不大,但速度能快好几倍。

类别不平衡是另一个容易被忽略的坑。ReliefF 抽样时从多数类抽到的概率天然更高,少数类的信息在权值里会被稀释。常见做法是先用datasample做分层采样,让各类别样本量大致持平,再喂给fsReliefF,这样多数类不会主导权值更新。

3.4 用 fsReliefF_cv_sv 做交叉验证评估

fs_sup_relieff目录里还有一个fsReliefF_cv_sv.m,从文件名可以看出它是 ReliefF 和交叉验证、支持向量机封装的组合。它的价值在于:不是只看特征排名,而是直接把「选完特征 → 训练分类器 → 交叉验证」这个流程串起来,输出一个分类精度指标,用来判断特征子集的真实效果。

我拿到手的第一件事是先看函数头确认签名:

help fsReliefF_cv_sv % 确认输入输出格式再传参

这个函数内部封装的是 SVM,使用时注意两点。第一,如果数据是多分类,要确认源文件里的 SVM 是否支持多分类,不支持就得自己改造成一对一策略。第二,交叉验证的折数要跟数据量匹配,几百样本做 5 折,几千样本做 10 折,折数太多单折样本过少,评估方差会变大。

4. 绕开三处暗坑:mex 文件不匹配、编译失败与数据格式问题

4.1 mex 文件平台不匹配:最常见的翻车点

现象:运行 fsMRMR 报Invalid MEX-file 'estmutualinfo.mexw64',或者报Undefined function 'estmutualinfo' for input arguments of type 'double'。第一次遇到这种报错很容易懵,以为代码有问题,其实问题出在二进制文件上。

原因:这个包同时带了.mexw64、.mexa64、.mexglx、.mexmac、.dll多种平台的编译产物,MATLAB 只会加载与当前系统匹配的那一个后缀。比如 Windows 64 位应该用.mexw64,但如果你从压缩包里拷贝文件时手抖,或者杀毒软件隔离了部分文件,MATLAB 找不到对应平台的 mex,就会用这个含糊的报错提示你。

解决:先确认当前平台,再重新编译。

disp(computer); % 输出如 PCWIN64 / GLNXA64 / MACI64

确认平台后,把不匹配后缀的 mex 文件从 MATLAB 路径里移走,然后运行makeosmex.m重新编译生成当前平台的二进制文件。注意杀毒软件有时会把.mexw64当可疑文件拦截,解压时放行一次就好。

4.2 编译失败:先mex -setup

现象:运行makeosmex时直接报错No supported compiler was found,或者编译到一半报一堆 C++ 头文件的语法错误。

原因:MATLAB 的 mex 编译依赖外部 C++ 编译器。新版 MATLAB 在 Windows 上常用 MinGW-w64,老版本需要 Visual Studio 或者 lcc。系统里没装这些编译器,或者装了但 MATLAB 没识别到,makeosmex就跑不起来。

解决:编译之前先手动指定编译器。

mex -setup C++ % 弹出编译器列表,选中已安装的那个 makeosmex % 再跑编译脚本

编译成功的标志是当前目录出现新的.mexw64或.mexa64文件,时间戳是刚刚生成的。如果编译栈报的是 C++ 语法错误,优先怀疑编译器版本太老——C++11 的基本语法在老编译器上过不去,换个新版本 MinGW-w64 基本能解决。

4.3 数据格式问题:行向量、NaN 与逻辑型标签

现象:fsReliefF能跑完但weight全为 NaN,或者fsMRMR报错行数不一致。

原因:本质是输入数据格式不满足函数的隐性约定。常见三种情况:y 传成了行向量,函数内部按列处理时维数错位;X 里混入了 NaN 或 Inf,直方图统计和距离计算直接失效;y 是逻辑型 true/false,diff 计算时被当成数值但边界行为异常。

解决:数据进函数之前先做统一清洗,这是我每次跑特征选择的习惯动作:

X = double(X); X(isnan(X)) = 0; X(isinf(X)) = 0; y = double(y(:)); % 强制转成 double 列向量

这套操作放在脚本最前面,能省掉大量定位问题的时间。特别是从表格文件读进来的数据,NaN 几乎是常态,不处理就跑特征选择,结果里出现 NaN 一点都不奇怪。

4.4 特征数 k 越界与高维下的运行效率

现象:设置 k 大于特征总数时报Index exceeds matrix dimensions;特征上千维时 MRMR 跑几分钟不出结果。

原因:k 越界是没做边界保护,MRMR 的增量循环访问到不存在的特征下标;高维慢是因为 MRMR 的互信息对计算是 O(k·p²) 量级,p 越大开销增长越猛。

解决:传参前加保护,高维数据先粗筛再精筛:

k = min(k, size(X, 2)); % 防止越界 v = var(X); X = X(:, v > 1e-6); % 先剔除零方差特征

零方差特征对分类没有任何信息量,留着只会拖慢互信息计算。粗筛之后再跑 MRMR,耗时可能掉一个数量级,而选出来的特征集几乎不受影响。如果粗筛后特征还是上千,考虑先用卡方检验或方差选择砍到几百,再交给 MRMR 精筛,这是高维表格数据的标准操作。

5. 两种方法怎么选:从数据规模、特征冗余到最终模型评估

5.1 一张参数对比表

把两种方法放在同一张表里对比,选型时会清晰得多:

维度MRMRReliefF
打分依据互信息相关性 + 冗余惩罚近邻距离差异的权值累计
输出形式按增量顺序的特征下标按权值降序的完整排名
冗余控制显式惩罚与已选特征高互信息的特征隐式,通过近邻距离间接体现
非线性关系互信息可直接捕捉近邻距离间接捕捉
回归任务适合,互信息不依赖类别不适合,依赖同类/异类近邻
计算开销高,特征多时近似平方增长较低,主要开销在近邻搜索
适用规模高维、冗余强的结构化数据小到中等规模、需要快速排名

这里最值得记住的是最后两行。MRMR 的显式冗余机制在特征高度相关的场景里优势明显,但代价是计算量;ReliefF 胜在快和直观,但它的结构性弱点在特征维度极高时会被放大——因为近邻距离是在全体特征上计算的,无关维度太多会把近邻关系扯歪。

5.2 三种典型场景的选择建议

第一种场景:特征几百维、彼此高度相关,比如图像特征、基因表达数据。这种数据冗余是主要矛盾,优先上 MRMR,它能主动避开和已选特征高度相似的候选,选出来的特征集更精简。

第二种场景:样本两三千、特征几十维,目标只是快速拿到一个可解释排名。这种规模下 ReliefF 几十秒就能出结果,而且权值直接代表区分度,解释起来比 MRMR 的增量得分更直观。

第三种场景:回归任务。MRMR 的互信息计算根本不依赖类别标签,直接用;ReliefF 的同类/异类近邻概念在回归里没有定义,不要硬上。

5.3 两套方法同场对比的评估流程

实际项目里我一般不会只信一种方法,而是两个都跑,再用同一个分类器做交叉验证对比。这样选型结论才有数据支撑:

rng(42); % 固定随机种子,让两次对比的折划分一致 addpath('fs_sup_mrmr'); addpath('fs_sup_relieff'); [idx_m, ~] = fsMRMR(X, y, 30); [idx_r, ~] = fsReliefF(X, y, 10); nsel = 15; % 两边都取前 15 个特征 acc_m = crossval(@(xtr,ytr,xte,yte) ... mean(fitcknn(xtr,ytr,'NumNeighbors',5).predict(xte) == yte), ... X(:, idx_m(1:nsel)), y, 'KFold', 5); acc_r = crossval(@(xtr,ytr,xte,yte) ... mean(fitcknn(xtr,ytr,'NumNeighbors',5).predict(xte) == yte), ... X(:, idx_r(1:nsel)), y, 'KFold', 5); fprintf('MRMR acc: %.3f, ReliefF acc: %.3f\n', acc_m, acc_r);

关键细节是rng(42)要放在交叉验证之前,这样两次对比的折划分完全一样,唯一的变量就是特征子集。如果忘了固定随机种子,两次精度差异可能只是折划分不同带来的噪声,对比就不公平了。

5.4 特征数 k 的取值经验:拐点图与 log2 经验值

特征数量选多少,这个问题的答案不能拍脑袋。两个经验可以结合使用。

第一个经验是信息论粗略估计:k = ceil(log2(p)),p 是特征总数,以这个为起点再上下搜索。比如 500 维特征,log2 约 9,那就在 8 到 15 之间试一轮。

第二个经验是看权值分布的拐点。ReliefF 跑完之后把权值降序画出来,如果曲线从某个位置开始明显变得平缓,说明后面的特征边际收益很小:

[~, w] = fsReliefF(X, y, 10); plot(sort(w, 'descend'));

找到拐点后,在拐点前后各取几个 k 值做交叉验证,选精度最高的那一个。这个「曲线找拐点 + 交叉验证精调」的组合方式,比我之前直接套固定 k 值稳定得多。

6. 把特征选择嵌进建模流程:一条可以复用的完整链路

6.1 一个可以直接改的完整脚本

前面的原理和参数都过了一遍,最后给一条完整链路,把清洗、粗筛、双方法对比、交叉验证串成一个函数,拿到自己的数据上改一行就能用:

function best_feat = run_fs_pipeline(X, y, k_top) rng(42); % 固定随机种子,保证可复现 X = double(X); X(isnan(X)) = 0; % 清洗 NaN X = X(:, var(X) > 1e-6); % 粗筛零方差特征 y = double(y(:)); % 强制列向量 addpath('fs_sup_mrmr'); addpath('fs_sup_relieff'); k = min(k_top, size(X, 2)); % 边界保护 [idx_m, ~] = fsMRMR(X, y, k); [idx_r, ~] = fsReliefF(X, y, 10); % 用 KNN 交叉验证对比两组特征集 acc_m = crossval(@(xtr,ytr,xte,yte) ... mean(fitcknn(xtr,ytr,'NumNeighbors',5).predict(xte) == yte), ... X(:, idx_m), y, 'KFold', 5); acc_r = crossval(@(xtr,ytr,xte,yte) ... mean(fitcknn(xtr,ytr,'NumNeighbors',5).predict(xte) == yte), ... X(:, idx_r), y, 'KFold', 5); if acc_m >= acc_r best_feat = idx_m; fprintf('选择 MRMR,acc=%.3f\n', acc_m); else best_feat = idx_r; fprintf('选择 ReliefF,acc=%.3f\n', acc_r); end end

这段代码把前几章的坑都堵上了:固定随机种子保证对比公平,数据清洗提前做掉,k 加边界保护,粗筛砍掉零方差特征。拿真实数据跑的时候,把 X 和 y 换成自己的变量就行。

6.2 验证这一步千万别省

特征选择本身不会提升模型精度,它的作用是去掉干扰、缩短训练时间、提升可解释性。所以最后一定要做一组对比:全特征训练一个模型,选中特征训练一个模型,比较精度和训练时间。如果精度持平但时间降了一半,说明特征选择起作用了;如果精度反而明显下降,可能是 k 值设得太小,把有区分度的特征砍掉了,回去把 k 调大再跑一轮。

刚拿到这套代码时,我图省事,直接把.mexa64文件拷到 Windows 机器上就想跑,结果 MATLAB 报了一下午的Invalid MEX-file,最后发现是平台后缀不匹配。从那以后,我每次拿到任何带 mex 的 MATLAB 工具包,第一件事永远是disp(computer)确认平台,再跑一遍makeosmex重新编译,绝不在这上面省时间。这个习惯帮我避开了后面好几次类似的问题,希望帮到你。

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

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

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

立即咨询