
简介光谱数据常包含大量相关且冗余的波长变量CARS自适应重加权特征选择算法可有效筛出对目标变量影响最大的关键波段。资源提供该算法的Python实现核心逻辑借助迭代过程动态评估特征贡献并调整权重逐步保留重要波段、剔除噪声适用于近红外光谱分析、化学计量学及机器学习建模前的特征降维场景。压缩包整体仅2KB内含1个py脚本覆盖数据预处理、特征重要性评估、特征子集选择、权重更新、模型验证与停止条件判断等模块结构紧凑便于二次开发。已有2104人学习下载。通过运行该脚本既能直观理解CARS的迭代筛选机制也可根据实际光谱数据调整迭代次数和阈值对比不同波段组合的预测效果为后续结合递归特征消除等策略提供可复用的代码基础。1. CARS特征选择为什么高维光谱建模总会先想起它做近红外光谱定量分析时CARS 特征选择几乎成了我先试的一步上千个波长变量压到几十个交叉验证误差反而更低。它全称是 Competitive Adaptive Reweighted Sampling竞争性自适应重加权采样专门解决“变量比样本多、变量之间还高度共线”这类问题。和普通过滤式筛选不同CARS 每轮都随机抽样本建模型用回归系数给变量打分再按“适者生存”的方式淘汰低分变量最后用交叉验证挑出最优变量子集。对做光谱、拉曼、高光谱或任何高维回归任务的工程师来说这是一套能直接落到代码里的降维方法而不是停留在理论层面的概念。2. CARS特征选择的原理从蒙特卡洛到“适者生存”的四步机制CARS 的核心不是“按权重排序后一刀切”而是把变量筛选当成一个重复的演化过程。每一轮迭代算法都重新采样、重新建立 PLS 模型、重新计算每个变量的适应度然后淘汰一部分、随机保留一部分。这个设计让 CARS 比单纯按变量重要性排序更抗共线性也更少陷入局部最优。2.1 蒙特卡洛采样与PLS回归系数每轮都在换样本第一层机制是蒙特卡洛采样。每一轮迭代开始时并不是把全部训练样本都用上而是按固定比例随机抽一批样本出来比如 80%再用这批样本拟合一个 PLS 回归模型。模型拟合完得到回归系数向量 b它的绝对值大小反映的是某个变量在“本次样本组合”下对 y 的贡献强弱。这个强弱每轮都在变。换一批样本原来关注的两个共线波长可能角色互换一个变得重要另一个变得不重要。这种随机性恰恰是 CARS 需要的它把变量的稳定性纳入了淘汰依据而不是只看一次全谱模型的结果。用 PLS 而不是普通线性回归是因为光谱数据几乎必然存在共线性普通最小二乘的系数会被共线变量分摊两个系数都不小很难排序。PLS 先把 X 和 y 同时投影到主成分空间再把载荷映射回原始变量得到的系数对共线性更稳定。这一步里有一个关键参数PLS 主成分数 ncomp。我一般先做一个全谱 PLSR用交叉验证把 ncomp 调到误差谷底附近的区间再固定这个值进入 CARS。每轮模型的 ncomp 必须保持一致否则不同轮次的系数不在同一尺度上没法排序比较。2.2 指数衰减函数强制淘汰低权重变量的节奏控制器有了回归系数下一步不是直接砍变量而是先算这一轮允许保留多少变量。CARS 用指数衰减函数来控制保留变量数的天花板公式可以写成 r_i exp(k × i)其中 k 由初始保留率和最终保留率反推。第一轮保留率约为 1最后一轮保留率通常设在 0.01 到 0.05 之间也就是说几千个变量最终只剩几十个。指数衰减的意义在于前期淘汰快、后期淘汰慢。变量多的时候少几个多几个对模型影响不大变量已经很少的时候每淘汰一个都可能把关键变量丢出去所以必须放慢节奏。把每轮保留的变量数随迭代次数画出曲线是一条先平后陡再趋于平缓的指数下降曲线。一个常见错误是把最终保留率设成 0最后几轮变量数直接压到 1 个PLS 模型都拟合不了。我一般设 0.01 到 0.05对应光谱数据大概还能剩 10 到 50 个变量。注意指数衰减淘汰的是“本次蒙特卡洛子模型”里排序靠后的变量不是全局固定排名。某个变量这一轮被淘汰下一轮换一批样本后可能又回到模型里。这是 CARS 与简单按特征重要性直接截断的本质区别。2.3 自适应重加权采样给低权重变量留一条活路指数衰减只定了保留变量数 p_i具体保留哪些变量CARS 用自适应重加权采样来完成。常见的工程实现是先把当前存活变量按权重降序排列取一个比 p_i 更大的候选池然后按权重归一化的概率做无放回抽样抽出 p_i 个变量进入下一轮。权重高的变量被抽中概率大但权重低的也有机会活下来。这个随机性是为了避免局部最优。有些变量单独看回归系数很小可一旦和别的变量组合在一起效果就很好。如果每轮都只保留权重前几名这些有组合潜力的变量就永远没机会参与后续建模型最终特征集可能偏窄。自适应重加权的“自适应”就在这里权重分布每轮都变抽样概率也跟着变变量池子不会锁死。不同开源实现在这个步骤上细节有差异有的用有放回抽样有的用无放回抽样。我的工程实践里偏好无放回因为它能严格保证每轮变量数等于指数衰减设定的目标值收敛曲线更干净。两种写法在大多数数据集上的结论一致差异主要体现在变量数递减速度和稳定点位置写论文时如实描述自己用的版本即可。2.4 CARS与互信息特征选择等方法对比各自的边界在哪里方法核心机制是否考虑变量间组合适用场景主要短板互信息特征选择逐个变量与目标计算互信息不考虑组合数据探索阶段的粗筛共线变量重复给分冗余度高VIP基于PLS载荷的投影重要性指标较弱快速初筛、降维阈值难定保留变量仍共线SPA在原始变量空间中找最小冗余投影较强共线性突出的光谱数据可能丢掉系数小但相关性强的重要变量随机森林特征重要性树模型的置换重要性非线性组合分类和复杂非线性问题变量多时波动大计算成本高CARS蒙特卡洛采样 EDF ARS强高维共线线性建模随机性强需要多次运行看稳定结果互信息特征选择这类过滤法是特征工程之特征选择里入门最快的手段适合先用它去掉明显无关的噪声列。但光谱数据经过互信息筛选之后剩余变量之间依然高度共线直接建模仍然会过拟合。CARS 属于包装法思路它把筛选过程和模型拟合绑定在一起每一轮都对“预测是否有用”负责。实际项目里两者不是替代关系可以串起来用先用互信息或方差过滤无信息列再上 CARS 精细筛选这个串行方案在我处理近红外数据时非常稳定。3. 用Python实现CARS特征选择最小可运行代码与参数说明下面给出一套完整的 Python 实现依赖 NumPy 和 scikit-learn 就能运行。代码分成两个部分第一部分是 CARS 核心采样流程返回每轮保留的变量索引第二部分是对采样路径做交叉验证选出最优变量子集。为了便于测试我用模拟数据演示真实数据只需替换 X 和 y。3.1 核心采样流程MC采样、EDF与ARS的代码骨架import numpy as np from sklearn.cross_decomposition import PLSRegression def cars_sample_path(X, y, n_iter500, ncomp8, sample_ratio0.8, end_rate0.05, pool_factor2): 返回每轮保留的变量索引列表 n, m X.shape k np.log(end_rate) / n_iter # EDF衰减系数 alive np.arange(m) # 当前存活的变量索引 history [] for i in range(1, n_iter 1): # 蒙特卡洛采样无放回抽取固定比例的样本 idx np.random.choice(n, int(n * sample_ratio), replaceFalse) # 只在当前存活变量上建立PLS模型 pc min(ncomp, len(alive) - 1) pls PLSRegression(n_componentspc) pls.fit(X[idx][:, alive], y[idx].reshape(-1, 1)) coef pls.coef_.ravel() # 回归系数绝对值作为适应度权重 w np.zeros(m) w[alive] np.abs(coef) # EDF计算本轮保留变量目标数 r_i np.exp(k * (i - 1)) p_i max(2, int(m * r_i)) p_i min(p_i, m) # 构造候选池权重降序前 pool_factor * p_i 个 order np.argsort(w)[::-1] pool_size min(m, max(p_i * pool_factor, 30)) pool order[:pool_size] # ARS按权重概率无放回抽样 p_i 个 pool_w w[pool] 1e-8 prob pool_w / pool_w.sum() alive np.sort(np.random.choice(pool, sizep_i, replaceFalse, pprob)) history.append(alive.copy()) return history随便找一段代码最值得理解的是两个地方。第一每次迭代只在当前存活的那些变量上做 PLS不是每次都拿全部变量建模否则系数的含义会变。回归系数长度等于当前存活变量数然后回填到一个全维度的权重向量 w 里让每一步都能和原始变量索引对齐。这样后续的排序、抽样全部统一在原始维度上操作不会出现索引错位。第二候选池和 ARS 的关系。如果直接取权重前 p_i 名进入下一轮那就退化成确定性筛序去掉随机性。这里故意把候选池扩大到目标数的两倍让低权重变量也有竞争机会。抽样概率用权重归一化同时加 1e-8 防止某个变量权重为 0 导致概率为 0 而永远失去生存机会。主要参数说明如下参数默认值作用调参建议n_iter500迭代轮数小数据先 200正式建模 500 到 1000ncomp8PLS主成分数先跑全谱CV取误差谷底值sample_ratio0.8每轮蒙特卡洛采样比例不建议低于0.7或高于0.9end_rate0.05EDF最终保留率光谱数据设0.01到0.05pool_factor2候选池倍数变量少时可适当降低到1.53.2 交叉验证选优从采样路径里挑最终变量子集from sklearn.model_selection import cross_val_score def select_best_by_cv(X, y, history, ncomp8): 对历史变量子集做5折交叉验证返回最优子集 best None for cols in history: if len(cols) 2: continue pc min(ncomp, len(cols) - 1) pls PLSRegression(n_componentspc) # 用负均方误差取平均值最小的一组 mse -cross_val_score(pls, X[:, cols], y, cv5, scoringneg_mean_squared_error).mean() if best is None or mse best[0]: best (mse, cols) return best这个函数做的事情很直白把 CARS 采样过程记录下来的每一轮变量子集都拿到交叉验证里跑一遍 PLS记录均方误差最小的那一个。之所以留到迭代结束后统一评估是为了避免每轮都做交叉验证带来的巨大计算量。实际运行中 history 里可能有几百条记录每条都要做 5 折 PLS计算总量不小但换来的是更可靠的变量选择。调用示例用一个模拟高维回归数据# 模拟数据300个样本800个变量其中20个真实有效变量 np.random.seed(1) X np.random.randn(300, 800) true_idx np.random.choice(800, 20, replaceFalse) B np.random.randn(800) B[~np.isin(np.arange(800), true_idx)] 0 y X B 0.3 * np.random.randn(300) history cars_sample_path(X, y, n_iter500, ncomp8) mse, cols select_best_by_cv(X, y, history) print(变量数:, len(cols), MSE:, mse) print(选中的变量索引:, np.sort(cols))注意 X 必须是二维数组行是样品、列是变量y 是一维数组长度等于 X 的行数。y 传入 PLSRegression 时需要用 reshape(-1, 1) 转成列向量否则部分版本的 scikit-learn 会打印弃用警告。如果你的数据是从 pandas DataFrame 读进来的记得用 df.to_numpy() 转成 NumPy 数组再传入。3.3 输出结果怎么看变量数、CV误差与迭代曲线运行上面代码正常会打印出类似“变量数: 47, MSE: 0.42”的结果。判断结果是否合理我会按三个步骤来。先看变量数量级。几百到上千变量的数据CARS 选出 20 到 100 个变量都是正常的。如果选出来只剩两三个大概率是 end_rate 设太小或者数据里本身真实变量就很少。如果依然保留了几百个变量说明 end_rate 设太大迭代没有充分压缩。再看 CV 误差。把 CARS 选出的变量子集拿去做 5 折交叉验证得到的误差应该和全谱 PLS 的交叉验证误差相当或更低。如果误差明显更高先检查 ncomp 是否是全谱最优值再检查数据预处理是否到位。最后可以把 history 里的每轮变量数画出来确认下降轨迹符合指数形状。如果曲线在某个位置突然断崖式下跌往往说明 p_i 计算里出现了异常比如 end_rate 设成了 0或者 n_iter 和 EDF 参数不匹配。4. CARS的四个必调参数迭代次数、采样比例与收敛准则CARS 虽然能自动淘汰变量但它并不是完全免调参的算法。四个参数直接决定筛选结果的质量下面按影响程度从高到低展开。4.1 迭代次数先500起步不是越大越好n_iter 是第一个要定下来的参数。500 轮在 300 乘 800 的模拟数据上只需几十秒在真实近红外 500 乘 2000 的数据上也还在可接受范围。设为 1000 可以让指数衰减更平滑但计算时间近似翻倍。现实中我很少把 n_iter 设到 2000 以上因为后期每轮变量数量已经很低CV 误差的提升幅度常常只有 0.001 级别属于边际收益。如果只是想快速验证 CARS 在当前数据上是否有效先跑 200 轮观察误差是否随变量减少而下降。如果 50 轮以内变量数就掉到 10 以下说明 end_rate 设太小或 n_iter 太少需要加大迭代轮数让衰减曲线变缓。正式用于建模时500 到 1000 是稳妥选择。4.2 蒙特卡洛采样比例与PLS主成分数影响变量命中率的两个旋钮sample_ratio 默认 0.8 是经过验证的健壮值。低于 0.7 时每轮模型样本太少回归系数方差增大权重排序噪声变多最后选出的变量容易分散高于 0.9 时每轮用的样本几乎一样蒙特卡洛采样的多样性丢失算法退化成接近确定性贪心搜索组合变量的机会被压缩。对于只有一两百个样本的小数据集可以把 sample_ratio 提到 0.85 到 0.9保证单轮 PLS 不至于因为样本太少而拟合崩溃。ncomp 是这个算法里最容易被低估的参数。ncomp 设太小模型欠拟合低权重变量的真实贡献被淹没在残差里设太大每轮模型过拟合噪声权重分布变尖少数变量被反复选中最终变量集合集中在个别区域。我的标准操作是先用全谱 PLSR 做 10 折交叉验证画出 ncomp 从 1 到 15 的 CV 误差曲线取误差最小且曲线开始变平的那个值再固定这个值进入 CARS。ncomp状态典型表现处理方式过小CV误差偏高变量集合分散不集中调大到误差谷底附近过大变量集合过度集中CV不稳定调小看全谱CV曲线合适变量数下降平滑CV误差稳步收敛保持不动4.3 连续多轮CV误差不降就收手提前停止规则大多数 CARS 实现是跑满全部迭代后统一找最小值但工程上可以加一个提前停止。原理很简单记录当前看到的最优 CV 误差如果连续 50 轮没有出现更低值就认为已经收敛终止迭代。这个规则在 500 轮的任务里经常能把计算量省下三分之一到一半。best_mse np.inf no_improve 0 stop_at n_iter for i, cols in enumerate(history, 1): if len(cols) 2: continue pc min(ncomp, len(cols) - 1) pls PLSRegression(n_componentspc) mse -cross_val_score(pls, X[:, cols], y, cv5, scoringneg_mean_squared_error).mean() if mse best_mse: best_mse mse no_improve 0 else: no_improve 1 if no_improve 50: stop_at i break这个循环每轮都做一次交叉验证计算量比跑完整路径更重。我实际用的时候会做一个折中每 10 轮采样一次做 CV粗定位最优区域后再在附近 20 轮内逐轮细算。这样既能捕捉到误差谷底又不至于为了提前停止付出额外计算代价。注意提前停止必须依据交叉验证误差而不是训练误差。训练误差只会随着变量减少而上升或走平用它判断会过早停止最终选出的变量子集往往过拟合。5. 排查CARS特征选择的五个坑现象、原因与解决步骤CARS 在真实数据上跑起来问题往往不在算法本身而在预处理、参数边界和随机性处理上。下面五条是我被问得最多、自己也翻过车的地方。5.1 现象每次运行选出的变量都不一样同一份数据什么参数都没改连续跑两次 CARS选出的变量集合却不同有人会怀疑代码写错了。其实这是正常现象CARS 内部有蒙特卡洛采样和 ARS 两步随机过程换随机种子就换结果。原因算法设计上就要求引入随机性通过不同样本组合和不同抽样结果来避免局部最优。变量集合不同不代表算法失效反而说明筛选结果对样本扰动敏感需要用多次运行来看稳定趋势。解决调试阶段固定随机种子比如在调用前加 np.random.seed(42)保证结果可复现。正式使用时不要依赖单次最优子集用多个种子跑 20 次统计每个变量被选中的频次把高频变量作为最终特征集合。这样得到的变量集稳定性高很多。5.2 现象选出的变量子集CV误差高于全谱模型CARS 的核心目标是降维而不是提升精度。如果把全谱 PLS 的交叉验证误差作为基准CARS 选出的变量在多数情况下能做到持平或小幅提升。如果明显更差问题通常出在参数或数据预处理上。原因ncomp 设置不合理是最大的嫌疑。CARS 内部每轮建模的 ncomp 和最后评估用的 ncomp 必须与全谱最优值一致一旦低估变量权重计算就不准。另外一个常见原因是数据预处理没跟上光谱数据如果直接拿原始反射率建模基线漂移和散射效应会干扰回归系数排序。解决先对全谱数据做预处理和 PLSR确认 ncomp 在误差谷底区间。然后把预处理后的数据重新传入 CARS。如果全谱模型本身的 CV 误差已经很高CARS 不可能凭空变好先回去解决数据采集或预处理的问题再来做特征选择。5.3 现象变量数在后期坍缩到1或2交叉验证报错跑完采样路径后变量数曲线在前几轮正常下降到后期突然只剩 1 个或 2 个变量select_best_by_cv 里做 PLS 时直接报错。原因end_rate 设得太小比如设成了 0.001 甚至 0指数衰减把最后一轮变量数压到了 2 以下。另一个触发点是算法内部没有对 ncomp 做动态限制当存活变量数小于 ncomp 时PLSRegression 的拟合本身就无法执行。解决把 end_rate 控制在 0.01 到 0.05 之间不要低于 0.01。同时在 PLS 调用处动态限制主成分数pc min(ncomp, len(alive) - 1)这样即使变量数降到 3 个模型也能用 1 或 2 个主成分继续拟合。另外在交叉验证函数里加一道过滤len(cols) 2 时直接跳过避免无意义评估。5.4 现象回归系数出现NaN或零方差变量导致PLS崩溃某些列全为常数或者某个变量在所有样本上的方差小到接近 0PLS 做标准化处理时就会除以接近 0 的标准差产生 NaN 权重。这种情况在真实光谱数据里并不少见尤其当光谱仪某个通道损坏或预处理后出现全零列时。原因PLSRegression 默认会对 X 做标准化即中心化后除以标准差。零方差列导致除零结果全是 NaN后续排序和抽样全部失效。解决在跑 CARS 之前先做一次变量过滤把方差小于 1e-12 的列直接剔除。同时检查数据里是否有 NaN 或 inf有就先填补或删除。光谱数据建议先做均值中心化或 SNV 标准化这两种预处理能消除基线偏移造成的伪方差让 CARS 的权重排序更接近真实化学信息。5.5 现象R语言和Python的实现结果对不上同一份数据用 R 里某个封装跑 CARS和用自己手写的 Python 代码跑最终变量集合差异很大。有人会认为某个版本有 bug。原因不同实现之间EDF 衰减公式的起点设置、ARS 抽样是有放回还是无放回、候选池放大倍数都可能存在差异。这些细节都会影响变量数量下降速度和最终稳定点所以结果对不上是常态。解决不要强行让两个平台逐变量一致。比较两个实现时看三件事最终 CV 误差是否在同一量级、选出的变量是否集中在相同波长区域、变量数量是否相近。如果这三项都对得上说明两个实现都是有效的可以选一个作为项目基线。生产环境中以自己平台的频率统计结果为准不要跨平台混用结论。6. 让CARS结果更稳的进阶用法多次运行做频率统计单次 CARS 的最优变量子集有一定偶然性换一个随机种子可能就换掉几个变量。如果拿这个结果直接去做长期监测模型或写论文解释性会很脆弱。更稳的做法是把 CARS 当作一个采样器用多次运行统计每个变量的被选中频次再从频次分布里取高频变量作为最终特征。from collections import Counter def cars_frequency(X, y, seeds20, **kwargs): 多次运行CARS统计每个变量被选中的次数 counter Counter() for seed in range(seeds): np.random.seed(seed) history cars_sample_path(X, y, **kwargs) _, cols select_best_by_cv(X, y, history) counter.update(cols.tolist()) return counter counter cars_frequency(X, y, seeds20, n_iter500, ncomp8) freq pd.Series(counter).sort_values(ascendingFalse) # 保留至少一半运行中都出现的变量 stable_vars freq[freq 10].index.tolist()这段代码的运行成本是单次 CARS 的 20 倍。对于真实光谱数据如果单次跑 3 分钟20 次就是 1 小时但换来的是变量集合的稳定性。我一般只在正式建模前做一次这个频率统计期间去整理数据说明文档不浪费等待时间。拿到 stable_vars 之后再拿这些变量直接建最终 PLSR 模型验证测试集表现。提示如果你的数据规模很大20 次运行扛不住可以改成 10 次同时把频率阈值从 50% 降到 40%效果会略差但依然远好于单次结果。这个进阶套路最重要的价值不在变量数量减少而在于结论可复现。我养成的习惯是任何 CARS 结果先跑一轮频次统计再决定要不要用它建模。曾经有个项目单次运行选中了 47 个波长看起来很有道理跑完 20 个种子后发现只有 12 个变量被稳定选中真正稳定可靠的波长只有一小部分最终模型反而比先用 47 个变量时更稳。先看稳定性再追求精度这套思路救过我很多次希望帮到你。本文还有配套的精品资源点击获取