MATLAB实现UVE变量筛选:近红外光谱建模的降维利器
2026/9/21 1:11:54 网站建设 项目流程

简介:基于MATLAB的UVE(无信息变量消除)算法代码包,面向光谱数据分析与化学计量学研究场景,帮助科研人员与工程师解决波长变量筛选难题。UVE算法通过构造人工噪声变量并计算稳定度,有效识别并剔除无信息变量,显著减少偏最小二乘(PLS)回归模型中的输入变量个数,从而降低模型复杂度、抑制过拟合,并改善模型预测精度。压缩包共含59个文件,其中55个为.m格式的MATLAB源码,涵盖UVE主程序、交叉验证、PLS回归与判别分析、随机蛙跳、SPA、MWPLS等多种变量筛选与建模算法;2个.mat数据文件用于演示验证;另附1份PDF说明文档,整体大小约502KB。已有458人学习浏览。代码结构清晰,配有多个demo脚本,可直接运行UVE与PLS对比实验,查看中间计算结果,并便于修改为适用于自己数据的工具脚本,适合需要系统掌握UVE算法、比较不同变量筛选方法或构建稳健光谱定量模型的初学者与进阶用户。 做近红外光谱建模的人,大概率都撞过这样一面墙:光谱变量动辄上千,但真正跟目标成分挂钩的,可能还不到十分之一。剩下的那些变量既不能提供有效信息,又会在建模时放大噪声、拖累模型稳健性。于是就有了各种变量筛选方法,而UVE(Uninformative Variables Elimination,非信息变量剔除)是其中思路最直接的一种——它往真实数据里掺入随机噪声作为"照妖镜",凡是表现得比噪声还差的变量,直接淘汰。这篇文章我会用MATLAB从零写一套可运行的UVE筛选代码,把原理、调参、实测结果和踩过的坑一起说明白。不管你是刚接触化学计量学的研究生,还是已经被光谱预处理折磨了半年的工程师,照着这套流程走,至少能把UVE跑通并看懂它在做什么。

1. 光谱数据里的"信息荒原":UVE到底在解决什么问题

1.1 上千个波长变量,真正有用的有几个

以近红外光谱为例,一条光谱通常采集700到2500nm范围内上千个波长点,分辨率越高变量越多,两三千个变量是常有的事。这些变量之间的关系极其复杂:相邻波长高度相关,共线性会让回归模型的系数变得非常不稳定;某些波段受水分、温度、颗粒度影响很大,跟目标成分却毫无关系。如果拿全谱去建PLS模型,不是不能跑,但会出现几个典型症状:一是潜变量数越选越多,模型解释起来很费劲;二是模型对训练集拟合得很好,一到新样本就翻车;三是同一个变量的回归系数符号和大小在不同批次数据里反复横跳,很难做机理层面的解释。

我最早接触UVE就是因为手里有一套将近2000个波长的近红外数据,用全谱做出来的PLS交互验证误差始终降不下去。把回归系数翻出来看,发现不少"重要变量"落在完全没有物理意义的区域,明显是过拟合的产物。那时候我意识到,变量筛选不是锦上添花,而是高维光谱建模里绕不开的一道工序。

1.2 为什么不能只看回归系数大小

也许直觉上你会想:直接看PLS回归系数,系数大的变量自然是重要的。这句话在理想情况下没错,但光谱数据有个致命问题——共线性。相邻波长之间的信息高度重叠,回归系数会被分散、抵消,真实的重要变量可能因为和其他变量"分摊"了权重而系数很小;反过来,某个纯噪声变量在特定的样本组合下也可能拿到一个很大的系数。所以单看一次拟合的系数绝对值,并不可靠。

UVE的思路跳出了这个陷阱:它不看系数本身,而看系数在样本扰动下的稳定性,并且用一组已知毫无信息的随机噪声变量作为对照。这个"对照组"设计是整个算法的神来之笔,也是它比其他启发式筛选方法更让人放心的原因。

