news 2026/10/1 12:29:08

CARS特征选择原理与Python实现:光谱建模高效降维指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CARS特征选择原理与Python实现:光谱建模高效降维指南

简介:这份资源提供基于 Python 的自适应重加权波段选择(CARS)特征选择代码,核心目标是解决近红外光谱等高维数据中的特征冗余问题。它通过迭代评估各波长对模型性能的贡献,动态更新权重,逐步筛选出对目标变量影响最大的关键波段,同时剔除噪声与冗余信息,从而简化模型并保持预测准确率。整个 rar 包内仅有 1 个 Python 文件(CARS.py),大小约 2KB,代码结构清晰,涵盖数据预处理、权重初始化、特征筛选、模型验证与结果输出等流程,便于研究者直接阅读、修改或嵌入自己的光谱分析任务,适合食品、药品、生物组织等无损检测场景的特征选择。已有 2104 人学习下载,作为轻量级工具,可与递归特征消除等其他方法结合使用,帮助从事化学计量学与机器学习建模的开发者快速构建高效特征选择流程。

1. CARS 特征选择:光谱建模里最值得先跑一遍的筛子

做近红外、中红外或拉曼光谱定量分析的同学,大概率遇到过这种局面:一条光谱几千个波长点,样本量却只有几十个,PLS 模型一跑就过拟合,换个验证集准确率立刻跳水。很多人下意识用方差筛选,把变化大的波段留下,结果模型更差了——因为光谱里真正有效的信号往往变化幅度不大,而基线漂移的方差倒是很大。CARS 特征选择(Competitive Adaptive Reweighted Sampling,竞争性自适应重加权采样)就是冲着这个矛盾来的:用蒙特卡洛采样配合指数衰减的波长筛选策略,把和待测组分真正相关的波段挑出来,而不是看谁“跳得欢”。这篇笔记会带你从原理、代码、参数到翻车现场,完整走一遍这个方向。

2. CARS 的采样逻辑:为什么你该关心“竞争”和“衰减”

2.1 从全光谱到小集合:CARS 是怎么做到自动降维的

CARS 的思路是模拟达尔文式的筛选。每一轮它先随机抽一部分样本做 PLS 回归,得到每个波长点的回归系数;系数的绝对值大小代表这个波长对模型贡献的强弱。然后它不是简单地把系数小的砍掉,而是用指数衰减函数(EDS)决定每一轮保留多少波长——前期砍得快,后期砍得慢,最后留一小撮“幸存者”。与此同时,每次随机抽样还会做自适应重加权采样(ARS),相当于给每个波长按贡献大小分配一个被选中的概率,下一轮从这个概率分布里再抽一波。几轮下来,真正稳定的波长会被反复选中,而与组分无关的噪声波段会逐渐被淘汰。

这里的“竞争”体现在每个波长每一轮都要和其余所有波长比一次回归系数。有的波长单独看相关性强,但和别的波长放在一起后就变得冗余,PLS 的系数会把它压下去;有的波长看起来平平无奇,却是模型里不可替代的补位选手。这种竞争机制和互信息特征选择不同,互信息只评估单个变量和目标的关联,不考虑变量之间的协作和替代关系;而 CARS 是放在一个建模语境里做筛选的,选出来的波长天生就适合进 PLS 模型。

2.2 核心公式拆解:指数衰减和 RMSECV 怎么配合

CARS 每一轮的保留波长数由这个公式控制:

[ r_i = r_0 \cdot e^{-k \cdot i} ]

其中 (r_0) 是初始波长数,(i) 是当前轮次,(k) 是衰减系数,由总轮数 (N) 和最终保留比例决定。常见做法是让最后一轮保留 2% 到 5% 的原始波长,于是 (k = \ln(r_0 / r_{final}) / N)。这个衰减不是线性而是指数的,目的是让前 20 轮就砍掉大半无关变量,后面几十轮慢慢精修,避免一次砍太狠把好波长误杀。

