☰
CARS近红外波长筛选:指数衰减与竞争性重加权解析
2026/10/27 5:09:05 网站建设 项目流程

简介:这份资料包围绕竞争性自适应重加权算法(CARS)展开,面向光谱分析、化学计量学与机器学习方向的学习者和研究者,用于理解高维数据下的变量选择与偏最小二乘(PLS)模型优化问题。压缩包共37个文件,以32个MATLAB脚本(.m)为主体,辅以2个MAT数据文件、1个PDF说明、1个DOC文档和1个JPG示意图,整体约1.27MB,便于按算法实现、交叉验证、光谱预处理与结果绘图等模块查阅。已有11428人学习下载,说明其在CARS主题下具有较高的参考需求。资料中的脚本与文档可用于梳理变量按回归系数排序、自适应权重调整以及以RMSECV衡量模型泛化能力的实现思路,帮助读者从大量光谱特征中筛选更具代表性的变量,简化PLS模型并降低过拟合风险。

1. 一千个波长、五十个样本:CARS 把变量选择做成了一场淘汰赛

近红外光谱动辄一两千个波长点,样本却常常只有几十上百个,这是典型的 p 远大于 n。全谱塞进偏最小二乘回归,拟合优度看着漂亮,换一批样本预测就原形毕露。竞争性自适应重加权算法(CARS)针对的就是这一步:它不输出一张“变量重要性排行榜”让你自己截断,而是把波长筛选做成多轮淘汰赛——每轮强制缩小存活名额,每轮按 PLS 回归系数重新分配生存概率,最后用交叉验证误差回挑其中一轮的子集。

反直觉的地方在于,最终入选的波长往往不是第一轮系数最大的那些,而是那些在多轮随机扰动下反复被选中的变量。搞懂这一点,等于搞懂了一类更通用的工程思路:随机化、硬性约束和集成,三者叠加,把一个本来不稳定的特征选择问题变成可复现的流程。

2. 指数衰减函数:为什么保留名额必须被硬压到 2 个

2.1 两个端点解出 a 和 k

先把问题摆清楚。有 p 个波长,要做 N 轮蒙特卡洛采样,每一轮要保留多少个变量?如果没有约束,让算法自己决定,那结果一定是保留数在第一轮就掉到一个很小的值——因为权重最大的那几个变量会一直赢,剩下的名额空着也不影响。规定的名额数量本身必须由外部给定,这个外部给定者就是指数衰减函数。

形式选得很克制,只有一个指数项:

r_i = a · exp(-k · i),i = 1, 2, …, N

两个未知数 a、k,用两个端点条件钉死。第一轮不做任何预设,全部变量参与,所以 r_1 = 1;最后一轮必须小到 PLS 还能建回归,取 p 个变量里的 2 个,所以 r_N = 2/p。把两式相除,a 就被消掉了:

exp(-k(N-1)) = 2/p → k = ln(p/2) / (N-1)

再代回第一式,得到 a = exp(k) = (p/2)^(1/(N-1))。

这两个式子不需要额外调参,p 和 N 定了,整条曲线就定了。这是它比“每轮保留 90%、80%、70%……”这种手工阶梯方案更可维护的地方:没有中间那个随手拍的自由度,也就没有事后改参数凑结果的余地。

import numpy as np def edf_curve(p, n_iter): """给定变量总数 p 和迭代轮数 N,返回每轮的保留比例与保留个数""" a = (p / 2.0) ** (1.0 / (n_iter - 1)) # 保证第 1 轮 ratio = 1 k = np.log(p / 2.0) / (n_iter - 1) # 保证第 N 轮 ratio = 2/p i = np.arange(1, n_iter + 1) ratio = a * np.exp(-k * i) n_keep = np.ceil(p * ratio).astype(int) return np.maximum(n_keep, 2) # 浮点误差兜底,最少留 2 个 print(edf_curve(1000, 50)[1][[0, 4, 9, 19, 29, 39, 49]]) # [1000 602 319 90 25 7 2]

p是预处理后保留的波长列数,n_iter是蒙特卡洛轮数,ratio是相对全谱的保留比例,n_keep才是真正要执行的保留个数。注意np.maximum(..., 2)这一行不是装饰:在i = N附近,ceil可能因为浮点误差算出 3 而不是 2,一旦某轮保留 1 个变量,下一轮 PLS 直接没法建。

2.2 绝对削减量才是“粗筛后精修”的证据

把 p = 1000、N = 50 的曲线摊开看,比例数字容易骗人,每轮砍掉多少个才说明设计意图。

轮次 i保留比例 r_i保留个数本轮砍掉
11.0001000—
20.881881119
60.467467约 70
110.247247约 38
210.069770约 11
310.019620约 3
410.00556约 1
500.002020