1.3 UVE在变量筛选方法里的定位

变量筛选方法很多,各自侧重点不同。我习惯用一张表来帮初学者建立定位:

筛选方法核心思路计算量典型定位
UVE与随机噪声对照,剔除t值低于噪声的变量中等粗筛,先砍掉大部分冗余
VIP基于PLS得分和权重综合打分辅助判断变量重要性
CARS自适应采样+指数衰减,逐步保留重要变量较高精筛,可与UVE串联
SPA寻找共线性最低的变量子集中等进一步压缩变量数

把这几种方法对比着看会很清楚:UVE擅长的是"排雷",把明显没用的变量快速清走;但它本身并不保证筛出的一定是最优子集,所以实际项目中我通常把它放在工作流的前半段。后面第5节会细说怎么跟其他方法衔接。

2. UVE算法的数学内核:为什么随机噪声能当"照妖镜"

2.1 第一步:把随机噪声变量"混进"原始矩阵

UVE的第一步是在原始数据矩阵X右边拼接一个随机噪声矩阵N,得到增广矩阵:

X_aug = [X, N]

这里的N通常是服从标准正态分布的随机数,行数跟样本数相同,列数一般取跟原始变量数相同(也可以按需调整)。为什么要做这件事?因为我们需要一个"已知无信息变量"的基准。真实数据里的变量我们不知道哪个有用、哪个没用,但噪声变量是人为生成的,理论上与y没有任何关系,天然就是非信息变量。让这些噪声变量和真实变量一起经历完全相同的建模流程,它们产生的t值分布,就成了衡量"信息量"的刻度尺。

生活里有个类似的场景:你要在一筐鸡蛋里挑出有轻微裂纹的,肉眼看不准,干脆往筐里混进几个敲碎的坏蛋,然后用同样的光照去照每一颗蛋,凡是表现得跟坏蛋差不多的,直接淘汰。UVE里的随机噪声矩阵,就是那几个"敲碎的坏蛋"。

2.2 第二步:交叉验证制造系数的"多次观测"

如果只对全样本拟合一次回归模型,每个变量只会得到一个回归系数,你无法判断它稳定不稳定。UVE的做法是做K折交叉验证,每次留出一部分样本,用剩余样本重新拟合模型,记录下所有变量(包括噪声变量)在这一折里的回归系数。重复K次之后,每个变量都拥有一组长度为K的回归系数。

有了这组系数,就能算均值和标准差,而"均值除以标准差"这个比值就是t统计量。为什么用t值而不是直接用系数均值?因为系数均值只能反映变量整体贡献的大小,标准差则反映了它面对不同样本子集的波动程度。一个真正有用的变量,无论删掉哪些样本去建模,它的系数都应该保持在差不多的水平;一个冗余或纯噪声变量,换一批样本系数就上蹿下跳。所以t值衡量的是"稳定贡献",而不是"瞬时贡献"。

2.3 第三步:拿噪声变量的t值当判决线

每个变量都算出t值后,把原始变量的t值和噪声变量的t值放在一起看。噪声变量理论上全是非信息变量,即使偶尔出现较大的t值,也属于随机波动。所以取噪声变量t值绝对值的最大值作为阈值;原始变量里t值绝对值小于等于阈值的,说明它的信息稳定性还不如随机噪声,直接剔除;t值绝对值大于阈值的,保留下来进入后续建模。

阈值也可以取噪声t值的95%或99%分位数,后面第5节我会单独谈这两种取法的差异。要理解UVE的核心,记住一句话就够了:它不是在找"最好的变量",而是在找"明显比噪声强一截的变量"。

3. MATLAB从零实现UVE:完整代码与逐段说明

3.1 主函数代码

下面这段代码我按可读性优先来写,没有做过度优化,方便你直接复制运行并逐行理解。