每一轮筛选完之后,CARS 会用当前保留下来的波长重新建 PLS 模型,用交叉验证算出 RMSECV(均方根交叉验证误差),一轮一轮记录。全部跑完后,RMSECV 最低的那一轮对应的波长子集就是最终结果。为什么不是最后一轮?因为指数衰减到后期,保留下来的波长已经很少,模型开始欠拟合,RMSECV 反而抬头了。所以 CARS 的输出不是“最后幸存者”,而是“历史上表现最好的一代”。

2.3 为什么偏偏是 PLS 而不是别的模型

CARS 的内部评估器几乎都是 PLS,这有两个原因。第一,光谱数据本身就是高维强相关的,PLS 通过潜变量提取能稳定给出每个原始波长的回归系数,而且计算快,跑 1000 轮蒙特卡洛也就几秒到几十秒。第二,CARS 的筛选目标和最终用途是一致的——绝大多数光谱定量任务最终就落在一个 PLS 模型上,用 PLS 的系数做筛选,选出来的波长不会“答非所问”。

有人会问,能不能把内部评估器换成随机森林或者 XGBoost?技术上可以,但实践中意义不大。随机森林给的是重要性分数,没有方向信息,而且对强相关变量的处理会偏向某个变量,导致筛选结果不稳定。如果你非要用树模型做特征选择,建议用 Boruta 或者置换重要性,而不是硬把 CARS 的内部结构改掉。CARS 的价值恰恰在于它和 PLS 的契合度。

3. 用 CARS 从光谱里筛特征:一套可以直接跑的 Python 流水线

3.1 先准备数据:标准化比你想的更关键

在跑 CARS 之前,数据预处理决定了一半的成败。光谱数据最常见的预处理是标准化(StandardScaler)或 Savitzky-Golay 平滑 + 一阶导。标准化是必须的,否则量级大的波段回归系数天然就大,CARS 的竞争机制会偏向它们;一阶导能消除基线漂移和背景干扰,但会放大噪声,所以一般配合平滑一起用。我通常的做法是:先做 SG 平滑(窗口 11,多项式阶数 2),再取一阶导,最后标准化。

import numpy as np from scipy.signal import savgol_filter from sklearn.preprocessing import StandardScaler def preprocess_spectra(X_raw, window=11, polyorder=2, deriv=1): # X_raw: (n_samples, n_wavelengths),光谱原始数据 X_smooth = savgol_filter(X_raw, window_length=window, polyorder=polyorder, deriv=deriv, axis=1) scaler = StandardScaler() X_scaled = scaler.fit_transform(X_smooth) return X_scaled, scaler

这里window是平滑窗口大小,不能超过光谱长度,而且必须是奇数;polyorder是多项式阶数,一般 2 或 3;deriv=1表示做一阶导数。标准化用StandardScaler会把每个波长点的均值变成 0、方差变成 1,这样 CARS 的回归系数比较的就是“标准化的贡献”,而不是原始量级。注意:标准化必须在训练集上 fit,然后应用到验证集,千万不能把全部数据一起 fit。

3.2 手写一个 CARS 核心循环:蒙特卡洛采样与重加权

