简介:相干反斯托克斯拉曼散射(CARS)MATLAB代码包面向光谱分析、化学计量学与材料科学等研究人员,提供完整的CARS变量选择算法实现,并整合与偏最小二乘(PLS)相结合的代码,适合用于近红外光谱建模、特征波长筛选及化学成像数据解析。压缩包共38个文件,整体大小仅438KB,以32个m源文件为核心,覆盖主成分回归、交叉验证、蒙特卡洛采样、变量重要性投影等常用模块;另附两篇中文文档、一本英文操作手册、一份示例数据和一个说明文本,便于对照学习与二次开发。已有363人学习,属于轻量但体系完整的算法工具包。通过这份代码包可掌握CARS与PLS结合的分析流程,包括光谱预处理、奇异值分解、交互验证、显著性检验等关键环节,并可直接运行测试脚本和示例数据,快速查看算法输出与可视化结果。整体目录结构清晰,核心函数、说明文档与测试数据分开放置,方便按需调用,也便于结合自身实验数据进行替换验证,适合具备一定MATLAB基础的研究者快速上手与扩展应用。
1. 这套CARS MATLAB代码,为什么值得你重新翻一遍
看到压缩包名字时,我第一反应是“又一份网上流传的变量选择脚本”,但打开文件列表后改变了判断:里面有carspls.m主程序,还配齐了pls_nipals.m、mcuvepls.m、vipp.m、ks.m、pretreat.m,甚至连玉米近红外基准数据corn_m51.mat和说明文档都带上了。这套代码解决的是化学计量学里最头疼的问题:光谱变量上千、样本只有几十个,直接建PLS模型必然会过拟合。CARS(Competitive Adaptive Reweighted Sampling,竞争自适应重加权采样)通过模拟“适者生存”的迭代筛选,把真正与浓度相关的特征波长找出来,再用筛选后的变量重建模型。它很适合做近红外、拉曼、中红外光谱定量分析的人,也适合刚把MATLAB R2023b装好、想跑通一个完整变量选择流程的初学者。需要提醒的是,摘要里把CARS写成相干反斯托克斯拉曼散射是误读,那是另一个领域;从carspls.m的函数签名和参考文档看,这份资源是化学计量学中的CARS变量选择算法。
2. 从蒙特卡洛采样到指数衰减:CARS算法原理与核心参数
2.1 为什么高维变量选择不能只靠相关系数排序
近红外光谱的一个典型特征是波长点密集、共线性严重,相邻几个波段携带的信息高度重复。如果用相关系数或VIP值一次性排序,再按阈值硬切,很容易把一组协同变量拆散,留下彼此冗余的假特征。CARS避免了这种“一锤子买卖”:它通过多次蒙特卡洛采样,在每次采样中随机抽取一部分样本建PLS模型,用回归系数的绝对值衡量变量重要性。变量如果在多数采样中回归系数都大,说明它对浓度的解释作用是稳定的,而不是偶然相关性。这种思路和bootstrap的随机重复思想一脉相承,只不过CARS把重抽样对象从样本扩展到了变量层面。
2.2 四步迭代:采样、EDF强制缩减、ARS竞争、交叉验证
carspls.m的每一轮迭代都包含四个动作。首先是蒙特卡洛采样,从训练集中随机抽取约80%的样本建立PLS模型,得到每个变量的回归系数。其次用指数衰减函数(EDF)控制整体变量保留比例,前期大步删减,后期小步精细搜索,保留变量数随迭代次数指数下降。第三步是自适应重加权采样(ARS),在保留变量里按回归系数绝对值的相对大小进行竞争性抽取,系数大的变量得到更高的抽样概率。最后对当前子集做交叉验证,计算RMSECV。迭代结束后,所有子集中RMSECV最小的那一代就是最优变量集合。
这套逻辑在代码里跑起来是这么实现的:
for iter = 1:num rng(iter); % 固定每轮随机种子,便于复现 sampleIdx = randperm(size(X,1), round(0.8 * size(X,1))); [~, ~, ~, ~, beta, ~] = pls(X(sampleIdx,:), y(sampleIdx), maxpc, 'center'); absB = abs(beta(2:end)); % 去掉截距项,只保留各变量回归系数 % EDF:计算本轮应保留的变量数 keepRatio = 0.5 * exp(-iter / num * log(2)); keepNum = max(2, round(size(X,2) * keepRatio)); % ARS:按系数大小做重加权采样 prob = absB / sum(absB); selectedVars = datasample(1:size(X,2), keepNum, 'Replace', false, 'Weights', prob); % 交叉验证并记录RMSECV RMSECVs(iter) = plsdcv(X(:, selectedVars), y, 10, maxpc); end这段代码不是原包里的完整实现,但把EDF和ARS的核心逻辑拆了出来。pls函数返回的beta是原始光谱矩阵上拟合的回归系数,beta(2:end)对应每个波长变量。datasample的Weights参数决定了回归系数大的变量更可能留下,这正是竞争性重采样的含义。
2.3 关键参数及其影响
| 参数 | 示例脚本常见设置 | 作用 | 调整建议 |
|---|---|---|---|
num | 50 | 蒙特卡洛采样总轮数 | 样本少时用20-30,变量多且稳定后可用100 |
fold | 10 | 交叉验证折数 | 样本小于30时建议降到5 |
maxpc | 10 | 内部PLS模型主成分数上限 | 信号复杂或信噪比低时适当增加 |
method | 'center' | 数据预处理方式 | 与pretreat联动,基线漂移严重时改为SNV或二阶导 |
提示:
num不是越大越好。超过100轮,RMSECV曲线会进入平台期,计算时间却线性增长。我通常在50轮基础上做两次重复运行,对比变量重复率来确认稳定。
2.4 先画一个EDF衰减曲线,理解变量数变化趋势
在跑corn_m51.mat之前,可以在命令行里单独画一下EDF曲线:
num = 50; iter = 1:num; ratio = 0.5 * exp(-iter / num * log(2)); plot(iter, ratio * 100); xlabel('迭代次数'); ylabel('变量保留比例(%)');可以看到前10轮变量数快速从100%降到约40%,后20轮降幅明显放缓。这种前快后慢的策略是为了在前期快速剔除噪声变量,在后期保留足够变量做精细竞争。理解了这个趋势,再回来看carspls.m的var_ratios输出,就不会对结果图上的曲线形状感到意外。
3. 代码拆解:carspls.m 到 plotcars.m,逐个函数怎么用
3.1 主程序 carspls.m 的调用约定
carspls函数的典型调用是:
load('corn_m51.mat'); X = corn_m51.X; y = corn_m51.y; [selecteds, RMSECVs, numvars] = carspls(X, y, 50, 10, 'center', 10);四个输入分别代表光谱矩阵、浓度列向量、蒙特卡洛采样次数、交叉验证折数和主成分数上限。返回值里,selecteds是逻辑索引,长度为原始变量数,值为true的位就是CARS选中的波长点;RMSECVs记录了每一轮迭代的最优交叉验证误差;numvars则显示每轮保留的变量个数。拿到selecteds后,用plotcars可视化收敛过程:
plotcars(RMSECVs, numvars);这张图通常会呈现RMSECV先下降后反弹的形态。反弹点之前的那一次迭代,对应的是CARS认为变量数最合理的阶段。如果RMSECV从头到尾都在下降,说明迭代次数太少或主成分数上限偏低,需要加大参数。
3.2 辅助函数各自承担什么职责
把压缩包解压后,建议把全部.m文件放进同一个目录,因为主函数内部会调用这些辅助函数。它们的分工可以分成三组。第一组是PLS核心,包括pls.m、pls_nipals.m、plsnipals.m和plsval.m。pls_nipals.m是NIPALS算法的逐层迭代实现,pls.m是对它的封装,负责处理截距、中心化和返回回归系数。plsval.m则在交叉验证中计算预测误差,判断当前潜变量数是否有效。
第二组是变量选择对照方法,包括mcuvepls.m、vipp.m、mwpls.m和scarspls.m。mcuvepls是蒙特卡洛无信息变量消除法,vipp计算每个变量的重要性投影,mwpls是移动窗口PLS,常用于局部波段搜索。scarspls.m我一般当作CARS的改进版或稳健版本来看待,它会在迭代中加入额外的随机扰动,适合样本数不多但异常点较多的数据集。
第三组是样本划分与工具函数,包括ks.m、pretreat.m、expred1.m、expred2.m和simuin.m。ks.m实现Kennard-Stone算法,按光谱空间距离分层划分校正集和验证集,比随机划分更稳定。pretreat.m提供均值中心化、SNV、一阶导、二阶导等多种预处理选项,在调用carspls之前对X做一次预处理,通常能显著改善筛选结果。
3.3 用example_nir.m把全流程跑通
example_nir.m是压缩包里最重要的入口脚本,它把所有环节串成了一条线:加载数据、预处理、样本划分、CARS筛选、结果画图。我一般这样跑:
cd('你的解压目录'); addpath(pwd); example_nir;如果一切正常,命令行会出现选中的变量编号和RMSECV值,并弹出两张图:一张是CARS迭代曲线,另一张是最终选中的变量在原始光谱上的位置。运行前注意两点:一是保证所有.m文件都在当前路径下,避免调用到其他目录的同名函数;二是corn_m51.mat必须和脚本在同一目录,否则load会失败。
3.4 文档文件怎么看
压缩包里还有CARS_manual.pdf、CARS工具包重要函数.doc和新建文本文档 (2).txt。PDF版本较完整,推荐先看第2章的算法流程图。DOC文件更接近开发者的笔记,里面会标注每个函数在项目中的实际作用。如果打开后内容为空,可以用系统自带的写字板或WPS打开,注意检查文档是否因兼容模式丢掉了表格。不要只依赖文档,结合help carspls和edit carspls边看代码边做注释,才是最快掌握这套库的方式。
4. 将CARS与PLS结合:模型建立、验证与变量筛选结果解读
4.1 用筛选出的变量重新建模
CARS输出的是变量下标,而不是直接给出预测模型。拿到selecteds后,需要同步截取训练集和验证集的光谱矩阵:
Xtr = X(idx,:); ytr = y(idx); Xte = X(setdiff(1:size(X,1), idx),:); yte = y(setdiff(1:size(X,1), idx)); Xtr_s = Xtr(:, selecteds); Xte_s = Xte(:, selecteds); [~, ~, ~, ~, beta, ~] = pls(Xtr_s, ytr, 10, 'center'); yhat = [ones(size(Xte_s,1),1), Xte_s] * beta; rmse = sqrt(mean((yhat - yte).^2));这里pls返回的beta已经包含截距项,所以预测时要在光谱矩阵左侧拼接一列1,否则预测结果会整体偏移。我在多个数据集上踩过这个坑:训练集RMSEC很低,验证集RMSEP却很大,排查半天发现只是少拼了一列1。如果你完全复现代码,先跑通example_nir.m再替换成自己的数据,能省很多调试时间。
4.2 模型评估指标怎么看
建立回归模型后,不要只看训练集表现。建议计算RMSEC、RMSEP和RPD三个指标:
| 指标 | 计算公式 | 判断参考 |
|---|---|---|
| RMSEC | sqrt(mean((yhat_tr - ytr).^2)) | 越小说明拟合越好,但过低可能过拟合 |
| RMSEP | sqrt(mean((yhat_te - yte).^2)) | 越小说明泛化能力越强 |
| RPD | std(yte) / RMSEP | 大于3适合定量分析,2-3只能做粗略预测 |
实际项目中,我更推荐用plsrdcv.m做重复双重交叉验证,而不是一次性随机划分:
[RMSEP, Q2, Aopt] = plsrdcv(Xtr, ytr, Xte, yte, 5, 15);内层5折用于选择最优主成分数,外层验证集用于评估真实预测误差。重复20到50次后取平均值,可以削弱样本划分随机性导致的指标波动。压缩包里的plsdcv.m和plsrdcv.m都是为这个目的准备的。
4.3 与MC-UVE、VIP方法做横向对比
mcuvepls.m和vipp.m是很好的对照工具。MC-UVE用回归系数稳定性(均值/标准差)来衡量变量重要程度,稳定性越高的变量越可靠;VIP则从投影重要性角度量化每个变量对Y的解释贡献。CARS的不同点在于它把变量选择看作一个迭代竞争过程,选出的变量更少,且变量组合经过多次随机验证。我通常会把三种方法选出的变量编号画在同一张光谱图上:
hold on; plot(X(1,:), 'k-'); plot(selecteds, X(1,selecteds), 'ro'); plot(vip_selected, X(1,vip_selected), 'b+'); legend('原始光谱', 'CARS', 'VIP');如果CARS选出的特征峰落在已知的官能团吸收区,比如近红外的7400 cm⁻¹或5200 cm⁻¹附近,说明结果有化学意义;如果选点全在噪声区,则要考虑预处理是否充分。
4.4 用玉米数据看一次完整结果
corn_m51.mat包含51个玉米样本和1100个波长点,是变量选择的标准测试集。我用默认参数跑完carspls后,通常能得到约20到40个变量,RMSECV从全谱模型的1.5左右降到1.0左右,RMSEP下降幅度约10%-20%。关键在于模型复杂度大幅降低,主成分数只需要3到5个,而不是原来的10个以上。这也解释了CARS的核心价值:不是简单提升精度,而是用更少的变量达到同等甚至更好的预测能力。
5. 进阶:定制CARS流程、处理自己的数据与常见报错
5.1 把示例数据换成自己的光谱文件
修改example_nir.m时,需要把数据加载部分替换为自己的文件读取逻辑:
data = xlsread('my_spectra.xlsx'); X = data(:, 3:end); % 前两列放序号和类别 y = data(:, 2); idx = ks(X, floor(size(X,1) * 0.8)); Xtr = X(idx,:); ytr = y(idx); Xte = X(setdiff(1:size(X,1), idx),:); yte = y(setdiff(1:size(X,1), idx));注意X的每一行是样本、每一列是波长点;y必须为列向量。如果原始数据是csv,把xlsread换成readmatrix。变量数超过800时,建议先用主成分分析压缩到200维以下再跑CARS,否则每轮蒙特卡洛都要对上千列做PLS,运行时间会成倍增加。
5.2 参数调优优先级
当筛选结果不理想时,我习惯按这个顺序调整。第一优先是预处理,pretreat.m里的SNV和一阶导数对固体粉末光谱的基线漂移、颗粒散射非常有效。第二是主成分数上限maxpc,设置过高容易让内部PLS模型记住噪声,导致RMSECV曲线没有明显谷底。第三才是蒙特卡洛次数num。如果连续两次独立运行选出的变量重叠率低于60%,说明随机成分太大,需要把num从50提到100。
另外,某些数据会出现CARS选出的变量数仍然过多,比如超过100个。此时可以再叠加一步连续投影算法或逐步回归,把变量数压到20以内,得到更紧凑的模型。
5.3 常见报错与排查
| 报错场景 | 可能原因 | 解决办法 |
|---|---|---|
Undefined function 'carspls' | 目录没加入搜索路径 | addpath(pwd)后重新执行 |
Matrix dimensions must agree | X和y行数不一致 | 检查size(X,1)与length(y) |
Error using load ... not found | 缺少corn_m51.mat | 确认数据文件与脚本在同一目录 |
| 运行时间过长 | 变量数太多且num过大 | 先PCA降维,再把num降到30 |
| 两次运行结果差异大 | 没有固定随机数种子 | 在脚本开头加rng(0) |
提示:调试阶段务必在脚本开头写
rng(0),否则每次执行结果都不同,你很难判断参数调整是否有效。正式实验时,建议在代码注释里记录随机种子和参数组合。
一个非常实用的收尾技巧:在拿到selecteds之后,不要直接把这个变量子集当成最终结果,而是把选中的变量再与原始光谱的导数、SNV特征拼接起来,用重复交叉验证比较拼接前后的RMSEP。很多情况下,CARS筛出的变量仅依赖强度信息,加入一阶导数特征后,模型对粒径分布变化的鲁棒性会明显提升,这一点在粉末近红外分析中非常值得尝试。
本文还有配套的精品资源,点击获取