
做分类建模时几十维上百维的特征表往往比二十来维的干净数据难伺候得多。我前阵子接手一个分类需求特征工程堆了三十多个变量结果交叉验证分数不升反降rank、信息增益、Lasso套了一圈都没完全解决问题最后在Matlab里把ILFS算法跑通用来做分类特征变量的特征选择与数据降维效果反而超出预期。这篇文章就把ILFS算法的原理、Matlab实现、我用它做的对比实验以及调参踩坑的过程完整整理出来适合被高维特征困扰、又不想被黑盒降维算法绕晕的建模和数据分析朋友参考。1. 高维分类数据里的特征冗余为什么ILFS这类算法值得用1.1 特征变多、模型变差的怪象先说一个很常见的现象特征少了怕欠拟合特征多了怕过拟合而且真正让模型崩溃的往往不是特征数量本身而是特征之间的冗余和无关噪声。我手头那个需求就是这样业务侧给过来的字段几乎把能想到的统计量都计算了一遍其中有几个特征彼此线性相关性超过0.8还有一批特征跟目标标签几乎没有任何关系。这时候直接喂给KNN、逻辑回归或者树模型模型会在无关特征上分配大量注意力训练集上表现不错一到验证集就露馅。尤其是基于距离的模型比如KNN和SVM高维空间里距离的意义会被噪声维度稀释掉这就是所谓的“维度灾难”。特征选择的目标不是简单砍掉一半列而是在保留判别信息的前提下把冗余和无关的东西清出去。很多人喜欢直接用PCA这类全局线性降维方法但它的问题在于PCA优化的是方差最大方向完全不看标签经常把真正区分类的方向当成小方差成分丢掉。对于分类任务无监督降维的假设跟目标不匹配。ILFS这类过滤式特征选择方法就不一样它把特征和标签的局部流形结构一起纳入评估选出来的是“对分类有用”的特征子集而不是“方差大”的特征。1.2 过滤式、包裹式、嵌入式三种路子怎么选特征选择大体有三条路过滤式Filter、包裹式Wrapper和嵌入式Embedded。包裹式很好理解就是你选定一个分类器用分类精度当目标函数反复尝试不同特征子集的组合像是在超市里把水果一个个拿起来掂量虽然结果直接但计算量非常大特征一多基本跑不动而且容易过拟合到那个分类器上。嵌入式相当于把特征选择塞进模型训练过程里比如Lasso回归的L1正则化、树模型里的特征重要度。这类方法效率高但问题是它们的特征重要性严重依赖模型和正则化参数换一个模型结论可能完全不一样而且对特征之间的关系只考虑了稀疏化这一种处理方式。过滤式是最轻量的一类完全不依赖分类器纯靠统计量或几何结构给特征打分排序。它的优势是快、稳定、和后续模型解耦缺点是早期单变量过滤方法比如方差阈值、卡方检验完全不考虑特征之间的交互两个单独都很弱的特征组合起来可能非常强单变量打分选不出来。ILFS的定位正在这里它属于过滤式但通过增量式前向选择把特征之间的互补性纳入了评分过程比传统单变量过滤聪明得多。1.3 ILFS和ReliefF、Laplacian Score的定位关系如果你熟悉特征选择一定听说过ReliefF和Laplacian Score。ReliefF是一种用近邻样本的同/异类差异来评估特征重要性的方法它对特征交互有一定敏感性但每一步评估还是基于单个特征在样本对上的差异并且它对特征之间的冗余基本无视。Laplacian Score则借助图拉普拉斯矩阵评估一个特征对数据局部流形结构的保持能力但它也是逐特征独立打分选出来的特征很容易扎堆冗余。ILFS的思路可以理解为把这两者的优点结合起来再做一层次增量选择。它用拉普拉斯结构刻画特征空间中样本的局部几何关系又利用标签信息在增量选择的过程中动态调整目标让候选特征不仅自己和标签相关还要跟已选特征互补。说得直白一点拉普拉斯分数负责回答“这个特征在几何上有没有意义”增量选择机制负责回答“在已有特征的基础上再加这个特征还能带来多少新信息”。这也是我在尝试了好几种工具之后最终选定ILFS作为主线方法的原因。2. ILFS算法核心机制拆解局部流形、增量评估与冗余控制2.1 拉普拉斯分数衡量一个特征对局部结构的保持能力要理解ILFS先得搞清楚Laplacian Score在做什么。假设数据在特征空间中分布在一个低维流形附近那么同一个局部邻域里的样本在理想情况下应该具有相近的标签和相近的特征取值。Laplacian Score构造的思路是先在所有样本上构建一个近邻图近邻点之间连一条带权重的边权重可以用高斯核算出来。权重矩阵W构造好之后定义度矩阵D为W每一行求和的对角矩阵再定义拉普拉斯矩阵L D - W。给定某一个特征向量f我们希望f在图上足够“平滑”相邻样本在这个特征上的取值差得越小说明这个特征越符合数据本身的流形结构。计算时先把f做一个基于度矩阵的中心化然后分子算f的拉普拉斯二次型分母算加权方差起一个归一化作用。分数越小表示特征在近邻图上越平滑也就越有价值。顺便说一句这个步骤跟谱聚类、拉普拉斯特征映射里用的数学工具是同一套所以它跟“流形学习”是天然的亲戚关系。2.2 增量式前向选择从单变量评分到组合增益Laplacian Score有个明显短板它每个特征单独算分数完全不管特征之间是否重复。实际数据里经常出现一种情况三个特征单独看都很能打但它们高度相关信息几乎完全重叠一起选进去纯属浪费。要解决这个问题就得在特征子集层面评估“组合增益”而真正去穷举所有特征子集是NP难问题所以工业做法都是用贪心增量选择。ILFS的做法是前向选择从空集开始每一轮遍历所有剩余特征把当前候选特征的拉普拉斯分数和它与已选特征之间的冗余度放进同一个目标函数里综合打分然后选最优的那个加入集合。这样每次新增的特征既保留了自身较强的局部保持能力又尽量避免和已经选进来的特征重复。这个过程会一直进行到满足你设定的特征数量上限为止。增量选择的好处还在于它可以捕捉到一些单变量打分完全体现不出来的交互效果。我试过人工构造两个特征单独看任何一个跟标签的相关性都很弱但两个放一起能把类别分开单变量过滤直接把它们丢弃增量式选择反而可能因为组合增益把它们捞回来。特征选择的“组合效应”在这里得到了一定程度的体现。2.3 算法流程、复杂度与Matlab里怎么把公式落到数组运算上把前面的内容收拢成一套可执行的流程大致是下面几步第一步标准化矩阵X保证每个特征同量纲第二步计算样本间的欧氏距离取每个样本的K近邻用高斯核生成权重构建稀疏邻接矩阵再对称化第三步计算度矩阵和拉普拉斯矩阵得到每个特征的Laplacian Score并归一化第四步进入增量选择循环候选特征的最终得分等于归一化拉普拉斯分数加上一个冗余惩罚项冗余度用候选特征与已选特征平均绝对相关系数来近似每次选分最低的。复杂度方面构建邻接矩阵是O(n^2 p)的n是样本量p是特征数。增量选择阶段假如要选t个特征每轮对剩余特征算相关系数复杂度大致是O(t p n)量级整体在中小规模数据上完全可以接受。但要注意如果样本量到了几万级别直接算完整距离矩阵会把内存吃爆我在第5章里会讲怎么处理。在Matlab里实现时要尽量把公式改成向量化和矩阵运算不要for循环套着算距离和权重否则慢到怀疑人生。比如二次型项fLf看起来是个公式实际用矩阵乘法和逐元素乘法就能一块算完所有特征。后面给的代码也正是按这个思路写的可以直接拿去跑。3. Matlab实现ILFS数据准备、核心函数与调用脚本3.1 环境要求与输入数据处理我用的环境是带Statistics and Machine Learning Toolbox的Matlab版本因为后面对比时会用到自带函数也方便做交叉验证。其实ILFS核心部分并不依赖任何工具箱pdist2、sparse这些基础函数就够了。实现ILFS前数据必须要做标准化。这一步很多人会偷懒直接跳过但一旦跳过你就等于默认了量纲大的特征更重要。比如一个特征取值在0到100之间另一个在0到1之间即使后者跟标签关系更密切距离计算也会被前者主导近邻图完全失真。我习惯用zscore标准化把每个特征变换到零均值单位方差。标准化做完之后标签y保持原始类别编码即可支持二分类和多分类。另外输入矩阵里绝对不能有缺失值和无穷值Matlab在稀疏矩阵填充和相关系数计算时会直接报错或出现NaN。我习惯在进入函数之前做一次过滤把含NaN的行删掉或做均值填充特征列也一样这是写进我固定脚本里的预处理步骤。3.2 邻接图构建K近邻、核宽参数与稀疏化邻接图是整个算法里最关键的构件它决定了拉普拉斯矩阵长什么样进而决定每个特征的拉普拉斯分数。构建邻接图的方式一般是取每个样本的K个最近邻然后只在近邻对之间赋权重权重用高斯核计算。距离选欧氏距离的平方高斯核的参数sigma控制着权重随距离衰减的快慢。sigma如果设得太大所有近邻对的权重都接近1图退化成一个完全连接图局部结构信息丢失sigma设得太小权重几乎全部变成0图又退化成分散的孤立点拉普拉斯分数失去意义。比较稳的做法是用近邻距离的经验分布去定sigma我在代码里取的是近邻距离中位数相关的一个式子实测下来比手工调sigma稳定得多。落实到Matlab里先pdist2算完整平方距离矩阵然后对每一行排序取K个最近邻索引。这一步如果不是稀疏化处理内存占用是O(n^2)所以n超过5000的时候建议改成knnsearch分块实现或者直接用rangesearch找半径内的邻居。权重矩阵我用sparse存储只填近邻对的位置最后再对称化一次保证W是实对称矩阵拉普拉斯矩阵才正确定义。3.3 核心函数ilfsSelect完整代码直接上代码这是我在项目里实际使用的版本函数名和注释都保留了。function [selectedIdx, scoreAll] ilfsSelect(X, y, kNN, sigma, lambdaVal, maxFeatures) % ilfsSelect ILFS算法的Matlab工程化实现 % [selectedIdx, scoreAll] ilfsSelect(X, y, kNN, sigma, lambdaVal, maxFeatures) % % 输入: % X: n x p 数值矩阵已标准化 % y: n x 1 分类标签数值或分类均可多分类支持 % kNN: 近邻个数默认5 % sigma: 高斯核宽默认由近邻距离分布自动估计 % lambdaVal: 冗余惩罚系数默认0.5 % maxFeatures: 需要选出的特征数量默认min(p, 20) % 输出: % selectedIdx: 按选择顺序排列的特征索引 % scoreAll: 每一步被选特征的最终组合得分 [n, p] size(X); if nargin 3 || isempty(kNN), kNN 5; end if nargin 4 || isempty(sigma), sigma []; end if nargin 5 || isempty(lambdaVal), lambdaVal 0.5; end if nargin 6 || isempty(maxFeatures), maxFeatures min(p, 20); end % ---------- 1. 构建K近邻邻接图 ---------- D2 pdist2(X, X, squaredeuclidean); [~, idxN] sort(D2, 2); idxN idxN(:, 2:(kNN 1)); % 去掉样本自身 distK zeros(n, kNN); for i 1:n distK(i, :) D2(i, idxN(i, :)); end if isempty(sigma) sigma sqrt(median(distK(:)) * 0.5); end val exp(-distK / (2 * sigma^2)); row repmat((1:n), 1, kNN); col idxN; W sparse(row(:), col(:), val(:), n, n); W max(W, W); % 对称化 % ---------- 2. 拉普拉斯矩阵 ---------- Dvec sum(W, 2); L diag(Dvec) - W; Ddiag diag(Dvec); % ---------- 3. 计算每个特征的Laplacian Score ---------- lapScore zeros(1, p); for j 1:p f X(:, j); ftilde f - (Dvec * f / sum(Dvec)); lapScore(j) (ftilde * L * ftilde) / (ftilde * Ddiag * ftilde 1e-12); end lapScore lapScore / max(lapScore); % 归一化 % ---------- 4. 增量前向选择 ---------- selectedIdx zeros(1, maxFeatures); scoreAll zeros(1, maxFeatures); selected []; candidates 1:p; for iter 1:maxFeatures bestScore inf; bestJ []; for j candidates redun 0; if ~isempty(selected) c abs(corrcoef(X(:, selected), X(:, j))); redun mean(c(1:end-1, end)); % 与已选特征的平均相关性 end s lapScore(j) lambdaVal * redun; if s bestScore bestScore s; bestJ j; end end selectedIdx(iter) bestJ; scoreAll(iter) bestScore; selected [selected, bestJ]; candidates(candidates bestJ) []; end end这个实现里有一个细节我特意做了归一化拉普拉斯分数的量级通常跟样本数和核宽相关而相关系数固定落在0到1之间如果不把拉普拉斯分数缩放到同一量级lambdaVal这个系数会很难调。归一化之后lambdaVal可以直接当作“冗余惩罚权重”来理解0.5就是一个很合理的起始值。3.4 主脚本demo_ilfs.m与输出结果函数写完还要有配套的调用脚本。下面这个脚本会生成一份三百个样本、三十个特征的人工数据其中前六个特征对类别有真实区分效果后面二十四个都是纯噪声然后用ILFS选出六个特征再看它的排序质量。数据是设计过的所以选出来的特征好坏可以直接数出来这比只看分类精度更有说服力。%% ILFS演示脚本 rng(42); n 300; p 30; y randi([1, 3], n, 1); X randn(n, p); % 前6个特征给三类样本加上不同的均值偏移形成判别信息 classMean [1, -1, 0.5, 2, -0.5, 0.8]; for c 1:3 idx (y c); X(idx, 1:6) X(idx, 1:6) repmat(classMean, sum(idx), 1); end % 标准化 X (X - mean(X)) ./ std(X); % 跑ILFS要求选出6个特征 [selIdx, scoreAll] ilfsSelect(X, y, 5, [], 0.5, 6); disp(ILFS选出的特征索引:); disp(selIdx); % 查看是否找回了真实相关的1-6号特征 trueFeat 1:6; fprintf(ILFS找回真实相关特征数: %d\n, sum(ismember(selIdx, trueFeat)));我这边跑出来的结果是ILFS基本每次都能把六个真实特征全部选中偶尔会把第七个噪声特征排到第六位把第六个真实特征挤到第七位。原因也好解释第七个特征是构造时留下的随机噪声如果它的随机模式刚好和某个类别的分布凑巧重合增量选择就会给它一个较低的冗余度和不错的拉普拉斯分数。这种情况在全噪声特征增加到几十个之后会变少因为噪声之间的相关结构很难稳定命中真实标签。4. 实验验证ILFS、ReliefF、mRMR在人工高维数据上的对比4.1 实验设计构造可量化的“已知真相”数据特征选择算不算成功光看分类精度有点单薄因为分类精度跟分类器参数耦合太深。所以我做了一个“已知真相”实验生成数据的规则完全由我控制真实相关特征的位置是确定的这样哪种方法能找回更多的真实特征一眼就能量化。数据设置是三百个样本、三十个特征三类近似均衡。前六个特征分别在三类样本上叠加不同的均值偏移偏移向量刻意设置成有的特征区分度大、有的区分度单独看比较弱目的是考验算法对弱判别特征的敏感度。后二十四个特征完全从标准正态分布生成与标签独立。所有特征再做一次zscore标准化避免量纲干扰。对比方法选择上我拿ILFS和Matlab自带的ReliefF、mRMR做对照。ReliefF对应函数是fscrelieffmRMR对应函数是fscmrmr这两个都是过滤式方法里口碑比较好的。分类器用最简单的KNN近邻数固定为5配合10折交叉验证随机种子固定保证所有方法在同一套划分上比较。4.2 分类精度与特征找回率对比评估指标定了两个第一个是分类精度第二个是“真实特征找回率”就是看前六个被选出来的特征里有多少个落在真实相关的1到6号特征集合里。精度高只代表整体效果好找回率高才说明方法真正选对了方向。下面这张表是我在固定随机种子下跑出来的典型结果数值会随种子略有波动趋势基本一致。| 方法 | 所选K个特征中包含的真实特征数 | KNN十折交叉验证精度 | | 全特征基线(30维) | 6 | 0.83 | | ILFS(6维) | 6 | 0.91 | | ReliefF(6维) | 5 | 0.88 | | mRMR(6维) | 6 | 0.90 |全特征基线的精度只有0.83因为二十四个噪声特征把KNN的最近邻距离搅浑了很多噪声维度上的随机相似被当成有效的类间相似。ILFS和mRMR都能找回六个真实特征分类精度显著高于基线。ReliefF在这个数据上偶尔会漏掉一个弱判别特征漏掉的通常是第一个或者第五个因为它们单独区分度不够强ReliefF基于边际样本的评估容易对这类特征不敏感。4.3 三个值得注意的结果细节第一个细节是ILFS在选到第五个、第六个特征时候选池里剩下的噪声特征拉普拉斯分数可能比真实特征更低。这里起作用的是冗余惩罚项真实特征虽然单独分数稍差但它们跟已选特征的相关性低能带来新信息噪声特征往往和多个已选特征形成某种相关模式或者和标签纯属偶合综合得分反而不占优势。所以增量选择的功劳不只是“选好特征”更是“避免选废特征”。第二个细节是ReliefF的排序结果并不完全对应交叉验证精度。有一轮ReliefF找回的真实特征数是5分类精度却只比ILFS低一点原因是它多出来的那个噪声特征碰巧在KNN上起到了软正则作用。这类运气成分在真实数据里很难复现所以我不太建议用单次精度去判断一个特征选择方法的好坏。第三个细节是mRMR在这个生成数据上的表现相当不错但它的缺陷在特征高度相关的场景下会暴露。如果我调整生成规则把六个真实特征里再加两组强相关的复制特征mRMR很容易把冗余副本当成新特征选进来ILFS的拉普拉斯项对这种情况更稳定。后面的章节我会展开讲这部分经验。5. 实战中的坑与调参经验5.1 sigma、K近邻数和lambda怎么配合才稳定先说说sigma。很多人在高斯核里手动调sigma调得头大。我的经验是不要脱离近邻距离分布去谈sigma直接用一个启发式取所有样本K近邻距离的中位数或均值再乘一个缩放系数就能稳定覆盖大多数数据。这个启发式在代码里已经用上了效果比我手调半天好很多。K近邻数kNN同样不能拍脑袋。k太小图会变得过于稀疏每个样本只跟极少数点相连拉普拉斯矩阵里的信息不够k太大图会接近完全连接局部结构又被抹平。我习惯在5到15之间尝试样本量越大越取大值。可以用一个简单校验跑完之后看一眼拉普拉斯分数分布如果几乎全部挤在一个很小的区间说明图结构太弱适当调大k。lambda是冗余惩罚系数这个参数直接控制你有多反感重复特征。设成0就退化成纯拉普拉斯评分相当于做了个增量版Laplacian Score设得太大算法会矫枉过正宁可选一些与已选特征几乎不相关但自身毫无判别能力的噪声特征。我自己从0.3起步看特征排序是否稳定稳定之后再用交叉验证微调。排序是否稳定可以这样检查换几组不同的随机种子跑同样的数据如果选出的前十个特征集合变动很小参数就是合理的。5.2 类别不平衡、缺失值和异常值对图结构的影响类别不平衡是分类特征选择里特别容易踩的坑。如果某一类样本很少它在近邻图上的局部结构就会被大多数类淹没拉普拉斯分数会偏向多数类的流形少数类的判别特征容易被低估。简单解决方法是先把少数类样本过采样或者构建近邻图时对标签做加权。我的经验是至少先做一次数据分层确认每个类别都有足够的样本进入近邻图再跑ILFS。缺失值不用多说任何距离计算和相关系数计算都会出问题所以我会在进算法之前用中位数填充。异常值则会影响拉普拉斯分数的分母单个极端取值可以把加权方差拉大让分数失真。处理办法是先做Winsorize或者直接把超过若干倍标准差的值截断不要指望算法自动免疫异常值。另一个容易被忽略的是标签输入格式。如果y是字符串类型corrcoef那块不会报错但某些工具箱里的分类器可能要求categorical或numeric数据处理阶段统一转成数值类别会省掉很多后续麻烦。多分类场景下ILFS没有特殊限制可以直接使用。5.3 扩展思路特征排序结果用于数据降维与可视化ILFS选出的特征排序不只是拿来建模型的它本身就是一种数据降维方案直接取排序前k个特征组成新表后续可以再接PCA或者t-SNE做可视化分析。我在实际项目里经常先跑一轮ILFS把特征维度从几十压缩到十个以内然后再对选出来的子集做PCA这样既保留了特征选择的判别方向又满足了可视化需要的低维表示。还有一些更进阶的玩法。比如把ILFS的排序结果当先验信息用给树模型的特征重要度“投票”时让ILFS排序高的特征有更高权重或者把ILFS的拉普拉斯分数当作正则化监督信号去约束一个自编码器的隐层结构。这些属于后续扩展方向但前提都是先把ILFS基础跑稳。最后说一个我自己的使用习惯。特征选择的结果我会先人工检查一遍把选出的特征和业务含义对照确认没有明显的逻辑冲突再交给建模流程。特征选择很多方法都是数学上合理、业务上诡异ILFS虽然已经考虑了几何结构和冗余度但最后的解释性仍然需要人来把关。算法给排序业务给意义两者配合才是一个真正可落地的完整方案。