CARS 没有统一的官方库,网上有各种实现,但核心逻辑都很一致。我一般自己写一个类,方便控制每一轮的随机种子和输出中间结果。下面这个实现包含完整的采样、PLS 系数计算、指数衰减和 RMSECV 记录,适合直接粘进你的项目里改。

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 class CARS: def __init__(self, n_components=10, n_runs=100, final_ratio=0.02, cv_folds=5, random_state=42): self.n_components = n_components self.n_runs = n_runs self.final_ratio = final_ratio self.cv_folds = cv_folds self.random_state = random_state def fit(self, X, y): rng = np.random.default_rng(self.random_state) n_samples, n_wavelengths = X.shape # 指数衰减系数,保证最后一轮剩 final_ratio 的波长 k = np.log(n_wavelengths / max(1, int(n_wavelengths * self.final_ratio))) \ / self.n_runs kept_indices = np.arange(n_wavelengths) history = [] for run in range(self.n_runs): # 蒙特卡洛采样:每次随机抽 80% 的样本 sample_idx = rng.choice(n_samples, size=int(n_samples * 0.8), replace=False) X_sub = X[sample_idx][:, kept_indices] y_sub = y[sample_idx] pls = PLSRegression(n_components=self.n_components) pls.fit(X_sub, y_sub) coef = np.abs(pls.coef_.ravel()) # 本轮的 EDS 保留数量:按指数衰减计算 retain_num = max(1, int(n_wavelengths * np.exp(-k * run))) # 系数排序,保留最大的 retain_num 个波长 order = np.argsort(coef)[::-1] selected_local = order[:retain_num] # 交叉验证评估当前子集 X_cv = X[:, kept_indices[selected_local]] y_pred = cross_val_predict(PLSRegression(n_components=self.n_components), X_cv, y, cv=self.cv_folds) rmsecv = mean_squared_error(y, y_pred, squared=False) history.append({ 'run': run, 'n_wavelengths': len(selected_local), 'rmsecv': rmsecv, 'indices': kept_indices[selected_local].copy() }) # 自适应重加权采样:按系数大小给概率,抽下一轮的子集 prob = coef / coef.sum() next_count = int(retain_num * 0.9) # 每轮再多抽掉 10% 作为竞争压力 next_count = max(1, next_count) chosen = rng.choice(kept_indices[selected_local], size=next_count, replace=False, p=prob[selected_local] / prob[selected_local].sum()) kept_indices = np.sort(chosen) # 选 RMSECV 最小的一轮 best = min(history, key=lambda h: h['rmsecv']) self.best_run_ = best['run'] self.selected_indices_ = best['indices'] self.history_ = history return self

逻辑说明很关键。第一,每次随机抽 80% 的样本做 PLS,这是蒙特卡洛采样部分,让模型每次看到的样本略有不同,从而检验波长的稳定性;第二,回归系数取绝对值后排序,只保留前列的波长,这是“竞争”的本体;第三,RMSECV 用五折交叉验证在完整样本上评估当前子集,记录每一轮的表现;第四,自适应重加权采样用系数的归一化概率去抽下一轮的子集,并且额外抽掉 10%,形成淘汰压力。几个参数值得注意:n_runs=100是最常见的选择,太少衰减曲线太陡容易错过好子集;final_ratio=0.02意味着最后只剩 2% 的波长;n_components建议用全谱 PLS 交叉验证选出的最优潜变量数,而不是随意设一个。

3.3 跑完 CARS 之后:看趋势图而不是只看结果

CARS 跑完,不要急着拿 selected_indices_ 去做模型。我强烈建议把 history 里的每一轮 RMSECV 和保留波长数画出来,会有两条曲线:波长数量从 1000 多一路掉到 20 几,RMSECV 先下降后上升。RMSECV 的最低点出现的位置通常在波长数还剩下原谱 5% 到 15% 的区间内,如果最低点出现在最后几轮,说明你的衰减太快,把重要波段过早删掉了;如果最低点出现在第一轮,说明全谱模型已经是最优解,CARS 可能不适合你的数据。

下面的可视化代码可以直接用:

import matplotlib.pyplot as plt def plot_cars_history(cars): runs = [h['run'] for h in cars.history_] n_waves = [h['n_wavelengths'] for h in cars.history_] rmsecv = [h['rmsecv'] for h in cars.history_] fig, ax1 = plt.subplots() ax1.plot(runs, n_waves, 'b-', label='wavelengths kept') ax1.set_ylabel('n_wavelengths', color='b') ax2 = ax1.twinx() ax2.plot(runs, rmsecv, 'r-', label='RMSECV') ax2.set_ylabel('RMSECV', color='r') ax1.axvline(cars.best_run_, linestyle='--', color='gray') plt.xlabel('run index') plt.show()

参数说明:ax1画的是每一轮剩余的波长数,ax2画 RMSECV,灰色虚线标出最优轮次。如果虚线左侧 RMSECV 还在下降,说明你砍掉的都是噪声;虚线右侧上升,说明已经砍过头了。这条虚线就是你的“后悔药坐标”——它告诉你,模型最省且最准的状态在哪。

