MATLAB特征选择库FSLib实战:从安装到交叉验证的完整指南
2026/9/20 10:37:58 网站建设 项目流程

简介:面向MATLAB研究者的FSLib_v7.0.1_2020_2特征选择库,专为高维数据预处理与机器学习建模前的降维需求而设计,汇集过滤法、包裹法、嵌入法等多种经典与现代特征选择算法。过滤法可基于相关性或统计量快速评分,包裹法通过搜索特征子集直接评估模型性能,嵌入法则在模型训练过程中自动筛选重要特征,适用于不同规模与类型的数据集。压缩包共含363个文件,其中321个m脚本是算法主体,覆盖实现代码、调用接口与示例程序;另有10个fig图形文件辅助可视化,以及若干C/C++源文件与mex接口文件,便于二次开发或编译加速。整体仅600KB,轻量且便于部署。目前已有258人学习下载。使用该库可一站式完成特征评估、子集搜索与结果对比,也能参考源码理解算法细节,非常适合在MATLAB环境下开展特征选择实验、复现论文方法或处理真实数据集前的特征工程。对于高维稀疏场景,合理选择特征还可显著减少冗余、缩短训练时间并提升模型可解释性。

1. 特征选择库 FSLib:为什么有人专门为 MATLAB 做一个包

一份 2000 行、800 列的数据摆在面前,先别急着丢进模型。这个阶段真正花时间的不是调参,而是从几百个候选特征里找出和标签跑得动的那一小组。FSLib 就是干这个的工具包:它把几十种特征选择算法收进同一个 MATLAB 工具箱,装好后只需按统一格式传特征矩阵和标签,就能在几分钟内对比 Filter、Wrapper、Embedded 好几条路线。

适合手里有表格数据、正在做分类或回归预处理的人,也适合嫌自己反复写相关系数、卡方、relief 的人。FSLib_v7.0.1_2020_2 这个压缩包名里,7.0.1 是库版本,2020-2 可以理解为维护者标记的 2020 年第二次更新批次,很多 MATLAB 工具包都习惯用这种“库版本+年份序号”的双层标记,本质是在文件层面做发布管理。

下面按使用习惯展开:先讲怎么装、数据要塞成什么形状,再跑通一个最小流程,接着讨论算法选型和最常见的参数坑,最后用交叉验证和稳定性检查确认这组特征值不值得进模型。

2. FSLib_v7.0.1_2020_2 的安装、目录结构与数据格式

2.1 把压缩包放进 MATLAB 搜索路径

FSLib 不是一个.mlappinstall安装包,也不是需要图形化安装的 App。它本质是一堆.m文件组成的函数库,所以解压之后唯一要做的是把目录加进 MATLAB 搜索路径。我一般这样处理:

unzip('FSLib_v7.0.1_2020_2.zip'); fslibPath = fullfile(pwd, 'FSLib_v7.0.1_2020_2'); addpath(genpath(fslibPath)); savepath

第一行解压到当前工作目录,第二行拼出绝对路径,第三行用genpath把该目录下所有子目录一次性加进去,第四行把当前搜索路径保存到pathdef.m。如果不执行savepath,下次启动 MATLAB 还得重新addpath,所以我建议在确认目录没有移动的情况下直接保存。

savepath偶尔会报路径权限错误,常见场景是 MATLAB 安装在C:\Program Files\MATLAB\...,当前用户没有写权限。解决办法有两个:一是把 FSLib 解压到userpath里,比如C:\Users\你的用户名\Documents\MATLAB;二是在 MATLAB 用户目录下的startup.m文件里写一行addpath(genpath('你的FSLib路径')),每次启动自动加载。这两种方式都比去修改 MATLAB 安装目录权限省事。

2.2 先看 examples,再进 methods

解压后的库通常按功能分成三类目录,不同打包习惯会有差异,但大方向一致:

目录或文件常见内容使用建议
methods/每个特征选择算法一个.m文件help查签名,不要随便改源码
utils/归一化、排序、交叉验证等辅助函数发生同名冲突时优先到这里确认
examples/演示脚本和说明文档先完整跑通,再替换成自己的 X 和 y
READMEContents.m版本记录、更新日志安装后第一时间打开

不要用run一口气跑完 examples 里所有内容,我看过不少人是直接全选 F9,结果变量名互相覆盖,最后根本说不清是哪个算法选出来的特征。正确做法是逐段执行,关注每段在做什么处理,以及变量区里 X、y、k、score 的形状变化。

查看实际文件列表:

ls(fullfile(fslibPath, 'methods')) ls(fullfile(fslibPath, 'examples'))