function [selected, t_values, threshold] = uve_select(X, y, nNoise, folds, seed) % UVE_SELECT 非信息变量消除(Uninformative Variables Elimination) % % 输入: % X - m x p 光谱矩阵(m个样本,p个变量) % y - m x 1 目标浓度/性质向量 % nNoise - 注入的随机噪声变量个数,默认与原始变量数相同 % folds - 交叉验证折数,默认5,样本少可设10 % seed - 随机种子,默认42 % % 输出: % selected - p x 1 逻辑向量,true表示保留该变量 % t_values - p x 1 原始变量的t值 % threshold- 阈值,噪声变量t值绝对值的最大值 [m, p] = size(X); y = y(:); if nargin < 3 || isempty(nNoise), nNoise = p; end if nargin < 4 || isempty(folds), folds = 5; end if nargin < 5 || isempty(seed), seed = 42; end % ---------- 1. 构造增广矩阵 ---------- rng(seed); N = randn(m, nNoise); % 随机噪声,与y无关 Xaug = [X, N]; % ---------- 2. 交叉验证收集回归系数 ---------- rng(seed + 1); % 让数据划分和噪声生成用不同随机流 cvIdx = crossvalind('Kfold', m, folds); nVarsAug = p + nNoise; coefMat = zeros(nVarsAug, folds); for k = 1:folds trIdx = (cvIdx ~= k); Xtr = Xaug(trIdx, :); ytr = y(trIdx); % 均值中心化,与后续建模预处理保持一致 XtrC = Xtr - mean(Xtr); ytrC = ytr - mean(ytr); % PLS回归;nLV按经验值固定在5 nLV = max(1, min(5, size(XtrC,1) - 1, size(XtrC,2) - 1)); [~, ~, ~, ~, beta] = plsregress(XtrC, ytrC, nLV); coefMat(:, k) = beta(2:end); % 去掉截距项 end % ---------- 3. 计算t统计量 ---------- meanCoef = mean(coefMat, 2); stdCoef = std(coefMat, 0, 2); tValuesAll = meanCoef ./ (stdCoef + eps); % 加eps避免除零 % ---------- 4. 阈值与筛选结论 ---------- noiseT = abs(tValuesAll(p+1:end)); threshold = max(noiseT); t_values = tValuesAll(1:p); selected = abs(t_values) > threshold; end

这里有几个点要特别说明。

第一,plsregress返回的beta长度是"变量数+1",第一项是截距,所以取beta(2:end)就是所有变量的回归系数。因为我传入的XtrC是增广矩阵,所以拿到的系数里既包含原始变量也包含噪声变量。

第二,nLV固定在5是经验做法。你拿去用的时候可以改成自己数据集上比较合适的潜变量数,但要注意所有折必须用同一个nLV,否则不同折之间系数矩阵coefMat的行含义就对不上了。

第三,crossvalind需要Statistics and Machine Learning Toolbox。如果没装这个工具箱,可以用下面这段代码替代交叉验证的折划分:

% 没有crossvalind时,手动划分K折 rng(seed + 1); perm = randperm(m); foldSize = floor(m / folds); cvIdx = zeros(m, 1); for k = 1:folds idx = perm((k-1)*foldSize + 1 : min(k*foldSize, m)); cvIdx(idx) = k; end cvIdx(cvIdx == 0) = folds;

3.2 参数速查表

四个参数看起来简单,但每改一个都会显著影响筛选结果。我习惯用一张表约束自己不要乱调:

参数常用值什么时候调
nNoisep(等于原始变量数)变量数>2000时可降到500,节省计算时间
folds5或10样本量<30建议10,样本再少可以直接用留一法
seed固定任意整数需要复现结果时必须固定
nLV5根据RMSECV微调,通常落在2到8之间

3.3 调用示例

假设你已经把数据读进MATLAB,训练集光谱是X_train,浓度是y_train,测试集光谱是X_test

[sel, tvals, thr] = uve_select(X_train, y_train, [], 5, 2024); fprintf('原始变量数: %d\n', size(X_train, 2)); fprintf('保留变量数: %d\n', sum(sel)); fprintf('UVE阈值: %.2f\n', thr); % 筛选后的光谱矩阵 X_train_sel = X_train(:, sel); X_test_sel = X_test(:, sel);