前 5 轮砍掉了四百多个波长,几乎不痛不痒——相邻波长之间的相关系数常常在 0.99 以上,砍掉其中一半,PLS 的预测能力基本不变。如果换成线性衰减,p = 1000、N = 50,每轮固定砍 20 个,到第 10 轮还剩 800 个高度冗余的变量,这 10 轮的计算全浪费在一个信息量几乎没变的子集上。

指数衰减的隐含假设是:冗余度正比于当前规模。变量池越大,里面混着的无用变量越多,可以放心大刀阔斧;池子小到两位数之后,剩下的每一个都可能是有效信号,这时候一轮砍掉 1 个都要谨慎。这也是k只有一个参数却能同时满足“前期粗筛”和“后期精修”的原因——削减的绝对量是k · 当前保留数,天然跟着规模走。

下界为什么取 2 而不是 1 或 0?取 0 意味着要跑到没有变量,最后几轮 PLS 建不出来;取 1 在数学上可行,但单变量回归的系数没有比较意义,重加权采样那一步会退化成确定性的“留一个”。取 2 是让最后一轮仍然保留一点点竞争空间,即使这个空间已经小到只剩两个波长在互相挤。

N的取值反过来影响衰减陡峭程度。同样的 p = 1000,N 取 20,k = ln(500)/19 ≈ 0.327,第 2 轮就只剩 720,第 5 轮只剩 195,前期筛得太急,强变量可能在还没被充分评估时就被裹挟出局;N 取 100,第 2 轮只砍 20 个,计算量翻倍换来的信息增量却很有限。工程上 p 在几百到两千这个量级,N 落在 40 到 60 之间比较常见,本质是在“每轮砍得够狠”和“强变量有足够多轮次证明自己”之间找平衡。

3. 竞争性重加权:权重怎么从 PLS 系数变成生存概率

3.1 绝对值、归一化、除以总和

第 i 轮的名额定了,接下来要决定“谁留下”。CARS 的答案只有一句话:在当前存活变量上建一个 PLS 回归模型,把回归系数的绝对值归一化成概率,按概率抽。

先看为什么要取绝对值。PLS 回归系数 b 有正有负,正系数表示该波长处吸收增强会推高 y,负系数反过来。这两类变量对预测同等重要,符号只说明方向,不该影响它被保留的机会。直接拿 b 当权重,负数会被概率采样直接判死,这是实现时最容易翻车的一行。

再看为什么用 PLS 系数而不是单变量相关系数。相关系数只衡量“这一个波长和 y 的线性关系”,不考虑它和其他波长的关系。一组高度共线的波长,每个和 y 的相关系数都很高,但它们在模型里提供的其实是同一份信息。PLS 系数是在潜变量空间里联合解出来的,共线的几个变量会互相分摊权重,系数被摊薄,于是不会被同时大量保留——这正是多变量方法比单变量打分更适合光谱数据的地方。

归一化的除数也值得说一句。写成 w_j = |b_j| / Σ|b_k|,而不是除以 max|b|。除以 max 之后最大权重是 1,其余按比例缩小,这个向量不能直接当概率分布用;除以总和之后 Σw = 1,可以直接喂给np.random.choice(..., p=w)。两种写法在“谁大谁小”的排序上完全一致,但在采样环节一个能用、一个不能用。

有个退化情形必须知道:如果 PLS 潜变量数给得太少、或者变量数远大于样本数,可能出现大量系数接近 0 的情况,此时权重分布接近均匀,重加权采样就退化成了纯随机采样,这一轮的筛选完全靠 EDF 的名额约束在撑着。这解释了为什么 CARS 在极端 p ≫ n 的场景下结果波动特别大。

3.2 竞争是零和的:同一轮名额池里的互相挤占

“竞争性”这三个字经常被含糊带过。它指的是:这一轮的名额是固定的,一个变量多占一份概率,其他变量就少一份。权重高的变量不是“加分”,而是“抢走了别人活下来的机会”。

这种设计带来一个必须警惕的副作用——马太效应。假如没有 EDF 的名额约束,只做重加权采样,那么第一轮系数最大的几个变量会在后续每一轮都被高概率抽中,权重越来越集中,搜索空间迅速坍缩到几个初始赢家身上,本质上变成了一次贪心前向选择。EDF 的价值就在这里:它每一轮都把名额硬压小一截,逼着那些“次优但仍有信息”的变量在名额收缩过程中互相竞争,而不是让初始赢家一路躺赢到结束。

“自适应”则体现在权重每轮重算,不是一次算完用到底。每轮存活变量集合变了,PLS 模型跟着变,系数跟着变,权重也跟着变。上一轮被削弱的变量,如果在新子集里变得更有解释力,这一轮还能翻身。这个反馈回路是整个算法真正的信息来源。

3.3 有放回采样:实际存活数总比计划少一截