看到方法文件名之后,用help确认函数签名。FSLib 中大部分方法都遵循“特征矩阵 X、标签 y、要选的特征个数 k”这个位置参数顺序,返回值通常只有两个:选中特征下标和对应评分。但不同版本的函数命名不一定一样,同样的 mRMR 实现可能叫mrmrmMRRmrmr_mid,所以help才是最终依据。

2.3 数据形状:样本在行,特征在列

FSLib 里的方法默认输入是二维数值矩阵:X是 n×p,n 是样本数,p 是特征数;y是 n×1 的列向量。分类任务的标签如果是字符串或 cell 数组,需要先转成整数索引。从 CSV 读入时我习惯这样写:

T = readtable('train_data.csv'); X = T{:, 2:end}; % 第一列是标签或样本ID yRaw = T{:, 1}; if iscell(yRaw) || isstring(yRaw) y = grp2idx(yRaw); else y = double(yRaw(:)); end

T{:, 2:end}用花括号取出 table 里的数值数据,出来是 double 矩阵。yRaw(:)的意义是统一成列向量,很多算法会对size(y, 1)size(X, 1)做一致性检查,行向量容易在边界处翻车。grp2idx会把字符串类别映射成整数,同时返回一个类别列表,后续画图或解释模型时可以用。

FSLib 多数算法不会替你做缺失值处理,所以进入任何选择方法之前,先做两个断言:

assert(all(isfinite(X(:))), 'X 里存在 NaN 或 Inf'); assert(size(X, 1) == numel(y), '样本数与标签数不一致');

第一句把矩阵展成一维向量检查所有元素,第二句核对样本数。遇到少量缺失,我会先用fillmissing或直接rmmissing按行删除,避免特征选择阶段出现“某列只有 3 个有效值却拿到很高分数”的假象。

2.4 2020-2 后缀到底是什么意思

压缩包名里的2020_2不是算法代号,它更像维护者在版本 7.0.1 基础上加的一个发布批次标记,表示 2020 年的第二次发布。很多 MATLAB 工具包会在 File Exchange 页面写一个正式版本号,压缩包内部再带年份序号,方便本地归档和追溯:哪天拿到一个 2020-2 的包,和 2020-1 对比就知道更新了什么。

拿到包之后先打开READMEContents.m看一眼更新日志。这个方法虽然很笨,但能避免很多“代码明明照着别人教程抄的,为什么报错”的问题,因为对方的版本可能是 2020-1,方法是旧签名。

3. 用 FSLib 跑通第一个特征选择最小流程:从原始矩阵到选中特征

3.1 准备一个真实规模的输入

起步数据不一定要多大,但最好具备“特征数明显多于样本数”或“特征间存在冗余”这两个特点之一,否则特征选择的意义不大。MATLAB 自带的ovariancancer数据就很合适,216 个样本、4000 个特征,而且标签就是两类:

load ovariancancer X = obs; y = grp2idx(grp); fprintf('样本数 %d,特征数 %d\n', size(X, 1), size(X, 2));

obs是 216×4000 的基因表达量矩阵,grp是 216×1 的 cell 数组。如果你的 MATLAB 没有这个数据集,或者压根不用 Statistics Toolbox,也可以直接用任意一份自己的表格数据,Section 2.3 的读入代码可以直接复用。重点是让 X 和 y 进入工作区时形状正确,这一步花 5 分钟确认,后面能省 50 分钟排错。

提示:如果你不确定当前工作区的 X 和 y 是否符合 FSLib 要求,先不要调用算法,直接whos X y看尺寸。

3.2 跑 mRMR 选 20 个特征

最小可运行流程我通常用 mRMR 开头,原因是它同时考虑特征和标签的相关性以及特征之间的冗余度,在高维数据上得到的特征集合比单独跑相关系数更均衡。代码就三行:

k = 20; [selected, score] = mrmr(X, y, k); disp(selected);

selected是选中的特征下标,比如[128 301 45 19 776 ...],这些下标直接对应X的列。score是每个选中特征进入序列时的得分,通常不是最终全局排序分,数值本身跨版本可能不可比,但大小关系仍然能反映特征被选入的先后。

由于 FSLib_v7.0.1_2020_2 里 mRMR 的函数命名可能在不同电脑上不完全一致,调用前先做一次身份确认:

which mrmr -all

如果 MATLAB 报 “mrmr not found”,就在 methods 目录里找以mrmr开头的文件,把函数名换成实际文件名再调用。如果which显示多个结果,说明发生了同名冲突,这是后面会专门处理的路径污染问题。

拿到评分后,建议直接画出来看分曲线:

figure; stem(score, 'filled'); xlabel('Feature index'); ylabel('FSLib score'); title('mRMR feature scores');

绝大多数真实数据里,前面几个特征分数明显高,后面会出现一个类似肘部的平台,平台之后的特征对模型的边际贡献开始变小。平台出现的位置就是 k 的第一个参考值。这里不是说 k 必须取在肘部,而是让用户对自己的数据特征分布有一个直观判断,避免闭着眼睛设 k=100。

3.3 把选中的特征写回原始表格

如果 X 是从 table 里拆出来的,选完后还需要把特征名对应回去。假设原始表第一列是标签,后面每列是一个特征:

varNames = T.Properties.VariableNames; selectedNames = varNames(selected + 1); % 第一列是标签,特征列从第 2 列开始 TSelected = T(:, [1, selected + 1]); writetable(TSelected, 'selected_features.csv');

这里最容易错的是列偏移:selected是从 X 的列号算的,而 X 对应原表T{:, 2:end},也就是 X 的第 1 列是原表的第 2 列,所以写回时一定要做+1。如果漏了,选中的特征名会整体错位一列,最后写进 CSV 的是完全不同的特征。

3.4 怎么判断这 20 个选得对不对

先别急着追求最优精度,我一般看三件事:第一,选出的特征和业务直觉是否一致,比如做信贷评分时如果前 20 个特征里全是同一个渠道衍生变量,就要怀疑冗余处理力度不够;第二,score 曲线是否在 k 附近出现明显下降;第三,后续模型在同样交叉验证条件下是否稳定。第三点放到最后一章专门做。

如果 mRMR 的结果不满意,还可以直接用评分中位数做阈值,不固定 k:

m = median(score); selectedByMedian = find(score > m);

这种做法的好处是不需要提前拍脑袋定 k,坏处是遇到 score 分布扁平的场景时,选出的特征数量可能超过预期。固定 k 和阈值法没有绝对优劣,关键是把选择依据写进脚本注释,后面回头审计时知道当时为什么这么设。

4. FSLib 特征选择算法的选型:Filter、Wrapper 和 Embedded

4.1 三种路线到底在选什么

很多刚开始用 FSLib 的人会把“特征选择”理解成一种操作,实际它是一族差异很大的算法。FSLib 把算法按搜索策略和评价方式分了三大类:Filter、Wrapper、Embedded。

Filter 思路是独立地评估每个特征和标签之间的关系,再按分数排序截取前 k 个。计算量最小,也最适合特征数上千的场景。缺点是不考虑特征之间的冗余,两个高度相关的特征可能同时排进前 20,造成信息重复。

Wrapper 把“特征子集”当成搜索对象,用分类器或回归器的效果给每个子集打分,典型的包括 SVM-RFE 和贪心前向选择。这类方法能更直接地反映最终模型需求,但计算复杂度随特征数增长非常快,4000 个特征跑完整 wrapper 在普通笔记本上可能要按小时计。

Embedded 介于两者之间,把特征选择嵌入模型训练过程,比如带 L1 惩罚的逻辑回归、决策树分裂时的特征重要性。FSLib 里这类方法通常会要求指定一个基础模型或核函数参数,使用门槛高一些,但效率和效果通常比纯 wrapper 更均衡。

算法族是否考虑特征间冗余计算成本适合场景
Filter通常不考虑,mRMR 是例外高维数据粗筛,保留前 100~500
Wrapper由模型间接考虑样本量中等,特征数几百以内
Embedded在训练中考虑特征和标签关系明显非线性

4.2 FSLib 里怎么快速对比多种方法

FSLib 没有强制统一的调用入口,但大多数方法的位置参数相似,所以我会写一个薄封装函数,把当前项目里常用的算法全部收敛到一个接口下:

function [idx, score] = runFSLib(method, X, y, k) switch lower(method) case 'mrmr' [idx, score] = mrmr(X, y, k); case 'fisher' [idx, score] = fisher(X, y, k); case 'relieff' [idx, score] = relieff(X, y, k); otherwise error('未识别的算法: %s', method); end end

这段代码本身没有任何智能逻辑,它的价值是把方法名、参数格式、返回值统一成同一种形状,这样后续循环对比、结果缓存、交叉验证都只需要处理一个函数签名。注意,fisherrelieff的名字要以本地which结果为准,如果你的版本里叫fisher_scorereliefF,直接改 case 后面的字符串和对应函数调用即可。

调用方式:

methods = {'mrmr', 'fisher', 'relieff'}; selectedMap = containers.Map(); for i = 1:numel(methods) [idx, score] = runFSLib(methods{i}, X, y, 20); selectedMap(methods{i}) = idx; fprintf('%-8s 前3个特征: %s\n', methods{i}, mat2str(idx(1:min(3, end))')); end

containers.Map在同一个循环里把每个方法选出的下标存起来,避免后面还得重新跑一遍。mat2str把行向量转成可打印字符串,min(3, end)防止 k 小于 3 时索引越界。循环结束后,可以用intersect看两组特征重叠度,比如 mRMR 和 Fisher 只重叠了 4 个,这就提示数据里相关结构比较复杂,单靠一个 Filter 方法不够,需要进入 wrapper 验证。

4.3 特征类型不匹配时的预处理

FSLib 里大量方法基于欧氏距离、方差或协方差计算,默认输入是连续数值。如果原表里有离散特征,比如“学历等级”或“设备型号”,直接用原始整数编码通常不合理,因为整数编码会强行给类别制造排序关系。我一般用onehotencode做展开:

Tcat = T(:, catCols); % catCols 是离散特征列号 Tenc = onehotencode(Tcat, 2, 'Categories', 'all'); Tnum = T(:, setdiff(2:width(T), catCols)); X2 = [Tnum{:,:}, Tenc];

onehotencode第二个参数 2 表示沿列方向展开,返回的Tenc是单列 table,展开后每个类别占一列。数值列继续保留原始值,最后拼成新的特征矩阵 X2。这里有一个容易被忽略的问题:离散列里某个类别只出现了 1 次时,one-hot 展开后那一列绝大多数是 0,FSLib 的 Filter 方法可能把它当成“高分特异特征”,实际只是一个离群点。遇到这种情况,我会先做频次过滤,出现次数小于 5 的类别先合并成“其他”。

5. FSLib 的高频参数调整、运行时报错与结果不一致问题

5.1 必调参数表

FSLib 虽然把算法封装得接近“黑盒”,但有几个参数几乎每个项目都要碰。我在不同数据集上反复用下来,优先级最高的是下面这些:

参数常见取值对结果的影响判断方式
k5~100直接决定最终特征数量看 score 曲线的肘部位置
距离度量euclideancosine影响 ReliefF、KNN 类算法同一数据集换距离对比稳定性
归一化zscore、minmax量纲不一致时距离类算法结果失真特征标准差相差 3 个数量级时必做
标签类型分类整数、连续值Filter 方法分回归和分类两套公式先用class确认 y 的类型

不要把 k 设成一个固定值就完事。曲线下降平缓时,k 多 10 个特征通常只会带来 1% 的精度变化,但模型复杂度会明显增加。反过来说,如果 score 曲线在 k=15 处断崖式下跌,那就没必要硬选 50 个。

5.2 特征尺度与归一化

基于距离的特征选择方法对尺度极其敏感。基因表达量可能是 0~10000,年龄是 20~80,体重是 40~120,直接算欧氏距离时,表达量的波动会完全淹没年龄和体重的影响。我先做一次 zscore 归一化:

X = normalize(X, 'zscore');

normalize按列计算均值和标准差,每一列变成零均值、单位方差。注意,如果某一列是常数,zscore 会得到 NaN,因为标准差为 0 时做了除法。后面任何 FSLib 方法碰到 NaN 都可能直接报错或返回空结果。稳妥写法:

X(:, var(X) < eps) = [];

删除近似常数特征。这个操作要放在归一化之前,否则标准差为 0 的列已经变成 NaN 了。

5.3 路径污染和函数名冲突

FSLib 踩坑率最高的地方不是算法参数,而是 MATLAB 工具箱自带函数和 FSLib 里的同名函数打架。最典型的是relieff,MATLAB 统计和机器学习工具箱自带一个relieff,FSLib 里如果也放了一个同名文件,which会显示多个:

which relieff -all

出现多个结果时,MATLAB 默认执行搜索路径里排在前面的那个,而 FSLib 的路径是后添加的,可能根本轮不到它执行。解决办法是把 FSLib 路径用-begin选项放到搜索路径最前面:

addpath(genpath(fslibPath), '-begin');

这样 FSLib 里的同名函数会优先被调用。这条命令会改变当前会话的搜索路径,但不会永久影响 MATLAB 自带函数,只要不savepath,重启后恢复原样。如果你已经保存过旧路径配置,先别急着restoredefaultpath,那会把用户自己的路径也清掉,我一般只在干净环境里这么做。

5.4 常见报错对照

FSLib 的报错信息经常不适合直接搜索,因为报错点通常在内部函数而不是你的入口。最常见的几个原因和解法:

报错信息特征真实原因处理方式
Index exceeds the number of array elementsk 大于特征列数k = min(k, size(X, 2))
Input must be a numeric matrixX 是 table 或 cellX = table2array(X)
Labels must be a column vectory 是行向量或列方向不对y = y(:)
NaN or Inf数据里有缺失或无穷值isfinite检查并清洗

我在所有 FSLib 调用之前都会放一段防御代码:

assert(ismatrix(X) && isnumeric(X), 'X 必须是数值矩阵'); k = min(k, size(X, 2)); y = y(:); assert(numel(y) == size(X, 1), '样本数与标签数不一致');

这四行不能提升算法精度,但能把一半以上的低级问题拦在入口处,让后面的报错信息真正指向算法参数。

5.5 结果不一致:随机性来源在哪

FSLib 里不少方法带有随机性,ReliefF 的邻居采样、wrapper 的初始子集、以及部分优化求解都会依赖随机数。每次运行选出的特征不完全一样,这不是库坏了,而是算法本身的随机机制。要让自己能复现结果,至少在每个脚本开头固定随机数:

rng(42);

固定种子只能保证同一台机器、同一版本 MATLAB 下可复现。不同平台或不同 MATLAB 版本之间,随机数生成器的算法细节可能导致结果仍然不一致。这类不一致不能靠 rng 完全消灭,只能接受,并用多次运行统计重叠率来判断特征子集是否稳定。

6. 特征选择结果验证:交叉验证与特征稳定性

6.1 用 5 折交叉验证对比“全部特征”和“FSLib 选中特征”

特征选择是否有效,最终要看模型泛化性能,而不是算法打分多高。我会用分层 5 折交叉验证,固定随机种子,保证全特征和选中特征在完全相同的训练测试划分下对比:

rng(42); cvp = cvpartition(y, 'KFold', 5); accFull = zeros(cvp.NumTestSets, 1); accSel = zeros(cvp.NumTestSets, 1); for fold = 1:cvp.NumTestSets tr = cvp.training(fold); te = cvp.test(fold); mdlFull = fitcdiscr(X(tr, :), y(tr), 'DiscrimType', 'pseudoLinear'); accFull(fold) = 1 - loss(mdlFull, X(te, :), y(te)); mdlSel = fitcdiscr(X(tr, selected), y(tr), 'DiscrimType', 'pseudoLinear'); accSel(fold) = 1 - loss(mdlSel, X(te, selected), y(te)); end fprintf('全部特征准确率: %.2f%%\n', mean(accFull) * 100); fprintf('选中特征准确率: %.2f%%\n', mean(accSel) * 100);

cvpartition的分层会尽量让每一折里的类别比例接近原始数据,避免某个 fold 恰好没有少数类样本。fitcdiscr在特征数多于样本数时必须指定pseudoLinear,否则协方差矩阵不可逆会直接报错。loss返回的是分类损失,用1 - loss得到准确率。

如果选中特征只有 20 个,却能在 4000 个特征里拿到接近或者更高的准确率,说明 FSLib 选择的特征是有效信号。如果反而大幅下降,可能不是特征选择算法的问题,而是基模型在低维空间需要重新调参,比如逻辑回归或 LDA 在特征减少之后决策边界简单了,原来的惩罚项可能突然过强。

6.2 特征稳定性检查:换个样本子集还是不是同一批特征

交叉验证能说明“这组特征在这个数据集上是否有效”,但回答不了“这组特征换成一批新数据还稳不稳”。做法是多次子采样,再统计选中特征的重叠率:

rng(2020); overlap = zeros(10, 1); numSel = numel(selected); for r = 1:10 idxSample = randsample(size(X, 1), round(0.8 * size(X, 1))); [idxRep, ~] = mrmr(X(idxSample, :), y(idxSample), numSel); overlap(r) = numel(intersect(idxRep, selected)) / numSel; end fprintf('平均重叠率: %.2f\n', mean(overlap));

randsample每次抽取 80% 样本,mrmr重新选择同样数量的特征,intersect计算两次选择的共同特征数量。平均重叠率在 0.8 以上说明这组特征对样本扰动不敏感,可以放心交给下游模型;在 0.5 以下时先别急着加大 k,那往往不是特征数不够,而是候选特征之间存在高度相关的“同义特征组”,算法只是随机从组里挑了一个代表。重叠率持续低于 0.5 时,先对选中特征算相关系数矩阵做层次聚类,把强相关特征分到同一组,再从每组挑代表特征重新进入交叉验证,直接加大 k 只会让模型复杂度上升而稳定性没有实质改善。

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

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

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

立即咨询