4. 调参不是玄学:把 CARS 的四个关键旋钮一次讲透

4.1 运行轮数与衰减系数:先盯 RMSECV 曲线形态

CARS 最核心的参数是总轮数n_runs和最终保留比例final_ratio,它们共同决定了衰减曲线的斜率。你不需要单独调指数衰减系数k,因为它在代码里是由这两个参数推导出来的。总轮数太少,例如 30 轮,每一轮平均要砍掉近 20% 的波长,容易把和组分相关但系数稍小的波段提前误杀;总轮数太多,例如 500 轮,最后几十轮保留波长数几乎不变,纯属浪费算力。经验上,光谱点数在 500 到 2000 之间时,100 轮是个稳妥的起点。

final_ratio的设置直接影响最终能选出多少波长。0.02 意味着如果原始光谱有 1000 个点,最后只留 20 个;0.05 留 50 个。如果你的样本量很少,PLS 潜变量也少,建议final_ratio设大一点,例如 0.05,否则最后留的波长太少,模型欠拟合。一个实用检查方法:跑完看 history 里最优轮的波长数,如果小于 PLS 潜变量数的 5 倍,就要调大final_ratio,因为 PLS 需要足够的变量支撑潜变量解释。

4.2 PLS 潜变量数和交叉验证折数:最常见的两个翻车来源

n_components如果设置过高,PLS 会开始拟合噪声,回归系数的绝对值分布变平,CARS 的竞争力下降,筛选结果趋于随机;设置过低,模型欠拟合,系数不稳定,筛选结果波动大。正确做法是先用全谱 PLS 做一遍 5 折或 10 折交叉验证,画潜变量数和 RMSECV 的关系图,取 RMSECV 最低的潜变量数或者“肘部”位置的值。注意这里用的潜变量数应该基于全谱数据,因为 CARS 每一轮都是在全谱子集上做 PLS,潜变量能力不会比全谱更强。

交叉验证折数cv_folds影响 RMSECV 的稳定性。折数太少,每次验证集的样本太少,RMSECV 方差大,最优轮次可能被随机噪声推动;折数太多,比如留一法,计算量翻倍且容易过拟合验证集。5 折是光谱建模场景里性价比最高的选择。如果你的样本量少于 20,建议改成留一法并固定随机种子,否则折数太少导致验证集代表性不足。

4.3 采样比例和随机种子:稳定性的最后一个开关

CARS 每次蒙特卡洛采样抽取 80% 的样本,这个比例一般不用改。但要注意,你必须在初始化时固定random_state,否则同一份数据跑两次,选出来的特征可能完全不同——尤其当你的数据里存在强共线波段时,CARS 的竞争结果对初始采样非常敏感。我见过有人因为没固定随机种子,报告里写的特征和复现实验不一致,审稿人直接拒稿。固定种子之后,CARS 的结果理论上可复现,这也是工程落地的底线。

参数推荐值调整方向判断依据
n_runs100数据量小时减到 50RMSECV 最低点不出现在最后 10 轮
final_ratio0.02~0.05样本少时调大最优轮保留波长数 ≥ 5 × n_components
n_components全谱交叉验证最优值过高会筛出噪声全谱 PLS 的 LV-RMSECV 曲线取肘部
cv_folds5样本 < 20 时用留一法最优轮次稳定、不随折数剧烈变化
random_state固定需要可复现时必须固定两次运行选中波段重合率 > 80%

5. CARS 高频翻车现场:五个坑位和对应的后悔药

5.1 不标准化就开跑:结果全偏向高波数段

现象:CARS 选出来的波长几乎全部集中在光谱信号强的区域,低波数段一个都不选,模型验证集误差反而比全谱还高。原因:光谱不同波段的响应量级差异很大,未经标准化的回归系数天然与量级成正比,CARS 的“竞争”实际上变成了比谁数值大,而不是比谁贡献大。解决:在跑 CARS 之前强制做标准化,且必须是按波长点分别标准化,不是对整个光谱做全局归一化。

5.2 潜变量数拍脑袋定:筛选结果像抽签

