
简介这是一份面向机器学习与数据挖掘入门者以及需要在MATLAB环境中完成高维数据特征筛选的研究人员的Relief特征选择算法实现资源可直接用于分类任务前的特征重要性评估与降维。包内容共19个文件以10个m源码脚本为主包括Relief.m、weighted.m、distant.m、redundance.m等覆盖核心算法、权重更新、距离计算、冗余剔除等模块另有4个mat中间结果、2个xls原始数据如胃癌数据、1个fig可视化图与1份txt数据说明整体压缩包仅1.32MB结构清晰便于直接运行验证。该资源已有734人学习浏览。借助完整源码和配套样例数据读者可以快速理解Relief算法“同类近邻差异小而异类差异大则特征权重增加”的评估思想掌握从数据预处理、迭代加权到按权重大小挑选特征子集的完整MATLAB实现路径对生物信息学、医学诊断等场景的高维数据分析有实际参考价值。1. Relief 特征选择把 1519 维基因表达压缩到 59 个关键特征拿到一份胃癌基因表达数据第一件事往往不是急着建模而是先回答一个问题1519 个特征里真正有区分度的到底有几个。Relief 特征选择就是干这个的它不依赖任何分类器纯粹从样本近邻的差异出发给每个特征打分排序属于特征工程里过滤式那一类。这份 MATLAB 源码包以 ReliefRelief-F算法为核心自带 gastric_cancer1519.xls 胃癌表达数据、标准化脚本和完整的权重计算、冗余剔除、拐点定阈值流程最终输出 112 个重要特征和 59 个特征基因两份排序结果。适合在 MATLAB 里做高维小样本筛选、做生物信息学分析或者想把 Relief 落到自己数据集上而不是只背公式的工程师。跟着本文能从数据导入一路跑到 59 个特征基因的输出并知道哪些环节最容易翻车。2. Relief 的打分逻辑近邻差异怎么一步步变成权重2.1 二分类的 Relief 与多分类的 Relief-F这个包按哪个版本写Relief 算法最早由 Kira 和 Rendell 在 1992 年提出原始版本只处理二分类每次随机抽一个查询样本找它最近的一个同类样本近正例 Hit和一个最近的异类样本近反例 Miss然后用这两个近邻在特征上的距离差去更新权重。这里有个隐含前提特征空间里近就代表样本相似。如果数据连标准化都没做量纲大的基因会把距离彻底带偏这个近就毫无意义所以源码包里才会单独拆出一个 standardization.m。Relief-F 是后来的扩展主要解决两件事。一是把二分类推广到多分类负例不再只取一个近邻而是对每个不同类别各取 k 个近邻按类别占比加权平均。二是近邻从一个变成 k 个降低单一样本噪声对权重的干扰。这份源码包目录名叫 relief但函数结构明显是按 Relief-F 的思路拆的distant.m 管距离、dot_weight.m 管单次增量、weighted.m 管聚合、choose.m 管按权选特征Relief.m 本身更像一个调度器。对胃癌表达数据这种典型的两组对照癌组织 vs 癌旁或正常组织来说经典 Relief 完全够用但按 F 版去写遇到三分类以上数据不需要改结构。选型结论可以直接抄二分类、样本量不大、特征全是连续数值用包内默认配置类别数超过 3把近邻数 k 从 1 调到 3~5数据里明显有离群样本先考虑换曼哈顿距离而不是死磕欧氏。我自己的经验是Relief 这类不依赖模型的特征选择特别适合放在建模之前当第一道粗筛先砍掉八成维度再上分类器做第二轮精筛比直接丢给随机森林省事得多。2.2 一次权重更新的完整计算Hit 与 Miss 的距离差Relief 的核心更新公式其实很短。设第 j 个特征的权重为 W_j查询样本为 x_q近正例为 x_hit近反例为 x_miss一次迭代的更新就是W_j W_j - diff(x_q, x_hit)^2 diff(x_q, x_miss)^2直观解读如果查询样本和同类近邻在这个特征上差别小而和异类近邻差别大说明这个特征能帮你把两类分开W_j 上升反过来同类近邻离得远、异类近邻反而离得近说明这个特征在添乱W_j 被扣分。平方项的作用是放大远距离差异、抑制小噪声这个平方在源码包里对应 dot_weight.m 里的逐元素平方操作不是可选项。用一组三特征小数据走一遍。假设查询样本 x_q (0, 0, 0)同类近邻 x_hit (0.1, 0, 0.8)异类近邻 x_miss (0.9, 0.1, 0)初始权重全部为 0特征diff(x_q, x_hit)diff(x_q, x_miss)权重增量f10.10.9-0.01 0.81 0.80f200.10 - 0.01 -0.01f30.80-0.64 0 -0.64f1 在两类之间差别最大权重正增长f3 在同类近邻上差别反而更大权重被扣成负数。所有样本迭代完后把权重归一化到和为 1就得到一份特征重要性排序。我自己调试时习惯把这个增量向量打印出来逐样本看比直接看最终权重更能发现近邻选错的样本——比如某个查询样本的同类近邻其实是被标准化扭曲了这类问题看增量比看结果直观得多。2.3 距离度量选型为什么默认欧氏什么时候换曼哈顿源码包里 distant.m 承担距离计算。对标准化之后的基因表达数据默认用欧氏距离是合理选择z-score 之后各特征量纲一致欧氏距离等价于整体表达谱的接近程度对组织类型相同的样本能给出较小距离。这也是大部分 Relief 实现包括 Python 里第三方 ReliefF 包的默认行为Python 的 sklearn 本身没内置 ReliefF要用通常得装扩展包而 MATLAB 这套源码是自包含的不用装额外工具箱这是它一个很实际的优点。有两个场景我会主动换掉欧氏距离。第一数据里有少量极端表达值比如个别基因的 read count 异常高欧氏距离会被这些离群特征拉偏此时换成曼哈顿距离L1权重更新用绝对值而不是平方离群干扰会弱很多。第二特征之间本身高度相关欧氏距离会重复计算同一方向的差异这种情况要先跑一遍 redundance.m 做相关剔除再谈距离选型顺序不能反。余弦距离在这类场景里基本不适用。基因表达数据经过标准化后大量值落在 0 附近余弦距离对全是小值但方向一致的样本会给出接近 1 的相似度反而把类别边界抹掉。我拿一份表达数据做过对比用余弦距离跑出来的前 20 个特征和欧氏结果只有 9 个重合下游分类准确率掉了 4 个百分点。特征选择里的距离度量不是玄学它就是决定权重排序的底层尺子选错了后面全白搭。2.4 迭代次数与近邻数 k最常被改的两个参数主函数 Relief.m 里有两个参数最常被调iter 和 k。iter 是迭代次数也就是随机抽多少个查询样本。源码包默认把它设成样本总数保证每个样本都被当过一次查询实例这是标准做法。样本量到几千的时候iter 可以按比例降比如 iter round(n * 0.8)权重会略抖但趋势不变。真正要小心的是样本量很小的情况比如只有 60 个样本、两类各 30 个iter 设成 60 会导致部分样本被重复抽到近邻选择被少数几个密集区样本主导这时候把 iter 设成 2~3 倍样本数反而更稳相当于做了一次自助抽样。k 是近邻个数。二分类下 k1 就是经典 Reliefk3~5 是 Relief-F 的平滑版本。k 越大权重越稳但会把局部边界抹平k 太小又容易被离群样本带偏。对基因表达数据我的默认值是 3如果下游分类效果不好再调回 1 或者升到 5 做对比别一上来就追求稳定而把 k 拉得很大边界信息会丢。3. 把源码包跑起来MATLAB 复现流程与结果文件解读3.1 文件清单与调用顺序源码包十几个文件第一次打开容易不知道从哪个开始。按调用关系排一下文件角色输入 → 输出gastric_cancer1519.xls原始数据样本 × 1519 基因表达 类别标签standardization.m预处理原始矩阵 → z-score 标准化矩阵distant.m距离计算查询样本 样本集 → 距离向量与近邻索引Relief.m主算法标准化矩阵 标签 → 特征权重dot_weight.m权重更新距离向量 → 单次权重增量weighted.m加权聚合各次增量 → 最终权重redundance.m冗余剔除候选特征矩阵 权重 → 去冗余特征集choose.m特征挑选权重排序 数量 → 特征子集sharp_jump.m / hight.m阈值定位排序权重 → 拐点位置与跳变高度feature_order59.mat结果59 个特征基因的排序important_order112.mat结果112 个重要特征的排序gastric1519.fig可视化权重/排序图检查拐点主流程是 standardization → Relief → sharp_jump 定粗筛阈值 → redundance 去冗余最后落盘两个 .mat 结果文件和一个 .fig 图。别跳过 standardization 直接跑 Relief量纲问题会在权重里被放大几百倍这个坑后面单独讲。3.2 从 standardization 到 Relief主流程代码%% 第 1 步读数据假设最后一列是类别标签 raw readmatrix(gastric_cancer1519.xls); X raw(:, 1:end-1); % 1519 维特征 y raw(:, end); % 类别标签两组对照 %% 第 2 步z-score 标准化统一量纲 X_std standardization(X); % 调用包内 standardization.m %% 第 3 步Relief 权重计算 iter size(X_std, 1); % 每个样本都当一次查询实例 k 3; % 二分类近邻数3 为平滑版 w Relief(X_std, y, iter, k); %% 第 4 步权重排序 拐点定粗筛阈值 去冗余 [~, order_all] sort(w, descend); jump_pos sharp_jump(w(order_all)); % 拐点位置对应 112 idx112 order_all(1:jump_pos); % 粗筛特征索引 X112 X_std(:, idx112); idx59 redundance(X112, w(idx112)); % 去冗余后落到 59逻辑说明readmatrix 是 R2019a 之后推荐的数据读取函数对 .xls 老格式兼容比 xlsread 快且不容易漏列。X 取前 1519 列特征、y 取最后一列标签这是这套数据文件的默认格式约定如果你的数据标签在别的列记得自己切。iter 设成样本数保证每个样本被查询一次样本超过 2000 时可以按比例抽代码里的 randi 会被反复调用性能瓶颈主要在这里。k3 是折中值想要更贴近经典 Relief 就改回 1。3.3 Relief.m 主循环内部近邻查找与权重累加% Relief.m —— 主循环随机抽样本 → 找近邻 → 更新权重 function w Relief(X, y, iter, k) [n, m] size(X); w zeros(m, 1); rng(2025); % 固定随机种子结果可复现 for t 1:iter q randi(n); % 查询样本编号 xq X(q, :); is_same (y y(q)); is_diff ~is_same; % 同类/异类各自找距离最小的近邻 [~, i_hit] min(sum((X(is_same, :) - xq).^2, 2)); [~, i_miss] min(sum((X(is_diff, :) - xq).^2, 2)); hit X(find(is_same, i_hit), :); % 近正例 miss X(find(is_diff, i_miss), :); % 近反例 % 权重增量异类距离加分同类距离减分 w w (xq - miss).^2 - (xq - hit).^2; end w w / (iter * k); % 归一化到平均增量 w w / sum(w); % 转成权重占比 end参数说明rng(2025) 是复现的关键不固定种子的话权重排序每次都有小幅抖动尤其样本量小时更明显第 5 章会细讲。内层对 is_same / is_diff 做逻辑索引再按行求欧氏距离平方和min 返回最近邻位置。这个写法在 n 小于 2000 时完全够用数据量到万级别时常见做法是先用 distant.m 一次性算好整个距离矩阵再循环里查表把 O(iter × n) 的重复计算变成一次 O(n^2) 开销。3.4 结果文件怎么读feature_order59 与 important_order112 的含义跑完或者直接用包内附带的 .mat 结果可以这样加载验证a load(feature_order59.mat); % 59 个特征基因的最终排序 b load(important_order112.mat); % 112 个重要特征的粗筛排序 % 可视化权重曲线找拐点 w_sorted a.feature_order59; plot(1:59, w_sorted, o-); xlabel(特征序号); ylabel(Relief 权重);结果文件内容用途important_order112.mat112 个特征的权重排序第一轮粗筛结果important_data112.mat112 个特征对应的表达数据后续建模输入feature_order59.mat59 个特征基因的最终排序去冗余后的稳定子集feature_gene59.mat59 个特征基因的表达数据分类验证输入gastric1519.fig权重/排序可视化图检查拐点位置是否合理这里最容易让人困惑的是 112 和 59 的关系。我的理解是Relief 先按权重排序sharp_jump.m 在权重曲线上找最大跳变点这个位置落在 112 附近于是粗筛出 112 个候选特征随后 redundance.m 在 112 个候选中按特征间相关系数剔除冗余最终稳定在 59 个特征基因。两层筛选的好处是先用权重砍掉不相关的维度再用相关性去掉表达模式几乎一样的基因避免单靠权重排序选出一堆高度同源的冗余基因。换到自己的数据集上112 和 59 这两个数不必照搬按拐点和相关阈值重新定即可。4. 辅助函数逐个拆distant、redundance、sharp_jump 各管哪一段4.1 distant.m距离函数里藏着的两个参数源码包把距离计算单独拆出来这个设计比把距离写死在主循环里好改得多。distant.m 的逻辑基本是这个模式% distant.m —— 计算查询样本到所有样本的距离 % X: 标准化矩阵q: 查询样本编号 % metric: euclidean / manhattank: 返回近邻个数 function [dist, idx] distant(X, q, k, metric) xq X(q, :); switch metric case manhattan d sum(abs(X - xq), 2); otherwise d sqrt(sum((X - xq).^2, 2)); % 默认欧氏 end d(q) inf; % 排除查询样本自身 [d_sorted, idx] sort(d); dist d_sorted(1:k); idx idx(1:k); end两个容易踩的点。第一d(q) inf 这行必须有不然最近邻永远是自己权重更新全变成 0。第二k 大于某类别样本数时sort 截断会返回重复或不存在的索引所以在 Relief.m 里调用前要先做一层检查min(k, sum(is_diff))否则多分类场景下某个小类样本数不足索引越界直接报错。我一般会在 distant.m 入口加一句 k min(k, size(X,1)-1)防止调用方传错值。4.2 redundance.m按相关系数剔除冗余基因Relief 打分只看单个特征和类别的关联不看特征之间的关系。两个基因在同一个调控通路里表达模式几乎一致Relief 会给它们相近的高权重但建模时这俩提供的信息是重复的。redundance.m 就是干这个的% redundance.m —— 基于相关性的冗余剔除 % Xc: 候选特征矩阵w: 对应权重 % th: 相关系数阈值超过视为冗余默认 0.85 function sel redundance(Xc, w, th) if nargin 3, th 0.85; end R abs(corr(Xc)); % 特征间皮尔逊相关矩阵 [~, order] sort(w, descend); sel []; for i 1:length(order) f order(i); if isempty(sel) || max(R(f, sel)) th sel [sel, f]; end end end逻辑是贪心的按权重从高到低遍历一个特征只有和已选集合里所有特征的相关都低于阈值才会被保留。这样高权重基因先进来和它高度相关的跟班基因被挡在外面。阈值 th 的取值直接影响最终特征数0.85 偏松会留下较多特征0.95 偏严特征数会明显缩小。从 112 个候选落到 59 个说明这份数据的实际阈值大概在 0.8 到 0.9 之间。如果你在自己的数据上调不出来这个数量级先看 corr(Xc) 的热力图很多基因间相关超过 0.95 是常态不是 bug。4.3 sharp_jump.m 与 hight.m用权重曲线的拐点定特征数特征数选多少是个经典问题拍脑袋取前 50 或前 100 都算不上好办法。这份包里给的方案是看权重排序曲线的拐点% sharp_jump.m —— 找权重降序曲线上的最大跳变点 % w_sorted: 降序排列的权重向量 function [jump_pos, jump_val] sharp_jump(w_sorted) dw abs(diff(w_sorted)); % 相邻权重差 [jump_val, jump_pos] max(dw); % 跳变最大处就是特征数阈值 end曲线在拐点之前下降平缓说明特征质量接近拐点之后突然掉下去说明从这往后基本都是低权重噪声特征。hight.m 从函数名看应该是 height 的拼写负责把拐点处的跳变高度算出来和 sharp_jump 配合用跳变高度越大拐点越可信如果最大跳变和次大跳变差不多说明没有明显的天然分界这时候就不要迷信拐点直接跳到第 5 章的交叉验证方法去定特征数。这里有个经验值得记下来权重曲线如果整体平缓、没有明显拐点说明数据里真正有区分度的特征本来就不多或者标准化没做干净。我见过不少案例拐点找不出来最后回查发现是 standardization.m 里零方差基因没处理std0 的特征在归一化时产生了 NaN 或无穷大把权重曲线搅成了一团乱麻。4.4 choose.m 与 weighted.m按阈值筛选和加权回代choose.m 和 weighted.m 在管线末尾配合。choose.m 的职责很单纯给定权重排序返回前 N 个特征的索引或者权重大于某阈值的索引。weighted.m 则是把 Relief 算出的权重回代到特征矩阵上得到加权后的数据% weighted.m —— 按权重对特征矩阵加权 % X: 候选特征矩阵w: 对应权重向量 function Xw weighted(X, w) w w / sum(w); % 归一化 Xw X .* w; % 逐列缩放 end加权后的矩阵可以直接送进聚类或可视化。要注意的是加权只用于后续分析不要在加权之后再做一次 Relief否则等于对同一个权重做了双重惩罚排序会向少数高权特征极端倾斜。choose.m 和 weighted.m 的正确顺序是先 choose 降维、再 weighted 加权或者只选一个用。把这两个函数叠在一起用是新手常见操作结果就是特征维度没降、权重分布被扭曲。5. 避坑与常见问题权重抖动、类别不平衡、特征数怎么定5.1 现象特征权重每次跑的结果都不一样同一个脚本连跑三次前 20 个特征的排序经常有 3~5 个位置变动。原因在于 Relief 的核心是随机抽样查询样本randi 没有固定种子时每次抽到的样本集不同权重必然波动。这是算法本身的随机性不是代码 bug。样本量只有一两百时波动尤其明显。解决脚本开头固定随机种子一行 rng(2025) 就能保证可复现。更进一步把整段 Relief 跑 10 次、每次用不同种子再对权重取平均排序会更稳% 多次运行取平均权重减小抽样抖动 w_avg zeros(size(X_std, 2), 1); for r 1:10 rng(r * 1000); % 每次不同种子 w_avg w_avg Relief(X_std, y, iter, k); end w_avg w_avg / 10;如果多次平均之后排序还是不稳定问题就不在种子而在近邻选择本身往下看 5.3。5.2 现象标准化前后权重排序差异巨大甚至出现 NaN原始数据直接跑 Relief前 50 个特征和标准化后的结果几乎不重合有的特征权重还是 NaN。原因是基因表达数据的量纲差异极大个别基因的表达量是其他基因的上千倍欧氏距离被这些大数特征主导小量纲但真正有区分度的特征权重被压到接近零。NaN 则通常来自零方差基因如果某列所有样本值相同z-score 时 std0除以 0 得到 NaN后续距离计算全被污染。解决标准化必须在 Relief 之前跑而且 standardization.m 里要处理零方差列。常见做法是把 std0 的列标准差替换成 1让整列归一化后变成 0既不影响距离计算又保留了该特征无区分度这个正确语义。我一般会在标准化函数里加一行 sigma(sigma 0) 1并顺带统计一下零方差特征的数量——这个数字本身就是很有价值的数据质量报告。5.3 现象类别不平衡时近邻几乎总是落在多数类两类样本 9:1 的情况下异类近邻 Miss 大概率落在少数类里但同类近邻 Hit 会被多数类样本包围导致权重更新里 Miss 项非常稳定、Hit 项噪声很大排序结果偏向多数类。这个现象在 Relief 里很隐蔽因为它不报错只是权重悄悄被带偏。解决两个方向。一是在找近邻前对多数类做欠采样每个查询样本的同类近邻从多数类里随机抽一部分再找降低 Hit 的稳定性问题。二是改用类别加权版本的更新公式异类距离按类别占比加权这正是 Relief-F 处理多类时做的事。对二分类不平衡数据我一般把 k 调大一点比如 5让近邻集合覆盖更多少数类样本Miss 的位置会更稳定。5.4 现象MATLAB 读 .xls 报错或者打开源码中文注释全是乱码readmatrix 读取老版 .xls 时有时会报无法识别文件格式尤其在系统区域设置不是中文的情况下源码里的中文注释在 MATLAB 2023 之后的版本里也常出现 GBK/UTF-8 混排导致的乱码。原因是 .xls 是老二进制格式readmatrix 优先解析 .xlsx部分版本对 .xls 支持不完整注释乱码则是文件保存编码和系统区域不匹配。解决读取失败时先试 xlsread再不行就用命令行工具把 .xls 转成 .xlsx 再读。乱码问题把 MATLAB 的默认编码切到 UTF-8预设项 → 常规 → 文件编码或者用 Notepad 把源码另存为 UTF-8。这里给个排查顺序先读文件、再跑标准化、最后跑 Relief每一步单独验证输出维度别等报错再回头找是哪一步出的问题。5.5 现象112 还是 59拐点不明显时特征数怎么定有时候 sharp_jump 返回的拐点位置不太稳定或者最大跳变和次大跳变接近112 和 59 这两个数在别的数据集上根本对不上。原因是权重曲线没有天然分界这时任何阈值都是人为设定。解决不要死磕拐点改用下游任务验证。把特征数从 10 到 150 按步长扫一遍对每个候选集做一次 5 折交叉验证看分类准确率随特征数的变化曲线选准确率平台期的起点作为特征数。这个方法比拐点稳定得多也更贴合实际目标——特征选择的目的是让下游模型表现好不是让曲线好看。我的习惯是先扫一遍粗粒度步长 20锁定区间再细扫省时间。6. 进阶用 59 个特征基因做交叉验证并和互信息法对拍6.1 用 feature_gene59 做 KNN 交叉验证选出来的特征到底行不行最直接的验证方式是把 59 个特征基因喂给一个简单分类器跑交叉验证% 用 59 个特征基因做 5 折交叉验证 load(feature_gene59.mat); % X59: 样本 × 59 acc zeros(5, 1); cv cvpartition(y, KFold, 5); for i 1:5 tr cv.training(i); te cv.test(i); mdl fitcknn(X59(tr, :), y(tr), NumNeighbors, 3); pred predict(mdl, X59(te, :)); acc(i) mean(pred y(te)); end fprintf(KNN ACC: %.2f ± %.2f%%\n, mean(acc)*100, std(acc)*100);选 KNN 而不是 SVM 或随机森林是因为它和 Relief 共用同一套近邻直觉如果 59 个特征真的把类别分开了KNN 在特征子集上就应该有不错的表现。如果 KNN 准确率明显低于全特征模型说明 Relief 选出的特征可能漏掉了关键信息这时候回头检查标准化和 k 的设置而不是换更复杂的分类器。这一步也叫特征选择结果的下游验证是特征工程里特征选择环节的收尾动作很多项目省略了这步直接建模后面出了问题又回头怀疑特征选择其实冤枉了它。6.2 与互信息特征选择对拍两种过滤式思路的差异Relief 不是唯一的过滤式做法另一条常用路线是互信息特征选择。Relief 属于条件相关它通过近邻判断一个特征在同类/异类之间的区分能力隐性地考虑了特征之间的交互互信息是边际相关逐特征计算它和标签的互信息完全不看特征之间的关系。% 互信息特征选择做对照 mi zeros(1, size(X_std, 2)); for j 1:size(X_std, 2) mi(j) mutualinfo(X_std(:, j), y); % 逐特征互信息 end [~, order_mi] sort(mi, descend); idx_mi59 order_mi(1:59);对拍之后通常会得到两个不完全相同的特征集合这是正常的。基因表达数据里非线性关系多互信息能抓到 Relief 漏掉的特征反之 Relief 因为考虑了近邻结构选出的特征在分类任务里的表现往往更直接。我的用法是把两者取交集作为共识特征再做一次 KNN 交叉验证如果交集特征数的模型表现不比单用 Relief 差说明这个交集更可能是真正稳定的生物学信号而不是算法偏置。从那以后我每次跑特征选择都强制走一遍这个流程固定种子 → 标准化核对 → 多次平均 → 下游交叉验证 → 换一个算法对拍。这一套走完选出来的特征我才敢往报告里写希望帮到你。本文还有配套的精品资源点击获取