简介:竞争性自适应重加权算法(CARS)配套代码与文档资源包,面向从事光谱分析、化学计量学及机器学习变量选择的研究生与科研人员,帮助解决高维数据下PLS模型变量筛选与过拟合抑制问题。压缩包共37个文件,约1.27MB,以32个m脚本为主体,辅以2个mat数据文件、1份pdf手册、1份doc说明及1张jpg示意图,覆盖算法实现、数据样例与使用文档。资源围绕自适应重加权采样与交叉验证展开,包含PLS建模、RMSECV评估、变量投影重要性、蒙特卡洛采样及多种预处理与奇异值分解等脚本,并配有示例脚本与玉米近红外数据,便于读者直接运行复现变量筛选流程,理解回归系数排序与最优子集选取逻辑。目前已有11422人学习下载,适合希望快速上手CARS并应用于光谱特征筛选的读者参考。
1. 竞争性自适应重加权算法(CARS):从光谱里挑出真正有用的波长
做近红外光谱定量的人多半经历过这种场景:PLS 模型在实验室标定集上 R² 能到 0.95,一换批次或换台仪器,预测偏差直接翻倍。回头查原因,往往是全波段建模把噪声、基线漂移和无关组分的吸收峰一起喂给了模型。竞争性自适应重加权算法(CARS)就是冲着这个问题来的——它不要求你事先知道哪些波长有用,而是让成千上万个波长点像赛马一样竞争,一轮轮淘汰掉对建模贡献小的,最终留下几十个真正扛事的特征波长。这套方法在食品掺假检测、农产品品质分析、制药过程监控里被反复验证过,适合已经跑通 PLS 回归、想进一步压缩变量数并提升模型稳健性的从业者。它不神秘,但参数设错一样翻车,下面把我自己踩过的路讲清楚。
2. CARS 的筛选逻辑:指数衰减权重和自适应重加权怎么配合
2.1 从 PLS 回归系数到波长权重
CARS 的核心动作只有一句话:用 PLS 的回归系数绝对值当作每个波长的“重要性得分”,得分低的波长被淘汰的概率高。但直接按得分硬砍会有一个问题——第一轮就把大量波长删掉,后面没得选。所以 CARS 引入指数衰减函数来控制保留比例。
假设原始波长数为 p,第 i 次迭代的保留比例记为 r_i,常见做法是让 r_i 从 1.0 按指数衰减到 0.001 左右。衰减公式一般写成:
r_i = a * exp(-k * i)
其中 a 和 k 由迭代次数和首末保留比例反推。比如迭代 50 次,首轮保留 100%,末轮保留 0.1%,那 a 和 k 就固定了。这个设计让前期淘汰得慢、后期淘汰得快,给真正有用的波长留出足够的竞争轮次。
每一轮里,CARS 不是只跑一次 PLS,而是做多次蒙特卡洛采样。每次随机抽 80% 左右的样本建 PLS 模型,记录回归系数。然后把多次采样的回归系数绝对值做加权平均,得到该轮每个波长的稳定性权重。权重越小,说明这个波长在不同样本子集下表现越不稳定,越该被踢掉。
2.2 自适应重加权到底“自适应”在哪
很多人第一次看 CARS 会疑惑:指数衰减不是固定死的吗,哪来自适应?自适应体现在两个层面。
第一层是权重更新。每轮淘汰后,剩下的波长重新跑 PLS,回归系数重新算,权重不是一次算完用到底。这意味着某个波长如果在早期被低估,只要它后面几轮在子模型里持续有贡献,仍可能留下来。
第二层是交叉验证误差的监控。CARS 通常记录每轮迭代后模型的 RMSECV。RMSECV 先降后升的那个拐点,就是推荐的最优波长子集。拐点之前,删掉的是噪声和冗余;拐点之后,开始删掉有用信号,模型变差。这个拐点不靠人猜,靠数据自己说话,这才是“自适应”的真正含义。
2.3 一次完整 CARS 运行的参数表
下面这张表是我在 1000 个波长点、200 个样本的近红外数据集上常用的起点配置。不同数据要微调,但方向一致。
| 参数 | 常用取值 | 作用 | 调参方向 |
|---|---|---|---|
| 蒙特卡洛采样次数 | 50~100 | 每轮建 PLS 子模型的数量 | 样本少取小,样本多取大 |
| 最大主成分数 | 10~15 | PLS 潜变量上限 | 用全波段 PLS 的最优值加 2 |
| 保留比例首值 | 1.0 | 第一轮保留全部波长 | 固定 |
| 保留比例末值 | 0.001~0.01 | 最后一轮保留比例 | 波长多取小,波长少取大 |
| 迭代次数 | 50~100 | 衰减轮数 | 保证末轮保留比例达标 |
| 交叉验证折数 | 5~10 | 计算 RMSECV | 样本少用 5 折 |
注意:迭代次数和末值保留比例要匹配。如果迭代 30 次却想把 1000 个波长压到 10 个,末轮保留比例应设 0.01,否则衰减太快,拐点还没出现波长就删没了。
3. 用 Python 跑通 CARS-PLS 的最小实现
3.1 环境准备与数据组织
我一般用 numpy 做矩阵运算,sklearn 的 PLSRegression 做子模型,scipy 做采样。不依赖任何冷门包,复现成本低。
import numpy as np from sklearn.cross_decomposition import PLSRegression from sklearn.model_selection import KFold # X: 样本×波长矩阵,shape=(n_samples, n_wavelengths) # y: 样本×1 浓度向量,shape=(n_samples, 1) # 假设 X 已做过 SNV 或 MSC 预处理,y 已中心化数据组织有两个硬要求。第一,X 必须做过散射校正,否则基线漂移会被 CARS 误判为有用信号。第二,y 要中心化,PLS 对均值敏感。这两步不做,后面权重全是玄学。
3.2 CARS 主循环代码
def cars_pls(X, y, n_mc=50, max_lv=12, n_iter=50, r_start=1.0, r_end=0.001, cv=5): n_samples, n_waves = X.shape # 指数衰减系数 a = (r_end / r_start) ** (1.0 / (n_iter - 1)) keep_ratio = r_start * (a ** np.arange(n_iter)) keep_idx = np.arange(n_waves) rmscv_history = [] coef_history = [] for it in range(n_iter): n_keep = max(2, int(n_waves * keep_ratio[it])) # 当前轮保留的波长索引 cur_idx = keep_idx.copy() coef_mc = np.zeros((n_mc, len(cur_idx))) for m in range(n_mc): # 蒙特卡洛抽 80% 样本 samp_idx = np.random.choice(n_samples, int(n_samples * 0.8), replace=False) X_sub = X[samp_idx][:, cur_idx] y_sub = y[samp_idx] pls = PLSRegression(n_components=max_lv) pls.fit(X_sub, y_sub) coef_mc[m] = pls.coef_.ravel() # 稳定性权重:回归系数绝对值的均值 weight = np.abs(coef_mc).mean(axis=0) # 按权重降序保留前 n_keep 个 order = np.argsort(weight)[::-1] keep_local = order[:n_keep] keep_idx = cur_idx[keep_local] # 用当前保留波长算 RMSECV kf = KFold(n_splits=cv, shuffle=True, random_state=42) errs = [] for tr, te in kf.split(X): pls = PLSRegression(n_components=max_lv) pls.fit(X[tr][:, keep_idx], y[tr]) pred = pls.predict(X[te][:, keep_idx]) errs.append(np.mean((pred.ravel() - y[te].ravel()) ** 2)) rmscv_history.append(np.sqrt(np.mean(errs))) coef_history.append(keep_idx.copy()) # 找 RMSECV 最低点 best_it = int(np.argmin(rmscv_history)) return coef_history[best_it], rmscv_history逻辑说明:外层循环控制衰减轮次,内层蒙特卡洛采样负责评估每个波长的稳定性。每轮结束后,保留权重最高的 n_keep 个波长进入下一轮。RMSECV 用 K 折交叉验证独立计算,不参与筛选,只用于事后选拐点。
参数说明:n_mc 控制稳定性评估的方差,太小权重抖动大,太大会慢;max_lv 建议先用全波段 PLS 的交叉验证最优主成分数,再加 2 作为上限;r_end 决定最终能压到多稀疏,1000 个波长想留 20 个左右,r_end 设 0.02 比较稳。
3.3 拐点选择与最终建模
跑完上面的函数,rmscv_history 是一条先降后升的曲线。我一般取最低点对应的迭代轮次,把那一轮的 keep_idx 拿出来,重新用全样本建一个 PLS 模型。
best_idx, rmscv = cars_pls(X, y) print(f"选中波长数: {len(best_idx)}") print(f"对应 RMSECV: {rmscv.min():.4f}") # 用选中波长建最终模型 final_pls = PLSRegression(n_components=10) final_pls.fit(X[:, best_idx], y)这里有个血泪经验:不要用 RMSECV 最低点对应的主成分数直接当最终模型的主成分数。波长压缩后,最优主成分数通常会变,需要重新做一次交叉验证确定。我见过有人直接沿用全波段的 15 个主成分,结果预测集 RMSE 比全波段还差。
4. CARS 落地时的避坑与排查清单
4.1 现象:RMSECV 曲线一路下降不回升
原因:保留比例末值设得太大,迭代结束时还剩几百个波长,噪声没删干净,曲线自然没拐点。或者蒙特卡洛采样次数太少,权重估计噪声大,掩盖了真实拐点。
解决:把 r_end 降到 0.01 以下,n_mc 提到 100。如果数据波长本身少于 200 个,r_end 可以设 0.05,但迭代次数要相应减少,避免末轮只剩两三个波长。
4.2 现象:选出的波长集中在首尾,中间大片空白
原因:光谱首尾往往是噪声大的区域,如果预处理没做好,CARS 会把噪声当信号保留。另一种可能是 y 的浓度范围太窄,PLS 回归系数本身就不稳定。
解决:先做 SNV 或一阶导数加平滑,再跑 CARS。浓度范围窄的数据集,建议先扩充样本或改用其他变量筛选方法做交叉验证。
4.3 现象:换一批预测样本,CARS 模型比全波段还差
原因:过拟合。CARS 在标定集上把 RMSECV 压得很低,但选出的波长和标定样本的偶然特征绑定了。蒙特卡洛采样如果每次抽的样本高度重叠,权重估计会偏向标定集的局部结构。
解决:蒙特卡洛采样比例从 80% 降到 60%~70%,增加子模型多样性。另外,最终模型的主成分数不要超过选中波长数的十分之一。
4.4 现象:每次运行选出的波长都不一样
原因:蒙特卡洛采样有随机性,n_mc 越小,重复性越差。这是 CARS 的固有特性,不是 bug。
解决:固定随机种子只能保证单次复现,不能解决本质波动。我一般跑 20 次 CARS,统计每个波长被选中的频率,取频率大于 60% 的波长作为最终集合。这样虽然多花时间,但换批次时稳健性好很多。
提示:CARS 不是万能筛选器。如果全波段 PLS 的 RMSECV 本身就没比均值模型好多少,说明光谱和浓度相关性弱,先查实验设计,别指望 CARS 救场。
5. 让 CARS 选出的波长真正扛住换批次:一个验证习惯
CARS 跑通不难,难的是选出的波长在预测集上不翻车。我现在的习惯是:不管 CARS 给出的 RMSECV 多漂亮,都要做一次“批次外验证”。具体做法是把数据按批次或测量日期分组,用一批建 CARS-PLS,用另一批预测,看 RMSE 和 RPD。如果批次外 RPD 低于 2,选出的波长再少也不值得用。
另一个技巧是给 CARS 加一个稳定性筛选的后处理。跑 20 次 CARS,记录每个波长被选中的次数,画一张频率分布图。频率高于 60% 的波长作为核心集,频率在 20%~60% 的作为候选集。核心集建模型,候选集只做验证。这样既保留了 CARS 的筛选能力,又避免了单次运行的偶然性。
# 多次 CARS 统计波长频率 from collections import Counter freq = Counter() n_runs = 20 for _ in range(n_runs): idx, _ = cars_pls(X, y, n_mc=50) freq.update(idx.tolist()) core_idx = [k for k, v in freq.items() if v / n_runs >= 0.6] print(f"核心波长数: {len(core_idx)}")这个习惯让我在三个不同批次的农产品数据集上,把预测 RPD 从 1.8 提到了 2.6。代价是计算时间翻了 20 倍,但比起重新做实验,这点机时不算什么。
最后说一句我自己的教训:CARS 的参数没有标准答案,别抄别人的迭代次数和保留比例就完事。先跑一次全波段 PLS 看 RMSECV 量级,再决定 CARS 的衰减速度。数据质量永远比算法花哨更重要。希望帮到你。
本文还有配套的精品资源,点击获取