
简介一套基于MATLAB实现的Relief特征选择算法工具包面向机器学习、数据挖掘及生物信息学等领域用于从高维数据中筛选有效特征提升模型效率。Relief算法通过随机选取查询实例比较其与同类近邻和异类近邻在特征上的差异为每个特征赋予权重最终按权重排序选出重要特征子集。压缩包共19个文件以m源代码为核心涵盖主程序、权重计算、标准化等模块并配有mat数据文件、xls示例数据、fig可视化图表及txt说明文档整体大小约1.32MB。该资源已有734人学习使用适合希望快速掌握Relief算法原理与MATLAB实现的开发者参考。通过运行自带数据可清晰理解近邻差异计算、权重更新与特征排序的完整流程附带的胃癌样本数据和特征排序结果也有助于验证算法效果为后续特征工程或模型优化提供实践基础。1. Relief特征选择到底是什么一个让特征权重自己说话的算法我处理过不少这样的数据样本量几百、特征几十上百个直接丢进分类器结果像个黑匣子——精度忽高忽低换成别的模型立马变脸。Relief特征选择就是在这种场景下被我想起来的它不用训练任何模型只靠“同类样本近、异类样本远”这个朴素标准给每个特征算一个权重让你在十分钟内知道该留谁、该扔谁。Relief算法虽然是上世纪九十年代初提出的老方法放在MATLAB里跑通核心逻辑只需要几十行代码。对准备做特征工程之特征选择、又不想引入复杂依赖的人来说这几乎是上手成本最低的一条路。它解决的痛点很直接特征太多导致过拟合、模型解释性差、训练时间被无效维度拖慢。Relief算法属于过滤式特征选择在建模之前就把特征筛一遍。适合两类人一类是建模前需要快速粗筛的工程师另一类是刚接触特征选择、想把原理看明白的学生。后面的内容我会从算法原理讲到MATLAB手写实现再讲到多分类扩展和排坑经验照着抄就能跑。2. Relief算法为什么管用近邻比对的权重更新逻辑2.1 从“同类近、异类远”说起Relief的三要素Relief算法的出发点很简单一个特征如果真有判别力那么在它的取值空间里样本分布应该是有规律的。具体来说任意挑一个参考样本R在全部样本里找它的最近邻一个同类最近邻H一个异类最近邻M。如果某个特征上R和M的距离大说明这个特征能把两个类别拉开是有用的如果某个特征上R和H的距离也很大说明同一个类别内部在这个特征上也不稳定它对分类反而起干扰作用。这里的三要素是参考样本R、同类最近邻H、异类最近邻M。算法循环很多次每次随机抽一个R计算这三个样本在各特征上的差异然后更新特征的权重。整个过程不需要训练分类器不需要假设数据分布。权重结果直接反映“这个特征对区分类别有多大的边际贡献”。它的计算复杂度不高。每轮迭代的核心操作是算距离、找最近邻总复杂度大约O(T·n·m)T是迭代次数n是样本数m是特征数。样本量几千以内MATLAB跑起来都是秒级。如果n上了几万建议改用分批近邻搜索这个后面细说。2.2 权重更新公式拆解作恶特征被罚行善特征被奖权重更新是算法的核心。假设第t轮抽到参考样本R找到同类最近邻H和异类最近邻M那么对第j个特征权重更新可以写成W(j) W(j) - diff(R, H, j) / T diff(R, M, j) / T其中diff表示两个样本在特征j上的归一化差异。对连续特征diff |R(j) - H(j)|前提是特征已经做了min-max归一化对离散特征diff 0或1取值不同就是1相同就是0。分母上的T是总的迭代次数每轮贡献被摊薄到±1/T最终权重落在一个大致[-1, 1]的区间内。拆开看这句话减号那一项在做什么如果某个特征在同类最近邻之间跳变很大说明这个特征内部噪声大把同类样本分开权重被扣分。加号那一项在做什么如果某个特征在异类最近邻之间差异大说明它能拉开两类样本权重被加分。最终跑完T轮每个特征的得分就是它对分类的“净贡献”。这个公式里最容易忽略的是归一化。diff用的是绝对差如果特征取值范围不同量纲大的特征在数值上天然占优。所以几乎所有Relief的实现都要求输入矩阵提前做min-max归一化。我在后面第5章会专门讲这个坑。2.3 为什么用MATLAB而不是Python来做这件事做特征选择Python里sklearn也有ReliefF的实现但MATLAB在这件事上有自己的优势。第一是矩阵化操作MATLAB对向量化的sum、min、sort操作支持得很顺手几十行代码就能把核心逻辑写明白中间变量看得一清二楚。第二是统计工具箱里自带relieff函数如果只是想要结果一行调用就行想看懂原理手写一份也不难。第三是可视化方便权重排序直接bar画图配合gscatter看特征分布调试体验比Python里来回切matplotlib要顺。我一般会在两种情况下优先选MATLAB一是数据已经存在.mat文件里二是后续建模打算用MATLAB的分类器。如果项目整套流程在Python里那直接用sklearn的ReliefF实现也没问题。工具不重要重要的是把算法逻辑吃透换语言只是换个语法外壳。3. 从零手写MATLAB版Relief可分步复现的核心代码3.1 数据准备把表格转成算法吃的样子Relief的输入就两个东西特征矩阵X和标签向量y。X是n行m列每一行是一个样本每一列是一个特征y是n行1列的二分类标签。标签建议统一成0和1方便做逻辑索引。实际项目里的原始数据通常有缺失值、有字符串列、有量纲差异大的列在进Relief之前要清理一遍。% 清理删除全缺失列其余缺失行直接删掉 data rmmissing(data); % 删除含NaN的行 X0 data(:, 1:end-1); % 特征列假设最后一列是标签 y0 data(:, end); % 标签列 y0 double(y0 max(y0)); % 转成 0/1正类设为1 % 只取出你需要的那几列比如第2、5、9列 X_sub X0(:, [2 5 9]);这段代码里rmmissing是R2018b以后才有的函数老版本可以用data(~any(isnan(data), 2), :)替代。X0(:, [2 5 9])这种多列索引是MATLAB的基础操作在“matlab数组取出多列”这个需求里很常用。y0 double(y0 max(y0))这行是把标签转成0/1思路是比较每个元素是否等于最大值转成double后正类是1、负类是0。数据清理完之后下一步是归一化。注意顺序先清理、再归一化、最后进Relief。归一化必须在找近邻之前做因为最近邻是基于欧氏距离选的量纲不同的特征会在距离计算里喧宾夺主。X_min min(X0); X_range max(X0) - X_min; X_range(X_range 0) eps; % 常值列保护避免NaN X (X0 - X_min) ./ X_range;这里把常值列的分母替换成eps是为了防止max和min相等时出现0除。常值特征本身没有判别力归一化后全为0Relief算出来的权重也会接近0不影响其他特征的结果。3.2 核心函数relief.m权重累积就在这几行Relief的核心函数不长我把注释写详细一点方便对照公式理解。这个函数不依赖任何工具箱用randi做随机抽样用隐式扩展算距离R2016b之后的版本都能跑。function W relief(X, y, T) % RELIEF 二分类Relief特征选择 % X: n×m 数值矩阵已做min-max归一化 % y: n×1 标签取值为0或1 % T: 迭代次数一般取50~200 [n, m] size(X); W zeros(1, m); % 每个特征的权重 for t 1:T ri randi(n); % 随机抽一个参考样本R R X(ri, :); labR y(ri); idxSame find(y labR); % 同类索引 idxDiff find(y ~ labR); % 异类索引 idxSame(idxSame ri) []; % 去掉R自己 if isempty(idxSame) || isempty(idxDiff) continue; % 某一类只有R一个样本跳过这轮 end % 欧氏距离平方找最近邻 dH sum((X(idxSame, :) - R).^2, 2); dM sum((X(idxDiff, :) - R).^2, 2); [~, hi] min(dH); [~, mi] min(dM); H X(idxSame(hi), :); M X(idxDiff(mi), :); % 同类近则罚异类近则奖 W W - abs(R - H) / T abs(R - M) / T; end end逻辑说明每一轮先随机抽取一个参考样本R然后在同类集合里找离R最近的样本H在异类集合里找离R最近的样本M。R与H的距离反映特征在同类里的稳定性R与M的距离反映特征在不同类之间的区分度。abs(R - H)是逐特征的绝对差因为X已经归一化到[0,1]绝对差直接就是归一化差异不再需要除以特征范围。参数说明T是迭代次数T越大权重越稳定但计算时间线性增长。样本量n小于200时T取100足够n在1000左右时T取200到300更稳。函数内部用randi(n)做有放回抽样所以每次运行结果会有随机性主脚本里要先用rng固定种子。距离计算用的是欧氏距离平方找最近邻时min取最小距离对应的下标如果要换成K近邻把min改成sort再取前k个即可。3.3 主脚本调用与权重可视化30秒看到哪个特征说话写一个演示脚本生成一份模拟数据12个特征只有第2、5、9列真正和标签相关其余都是噪声。跑完Relief后画权重条形图验证算法能不能把这三个特征找出来。rng(42); % 固定种子结果可复现 n 200; m 12; X0 randn(n, m); % 12维高斯噪声 % 只有第2、5、9列影响标签加一点噪声 score sum(X0(:, [2 5 9]), 2) 0.5 * randn(n, 1); y0 double(score 0); % 转成0/1标签 % min-max归一化 X_min min(X0); X_range max(X0) - X_min; X_range(X_range 0) eps; X (X0 - X_min) ./ X_range; % 跑Relief W relief(X, y0, 200); % 排序和可视化 [Wsorted, idx] sort(W, descend); bar(W); xlabel(特征编号); ylabel(Relief权重);跑完之后看idx的前几位。正常情况下第2、5、9列会排在前三名权重明显高于其他噪声特征。如果用了不同的随机种子或者T太小偶尔会出现噪声特征挤进前三的情况这是抽样随机性导致的不是算法错了加大T就能缓解。bar(W)画出来的图里正权重的柱子是“对分类有贡献”的特征负权重和接近0的柱子可以直接淘汰。真实数据里不建议机械地按权重大小直接截断更合理的做法是把权重排序后从最大值往下累加累加到总权重绝对值70%~90%的位置作为截断点或者干脆只保留正权重特征。4. 从Relief到ReliefF多分类、回归与三个必调参数4.1 ReliefF在做什么k近邻与按类先验加权原始Relief只能处理二分类而且每轮只找一个同类最近邻和一个异类最近邻对噪声敏感。ReliefF是它的升级版做了两处关键改动。第一处改动是把“单个近邻”换成“k个近邻”。每一轮找k个同类近邻和k个异类近邻权重更新时求平均。这样单个离群点对权重的影响被摊薄了稳定性明显提升。第二处改动是处理多分类。当类别数大于2时每轮的“异类”不再是一个类而是除了R所在类别以外的所有类。算法对每个异类类别都找k个近邻按该类的先验概率加权。权重更新的思想是特征如果能把R和“任何一个其他类”拉开它就有贡献但贡献大小要考虑这个类出现的概率样本多的类权重影响更大。MATLAB统计工具箱里内置了relieff(X, y, k)函数直接传入特征矩阵、标签和近邻数就能用多分类和回归都支持。但内置函数的迭代细节是个黑匣子不方便调。自己手写一份ReliefF改动并不大后面3.3里给出改造方案。4.2 三个必调参数迭代次数T、近邻数k、归一化开关手写版ReliefF有三个参数需要关注我用表格列一下推荐值和影响参数推荐范围作用改错了会怎样迭代次数T100300控制权重估计的稳定性T太小权重噪声大每次跑结果都变近邻数k510控制近邻平均的平滑程度k太大近邻跑到类边界上区分度被稀释归一化开关必须开消除量纲对diff和距离的双重影响不开大数值特征全占权重前列T和样本量n的关系值得多说一句n只有几十的时候T取100就够了因为样本空间小反复抽到的样本很快覆盖全体n上千时T要取200以上否则很多样本可能没被抽到。k的取值一般取5到10类别本身很小的极端情况下k要小于少数类的样本数否则近邻会跨到别的类里去。归一化这个开关在MATLAB自带函数里是默认做的但手写版里必须自己记得做。我见过不止一次数据从Excel拷出来年龄、收入、消费次数混在一起直接喂进Relief结果收入这个特征的权重碾压全场其他特征全部接近0。这不是算法失效是没做归一化。4.3 手写版relieff.m只改三处循环逻辑基于第3章的relief函数把单近邻改成k近邻把异类搜索改成遍历所有非当前类就能得到ReliefF。function W relieff2(X, y, T, k) % RELIEFF2 多分类ReliefF近似实现 % X: n×m 已归一化特征矩阵 % y: n×1 标签取值为1,2,...,C % T: 迭代次数 % k: 近邻个数 classes unique(y); [n, m] size(X); W zeros(1, m); for t 1:T ri randi(n); R X(ri, :); labR y(ri); % 同类k近邻 idxSame find(y labR); idxSame(idxSame ri) []; dH sum((X(idxSame, :) - R).^2, 2); [~, oH] sort(dH); nH min(k, length(oH)); Hs X(idxSame(oH(1:nH)), :); W W - sum(abs(R - Hs), 1) / (T * nH); % 遍历所有异类 for c classes if c labR continue; end idxDiff find(y c); dM sum((X(idxDiff, :) - R).^2, 2); [~, oM] sort(dM); nM min(k, length(oM)); Ms X(idxDiff(oM(1:nM)), :); W W sum(abs(R - Ms), 1) / (T * nM); end end end逻辑说明每一轮对同类找k个最近邻累减它们的平均diff对每一个异类类别分别找k个最近邻累加它们的平均diff。这里没有按类先验概率加权是一个简化版本。在类别均衡的数据集上这个简化版的排序结果和内置relieff基本一致类别极不均衡时内置版会更好一些因为它对每个异类按P(C)/(1-P(class(R)))加权。参数说明k近邻的距离排序用了sort取前k个下标。min(k, length(oM))这个保护很重要如果一个类别只有4个样本而你设了k10不截断的话索引就越界了。内置relieff函数遇到这种情况也是自动截断的手写版必须自己处理。如果需要处理回归问题思路是把标签y换成实数近邻不再按同类异类划分而是把“样本间y差异大”的近邻当作异类近邻差异小的当作同类近邻。这个改动相对复杂实际项目里建议直接用内置relieff的回归模式手写版主要用在学习场景。5. Relief特征选择的避坑记录5个容易翻车的细节5.1 中文注释乱码2023b写的脚本换台机器就花屏现象在MATLAB 2023b上写的.m文件含中文注释拷贝到2022b或者更老版本打开中文全部变成乱码严重的时候重新保存会把文件搞坏。原因新版MATLAB默认以UTF-8编码保存.m文件旧版本2022b及以前在中文Windows上默认是GBK编码。编码不匹配打开时按GBK去解析UTF-8的字节流中文自然显示成乱码。反过来旧版写的GBK文件拿到新版打开也会出问题。解决最简单粗暴的办法是不在代码里写中文注释全部用英文。如果团队里必须用中文注释约定所有人统一用同一版本MATLAB比如都用2023b或2026b并且统一把文件保存为UTF-8。已经乱码的文件用VS Code或Notepad强制转码再打开是一个临时补救手段但治标不治本。5.2 量纲没统一大数值特征把权重全部带偏现象特征矩阵里有一个“用户累计消费金额”列范围在几千到几万其他特征范围都在0到1之间。跑完Relief消费金额的特征权重巨大其他特征全部趋近于0。如果把那个特征直接删掉剩余的权重排序才看起来合理。原因两层问题叠加。第一层是权重更新公式里的abs(R - H)直接算绝对差量纲大的特征天然得分高这是表象。第二层更隐蔽Relief找最近邻用的距离是欧氏距离量纲大的特征在距离计算中占了绝对主导导致找出来的最近邻实际上只参照了这一个特征。即便你把权重公式里的diff改成归一化差异近邻还是按原距离选的这个问题依然存在。解决必须在进Relief之前做min-max归一化。顺序不能乱先归一化再算距离再更新权重。如果某些特征本身服从长尾分布可以先做log1p变换再归一化效果更好。5.3 随机种子没固定两次结果不一样现象同一份数据、同一套参数上午跑一次和下午跑一次出来的权重排序差了好几位有时候连前三名都不一样。原因Relief的每次迭代都随机抽样参考样本R。如果T设得比较小比如50以下抽样随机性对权重的影响会非常明显。这会让使用者对结果失去信任以为是算法不稳定。解决脚本开头加一行rng(42)固定全局随机种子。注意rng(42)要在调用relief之前执行而且要确认没有其他函数在中间调用了rng(shuffle)。如果固定种子后结果稳定了说明算法本身没问题如果固定了种子还是不稳定先把T调大再说。5.4 常值特征让权重变成NaN现象数据里有一列所有样本数值相同比如“性别”字段已经转成哑变量后残留了一列全为0。跑归一化时报错或者警告Relief权重这一列变成NaN甚至影响整个特征排序。原因min-max归一化的分母是max - min常值列的分母为0得到NaN。NaN进入权重向量后sort和后续绘图都会出问题。解决预处理里先删掉方差为0的列或者像我第3章代码里那样把分母替换为eps。常值特征本来就没有判别力删掉是合理操作。另外建议看一下你的数据里有没有“一列取值只有一种”的哑变量残留这种列删掉还能顺便降低后续建模的维度。5.5 类别不平衡少数类特征被系统性低估现象二分类任务中正类占90%负类占10%。Relief跑出来的权重排序里那些能区分少数类的关键特征排名明显靠后甚至变成负权重。原因随机抽样R时样本服从全局分布90%抽到多数类。Relief的权重是“所有被抽中的R的贡献平均”多数类样本贡献的信息主导了最终结果。少数类样本很少被抽到它们的区分度信息几乎被淹没了。这不是算法错了而是它的设计假设是类别均衡。解决抽样阶段做分层抽样每一轮先从各类中轮流选R保证少数类被抽到的次数和多数类相当或者在跑Relief之前对少数类做SMOTE过采样。还有一种思路是分别对每个类别单独跑一遍Relief看哪些特征在每一类内部都排名靠前这类特征往往是更稳健的选择。6. 验证Relief选出的特征是不是真有用交叉验证与基线对照6.1 用嵌套交叉验证判断特征子集真假Relief给出的权重排序只是“候选名单”不能直接当作最终结论。我会用交叉验证来验证特征子集换到没见过的测试数据上精度有没有真的提升。rng(1); cvp cvpartition(y0, KFold, 5); for i 1:cvp.NumTestSets tr cvp.training(i); te cvp.test(i); Wtr relief(X(tr, :), y0(tr), 200); [~, topIdx] sort(Wtr, descend); topIdx topIdx(1:5); % 取前5个 mdl fitcknn(X(tr, topIdx), y0(tr), NumNeighbors, 5); acc(i) 1 - loss(mdl, X(te, topIdx), y0(te)); end mean(acc)这里最关键的一点是Relief权重只在训练折上计算测试折的数据在特征选择环节不能碰否则就是数据泄漏验证出来的精度虚高。cvpartition是统计工具箱的函数没有工具箱的话可以自己写一个分层分折循环。能做个对比全特征精度是多少、只用Relief前5个特征精度是多少。如果后面者没有明显下降甚至更高说明删掉的特征确实是噪声。6.2 个人习惯权重只做粗筛稳定才是红线我现在的固定流程是先跑一版Relief做粗筛把权重为负和接近0的特征直接淘汰剩下特征再交给带L1正则的模型做第二轮压缩。如果数据类别不均衡我会分别对每类跑Relief取交集特征。选择特征时最看重的信号是稳定性在不同随机种子下重复跑Relief排名波动不超过两三位的特征才值得保留。波动特别大的特征多半本身就没什么判别力权重大只是运气好。这个习惯救过我很多次希望帮到你。本文还有配套的精品资源点击获取