现象:同样的数据,换一个n_components,选中的波长完全不同;或者 CARS 跑两次(未固定种子)结果差异巨大。原因:潜变量数偏离最优值时,PLS 系数进入不稳定区,回归系数的排名会被噪声主导,重加权抽样自然不稳定。解决:先用全谱 PLS 做潜变量数扫描,固定最优潜变量数,再固定random_state,跑两次验证交集数量。如果两次选中的波长重合率低于 70%,说明你的数据可能本身就不适合 CARS。

5.3 用最后一轮的结果而不是 RMSECV 最优轮

现象:有人把 CARS 循环结束后kept_indices的最终值当作输出,模型表现比预期差很多。原因:最后一轮保留波长数最少了,但欠拟合已经很严重,RMSECV 已经过了最低点;CARS 的目标不是“砍到最少”,而是“砍到正好”。解决:始终取history中 RMSECV 最小的一轮,而不是最后一轮。这也是为什么上面代码里要把每次的 indices 完整记录进 history,防止事后找不到后悔药。

5.4 样本量太少还叠加蒙特卡洛抽样

现象:样本总量只有 30 个,CARS 每轮再抽 80% 即 24 个去拟合 PLS,交叉验证又是 5 折,训练集经常只有 19 个样本,PLS 的潜变量数稍微高一点就完全过拟合。原因:CARS 的蒙特卡洛设计本身就需要一定的样本冗余,太少样本会导致每一轮模型的误差很大,RMSECV 曲线噪声淹没信号。解决:样本量小于 50 时,把采样比例从 0.8 提到 0.9,交叉验证改成留一法,同时把潜变量数上限压到 5 以下;如果仍然不稳,考虑改用互信息特征选择或 Boruta 做交叉验证对比。

5.5 把 CARS 用到非线性关系的数据上

现象:做荧光光谱或某些非线性响应体系的定量分析时,CARS 选出来的波长建模效果远不如全谱,甚至不如随机选择。原因:CARS 的内在评估器是线性 PLS,当变量与浓度的关系明显非线性时,PLS 的回归系数并不能体现真实贡献,竞争的排名失真。解决:先用简单的手段判断线性度,比如比较线性 PLS 和高阶多项式 PLS 的交叉验证误差;如果非线性明显,换成基于非线性模型的特征选择方法,例如互信息特征选择排序或递归特征消除配合随机森林,不要硬凹 CARS。特征工程里没有万能锤子,CARS 的边界就在线性光谱建模这条线上。

6. 把 CARS 放进你的特征工程流水线:互信息兜底、标准化先行

6.1 CARS 与互信息特征选择的组合策略

单纯跑完 CARS 拿到几十个波长直接建模,是很多新手会做的事,但不一定是最稳的路径。更稳妥的做法是两阶段筛选:第一阶段用互信息特征选择对所有波长做一个初筛,把互信息值明显低于阈值的噪声波段直接剔除,通常能把 2000 个点压到 800 个左右;第二阶段用 CARS 在这 800 个点上继续竞争筛选,得到最终的特征子集。这样做的理由是互信息对弱线性关系也敏感,但计算量大而且不考虑变量之间的协同,适合做粗筛;CARS 擅长处理高相关数据中的协同冗余,适合做精筛。两个方法互补,比单用任何一个都稳。

Stage 1: 互信息特征选择(粗筛)—— 500~1200 个波长 Stage 2: CARS(精筛)—— 10~60 个波长 Stage 3: PLS 建模与交叉验证 —— 对比全谱与筛选后的 RMSECV

当然,如果数据量适中且光谱高度相关,直接跑 CARS 也不会出大问题;两阶段的价值在于减少 CARS 每一轮的排序计算量,同时给非线性区间设置一层保护。需要注意的是,初筛阈值不宜太激进,否则会把 CARS 原本能靠竞争机制救回来的弱信号波长提前淘汰。

6.2 验证 CARS 是否真的有效:一个可靠的基准流程