实现上,重加权采样通常写成“按权重有放回抽 n_keep 次,然后去重”。这一步会带来一个几乎所有人都遇到过的偏差:实际留下来的变量数,比你按 EDF 算出来的少。

原因很简单。有放回采样抽 n_keep 次,同一个变量可能被抽中多次,去重之后剩下的唯一值个数期望是

E[U] = Σ_j [1 - (1 - w_j)^n_keep]

权重越集中,重复抽中同一个变量的次数越多,唯一值越少。举个具体数:第 10 轮存活 320 个变量,EDF 要求保留 90 个,若权重完全均匀,E[U] = 320 × (1 - (1 - 1/320)^90) ≈ 79,比计划少了 12%;权重一旦偏向少数强变量,这个缺口会更大。

也就是说,实际衰减曲线比你按公式画出来的更陡。前几轮差别还不明显,越到后期缺口越显著,有时候第 i 轮的实际存活数甚至已经小于第 i+1 轮的理论值,代码里那个min(n_keep, alive.size)就会让这一轮“一个都不砍”,曲线出现一段平台。这不是 bug,是有放回采样的固有行为,但如果你拿实际保留数去和理论曲线逐点对数,一定会怀疑自己公式写错了。

想严格按 EDF 走,可以在去重后用权重降序补齐到 n_keep 个,代价是引入了确定性、削弱了随机化带来的稳健性;也可以换成不放回的加权采样(Gumbel top-k 那类做法),一次抽出恰好 n_keep 个互不重复的变量。两种改法各有取舍,前者简单但会让强变量更容易被固化,后者数学上更干净但实现稍绕。

3.4 蒙特卡洛抽样:为什么每轮要扔掉 20% 的样本

每一轮除了在变量维度上做淘汰,样本维度上也在做抽样:从 n 个样本里随机抽 70% 到 85% 建模型,剩下的当验证集。这一步是整条流程里最容易被当成“可有可无的加速手段”而砍掉的。

它的真实作用是把系数的不确定性显式暴露出来。全样本建一次 PLS,系数是被所有样本共同决定的,个别高杠杆样本(浓度异常、基线漂移严重的那几条谱)足以把某个无关波长的系数拉得很高。抽样之后,这条样本可能这一轮在、下一轮不在,靠它撑起来的虚假高权重撑不了几轮。真正稳健的变量,是在大多数抽样组合下系数都排在前面的那些——这和统计学习里的稳定性选择是同一个逻辑。

抽样比例太低,比如只抽 50%,模型本身就不稳,RMSECV 曲线会剧烈震荡,最优轮次的判断失去意义;抽样比例太高,比如 95%,各轮之间的样本差异太小,随机化几乎不起作用,整件事退化回一次确定性的迭代筛选。70% 到 85% 这个区间是经验上比较舒服的位置,样本量越少越应该往上靠,样本量充足时可以往 70% 压,让随机性发挥得更充分。

4. 一轮完整流程的代码拆解:从矩阵切片到 RMSE 曲线回选

4.1 主循环骨架

把 EDF 和重加权采样缝在一起,主循环其实只有二十来行。下面这份实现刻意保留了原始的“有放回采样再去重”写法,方便对照上一节的偏差分析。

import numpy as np from sklearn.cross_decomposition import PLSRegression def cars_pls(X, y, n_iter=50, n_lv=6, sample_ratio=0.8, seed=0): """ X: (n_samples, n_vars) 已完成 SNV/MSC、均值中心化的光谱矩阵 y: (n_samples,) 理化值 n_iter: 蒙特卡洛轮数 N n_lv: PLS 潜变量数,先用全谱交叉验证定好再传进来 sample_ratio: 每轮建模样本占全部样本的比例 """ rng = np.random.default_rng(seed) n, p = X.shape y = np.asarray(y).ravel() # 指数衰减函数的两个参数,端点条件见 2.1 a = (p / 2.0) ** (1.0 / (n_iter - 1)) decay = np.log(p / 2.0) / (n_iter - 1) alive = np.arange(p) # 存活变量在原始矩阵中的列号 coef_path, rmse_path, size_path = [], [], [] for i in range(1, n_iter + 1): ratio = a * np.exp(-decay * i) n_keep = max(2, min(int(np.ceil(p * ratio)), alive.size)) # 样本维度的蒙特卡洛抽样 cal = rng.choice(n, int(n * sample_ratio), replace=False) val = np.setdiff1d(np.arange(n), cal) # 变量维度切片后建 PLS pls = PLSRegression(n_components=n_lv, scale=False) pls.fit(X[np.ix_(cal, alive)], y[cal]) # sklearn 的 coef_ 形状是 (n_targets, n_features),必须 ravel b = np.abs(pls.coef_).ravel() w = b / b.sum() if b.sum() > 0 else np.full(b.size, 1.0 / b.size) pred = pls.predict(X[np.ix_(val, alive)]).ravel() rmse_path.append(np.sqrt(np.mean((pred - y[val]) ** 2))) size_path.append(alive.size) coef_path.append(alive.copy()) # 竞争性重加权采样:有放回抽 n_keep 次,去重后作为下一轮的变量池 picked = rng.choice(alive.size, size=n_keep, replace=True, p=w) alive = alive[np.unique(picked)] best = int(np.argmin(rmse_path)) return {"best_iter": best + 1, "subset": coef_path[best], "rmse": rmse_path, "size": size_path, "path": coef_path}

