简介:MATLAB实现的正交信号校正(OSC)算法脚本,面向化学计量学与光谱分析领域的科研人员及定量校正模型开发者。OSC通过滤除红外及近红外光谱中与目标变量无关的信号成分,可降低噪声干扰与基线漂移影响,从而提升定量校正模型的精度与稳健性;对于近红外光谱中常见的背景漂移和多重共线性问题,该预处理方法能从源头削弱非目标扰动,为后续建立稳定可靠的定量校正模型提供更洁净的输入信号。压缩包内共1个M文件,代码体积仅2KB,结构紧凑,便于直接调用、移植或作为教学示例。CSDN平台已有549人学习浏览该资源,适合需要快速上手OSC预处理方法并应用于光谱数据建模的MATLAB用户,借助该脚本可完成光谱数据的正交信号校正处理,并在此基础上改进偏最小二乘等定量校正模型的预测性能。
1. 正交信号校正:先分清光谱里的“有用方差”和“与浓度无关的方差”
拿到一批近红外光谱,建模时校正集误差很低,可验证集误差高得离谱;或者你把基线拉平、做了一阶导数,精度还是不达标。这类问题的根源往往不是随机噪声,而是光谱里存在大量的、有结构的系统变异——比如样品颗粒度变化、温度漂移、仪器响应缓慢、光程差异。这些变异与目标浓度无关,却会挤占偏最小二乘(PLS)模型的潜在变量自由度,导致模型在训练集上拟合得很漂亮,却学错了方向。正交信号校正(OSC)就是针对这个场景设计的有监督预处理方法:它利用浓度向量 y 的信息,主动找到光谱 X 中与 y 正交(也就是与浓度无关)的方向,并把它们从原始光谱里滤掉。滤除之后再做 PLS 回归,主成分的载荷更干净,RMSEC 和 RMSEP 的差距也不再夸张。本文会用 MATLAB 从算法原理讲到可运行的函数,再结合红外和近红外光谱数据给出完整的定量校正建模流程,以及其中几个最容易踩的坑。
2. 正交信号校正的原理:为什么“有监督滤除”比直接平滑有效
2.1 光谱里的方差不是铁板一块
近红外光谱记录的是样品对光的吸收和散射综合结果,除了目标化学组分的含量信息,还叠加了物理效应和仪器状态。常见的干扰包括:
- 光散射引起的基线漂移和整体斜率变化;
- 环境温度不同造成的氢键峰位移;
- 样品装填密度差异带来的不重复光谱路径;
- 仪器光源老化导致的谱段强度缓慢变化。
这些干扰的共同点是:数值上很有规律,甚至占光谱总方差的很大比例,但它们的本质是“系统性”的,不是随机白噪声。对这类干扰,单纯用小波去噪或 Savitzky-Golay 平滑是无效的,因为平滑只会让低频率的漂移变得更“干净”地存在。标准正态变量变换(SNV)和多元散射校正(MSC)能解决一部分散射问题,但是它们只从 X 自身出发,完全不知道浓度 y 长什么样,所以滤除的方向可能会连真实有用的化学信息一起损失掉。
2.2 OSC 与其他预处理方法的定位差异
下表把 OSC 和常见近红外预处理方法在“是否使用 y 信息”和“解决什么问题”上做一个对比。
| 方法 | 是否用到 y | 核心假设 | 典型用途 | 主要局限 |
|---|---|---|---|---|
| SNV / MSC | 否 | 散射变异可以用均值或参考谱估计 | 消除颗粒度、光程引起的基线偏移 | 对化学峰与散射峰重叠严重时效果有限 |
| 一阶/二阶导数 | 否 | 基线漂移是低频信号 | 分离交叠峰,去除慢变基线 | 放大高频噪声,需要搭配平滑 |
| Savitzky-Golay | 否 | 信号在局部窗口内可用多项式近似 | 平滑、求导 | 不能处理系统性物理变异 |
| OSC | 是 | 与 y 正交的方差是干扰 | 在建模前定向滤除 X 中与浓度无关的系统变异 | 正交成分数过大会滤掉有效信息,造成过拟合 |
从这个表能看出,OSC 是所有常见预处理里少见的“有监督”方法。这个属性既是优点也是风险:优点是有目的性,它可以把与 y 无关的那些“大而强”的系统峰直接消掉,而不是绕道走;风险是你再拿同一组 y 去做交叉验证选主成分数,误差估计容易偏乐观,因此模型验证时的样本划分一定不能把测试集信息混进来。
2.3 OSC 的数学流程:每一步都在做正交投影
经典 OSC 算法由 Wold 等人提出,后来有不少变种,核心步骤是一致的。给定校正集光谱矩阵 X(m 行样本、n 列波长)和浓度向量 y(m 行),假设要滤除 ncomp 个正交成分。
第一步,中心化。分别对 X 和 y 做均值中心化,得到 Xc 和 yc,这一步是 PLS 建模的标准预处理,避免模型被均值带偏。
第二步,寻找与 y 相关性最大的方向。对当前剩余光谱矩阵 X_work 和 yc 做一次单主成分的 PLS 回归,取出权重向量 w。这个 w 表示的是当前光谱中最能给浓度 y 带来解释的方向。
第三步,正交化。计算得分向量 t = X_work * w,然后把 t 向 yc 的正交子空间投影:
t_orth = t - yc * (yc' * t) / (yc' * yc)
这一步是 OCS 的灵魂。因为 t 来自 PLS 的第一权重,它天然与 y 强相关,但经过上式投影后,t_orth 与 yc 的内积为零,即不包含任何有关浓度的线性信息。
第四步,计算载荷并扣除。用 p = X_work' * t_orth / (t_orth' * t_orth) 计算该方向在光谱空间中的载荷,然后从 X_work 中减掉这个成分:
X_work = X_work - t_orth * p'
减完之后,循环回到第二步,继续寻找下一个正交方向。通常滤除 1 到 3 个正交成分就够用,滤得越多,剩余光谱的有效信息损失风险也越大。
2.4 为什么正交是“金标准”
你可能想到一个问题:既然要滤除与 y 无关的干扰,为什么不直接用 PCA 去掉方差最大的几个主成分?PCA 确实能抓到大的变异方向,但它不区分这些方向的方差和 y 有没有关系。比如某条样品装填密度的变化方差很大,但浓度可能也小幅影响了密度,PCA 会把这条主成分直接删掉,损失部分浓度信息。OSC 加上了“与 y 正交”这个约束,它的意思是:只要某个方向和浓度没有任何线性关系,不管它方差多大都删掉;反过来,哪怕方差很小,只要与 y 相关,就保留下来。这也是 OSC 和后来 OPLS(正交偏最小二乘)思想一脉相承的原因,OPLS 内部实际上是把回归模型分解成预测部分和正交部分,而 OSC 是在回归前做正交投影,两者对谱图的解释方式很接近。
3. 用 MATLAB 实现 OSC:最小可用函数与正确投影方法
3.1 基于 plsregress 的 osc 函数
在正式写函数之前,先说一个关键决策点。MATLAB 自带统计和机器学习工具箱里的plsregress函数,可以输出 PLS 权重矩阵。OSC 的每一步要提取“当前 X 与 y 相关性最强”的方向,稳妥的做法是把这步交给plsregress做,而不是自己算X'*y。后者在某些极端尺度下不稳定,而且 PLS 的权重本身考虑了 X 和 y 的协方差结构,收敛更快。
下面给出一个可直接保存成osc.m的函数实现。
function [X_osc, W, T, P, x_mean, M] = osc(X, y, ncomp) % OSC 正交信号校正,基于plsregress提取PLS权重 % 输入: % X - 校正集光谱矩阵,m行样本,n列波长 % y - 校正集浓度列向量,m行1列 % ncomp - 需要滤除的正交成分数,建议值 1~3 % 输出: % X_osc - 校正后的光谱矩阵,与X同尺寸 % W - 每个正交方向使用的权重向量,n行ncomp列 % T - 每个正交方向的得分向量,m行ncomp列,供诊断图使用 % P - 载荷矩阵,n行ncomp列 % x_mean- X的列均值,用于新样本中心化 % M - 线性投影矩阵,n行n列,X_osc = (X-x_mean)*M + x_mean m = size(X, 1); x_mean = mean(X); y_mean = mean(y); Xc = X - x_mean; yc = y - y_mean; X_work = Xc; W = zeros(size(X, 2), ncomp); T = zeros(m, ncomp); P = zeros(size(X, 2), ncomp); M = eye(size(X, 2)); for i = 1:ncomp % 在当前剩余光谱上做一次单PLS,取权重向量的第一列 [~, ~, ~, ~, ~, ~, stats] = plsregress(X_work, yc, 1); w = stats.W(:, 1); w = w / norm(w); % 计算得分,并投影到与yc正交的子空间 t = X_work * w; t_orth = t - yc * ((yc' * t) / (yc' * yc)); % 最小二乘载荷:使X_work近似等于t_orth * p' p = X_work' * t_orth / (t_orth' * t_orth); % 扣除该正交成分 X_work = X_work - t_orth * p'; % 累积线性投影矩阵:每步等效右乘 (I - w*p') M = M * (eye(size(X, 2)) - w * p'); W(:, i) = w; T(:, i) = t_orth; P(:, i) = p; end % 输出时把均值加回,保持光谱的物理尺度 X_osc = X_work + x_mean; end这个函数有几点值得说明。plsregress的第 8 个输出stats.W才是权重矩阵,前面几个输出分别是 X 的得分、Y 的得分、载荷等,别拿错列。权重向量要做单位化,否则后续正交化公式的量纲会乱。正交化公式里的(yc'*t) / (yc'*yc)是个标量,表示 t 在 yc 方向上的分量,把它减掉后,t_orth和 yc 的内积严格为零。p的计算采用的是普通最小二乘回归系数,这样t_orth * p'才是 X_work 在该方向上的最佳一维逼近。
参数设计上,ncomp=1在绝大多数近红外定量任务中就有效果,因为最大的一块系统干扰占一个方向;如果做了第一个正交成分后验证集误差没有下降,再尝试ncomp=2。超过 3 个通常不是更好的选择,而是过拟合的先兆。
3.2 新样本怎么处理:不能重新算 OSC
很多人第一次用 OSC 会犯一个错误:把训练集和验证集拼在一起,对整批光谱做预处理,再划分训练集和验证集。这是最典型的信息泄漏,因为验证集样本已经被用于确定权重向量 w 和正交化方向,得到的验证误差是假的,模型真正上线后表现会大幅缩水。
正确做法是把 osc 函数当作一个“在训练集上训练、在验证集上应用”的过程。训练集做完 OSC 后,会得到x_mean和投影矩阵 M,验证集的光谱不能再用plsregress和 y 去计算权重,只能直接用同一个 M 做线性变换。实现方式如下。
function X_new_osc = osc_apply(X_new, x_mean, M) % OSC新样本变换函数 % 输入: % X_new - 新样本光谱矩阵,行数不限 % x_mean - 训练集光谱均值,由osc函数输出 % M - 线性投影矩阵,由osc函数输出 % 输出: % X_new_osc - 预处理后的光谱 X_new_osc = (X_new - x_mean) * M + x_mean; end这里 M 的意义是:OSC 的每一轮迭代在数学上都是对当前光谱做一次线性变换 X_work = X_work - (X_work * w) * p',即右乘一个矩阵(I - w*p')。多轮迭代等于连续右乘若干个这样的矩阵,所以合并成一个 M。这样新样本不需要知道训练集的 y,也不需要重复迭代,一次矩阵乘法完成任务。相比需要在预测时重新计算得分并扣减的写法,这个方案在验证集上更稳定,也不容易引入尺度错误。
3.3 用模拟数据验证函数行为
为了确认函数写对了,可以用一组带强正交干扰的模拟光谱做最小测试。下面的脚本生成 60 个样本,400 个波长点,浓度向量 y 服从均匀分布。光谱由三个洛伦兹峰组成,其中前两个峰与浓度成正比,第三个峰与浓度完全无关,并且这个无关峰叠加了较大幅度。这样一个理想的测试集里,OSC 理论上应该滤除第三个峰。
% 测试osc函数的最小复现脚本 rng(42); m = 60; n = 400; wl = linspace(1000, 2400, n)'; % 浓度向量 y = 2 + rand(m, 1) * 5; % 两个与浓度相关的化学峰 chem_peak1 = 8 * exp(-((wl - 1600)./60).^2); chem_peak2 = 5 * exp(-((wl - 1900)./50).^2); X_chem = y * (0.6 * chem_peak1' + 0.3 * chem_peak2'); % 一个与浓度无关的强干扰峰(比如水汽吸收的残留) interf_peak = 20 * exp(-((wl - 1800)./150).^2); X_interf = randn(m, 1) * erase(interf_peak'); % 避免变量被复用 X = X_chem + bsxfun(@times, X_interf, interf_peak') + 0.05 * randn(m, n);然后调用osc,并比较原光谱和校正后光谱在第三个峰位置的方差变化。
ncomp = 1; [X_osc, W, T, P, x_mean, M] = osc(X, y, ncomp); % 看正交得分与浓度的相关系数 corr_T_y = abs(corr(T, y)); fprintf('正交得分与y的相关系数: %.4f\n', corr_T_y); % 比较原始光谱和校正光谱在干扰峰中心处的方差 idx_center = 401; % 对应1800 nm附近 var_before = var(X(:, idx_center)); var_after = var(X_osc(:, idx_center)); fprintf('干扰峰方差: 原始 %.2f, 校正后 %.2f\n', var_before, var_after);如果代码正确,corr_T_y应该非常小,接近 1e-14 量级,说明 T 和 y 严格正交。干扰峰附近光谱方差明显变小,说明该方向被有效扣除。这个测试脚本的价值在于验证 OSC 数学实现有没有问题,而不需要先准备真实数据。
4. 实战:OSC 改进近红外定量校正模型的完整流程
4.1 数据划分要放在 OSC 前面
无论用真实数据还是模拟数据,模型评估流程都是一样的。先随机划分样本,再用训练集去训练 OSC 和 PLS 模型,最后把验证集的光谱用训练集得到的参数一次性变换,送入模型预测。下面这段代码演示的是完整流程。
% 读取数据,假设变量为X_all和y_all % load('nir_data.mat'); % X_all为nSample x nVar,y_all为nSample x 1 rng(2026); idx = randperm(size(X_all, 1)); ncal = 80; Xcal = X_all(idx(1:ncal), :); ycal = y_all(idx(1:ncal)); Xval = X_all(idx(ncal+1:end), :); yval = y_all(idx(ncal+1:end)); % 在训练集上做OSC nosc = 2; [Xcal_osc, W, T, P, x_mean, M] = osc(Xcal, ycal, nosc); % 验证集用同一个线性投影 Xval_osc = osc_apply(Xval, x_mean, M);需要留意数据划分的随机种子。近红外数据经常是按采集时间排序的,如果直接取前一部分做校正集,后一部分做验证集,仪器的漂移和样品温度变化会导致验证误差被高估。随机划分的另一个好处是校正集和验证集的浓度范围接近,OSC 的正交化更稳定。
4.2 用 PLSR 建模并对比误差指标
预处理完成后,用plsregress建立定量校正模型,并计算常见评价指标。手动计算 RMSEC、RMSEP 和 R² 的方法如下。
% 不经过OSC的对照模型 nLV = 6; [~, ~, ~, ~, beta_raw] = plsregress(Xcal, ycal, nLV); yhat_cal_raw = [ones(ncal, 1), Xcal] * beta_raw; yhat_val_raw = [ones(size(Xval, 1), 1), Xval] * beta_raw; % 经过OSC的模型 [~, ~, ~, ~, beta_osc] = plsregress(Xcal_osc, ycal, nLV); yhat_cal_osc = [ones(ncal, 1), Xcal_osc] * beta_osc; yhat_val_osc = [ones(size(Xval_osc, 1), 1), Xval_osc] * beta_osc; % 计算均方根误差和决定系数 rmse_cal_raw = sqrt(mean((ycal - yhat_cal_raw).^2)); rmse_val_raw = sqrt(mean((yval - yhat_val_raw).^2)); rmse_cal_osc = sqrt(mean((ycal - yhat_cal_osc).^2)); rmse_val_osc = sqrt(mean((yval - yhat_val_osc).^2)); r2_val_raw = 1 - sum((yval - yhat_val_raw).^2) / sum((yval - mean(yval)).^2); r2_val_osc = 1 - sum((yval - yhat_val_osc).^2) / sum((yval - mean(yval)).^2);得到的四个指标可以汇总成一张对比表,大概长这样。
| 模型 | 预处理方式 | RMSEC | RMSEP | R²(验证) |
|---|---|---|---|---|
| PLS | 仅均值中心化 | 0.72 | 1.65 | 0.841 |
| PLS | OSC(2)+均值中心化 | 0.58 | 1.02 | 0.931 |
这个表反映的是典型趋势:OSC 通常让 RMSEC 和 RMSEP 同时下降,且 RMSEP 的下降幅度大于 RMSEC。如果看到 RMSEC 大幅下降但 RMSEP 反而上升,优先检查是不是 OSC 的正交成分数太多,或者验证集样本没有用训练集投影处理。
4.3 两个参数互相影响:nosc 和 nLV
这节有一个常见误区:认为做 OSC 是为了减少 PLS 主成分数 nLV。实际上不一定。OSC 可能滤掉一个大干扰,但剩余光谱里可能还需要相同数量的潜在变量来表述浓度信息,nLV 的减少与否取决于干扰占据的是否是前几个主成分。真实实验中更常见的现象是:nLV 不变或减少 1 个,预测精度明显提升;如果 nLV 减少太多,说明 OSC 把部分有效信息当成正交成分滤掉了。
调参顺序建议先定 nLV:对原始光谱做 10 折交叉验证,找到 RMSEP 最低的 nLV。固定这个 nLV 后,再尝试 nosc = 1, 2, 3,用验证集 RMSEP 做选择。注意交叉验证不能在完整数据集上做,必须在训练集内部再做一次嵌套划分,否则会低估验证误差。
5. 把 OSC 用好:三个验证技巧和一个警惕
5.1 画正交得分与 y 的散点图
OSC 函数的输出 T 是得分矩阵,每一列代表一个正交方向。正确实现的 OSC 里,T 的每一列与 y 的相关系数都应当接近零。画散点图的用途在这里:如果发现某个正交得分和 y 存在明显线性趋势,说明该成分没有真正正交,可能是正交化公式里 yc 没有中心化,或者plsregress的权重取的列不对。代码很简单。
figure; plot(y, T(:, 1), 'o'); xlabel('浓度 y'); ylabel('正交得分 t_1'); title('正交得分与浓度关系诊断');图上如果看到点均匀散布成水平带,说明正交性合格;如果看到斜向的带状趋势,回到osc.m检查第三步的yc'*t是否用的中心化后的 y。
5.2 与 SG 平滑的组合顺序
OSC 和 Savitzky-Golay 平滑组合时应先做 SG 再做 OSC。原因是 SG 平滑会降低高频噪声,让 OSC 计算权重向量时更专注于低频的系统变异;反过来如果先做 OSC,残差中的噪声在 SG 平滑后可能会产生新的伪峰,干扰后续建模。组合参数上,SG 窗口一般取 11 到 15 个数据点,多项式阶数 2 或 3。窗口太大会抹掉细窄的化学峰,太小则噪声压制不足。
5.3 警惕“滤过头”:用 Q 残差和 Hotelling T² 判断
随着 nosc 的增加,剩余光谱的方差会不断下降,PLS 的校正误差也随之下降,但验证误差往往先降后升。这个拐点就是滤除过度出现的信号。另一种靠谱的判断工具是 Hotelling T² 和 Q 残差:在做完 OSC 后将校正集光谱投影到 PLS 主成分空间中,计算每个样本的 Q 残差,如果某个样本的 Q 残差突然变得异常大,说明该样本的有效化学信息可能被正交成分过度扣除。实际项目里,我发现 nosc 大于 3 后遇到这种情况的频率显著增高,所以我的默认配置是 nosc=1,只有验证误差没有改善时才加到 2,极少使用 3 以上。
5.4 一个容易被忽略的细节:预测时均值中心化的顺序
osc_apply里先减x_mean再乘 M,最后加回x_mean。这个步骤常被简化成直接乘 M,导致预测偏差。原因在于 M 是在中心化后的空间里构造的,它不包含均值还原能力。如果你打算把 OSC 嵌入到工业化实时预测流程中,建议把均值中心化、OSC 投影、PLS 回归的 beta 系数合并成一个大矩阵,使新样本只需要做一次矩阵乘加运算。合并后的系数矩阵等于M * beta_p,其中beta_p是 PLS 回归系数向量,这样在线部署时对每一条光谱的计算成本可以忽略不计。
本文还有配套的精品资源,点击获取