简介:面向机器学习与数据挖掘实践者的MATLAB资源包,聚焦基于支持向量机(SVM)的递归特征消除(RFE)特征选择方法。RFE通过反复训练模型并剔除最不重要的特征,帮助降低高维数据中的冗余与过拟合风险,尤其适合特征维度较高、需要提升模型泛化能力的场景。包体非常精简,共3个文件,含2个MATLAB脚本与1个dat格式数据集,整体仅3KB:脚本分别承担数据加载预处理与RFE迭代删减核心流程,数据文件则提供了肝脏疾病诊断样本,可直接用于演练完整的特征选择过程。已有447人学习下载,适合初学者观摩SVM-RFE的完整实现思路与流程。通过该资源可快速掌握特征重要性排序、逐步剔除冗余特征、比较不同特征子集下的模型性能,为后续高维建模提供可靠的预处理依据。
1. 递归特征消除配 SVM:把“特征选择”从玄学变成可复现实验
做机器学习建模的人几乎都经历过这种尴尬:模型在训练集上漂亮得不像话,一换数据就原形毕露。问题往往不在模型,而在特征——冗余特征太多,SVM 被噪声牵着鼻子走。这份digui.rar资源包里实现的是递归特征消除(RFE)配合 SVM 的完整 MATLAB 流程,配套一份肝脏疾病诊断数据liver.dat,跑一遍就能拿到“哪个特征对分类贡献最大”的排序结果。它适合两类人:一是刚接触特征工程的建模新手,需要一个能运行的代码模板;二是想给高维数据降维、又不想牺牲可解释性的熟手。资源不大,三个文件,没有花哨包装,核心就是digui.m这个递归消除函数。
2. 拆解源码包:三个文件的分工,以及 RFE 为什么总爱配 SVM
2.1 递归特征消除的运作逻辑:“删掉最不重要的一个”这句话的执行细节
递归特征消除听起来玄乎,拆开看其实是一个贪心策略:先用当前全部特征训练一个模型,根据模型给出的特征重要性排序,删掉最不重要的那个特征,然后用剩下的特征重新训练,再删一个,循环往复,直到特征数量达到预设值。关键点是“每次只删一个”,而不是一次砍掉一批。因为特征之间可能存在交互,一次性删掉多个,很可能把单独看权重小、组合起来却有区分力的特征误伤掉。
在 SVM 场景里,特征的“重要性”由谁说了算?线性核 SVM 训练完成后会得到一个权重向量w,每个特征对应一个权重分量。权重的绝对值越大,说明该特征对超平面方向的贡献越大,也就是对分类决策越重要。RFE 就拿着这个w做“排序 → 删除 → 重训”的循环。这里的隐含假设是:特征贡献与权重绝对值单调相关。这个假设在特征经过标准化之后基本成立。
RFE 的停止条件有两种常见设置:一是指定最终保留的特征数,比如保留到 3 个就停;二是观察模型性能指标,当删除某个特征后性能下降明显,就不再继续删。资源包里的digui.m采用的是第一种,预设最少保留特征数,逻辑更直接,也方便做对比实验。
2.2 liver.dat、liver_data.m、digui.m:三个文件各自扮演什么角色
解压digui.rar后有三个文件,先搞清楚它们的分工再动手,不然容易跑出莫名其妙的结果。
| 文件名 | 类型 | 职责 |
|---|---|---|
liver.dat | 数据文件 | 肝脏疾病诊断数据集,每行一个样本,前若干列为特征,最后一列为类别标签 |
liver_data.m | MATLAB 脚本 | 负责加载数据、标签转换、归一化预处理,为主流程准备输入 |
digui.m | MATLAB 函数 | 实现 RFE 核心迭代逻辑,输入特征矩阵和标签,输出特征重要性排序 |
liver.dat是典型的二分类医学数据。特征列是生化检验指标,数值范围各不相同,有的指标可能只有 0 到 1,有的可能到几百,如果不做归一化,SVM 的权重向量会被数值量级大的特征带偏,得出的特征排序就没有参考意义。所以liver_data.m里除了加载数据,还会做标准化处理。digui.m是核心,它不关心数据本身是什么领域,只认“特征矩阵 + 标签”这对输入,换任何数据集都能复用,这是这份资源最值得拿走的模块。
2.3 为什么是 SVM,而不是 Lasso 或互信息
特征选择方法很多,Lasso 通过 L1 正则化让一部分特征的系数变成 0,也能选出特征子集;互信息特征选择则是完全不依赖模型,纯粹靠计算特征与标签之间的相关性来打分。那为什么 RFE 要配 SVM?
主要原因是 SVM 在高维小样本场景下泛化能力相对稳,而且线性核 SVM 天然产出权重向量,RFE 可以直接拿它当特征打分器,不需要额外训练一个代理模型。Lasso 的系数路径对特征间的共线性敏感,两个高度相关的特征会被随机地“二选一”,排序稳定性差;互信息只看单变量关系,忽略了特征组合的交互效应。相比之下,RFE 是“带着模型一起选”,每轮删除都考虑了当前特征子集下的真实判别贡献,更贴近最终分类任务的目标。
说到边界,RFE 也不是万能的。如果样本量远小于特征维度,每一轮 SVM 训练本身就在过拟合,排序结果的可靠性会打折扣。这种情况我会先做一轮粗筛,比如用互信息排序砍掉明显无关的特征,再交给 RFE 细选。资源包里没有这一层,但你可以照着这个思路自己加。
3. MATLAB 实操复现:从加载 liver.dat 到拿到特征排序
3.1 第一步:加载数据并做标准化
打开 MATLAB,把解压后的三个文件放进当前工作目录。先运行liver_data.m里的加载逻辑,你也可以直接在命令行粘贴执行:
% liver_data.m 核心片段 % 1. 加载 liver.dat,文件与脚本同目录 data = load('liver.dat'); % 2. 分离特征矩阵和标签:最后一列是类别标签 X = data(:, 1:end-1); y = data(:, end); % 3. 归一化:每一列减去均值再除以标准差 X = zscore(X); % 4. 标签转成 SVM 需要的 +1 / -1 形式 y(y == 1) = 1; y(y == 2) = -1;这段代码做了三件事。load把文本数据读成 MATLAB 矩阵;data(:, 1:end-1)取除最后一列以外的所有列作为特征矩阵X,最后一列作为标签y;zscore是 MATLAB 内置的标准化函数,按列计算均值和标准差。最后一步标签转换很关键,fitcsvm默认要求二分类标签是+1和-1,如果原数据里类别是 1 和 2,不转换会直接报错或者训练出错误的分类器。
标准化这一步别省略。liver.dat里各特征的量纲相差很大,如果不做zscore,digui.m里算出来的权重w会被大数值特征主导,特征排序结果基本失真。这是跑这个资源最容易踩的坑之一,后面避坑章节会展开说。
3.2 第二步:digui.m 核心函数——每一轮迭代做了什么
digui.m是整个资源包的核心,先看它的完整实现:
% digui.m —— 递归特征消除核心函数 % 输入: % X 标准化后的特征矩阵 (n_samples x n_features) % y +1 / -1 标签向量 % keep_min 最少保留特征数,默认保留到 1 个 % 输出: % ranking 特征索引排序,从最不重要到最重要 function [ranking, history] = digui(X, y, keep_min) if nargin < 3 keep_min = 1; end [~, n_features] = size(X); remaining = 1:n_features; % 当前还保留的特征索引集合 ranking = []; % 按“先淘汰”的顺序收集特征 history = {}; % 记录每一轮剩余特征,方便后续复盘 while length(remaining) > keep_min % 用当前剩余特征训练线性 SVM model = fitcsvm(X(:, remaining), y, ... 'KernelFunction', 'linear', ... 'Standardize', false); % 线性核 SVM 的 Beta 就是特征权重 w = model.Beta; % 找到权重绝对值最小的特征,判定为“最不重要” [~, rm_idx] = min(abs(w)); % rm_idx 是在 remaining 局部数组里的下标,要映射回原始特征编号 removed_feature = remaining(rm_idx); % 记录被淘汰的特征,并从保留集合中删除 ranking = [ranking, removed_feature]; remaining(rm_idx) = []; % 存一份当前剩余特征,后面做性能曲线要用 history{end + 1} = remaining; % 命令行打印进度 fprintf('第 %d 轮删除特征 %d,剩余 %d 个特征\n', ... length(ranking), removed_feature, length(remaining)); end % 最后剩下的特征最“重要”,接在 ranking 末尾 ranking = [ranking, remaining]; end这段代码里有两个细节值得注意。第一个是remaining(rm_idx) = [],MATLAB 支持用下标删除数组元素,删除后remaining自动变短,下一次循环的fitcsvm自然就只用剩余特征。第二个是ranking的拼接顺序:每轮被淘汰的特征按先后顺序放进ranking前面,迭代结束后把最终保留的特征补在末尾,所以ranking是从“最不重要”到“最重要”排列的。要看最重要的特征,取ranking最后几位即可。
参数说明:fitcsvm里KernelFunction设为linear,是为了能拿到model.Beta作为特征权重;如果换用rbf核,Beta字段不存在,特征排序就得另想近似方案,具体在避坑章节讨论。Standardize设为false是因为我们已经在liver_data.m里做过zscore,不需要让模型再做一遍。第三方参数如BoxConstraint默认取 1,对小数据集够用,不需要动。
3.3 第三步:运行完整流程,输出特征重要性排序
主流程脚本把数据加载和 RFE 函数串起来:
% 完整主流程:加载数据 -> 标准化 -> RFE -> 输出排序 data = load('liver.dat'); X = data(:, 1:end-1); y = data(:, end); X = zscore(X); y(y == 1) = 1; y(y == 2) = -1; % 调用 digui.m,至少保留 2 个特征 [ranking, history] = digui(X, y, 2); % 打印特征重要性:从最重要到最不重要 fprintf('\n=== 特征重要性排序(最重要在前)===\n'); for i = length(ranking):-1:1 fprintf('第 %d 重要 -> 原始特征列 %d\n', length(ranking) - i + 1, ranking(i)); end % 用最重要的前 3 个特征重新训练 SVM,对比全特征效果 top3 = fliplr(ranking(end-2:end)); model_full = fitcsvm(X, y, 'KernelFunction', 'linear', 'Standardize', false); model_top3 = fitcsvm(X(:, top3), y, 'KernelFunction', 'linear', 'Standardize', false); % 计算训练集准确率作为快速参考 acc_full = 1 - loss(model_full, X, y); acc_top3 = 1 - loss(model_top3, X(:, top3), y); fprintf('\n全特征 SVM 训练集准确率: %.2f%%\n', acc_full * 100); fprintf('Top3 特征 SVM 训练集准确率: %.2f%%\n', acc_top3 * 100);fliplr的作用是把ranking倒过来,因为ranking末尾是最后保留下来的特征,也就是最重要的特征,倒序后正好从头到尾按重要性排列。这里用训练集准确率做快速参考只是为了验证流程通不通,真正的模型评估要用交叉验证,下一章会专门写。
运行完这段,你会在命令行看到类似这样的输出:每一轮删掉了哪个特征、剩余多少特征、以及最终的特征重要性排序。liver.dat的特征数不多,所以迭代很快,几秒钟就能跑完。这个结果就是你后续所有分析的起点。
4. 特征选择避坑指南:三个翻车现场和修复办法
4.1 现象:先对全量数据做 zscore,再划分训练集,测试集准确率虚高
跑通流程的人十个里面有八个犯过这个错。我一开始也是这样——把X = zscore(X)放在数据划分之前,整个数据集一起做标准化,然后随机抽 70% 训练、30% 测试。测试准确率漂亮得惊人,但换到真实场景就崩。
原因:zscore用的是全量数据的均值和标准差,测试集的信息在标准化阶段就被“偷看”了,这叫数据泄漏。正确做法是先划分训练集和测试集,只对训练集计算均值和标准差,再用训练集的统计量去标准化测试集。zscore函数不保存均值和标准差,所以要么手动算,要么用[Z, mu, sigma] = zscore(X_train)的第二种返回形式。修复代码:
% 先划分再标准化 rng(42); % 固定随机种子,结果可复现 idx = randperm(size(X, 1)); train_idx = idx(1:floor(size(X, 1) * 0.7)); test_idx = idx(floor(size(X, 1) * 0.7) + 1:end); % 只用训练集计算均值和标准差 [Z_train, mu, sigma] = zscore(X(train_idx, :)); % 测试集用同一组统计量处理 Z_test = (X(test_idx, :) - mu) ./ sigma;从那以后我每次跑特征选择,都强制先划分再标准化,宁可在心里默念三遍“测试集不能碰训练集”。
4.2 现象:一次删掉多个特征,最优特征子集被跳过
有段时间我嫌 RFE 跑得慢,改成了每轮删 3~5 个特征,结果得到的特征组合表现还不如随机选的。原因在于特征之间存在交互效应,单独看权重都很低的两个特征,放在一起可能对分类有强判别力。RFE 每次只删一个,正是为了避免这种误伤——删掉一个特征后,剩余特征的权重会重新分配,原本被压抑的特征可能变得重要。一次删多个,等于是用旧模型的视角去评判新子集,当然会翻车。
解决:老老实实每次删一个。如果嫌迭代次数多,可以先观察history里每一轮的准确率变化,在性能开始明显下滑的前两轮停下来,而不是盲目加速。
4.3 现象:把核函数换成 RBF 之后,RFE 结果乱跳
fitcsvm默认核函数是rbf,如果不显式指定'KernelFunction', 'linear',model.Beta会是空数组,min(abs(w))直接报错。就算用脚本强行近似,RBF 核的 SVM 权重不具有“每个特征一个系数”的结构,没法直接对应到原始特征,排序结果也不稳定,重跑两次排名可能都不一样。
解决:用这份资源做 RFE,核函数必须固定为linear。如果你确实想用非线性核,需要换成基于分类准确率的特征打分策略——每轮删除一个特征,用交叉验证算删除前后的准确率差,差得小就说明该特征可有可无。这种做法的缺点是计算量成倍增加,liver.dat这种小数据集能承受,上到几百维特征就吃力了。
4.4 现象:迭代结束后,保留的特征数量不同,排序结果对不上
digui.m的ranking输出是“从最不重要到最重要”的顺序,而history里存的是每一轮“剩下的特征索引”。如果你在循环外面直接拿history{end}当最终保留特征,和ranking末尾对不上——因为ranking末尾包含的是最后一次迭代后剩余的keep_min个特征,而history{end}也是这次的剩余特征,两者应该一致。不一致往往是下标映射出了问题,比如在删除remaining(rm_idx)之后又用了原始下标去访问X。解决:删除前先把removed_feature = remaining(rm_idx)记下来,所有对特征列的操作统一走remaining映射,别直接拿循环内局部下标当全局下标用。
5. 性能验证与调参:用交叉验证确定该保留几个特征
5.1 不同特征数的交叉验证对比:从 6 个特征看到 1 个特征
RFE 给的是排序,但“保留几个特征”这个问题,排序本身回答不了。常见做法是拿排序结果做消融实验:从最重要的 1 个特征开始,逐步增加特征数量,每一档都做一次交叉验证,画出准确率随特征数变化的曲线,取曲线峰值对应的特征数。
% 基于 digui.m 排序结果做交叉验证消融实验 rng(42); [ranking, history] = digui(X, y, 1); % ranking 从最不重要到最重要,翻转后从最重要开始 important = fliplr(ranking); cv_acc = zeros(length(important), 1); for k = 1:length(important) % 取前 k 个最重要的特征 X_sub = X(:, important(1:k)); % 5 折交叉验证 mdl = fitcsvm(X_sub, y, 'KernelFunction', 'linear', ... 'Standardize', false, 'CrossVal', 'on', 'KFold', 5); cv_acc(k) = 1 - kfoldLoss(mdl); fprintf('保留 %d 个特征, 5 折交叉验证准确率: %.2f%%\n', ... k, cv_acc(k) * 100); end % 画出特征数与准确率的关系 figure; plot(1:length(important), cv_acc * 100, '-o'); xlabel('保留特征数量'); ylabel('5 折交叉验证准确率 (%)'); grid on;KFold设为 5,在 345 个样本左右的小数据集上足够稳定。跑完之后你会看到一个典型趋势:从 1 个特征到 3 个特征,准确率快速上升;3 到 5 个趋于平缓;到 6 个时可能略微下降,因为冗余特征开始引入噪声。不同核参数下曲线形态会变,但“中间存在一个最优区间”是常见的。
5.2 与 Lasso、互信息特征选择做对照,判断 RFE 是否值得用
只跑 RFE 不够,说服力最强的实验是和主流特征选择方法做对照。MATLAB 里做互信息特征选择可以调用fscmrmr(最小冗余最大相关性算法),Lasso 用lassoglm或者fitrlinear带 L1 正则。
% 方法一:互信息特征选择(MRMR) [idx_mrmr, scores_mrmr] = fscmrmr(X, y); % idx_mrmr 按重要性排序,取前 k 个即可 % 方法二:Lasso 特征选择 [B, FitInfo] = lasso(X, y, 'CV', 5); % 取 Lambda1SE 对应的非零系数特征 non_zero_idx = find(B(:, FitInfo.Index1SE) ~= 0);对照实验跑完,通常能看到这样的规律:互信息选出的特征单变量判别力强,但组合起来可能冗余;Lasso 选出的特征少而精,但遇到强共线性时特征选择不稳定,每次重跑选的列可能不一样;RFE 在稳定性和组合效果之间比较均衡,而且它的输出是完整的排序,比另外两种更直观。下表是liver.dat上我跑出来的典型对比,随着你改随机种子会有波动,但趋势基本一致:
| 方法 | 选出的特征数 | 5 折 CV 准确率 | 重跑稳定性 |
|---|---|---|---|
| RFE-SVM(线性核) | 3 | 最高 | 高,排序稳定 |
| 互信息(MRMR) | 4 | 略低 | 高,但冗余偏多 |
| Lasso(Lambda1SE) | 2 | 略低 | 低,共线性时波动 |
这组对比的价值是告诉你:选特征没有银弹,RFE 的优势在于“结合模型看贡献”,在模型可解释性这个诉求上,它的排序结果比系数绝对值更直观。如果你的项目里特征数以百计,建议先用互信息做一轮粗筛砍到 50 维以下,再跑 RFE,这是我在实际项目中验证过的效率方案。
6. 进阶:把这份代码改造成你自己的特征选择工具箱
6.1 把“一次运行”变成“一次可复现实验”:封装、固定种子、输出图表
digui.m现在是一个手写风格的函数,要变成能反复调用的工具箱,建议做三件事:固定随机种子、把交叉验证封装成独立函数、把特征数-准确率曲线图和数据一起存盘。固定种子的重要性前面避坑章节提过,SVM 训练本身不依赖随机数,但交叉验证的数据划分依赖,不固定种子,两次实验的准确率差异会干扰你的判断。
% 封装一份通用的 rfe_svm_demo.m,参数化输入 function [ranking, cv_table] = rfe_svm_demo(X, y, kfold, plot_flag) rng(42); [ranking, ~] = digui(X, y, 1); important = fliplr(ranking); cv_acc = zeros(length(important), 1); for k = 1:length(important) mdl = fitcsvm(X(:, important(1:k)), y, ... 'KernelFunction', 'linear', 'CrossVal', 'on', ... 'KFold', kfold); cv_acc(k) = 1 - kfoldLoss(mdl); end if plot_flag plot(1:length(important), cv_acc * 100, '-o'); end cv_table = table((1:length(important))', cv_acc * 100, ... 'VariableNames', {'FeatureCount', 'CVPctAcc'}); end这样写的好处是,换数据集时只需要保证输入X是标准化后的数值矩阵、y是 +1/-1 标签,其他逻辑完全不用动。
6.2 泛化到自己的数据:两个必须重写的点,和一个必须保留的底线
换到自己项目的数据时,要重写的是两部分:数据加载和预处理。加载逻辑因文件格式而异,这没什么好说;预处理的底线是“测试集的统计量必须来自训练集”,这条我在第四章跌过一次以后就刻进肌肉记忆了。digui.m本身不需要改动,这是它设计得精简的好处。
我个人的习惯是把它变成一个更大流程的子函数:先跑 MRMR 粗筛,再跑digui.m细选,最后用rfe_svm_demo输出曲线。从那以后我每次做特征选择,都强制走一遍“先划分 → 训练集标准化 → RFE 排序 → 交叉验证消融 → 对照实验”这五步,不再凭所谓经验拍脑袋定特征。这套流程适用不了所有场景,高维稀疏数据可能更适合直接上嵌入法,但对大部分中小规模表格型数据,它至少能给你一个不翻车的起点。
希望帮到你。
本文还有配套的精品资源,点击获取