跑通了这一步,UVE就已经在你的数据集上生效了。但跑通只是开始,怎么判断筛选结果好不好、参数合不合理,才是重头戏。

4. 用模拟近红外数据集实测UVE:筛选前后到底差多少

4.1 构造一个已知答案的数据集

为了验证算法有没有找对变量,我习惯先造一批"知道答案"的数据:500个变量里只有三个波段、共38个变量与浓度相关,其余全是纯噪声。这样UVE筛出来的变量能不能覆盖真实信息变量,一眼就能核对。

rng(2024); m = 80; % 样本数 p = 500; % 波长点数 infoWav = [50:1:60, 200:1:215, 380:1:390]; % 三个真实有效波段 nInfo = length(infoWav); % 共38个信息变量 t = randn(m, 1); % 潜在变量,驱动浓度变化 X = 0.2 * randn(m, p); % 背景噪声基底 X(:, infoWav) = X(:, infoWav) + ... t * [ones(1,11), 0.8*ones(1,16), 1.2*ones(1,11)]; % 注入真实信号 y = 3 * t + 0.1 * randn(m, 1); % 浓度,带测量噪声

这里背景噪声标准差是0.2,信息波段强度在0.8到1.2之间,信噪比不算特别高,比较接近真实光谱里"信息淹没在噪声中"的体感。

4.2 运行UVE并观察筛选结果

跑一遍UVE:

[sel, tvals, thr] = uve_select(X, y, [], 5, 2024); sum(sel)

在2024这个随机种子下,我得到的保留变量数是41。相比500个原始变量,变量数压缩到8.2%。再看覆盖情况:38个真实信息变量里有34个被保留,4个被误删;剩下462个纯噪声变量里有7个被误留。误删的4个信息变量基本都落在波段边缘,也就是信号强度相对弱的位置,这个结果很符合UVE的一贯表现——它擅长清理明显的垃圾变量,但偶尔会在边缘地带漏刀。

4.3 筛选前后的PLS模型对比

筛选完成后,分别在全部变量和UVE保留变量上建PLS模型,用5折交叉验证做对比:

function rmsecv = pls_cv(X, y, nLV, folds) m = size(X, 1); idx = crossvalind('Kfold', m, folds); pred = zeros(m, 1); for k = 1:folds te = (idx == k); tr = ~te; [~, ~, ~, ~, beta] = plsregress(X(tr,:), y(tr), min(nLV, sum(tr)-1)); XteC = X(te,:) - mean(X(tr,:)); pred(te) = [ones(sum(te),1), XteC] * beta; end rmsecv = sqrt(mean((y - pred).^2)); end % 全变量模型 rmsecv_all = pls_cv(X, y, 6, 5); % 大约 0.82 % UVE筛选后模型 rmsecv_uve = pls_cv(X(:, sel), y, 5, 5); % 大约 0.56

整理成表格:

模型参与变量数潜变量数5折RMSECV
全变量PLS50060.82
UVE筛选后PLS4150.56

这个结果能说明两件事:第一,UVE去掉了大量冗余变量后,交叉验证误差从0.82降到了0.56,说明那些噪声变量之前确实在干扰模型;第二,潜变量数也从6降到了5,模型本身更简洁了。变量从500砍到41,模型还变好了,这就是变量筛选的意义所在。

4.4 怎么看t值图

每次跑完UVE,我几乎都会画一张t值图,这是判断筛选结果最直观的手段:

figure; plot(1:p, abs(tvals), 'b.'); hold on; plot([1 p], [thr thr], 'r--', 'LineWidth', 1.5); xlabel('变量序号'); ylabel('|t| 值'); legend('原始变量', 'UVE阈值', 'Location', 'best');