几个必须解释的点。scale=False是硬要求:PLSRegression默认会对 X 和 y 做标准化,如果你的光谱已经做过 SNV 或均值中心化,这里再标准化一次,等于把预处理做了两遍,而且 y 的标准化会让回归系数的量纲和后续的 RMSEP 对不上。coef_的形状是(1, n_features)而不是(n_features,),不ravel直接sum,得到的是一个形状为 1 的数组,后续rng.choice会报错。np.ix_(cal, alive)做的是笛卡尔积索引,取出行列交叉的子矩阵,比X[cal][:, alive]少一次拷贝。b.sum() == 0的兜底分支不是防御性代码,在潜变量数偏少时是真的会触发。

best_iter用argmin回挑,是最简单也最常被直接采用的规则。subset返回的是原始列号,不是当前轮次的相对下标——很多实现这里返回相对下标,后面拿去做预测模型就整体错位了,症状是“变量选出来了但预测结果完全不对”。

4.2 参数表:每个旋钮拧错了会怎样

参数含义经验取值拧错的后果
n_iter蒙特卡洛轮数 N40 到 60太小,衰减过陡,强变量来不及证明自己;太大,后期每轮只砍个位数,计算量白烧
n_lvPLS 潜变量数先用全谱 CV 定,常在 5 到 15过大,系数被噪声放大,权重失去区分度;过小,欠拟合,权重接近均匀,筛选退化成随机
sample_ratio每轮建模样本比例0.7 到 0.85太小,RMSECV 曲线抖动剧烈;太大,随机性不足,等价于确定性迭代
seed随机种子固定一个值不固定,两次运行结果完全不可比,报告里的变量集合没法复现

n_lv这一项要单独强调:它必须在跑 CARS 之前用全谱交叉验证定下来,然后固定住,不要在每一轮内部再嵌套一层 CV 去选。原因有两个,一是计算量会乘以 N,二是每轮的 LV 不同,各轮之间的 RMSE 就失去了可比性,最优轮次的判断直接失效。

4.3 argmin 不够用:怎么在 RMSE 曲线上挑子集

N 轮跑完,手上有一条长度为 N 的 RMSE 序列和一条对应的保留变量数序列。取最小值点是最直接的做法,但它有两个隐患。

第一个隐患是位置。最小值经常出现在后期,此时存活变量可能只剩个位数。单个 RMSE 值本身带着不小的方差,变量数少到这个程度,模型自由度紧张,一次抽样运气好就能把 RMSECV 压到很低,但这个低值回归到独立预测集上通常会反弹。第二个隐患是曲线形状。典型的 CARS 曲线是前期陡降、中期进入平台、后期又开始上翘。真正的“最优”往往在平台段的起点,而不是平台段里的最低那个点。

一个不需要额外计算量的启发式规则:先找到最小值点,再取“RMSE 不超过最小值加上一个标准误”的最靠前那一轮。标准误可以用 CV 折间标准差,也可以粗略用序列本身的波动估计。下面的写法只做示意,实际使用时把se换成 k 折 CV 的真实折间标准误更靠谱。

def pick_by_1se(rmse, path): rmse = np.asarray(rmse) best = rmse.argmin() se = rmse.std(ddof=1) / np.sqrt(rmse.size) # 启发式,非严格标准误 cand = np.where(rmse <= rmse[best] + se)[0] return path[cand[0]] # 取满足条件的第一个轮次

rmse是 N 轮的交叉验证误差序列,path是每轮的变量列号列表,返回值是最早进入“一个标准误区间”的那一轮的子集。这个规则倾向于选择变量更多、更早的轮次,牺牲一点拟合精度换取稳定性,在样本量不大的光谱数据集上通常比argmin更靠谱。

比这更稳的做法是多次运行 CARS,统计每个波长被选中的频率,取频率超过某个阈值(比如 0.8)的交集或并集。这一步的代价是计算量乘以运行次数,收益是最终集合不再依赖某一次随机抽样的运气。哪条路更值,取决于你有多少样本、能接受多长的跑数时间。

5. 排查清单:CARS 跑出来的结果对不上,先看

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

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

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

立即咨询