简介竞争性自适应重加权算法CARS配套代码与文档资源包面向从事光谱分析、化学计量学与机器学习变量选择的研究生、科研人员及工程师帮助解决高维数据下PLS模型变量筛选与过拟合控制问题。压缩包共37个文件约1.27MB以32个m脚本为主体涵盖CARS主流程、PLS建模、交叉验证与预处理等函数另含2个mat数据文件、1份PDF手册、1份doc说明及1张示意图便于直接运行与二次开发。资源围绕自适应重加权采样与RMSECV评估展开提供从变量排序、权重迭代到最优子集筛选的完整实现并附示例脚本演示近红外光谱场景下的调用方式。已有11422人学习下载适合希望快速复现CARS算法、对比不同变量选择策略并理解其鲁棒性来源的读者参考使用。1. 光谱特征筛选的硬骨头CARS 到底解决什么问题做近红外或拉曼光谱定量建模的人大概率都经历过这样的场景一个波段动辄上千个波长点全塞进 PLS 回归里模型在训练集上 R² 漂亮得不像话一换验证集就崩。问题往往出在——参与建模的变量太多其中大量是噪声和无信息波段。竞争性自适应重加权算法CARS就是冲着这个痛点来的。它把达尔文进化论里的“适者生存”搬到波长筛选上每一轮按回归系数的绝对值大小做指数衰减式重加权权重小的变量被淘汰权重大的留下反复迭代最终挑出一组既精简又有预测力的特征波长。适合谁做光谱定量、需要把模型从“能跑”推到“能落地”的人。如果你正被全波段 PLS 的过拟合折磨CARS 值得花时间拆一遍。2. CARS 的运行骨架从指数衰减权重到自适应重加权采样2.1 为什么用指数衰减函数控制变量保留率CARS 的核心机制之一是用一个指数衰减函数来动态控制每轮迭代保留多少变量。设第 i 次采样时保留的变量比例为 r_i它的计算方式是r_i a × exp(-k × i)其中 a 和 k 是两个常数由初始保留比例和最终保留比例反推得到。常见做法是第一轮保留全部变量或接近全部最后一轮保留到只剩几个变量。这个衰减曲线不是线性的前期下降慢、后期下降快好处是给回归系数足够的时间“竞争”——真正重要的变量在前几轮不会被误杀而噪声变量会在后期被快速清理。我一般会把初始保留比例设成 1.0最终保留比例设成 0.01 左右迭代次数 N 取 50 到 100 之间。N 太小变量来不及充分竞争N 太大计算量上去了而且后期可能把有用变量也砍掉。这个参数没有万能值得结合你的波长点总数来调。比如 1000 个波长点最终保留 10 个左右那最终比例就是 0.01。2.2 自适应重加权采样ARS怎么执行每一轮迭代里CARS 不是简单地按系数大小硬阈值筛选而是用自适应重加权采样。具体来说每个变量被选中的概率与它的回归系数绝对值成正比。系数绝对值越大被抽中的概率越高但系数小的变量也不是完全没机会——这保留了随机性避免陷入局部最优。实现上常见做法是import numpy as np def ars_sampling(coefficients, num_select): 自适应重加权采样 coefficients: 当前轮次的回归系数向量 num_select: 本轮需要保留的变量数 返回: 被选中变量的索引 weights np.abs(coefficients) # 防止全零导致概率计算异常 if weights.sum() 0: weights np.ones_like(weights) probs weights / weights.sum() # 按概率无放回抽样 selected np.random.choice( len(coefficients), sizenum_select, replaceFalse, pprobs ) return selected这段代码的逻辑是先把回归系数取绝对值当作权重归一化成概率分布然后按这个分布无放回地抽取指定数量的变量。参数num_select由上一节的指数衰减函数算出。注意replaceFalse保证同一轮不会重复选同一个波长点。实际跑的时候每轮迭代都要重新拟合 PLS 模型拿到新的回归系数再喂给这个采样函数。2.3 用 PLS 回归系数驱动变量竞争CARS 每轮迭代的内部流程是这样的先用当前保留的变量子集拟合一个 PLS 模型取出回归系数向量然后根据系数绝对值做自适应重加权采样得到下一轮的变量子集。如此循环 N 次每一轮都记录下交叉验证均方根误差RMSECV。这里有个关键点PLS 的成分数怎么定。成分数太多回归系数会包含噪声成分数太少系数无法反映真实的变量重要性。我一般会先用全波段做一次 PLS 交叉验证找到最优成分数然后在整个 CARS 迭代过程中固定这个成分数。也有做法是每轮重新优化成分数但那样计算量会翻好几倍而且容易过拟合。from sklearn.cross_decomposition import PLSRegression from sklearn.model_selection import cross_val_predict from sklearn.metrics import mean_squared_error def cars_one_round(X, y, selected_idx, n_components): 执行一轮 CARS 迭代 X: 全波段光谱矩阵 y: 目标值向量 selected_idx: 当前保留的变量索引 n_components: PLS 成分数 返回: 回归系数, RMSECV, 新选中的索引 X_sub X[:, selected_idx] pls PLSRegression(n_componentsn_components) # 用交叉验证预测值算 RMSECV y_pred cross_val_predict(pls, X_sub, y, cv5) rmsecv np.sqrt(mean_squared_error(y, y_pred)) # 全量拟合拿回归系数 pls.fit(X_sub, y) coef pls.coef_.flatten() return coef, rmsecv这段代码把一轮迭代拆成两步先交叉验证算 RMSECV 用于后续选最优轮次再全量拟合拿回归系数用于采样。cv5是常见选择样本量小的时候可以改成留一法。coef_的形状取决于 sklearn 版本展平一下更稳妥。3. 手把手跑通 CARS参数设定、迭代记录与最优子集提取3.1 完整流程的代码骨架把前面几节的碎片拼起来一个完整的 CARS 实现大概长这样import numpy as np from sklearn.cross_decomposition import PLSRegression from sklearn.model_selection import cross_val_predict from sklearn.metrics import mean_squared_error def cars(X, y, n_iter50, n_components10, final_ratio0.01): CARS 主流程 X: 光谱矩阵 (n_samples, n_wavelengths) y: 目标值 (n_samples,) n_iter: 迭代次数 n_components: PLS 成分数 final_ratio: 最终保留变量比例 返回: 最优变量索引, 每轮记录 n_samples, n_vars X.shape # 指数衰减参数 a 1.0 k np.log(a / final_ratio) / (n_iter - 1) selected np.arange(n_vars) records [] for i in range(n_iter): # 计算本轮保留数量 ratio a * np.exp(-k * i) n_keep max(int(n_vars * ratio), 2) X_sub X[:, selected] pls PLSRegression(n_componentsn_components) y_pred cross_val_predict(pls, X_sub, y, cv5) rmsecv np.sqrt(mean_squared_error(y, y_pred)) pls.fit(X_sub, y) coef pls.coef_.flatten() records.append({ iter: i, n_keep: n_keep, rmsecv: rmsecv, selected: selected.copy() }) # 自适应重加权采样 weights np.abs(coef) if weights.sum() 0: weights np.ones_like(weights) probs weights / weights.sum() new_local np.random.choice( len(selected), sizen_keep, replaceFalse, pprobs ) selected selected[new_local] # 找 RMSECV 最小的轮次 best min(records, keylambda r: r[rmsecv]) return best[selected], records逻辑说明外层循环控制迭代轮次每轮先算保留数量再拟合 PLS 拿系数记录 RMSECV最后做重加权采样更新变量子集。参数n_iter控制迭代次数n_components是 PLS 成分数final_ratio决定最后一轮保留多少变量。records里存了每轮的变量索引和 RMSECV方便回溯。3.2 关键参数怎么定迭代次数、成分数与最终保留比例这三个参数是 CARS 调参的主要抓手我按经验给个参考范围参数常用范围影响调整建议n_iter50100迭代太少变量竞争不充分太多计算量大且后期可能砍掉有用变量波长点 1000 左右取 502000 以上取 80100n_components515太大系数含噪声太小系数无法反映变量重要性先用全波段 PLS 交叉验证定最优值然后固定final_ratio0.0050.02太小最终变量太少可能欠拟合太大筛选不彻底1000 个波长点取 0.01 左右即最终留 10 个注意n_components不要超过样本量的十分之一否则 PLS 本身就不稳定CARS 的筛选结果也没意义。样本量小于 50 的时候交叉验证折数改成留一法更稳。3.3 用 RMSECV 曲线定位最优迭代轮次CARS 跑完之后records里存了每轮的 RMSECV。把这个曲线画出来通常能看到一个先降后升的形状前期变量多、噪声大RMSECV 高中期变量精简到合适数量RMSECV 最低后期变量被砍得太狠模型欠拟合RMSECV 又上去。最优轮次就是 RMSECV 最低的那个点。但实际操作中曲线最低点附近往往比较平坦这时候可以选变量数更少的那个轮次——模型更简单泛化能力通常更好。我一般会在最低点附近取三到五个候选轮次分别建 PLS 模型看验证集表现再定。import matplotlib.pyplot as plt rmsecv_list [r[rmsecv] for r in records] n_keep_list [r[n_keep] for r in records] fig, ax1 plt.subplots() ax1.plot(rmsecv_list, b-, labelRMSECV) ax1.set_xlabel(Iteration) ax1.set_ylabel(RMSECV, colorb) ax2 ax1.twinx() ax2.plot(n_keep_list, r--, labelVariables kept) ax2.set_ylabel(Number of variables, colorr) plt.show()这段代码把 RMSECV 和保留变量数画在同一张图上双纵轴。看的时候重点找 RMSECV 最低点对应的迭代轮次同时看那个轮次保留了多少变量。如果最低点对应的变量数还是太多可以往右多看几轮选一个变量更少、RMSECV 略高但可接受的轮次。4. 避坑与排查CARS 跑出来的结果不对劲怎么办4.1 现象每轮 RMSECV 波动巨大曲线像锯齿原因自适应重加权采样本身带随机性如果样本量小或者 PLS 成分数偏高回归系数每轮变化剧烈导致采样结果不稳定。解决固定随机种子先把np.random.seed(42)加上保证结果可复现。如果波动仍然大降低 PLS 成分数或者增大交叉验证折数。样本量特别小的时候可以考虑跑多次 CARS 取交集而不是只跑一次。4.2 现象最终选出的变量在验证集上表现还不如全波段原因CARS 是在训练集内部做交叉验证选变量如果训练集和验证集分布差异大选出的变量可能过拟合了训练集的噪声。解决检查训练集和验证集的划分方式尽量用 Kennard-Stone 或 SPXY 这类基于光谱距离的划分方法保证两者分布一致。另外最终保留比例不要设得太小留一点冗余变量反而更稳。4.3 现象迭代到后期变量数已经很少了RMSECV 还在降原因指数衰减函数的 k 值算错了导致保留比例下降太慢或者n_iter设得太大。解决检查k np.log(a / final_ratio) / (n_iter - 1)这个公式确认final_ratio和n_iter的乘积关系合理。如果n_iter100而final_ratio0.01那第 50 轮左右保留比例就应该降到 0.1 附近了。如果没降下来说明 k 算小了。4.4 现象选出的变量集中在光谱两端中间波段全被砍掉原因光谱两端的噪声通常较大但如果 PLS 回归系数在两端绝对值偏大CARS 会优先保留它们。这往往意味着成分数没选对或者数据预处理有问题。解决先做 SNV、一阶导数等预处理把噪声压下去再跑 CARS。另外检查 PLS 成分数成分数太高时回归系数会变得杂乱容易在噪声波段产生大系数。我一般会先用全波段做一次 PLS看回归系数分布如果两端系数异常大说明预处理需要加强。4.5 现象跑一次 CARS 要十几分钟迭代次数降不下来原因每轮都做交叉验证样本量大或者波长点多的时候计算量确实大。解决把交叉验证折数从 10 降到 5或者用sklearn.cross_decomposition.PLSRegression的scaleFalse避免重复标准化。如果还是慢可以先用 PCA 降维再跑 CARS但这样选出的变量是主成分而非原始波长解释性会打折扣。常见做法是先用相关系数法或 VIP 做一轮粗筛把波长点从几千降到几百再跑 CARS。5. 进阶技巧用多次运行取交集稳住 CARS 的随机性CARS 的随机性是把双刃剑。一方面它帮助算法跳出局部最优另一方面单次运行的结果可能不够稳定。我现在的习惯是跑 20 到 50 次 CARS每次用不同的随机种子然后统计每个波长点被选中的频率。频率高于某个阈值比如 80%的波长点认为是稳定重要的变量。def cars_stability(X, y, n_runs30, threshold0.8, **kwargs): 多次运行 CARS按选中频率筛选稳定变量 n_runs: 运行次数 threshold: 选中频率阈值 n_vars X.shape[1] hit_count np.zeros(n_vars) for run in range(n_runs): np.random.seed(run) selected, _ cars(X, y, **kwargs) hit_count[selected] 1 freq hit_count / n_runs stable_idx np.where(freq threshold)[0] return stable_idx, freq这段代码的逻辑很直接每次换一个随机种子跑 CARS记录哪些变量被选中最后算频率。threshold0.8表示要求变量在 80% 的运行中都被选中。这个阈值可以调样本量小的时候可以降到 0.6 到 0.7避免选不出变量。拿到稳定变量之后用它们重新建 PLS 模型和全波段模型、单次 CARS 模型做对比。我一般会看三个指标验证集 RMSEP、R² 和模型复杂度变量数。稳定 CARS 选出的变量数通常比单次 CARS 多一些但验证集表现更稳换一批验证样本也不会崩得太厉害。还有一个技巧把 CARS 和 VIP 结合。先用 VIP 做一轮粗筛把 VIP 小于 0.8 的变量去掉再跑 CARS。这样既减少了计算量又降低了 CARS 在噪声波段乱选的概率。VIP 的计算依赖 PLS 模型和 CARS 用的是同一套框架衔接起来很自然。从那以后我每次跑 CARS 都强制走一遍多次运行取交集哪怕多花十几分钟也比拿着一个随机结果去汇报强。希望帮到你。本文还有配套的精品资源点击获取