筛选后再建模,必须和全谱模型做严格的对比,否则你不知道特征选择到底赚到了什么。我的常规做法是:同一份数据分别跑全谱 PLS 和 CARS-PLS,都用同样的潜变量数选择策略,用 10 次重复 5 折交叉验证取平均 RMSE 和 R²,比较预测精度。重点看两个指标:一是 RMSE 有没有下降或持平,二是模型使用的变量数减少了多少。有用的特征选择通常能让你用 2% 的变量换到更低的 RMSE;如果 RMSE 变高但变量少了 95%,在模型部署上有价值,但要明确说出来,不能含糊其辞。

验证时还要注意类别平衡和样本划分,尤其是在做预测任务时,要先划分外部验证集,再在训练集内部做 CARS 选择。CARS 是监督算法,如果你在包含测试集的全量数据上做特征选择再切分评测,结果会虚高——这属于典型的数据泄漏,血泪教训。我在早期做实验时就翻过这个车,评测结果看似完美,一换新批次样品立刻崩盘。后来我固定了一套流程:先分层划分训练/测试,在训练集上做预处理和 CARS,把选中的波长索引固化,再对测试集执行同样的预处理和索引切片,绝不跨越数据边界。希望这个习惯也能帮到你少走一段弯路。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 12:27:38

Profile级隔离到底是哪几层,缺一层会出什么问题?

做多账号运营的人&#xff0c;大多都经历过一个临界点。账号数量在几十个的时候&#xff0c;手动新建环境、逐个粘贴代理、挨个登录&#xff0c;虽然烦但还能扛。等规模跨到几百、上千&#xff0c;这套人力模式立刻崩盘&#xff1a;一个人一天最多手动配几十个环境&#xff0c;…

作者头像 李华
网站建设 2026/10/1 12:27:17

从零开始训练LLM:数据、模型、训练与推理的完整工程实践

1. 项目定位&#xff1a;从零开始到底意味着什么 1.1 是抄代码&#xff0c;还是搞懂每一层 很多人看到“ai-engineering-from-scratch”第一反应是&#xff1a;我知道&#xff0c;就是照着书把一个大语言模型写出来。我最初也是这么想的&#xff0c;但真正动手之后才发现&…

作者头像 李华
网站建设 2026/10/1 12:26:26

懒人理财法则:用定投与资产配置让复利自动滚雪球

你有没有发现一个现象&#xff1a;身边那些天天盯盘、四处打探消息、买进卖出比上班还勤快的人&#xff0c;几年下来往往是白忙一场&#xff0c;甚至越折腾越亏&#xff1b;反而是那些看起来“不闻不问”、连交易软件都很少打开的人&#xff0c;账户里的数字在悄悄变厚。在理财…

作者头像 李华
网站建设 2026/10/1 12:25:52

储能参与调峰的配置与经济性分析Matlab实现全流程

论文里那句“系统总成本最小”看着简单&#xff0c;真落到Matlab代码里才发现到处都是坑。我去年完整复现过一个参与调峰的储能配置与经济性分析算例&#xff0c;从容量定容、时序调度到经济性评估&#xff0c;前前后后折腾了两周多&#xff0c;这里把整个过程中的模型设计、代…

作者头像 李华
网站建设 2026/10/1 12:25:47

WinForms 两个模板怎么选:Framework 与 .NET 8 差异

在“创建新项目”的搜索框里敲下“Windows 窗体”几个字&#xff0c;列表里通常会跳出两条长得几乎一模一样的条目&#xff0c;一条叫“Windows 窗体应用 (.NET Framework)”&#xff0c;另一条就叫“Windows 窗体应用”。很多刚上手 Visual Studio 的人在这里随手点一个&#…

作者头像 李华
网站建设 2026/10/1 12:25:43

MMC整流器混合FCS-MPC控制策略及Simulink复现指南

搞多电平变换器的兄弟应该都有过这种体验&#xff1a;一篇论文看完公式觉得懂了&#xff0c;真到自己动手复现的时候&#xff0c;光是Simulink里那个模块化多电平换流器&#xff08;MMC&#xff09;就够折腾一晚上。去年我复现一篇IEEE Trans. Power Electronics上关于混合有限…

作者头像 李华