简介这是一套基于MATLAB的Relief特征选择算法实现包面向机器学习、数据挖掘领域需要处理高维数据的研究者与学生。资源涵盖Relief算法核心脚本、特征权重计算与排序、冗余特征剔除等完整流程同时附带胃癌相关基因表达数据集、.mat格式的特征排序结果以及特征重要性可视化图便于直接运行与验证。包内共19个文件以.m源码和.mat数据为主辅以.xls原始数据、.fig可视化图像与.txt说明文档压缩包整体1.32MB体积小巧、结构清晰适合快速上手和迁移到其他特征选择任务。目前已有739人学习下载侧面印证其实用价值与参考意义。通过此套代码使用者可深入理解Relief算法基于近邻差异评估特征重要性的原理掌握MATLAB实现细节并将其应用于生物信息学、医学诊断等场景有效降低模型复杂度、提升预测性能。1. Relief特征选择高维小样本建模的第一把筛子一个真实又常见的场景样本量只有80特征却堆到1500列常规特征选择方法一上去就出问题——要么排序结果换个数据划分就翻江倒海要么包装式方法跑半小时都不出结果。我后来遇到这类问题第一反应就是用Relief特征选择。它不假设线性关系、不训练模型通过近邻样本的差异直接给每个特征打分一次遍历就能给出稳定的特征权重。这篇笔记会把Relief算法在MATLAB里的原理、内置函数、手写实现和踩过的坑讲清楚适合正在做分类或回归特征筛选的工程人员也适合刚接触特征选择的在校同学。2. Relief算法原理与选型理由为什么它天然适配高维小样本2.1 核心思想用近邻样本的差异给特征打分Relief的直觉非常朴素一个特征如果有用同类样本在这个特征上应该“长得差不多”异类样本应该“差得很远”。算法在训练集上随机抽样每抽到一个样本x就找到它的同类最近邻H和异类最近邻M然后逐特征比较两组距离。连续特征的差异通常写成% 连续特征除以取值范围消除量纲影响 dCont abs(x(f) - h(f)) / (max(f) - min(f)); % 离散特征相同为0不同为1 dDisc double(x(f) ~ h(f));如果x和M在某个特征上的差异明显大于x和H说明这个特征对区分两类有帮助权重就增加反过来如果同类样本在特征上差异很大说明这个特征在“捣乱”权重就减少。每次迭代的更新规则是% 二分类基础版Relief的权重更新 w(f) w(f) - dH / m dM / m;其中m是总迭代次数。除以m是为了让最后权重落在可比较的范围内。我看到不少人跳过取值范围归一化直接算dCont结果量纲大的特征天然拿高分排序根本不是真实区分能力。这一点在第5章还会专门展开。2.2 Relief和ReliefF的差别二分类、多分类、回归原始的Relief算法只能处理二分类而且每轮只取一个同类最近邻和一个异类最近邻对噪声非常敏感。实际工程里更常用的是ReliefF它有三点关键变化支持多分类每类不只取一个近邻而是取k个近邻并加权平均能直接做回归任务。对比项基础ReliefReliefF适用任务二分类多分类、回归近邻数量1个同类 1个异类每类k个近邻按类比例加权抗噪能力较弱更平稳MATLAB内建不单独提供由函数relieff实现理解ReliefF的多分类处理对后续选型很重要。比如某个三分类数据集三类样本数比例是3:1:2算法在计算每个样本的异类近邻差异时会按每个类别的样本比例贡献权重少数类不会被直接忽略。2.3 为什么在过滤式和包装式之间Relief往往是更稳的折中做特征选择时过滤式方法像卡方检验、互信息计算快但完全忽略特征之间的交互包装式方法像前向搜索精度高但每次迭代都要训练模型高维数据直接劝退。Relief刚好踩在中间它通过近邻计算隐式捕捉了局部交互又不依赖训练模型因此在高维小样本场景下格外顺手。还有一个工程上的实际好处Relief的权重计算是可并行的每个样本抽样可以独立计算最后汇总权重即可。所以在MATLAB里即使特征数到几千跑起来也远比你预想的快。我自己做基因表达谱这类特征数极多、样本数少的任务时第一轮筛选用Relief几乎是标准动作。2.4 内置relieff还是自己写一张表说清楚MATLAB的统计和机器学习工具箱提供了relieff函数名字里有双f实际实现的是ReliefF算法。对多数项目内置函数够用但它确实是个黑匣子内部的距离度量和近邻搜索方式对你不可见。如果你需要自定义距离度量比如改成曼哈顿距离、余弦相似度或者要处理混合类型的特征那手写一版更踏实。维度内置relieff手写Relief上手成本很低一行调用需要维护几十行代码连续/离散处理自动处理较可靠全靠自己定义距离度量固定不可改可以完全自定义调试与教学黑匣子可逐步打印中间结果计算性能经过优化自行实现可能偏慢内置函数适合快速出结果、做基线手写版本适合做定制指标、做教学、或给论文补充方法细节。两者不冲突我在项目里常常先用内置版本跑一版结果再手写定制版本做交叉验证。3. MATLAB里跑通Relief数据准备、内置函数与参数3.1 数据组织X、y的格式和归一化动手前先明确数据格式。X必须是数值矩阵行是样本列是特征。y是响应向量分类任务可以是类别标签或数值标签回归任务必须是连续数值。缺失值要先处理relieff不会自动填充NaN。% 示例用自带数据集跑通流程实际项目换成自己的X和y load fisheriris; X meas; % 150x4四个花部特征 y species; % 150x1三种类别标签 % 0-1归一化把每列缩放到[0,1] X_norm (X - min(X)) ./ (max(X) - min(X));归一化这一步我用得很谨慎。虽然内置函数内部可能会做某种标准化但如果你自己在代码里提前归一化至少能保证后续手写版本和内置版本的结果可比。特别是当特征量纲差异很大时不归一化会让权重排序严重失真。3.2 用内置relieff跑分类特征排序的最小命令在MATLAB里relieff的返回值有两个排序后的特征索引和对应的权重。下面是最小调用方式% 分类任务k取10表示每个样本考虑10个近邻 k 10; [idx, weights] relieff(X_norm, y, k, method, classification); % 展示结果原始特征号、排序位置、权重 T table((1:size(X, 2)), idx, weights, ... VariableNames, {原始特征号, 排序位置, 权重}); disp(T);idx里存的是特征编号按权重从高到低排列。比如idx(1)3说明第3个原始特征在全部特征里排名第1。weights的顺序和idx是对应的不是原始特征顺序。我见过有人把weights直接按原始顺序画图结果对不上这一点要留意。这个例子里只有4个特征跑不出太大差异但流程是对的。改成你自己的数据后先用排序结果画一个柱状图% 权重柱状图方便直观挑选拐点 bar(weights); xlabel(排序后的特征序号); ylabel(Relief权重);3.3 多分类和回归任务一套代码覆盖两种场景多分类时标签y建议转成categorical类型这样内置函数能正确识别类别关系。如果只有一组数字标签也可以用numeric但语义上不够清晰。% 多分类把字符串类别转为分类数组 y_cat categorical(y); [idx2, w2] relieff(X_norm, y_cat, 10, method, classification); % 回归任务y是连续值必须指定method为regression y_reg randn(size(X_norm, 1), 1); % 这里只是造一组示例连续值 [idx3, w3] relieff(X_norm, y_reg, 5, method, regression);回归版本的差异计算方式不太一样。它不再用“同类最近邻”和“异类最近邻”区分而是找目标值接近的k个近邻计算它们在不同特征上的平均差异差异越大说明特征对预测目标的局部波动贡献越大。实际做回归特征筛选时我通常会把k调大一些因为连续值本身噪声多近邻少了权重容易抖。3.4 必调参数k值、method以及权重排序的正确用法参数含义常见取值影响k近邻个数分类取5~20回归取10~30太小噪声大太大区分度被稀释method分类或回归classification / regression选错结果没有意义weights特征权重负数也正常只看相对大小不看绝对值idx排序后特征编号由relieff返回用于构造特征子集关于k值没有万能默认。我经验是先把k设成10跑一版然后扫描5、10、15、20观察前10个特征的排序是否稳定。如果排序变化很大说明数据噪声本身不小k往大调一点会平缓。权重值为负不代表特征没用只是它在这个数据集上对区分样本贡献弱排最后的特征可以放心丢掉。idx的用法是给后续建模准备数据子集% 取前10个特征作为建模输入 topK idx(1:10); X_selected X_norm(:, topK);4. 手写Relief算法把内置函数的黑匣子打开按自己需求改4.1 算法步骤基础版Relief的四步循环手写版本不是为了重新发明轮子而是为了能自定义距离度量也方便在论文里完整描述方法。基础版Relief分类任务步骤如下初始化权重向量w为全零长度等于特征数。重复m次随机抽取一个样本x从同类别样本中找到最近邻H从其他类别样本中找到最近邻M。对每个特征计算dH和dM如果同一个特征上dM远大于dH权重增加反之减少。迭代结束后所有权重除以m得到平均权重。这里的“最近邻”默认用欧氏距离但距离度量完全可以替换。4.2 可直接运行的MATLAB实现下面这段代码把上面步骤落成两个函数可以直接复制到MATLAB里运行function w reliefManual(X, y, m) % 基础版Relief分类特征选择 % X: N行P列数值矩阵y: N行1列类别标签 % m: 抽样迭代次数一般取100或更大 [N, P] size(X); w zeros(1, P); maxv max(X) - min(X); % 各特征取值范围 maxv(maxv 0) 1; % 防止出现常数列时除零 for t 1:m i randi(N); % 随机抽一个样本 xi X(i, :); sameMask (y y(i)); % 同类样本掩码 diffMask ~sameMask; % 异类样本掩码 sameIdx find(sameMask); sameIdx(sameIdx i) []; % 去掉样本自己 diffIdx find(diffMask); % 找同类最近邻和异类最近邻 H nearestNeighbor(xi, X(sameIdx, :), maxv); M nearestNeighbor(xi, X(diffIdx, :), maxv); % 更新每个特征的权重 for f 1:P dH abs(xi(f) - H(f)) / maxv(f); dM abs(xi(f) - M(f)) / maxv(f); w(f) w(f) (dM - dH) / m; end end end function n nearestNeighbor(ref, candidates, maxv) % 在候选集中找与ref欧氏距离最近的一行 delta abs(candidates - ref) ./ maxv; dist2 sum(delta.^2, 2); [~, idx] min(dist2); n candidates(idx, :); end代码逻辑已经很直白重点说两个边界处理。maxv里如果有特征取值恒定不变分母会变成0所以要置为1否则这段代码会直接报错。sameIdx里必须剔除当前样本自己不然最近邻就是自己dH永远是0权重更新完全失真。这段基础版代码在类别不平衡时会出问题因为随机抽样很可能一直抽到多数类样本。后面5.3节会讲怎么改这里先留个印子。4.3 自定义距离把欧氏距离换成曼哈顿或秩变换内置relieff没法改距离度量手写版本的主要价值就在这里。最简单的一步是改用曼哈顿距离只需要把nearestNeighbor里的平方和改成绝对值之和% 曼哈顿距离版本对异常值更稳健 function n nearestNeighborL1(ref, candidates, maxv) delta abs(candidates - ref) ./ maxv; dist1 sum(delta, 2); [~, idx] min(dist1); n candidates(idx, :); end另一个我常用的改动是在进算法前做秩变换。原始数值如果被极端值拉偏距离计算会失真先用tiedrank把每列转成秩再跑Relief能显著减轻异常值影响% 把X按列转成秩再进入手写Relief X_rank tiedrank(X); w_rank reliefManual(X_rank, y, 200);注意秩变换之后maxv的含义变了但手写代码里依旧按取值范围归一化不会出错。相比欧氏距离曼哈顿和秩变换在特征工程实践中更常见因为它们对单个异常样本不那么敏感。4.4 排序输出与构造特征子集手写版本的输出是权重向量w排序逻辑和内置函数一致% 按权重降序排列 [~, order] sort(w, descend); % 取前20个特征作为候选子集 topK order(1:min(20, numel(w))); X_selected X_rank(:, topK);我再强调一次排序看的是相对顺序不是权重的绝对值。权重的正负和大小受迭代次数、距离度量影响很大直接拿权重做阈值判断容易误判。我习惯的做法是先用手写版本算权重排序后固定取前10%或前20%的特征再进入下一轮模型筛选。5. 高频避坑与排查Relief特征选择的五个教训5.1 未归一化导致权重被量纲绑架现象有一个特征取值范围是0到100000其他特征在0到1之间Relief跑完这个特征的权重必然排第一但它可能毫无区分能力。原因差异计算里abs(x(f)-y(f))没有除以取值范围量纲大的特征天然被放大。内置函数虽然内部可能有处理但如果你自己做了数据清洗却没做归一化手写版本会直接踩雷。解决无论用内置还是手写先做0-1归一化或z-score标准化。手写版本里确保maxv max(X) - min(X)不是0否则还要做除零保护。5.2 分类变量被当成连续值距离现象有一列特征是0/1/2三档离散值模型训练时没问题但Relief给的权重和实际业务认知明显不符。原因MATLAB里的数值矩阵如果全是doublerelieff会默认把这些列当连续特征处理。0和1之间的距离虽然是1但当1和2的距离也是1时它不会意识到“1和2之间没有中间状态”。解决把离散特征转成categorical类型传入relieff或者在自定义实现里对离散列单独写double(x~y)这样的判断逻辑。混用连续和离散特征时我一般提前在数据清单里标注好哪些列是离散的不交给算法自己猜。5.3 类别不平衡造成少数类特征被淹没现象二分类问题中正类占95%、负类占5%Relief选出来的特征全是区分多数类内部的差异负类的最强特征可能排到一百名以后。原因基础版随机抽样每次抽中多数类的概率远高于少数类即使抽中少数类异类最近邻大概率还是多数类权重更新方向也被带偏。解决把随机抽样改成分层抽样保证每次迭代里正类和负类都有机会被抽到。在手写代码里可以直接改抽样逻辑% 分层抽样示例每次以50%概率从少数类中抽 r rand; if r 0.5 i minorityIdx(randi(numel(minorityIdx))); else i majorityIdx(randi(numel(majorityIdx))); end在项目里做类别不平衡时我不会只跑一轮Relief就拍板而是给少数类分层抽样并跑多个随机种子最后看特征排名的交集。5.4 一次性全量计算换个验证集排序就变现象用全部样本跑Relief选出的前20个特征在训练集和测试集上分别重新计算后排序结果变了甚至重合率不到一半。原因Relief是数据驱动算法训练集分布只要微调权重就会波动。一次性全量计算等于把训练集和验证集全混进去了看过全部数据后用测试集验证结果自然不稳定。解决把Relief放进交叉验证流程只对训练折计算权重再用测试折评估。一个稳妥做法是% 拆成5折每折内重算Relief权重并记录排序 rng(42); foldIds cvpartition(y, KFold, 5); rankMatrix zeros(size(X,2), 5); for f 1:5 trIdx training(foldIds, f); [~, wFold] relieff(X(trIdx,:), y(trIdx), 10); [~, orderFold] sort(wFold, descend); rankMatrix(:, f) orderFold; end看rankMatrix的每行是否稳定如果前几行在几折中跳来跳去说明这些特征本身就不稳定不要留。5.5 回归任务忘了指定method现象y是连续值直接调用relieff(X,y,k)不报错但权重排序出来乱七八糟和你用回归模型做的变量重要性对不上。原因默认method会把连续y当类别处理或者版本行为不一致导致距离计算语义错误。解决回归任务显式写method,regression。同样分类任务也可以显式写method,classification避免版本差异带来的不确定性。别嫌麻烦显式写完整参数能省掉一大半排查时间。6. 把Relief结果落进模型我固定使用的三个习惯第一任何Relief结果出来先做k值敏感性检查。我不相信单次运行的结果至少扫描k等于5、10、15、20四组看前10个特征的重叠率。重叠率低于60%我会怀疑数据噪声过大或者特征本身冗余严重不会直接拿结果去建模。这一步花不了多少时间但能避免后面模型性能波动时到处找原因。第二Relief只当第一道粗筛。它给出的权重是“局部区分能力”的估计不是最终模型视角的重要性。我通常用Relief把几千个特征先砍到一两百个再接入一个小模型比如线性回归或轻量梯度提升树做二次筛选。这样既享受了Relief的速度又保留了模型对交互效应的判断。某次项目里我用Relief筛掉90%的无效特征后再做wrapper搜索整体计算时间下降了不止一个量级。第三固定随机种子再跑。Relief有随机抽样过程不固定种子两次运行排序可能差一两个位置。我在代码里第一行就会写rng(42)记录下种子编号。后来换数据重新实验时这个习惯帮我复现了不少细节结果。有一回A同学跑出和我不同的特征排序最后发现就是少了固定种子这一步。这些方法都不是玄学是从反复试错里沉淀下来的流程。Relief算法本身不复杂真正决定成败的是数据处理和验证方式。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?