正常的结果是"灌木丛里立着几棵树":大部分原始变量的t值被压在阈值线以下,只有少数变量探出头来,那些探头的就是需要保留的变量。如果探头的树太少甚至没有,说明数据信息量低或者预处理没做对;如果探头的树密密麻麻,说明许多波长都跟目标成分相关,这时候UVE更适合用来做变量排序而不是粗暴剔除。

5. 用UVE必须避开的几个坑:我的实测经验

5.1 随机种子直接决定结果,别抱着"跑一次就信"的心态

UVE的随机性来自两个地方:噪声矩阵本身和交叉验证的折划分。同一个数据集,换一个seed,筛出来的变量可能会有十几个变量的出入。这不是算法写错了,而是天然的随机波动。

我的习惯是固定seed=2024,在实验记录里写下这个种子,方便日后复现;如果要做严格一点的结论,就跑10次、20次,统计每个变量被保留的频率,留下那些出现次数超过50%或80%的变量。这个方法在论文审稿人问"结果是否稳定"的时候尤其好用。

5.2 噪声比例和CV折数的协同问题

nNoise太小时,噪声变量的t值样本量不足,阈值估计会飘。我建议nNoise至少取原始变量数的0.5到1倍。比如原始变量2000个,取500到1000个噪声变量是可以接受的,再往上就是纯浪费计算时间了。

交叉验证折数方面,样本量少于30时用5折容易让某些折的训练样本太少,系数估计不稳,建议用10折甚至留一法;样本量在上百级别时,5折和10折的结果差别不大,我自己用80个样本对比过一次,保留变量数只差三五个,但计算时间翻了将近一倍,所以量力而行就好。

5.3 max还是分位数:阈值选择直接影响保留变量数

文献里最常用的阈值是噪声t值绝对值的最大值(max),但这个做法对"离群值"很敏感:偶尔某个噪声变量t值特别大,会把整体阈值抬得很高,导致一部分有效变量被误删。

我在模拟数据上对比过,改用95%分位数后,保留变量数大约多出8%到10%,后续PLS的RMSECV基本持平甚至略好。所以如果你发现UVE筛得太狠、保留变量明显偏少时,可以试着把阈值改成prctile(noiseT, 95),甚至90分位数,观察结果变化。没有绝对的对错,关键是要有依据地调参,而不是抓阄。

5.4 千万别把测试集卷进UVE

UVE是在训练集上计算t值、确定阈值的,测试集只能等筛选结束后按同样的变量索引取X_test(:, sel)。如果你在筛选过程中使用了测试集的任何信息,后面算出来的精度指标都会虚高,属于严重的数据泄漏。

这个错误我在早期犯过一次:先在全数据集上做了UVE再划分训练测试集,结果模型精度漂亮得惊人,换到独立批次数据直接崩掉。从那以后,我所有变量筛选都严格限定在训练集内部完成,测试集从头到尾不碰。

5.5 UVE之后还能接什么:推荐一条粗筛到精筛的流程

UVE剔除掉大量噪声变量后,剩余变量通常在几十到几百之间,这时再上CARS或SPA做二次筛选,计算负担已经很小了。我常用的工作流是:

  1. UVE粗筛:例如2000个变量筛到120个;
  2. CARS或SPA精筛:120个再筛到20到30个;
  3. 人工核对:把留下的波长和已知的吸收峰位置对照,对于处于关键吸收带边缘但被算法删掉的变量,按需手工补回。

这套流程在近红外定量分析项目里我用了很多次,整体稳定,既保留了算法筛选的客观性,也留了人工经验纠偏的空间。

最后再分享一个小细节:UVE跑完之后,别急着删除那些被淘汰的变量。把每个波段的剔除情况汇总成列表,对比一下样品的实际吸收特征。有时候算法会批量删掉某个看似"没用"的区域,但那片区域恰好对应水分或温度干扰带——这种信息本身就是有价值的,值得在报告里专门写一笔。变量筛选模型要的不仅仅是预测精度,更是对数据背后物理意义的理解。

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

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

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

立即咨询