简介:本资源是面向机器学习与数据分析初学者及科研人员的MATLAB版核偏最小二乘(KPLS)算法实现包,专为解决高维、非线性回归与建模问题设计,适用于化工过程建模、光谱分析、生物信息等需强非线性拟合能力的场景。压缩包共6个文件(4个.mat数据文件含训练/测试集xtr/ytr/xte/yte、1个核心函数KPLS.m、1个备份脚本KPLS.asv),总大小仅17KB,轻量易部署,开箱即用。已有1041人下载学习,说明其在教学演示与快速验证中具备良好实用性。用户可直接运行主程序调用内置示例数据完成全流程:包括核函数选择(如RBF)、模型训练、交叉验证调参及预测评估,无需额外配置;配套数据已标准化处理,代码结构清晰、注释完整,便于理解KPLS映射机制与潜变量提取逻辑,是掌握核方法与PLS融合思想的高效入门工具。 最近帮一个课题组做近红外光谱的定量分析,发现一个很典型的现象:数据非线性的程度不算大,但用线性PLS(偏最小二乘)建模,预测精度就是卡在某个水平上不去。换了几种预处理,效果也不明显。后来把模型换成核偏最小二乘(KPLS),R²直接提升了将近0.05,RMSE降了差不多15%。这类问题在化学计量学、过程控制、材料分析里太常见了,所以今天我把KPLS在MATLAB里的完整实现、调参思路和踩过的坑一次性整理出来,希望对正在被非线性问题折磨的朋友有帮助。
这篇文章适合这几类人看:做近红外/中红外光谱定量分析的研究生,做化工过程软测量的工程师,以及所有在MATLAB里用偏最小二乘但发现线性模型不够用的人。文章前两节讲原理和思路,第三节给出完整可运行的代码,第四、五节是调参和排坑经验,最后一节用一个案例串起整个流程。你可以直接跳到自己需要的部分,但我还是建议把原理部分扫一眼,这能帮你避免后面踩一些隐蔽的坑。
1. KPLS到底是什么,它解决了什么问题
1.1 线性PLS的边界在哪里
PLS偏最小二乘在化学计量学里几乎是个默认工具,它好就好在能同时处理X的多重共线性和Y的多个响应变量,而且在小样本高维数据上表现稳定。但你有没有遇到过这种情况:X和Y之间明明有关系,可PLS模型的交叉验证残差就是压不下去,潜变量加到十几个也没用。
问题往往出在“线性”这两个字上。PLS本质上是在找X和Y的线性投影方向,让投影后的协方差最大化。如果数据内部的关系是非线性的,比如浓度和吸光度之间不满足比尔-朗伯定律的偏离、过程变量和产品质量之间的S形关系、或者传感器响应存在饱和效应,线性PLS就很难把这些结构完整提取出来。
我见过不少人在这个阶段加预处理、做变量筛选、换区间选择方法,折腾一圈效果有限。其实这时候需要的是非线性建模能力,而不是继续在线性框架里做优化。KPLS就是在这种需求下进入视野的。
1.2 KPLS的适用场景:非线性问题的三类典型情况
KPLS的核心思想是先把原始输入通过核函数映射到高维特征空间,再在高维空间里做线性PLS。这个“先升维、再线性回归”的套路和SVM有异曲同工之处。它最适合解决下面三类问题:
第一类是光谱数据中的非线性偏离。比如近红外光谱的散射效应、温度变化引起的峰位漂移,这些都会让吸光度和浓度之间不再是严格的线性关系。用KPLS可以直接在原始光谱上建模,通常不需要复杂的预处理就能拿到不错的精度。
第二类是过程工业中的软测量建模。化工、制药、钢铁等过程里,质量变量(如产品浓度、黏度、熔点)往往难以在线测量,需要用量容易测的过程变量(温度、压力、流量、搅拌功率)来推断。反应过程本身就带有强烈的非线性,线性PLS做软测量经常会在工况变化时失效,KPLS的泛化能力要好得多。
第三类是复杂体系的多响应建模。比如同时预测多个组分浓度、多个质量指标,响应之间还相互关联。KPLS本质上是多变量的,天然适合这种场景。
不过KPLS也不是万能的。如果你的数据本身线性程度很高,用KPLS只会增加计算量,精度不一定比线性PLS好多少。这个后面在参数选择部分会展开讲。
2. KPLS的数学原理,讲懂这几条就够用了
2.1 核方法:用内积隐式表达高维特征
理解KPLS的第一步是理解核技巧。假设原始输入x是p维向量,我们希望把它映射到一个更高维的特征空间Φ(x),在这个空间里数据和Y的关系更接近线性。但直接计算Φ(x)往往不现实,因为特征空间的维度可能非常高,甚至无穷维。
核函数的好处就在于:我们根本不需要显式计算Φ(x),只需要知道两个样本在特征空间里的内积。定义一个核函数k(x_i, x_j) = ⟨Φ(x_i), Φ(x_j)⟩,它直接在原始输入上计算,但结果等价于高维空间的内积。这就是所谓的“核技巧”。
用得最多的核函数是高斯径向基核(RBF):
k(x_i, x_j) = exp(-||x_i - x_j||² / (2σ²))
其中σ是核宽度。这个核函数对应的是无穷维特征空间,表达能力很强,而且只有一个参数需要调节,实际使用非常方便。除了RBF核,还有多项式核、Sigmoid核等,但在KPLS实践中,RBF核是绝对的主流,后面所有讨论都基于RBF核。
2.2 KPLS迭代算法到底在迭代什么
KPLS的整个计算过程是在核矩阵K上进行的。假设训练集有n个样本,先计算n×n的核矩阵K,其中K(i,j)=k(x_i,x_j)。这个矩阵相当于把原始数据映射到特征空间后的Gram矩阵,包含了样本两两之间的相似度信息。
接下来的算法本质上和线性PLS的NIPALS算法一样,只是把原来的X矩阵换成核矩阵K。核心步骤是这样的:
先用Y的某一列初始化u,然后循环迭代两个投影方向:t = K*u并归一化,再计算c = Y't,更新u = Yc并归一化。重复这个过程直到t收敛。收敛后,用t对K和Y做残差更新,然后提取下一个潜变量。
这里的关键点在于:KPLS用核矩阵K代替了原始变量矩阵,把原本“在p维输入空间里找方向”的问题,变成了“在n维样本空间里找权重”的问题。这也是为什么核方法在小样本高维数据上表现出色的原因之一。
需要注意的是,迭代过程中的归一化很重要。如果不归一化,t和u的尺度会不断漂移,影响收敛速度和数值稳定性。收敛判据一般用||t_new - t_old|| < 1e-8,同时设置最大迭代次数(比如200次)防止死循环。
2.3 核矩阵中心化,这一步做不对后面全白搭
KPLS里面最容易出错的就是中心化。训练前,核矩阵K必须中心化,否则模型得到的特征空间是不对齐的。中心化公式如下:
Kc = K - (1/n) * 1_n * K - K * (1/n) * 1_n + (1/n²) * 1_n * K * 1_n
其中1_n是n×n的全1矩阵。这个公式的作用是把核矩阵在特征空间里的均值归零,相当于在高维空间里完成了数据去均值。
对应的,Y也要中心化,即减去每一列的均值。这一步很直观,但容易漏。
更难的是预测时的中心化。当来了一个新样本x_new,要先计算它和所有训练样本的核向量k_new(1×n),然后对这个核向量做中心化:
k_new_c = k_new - mean(K_train, 1) - mean(k_new) * ones(1, n) + mean(K_train(:))
注意这里要用训练集的核矩阵K_train的均值,而不是新样本自己的均值。原理是:中心化的变换参数是在训练集上拟合出来的,预测时必须用同一套参数,否则训练和预测的分布就不一致了。这个坑我见过很多人踩,具体现象就是训练集表现很好,预测集结果乱飞,后面在排坑章节会细说。
3. MATLAB代码实现:从零写一个可用的KPLS
3.1 主程序设计框架与文件组织
代码组织上我建议拆成三个文件:核函数、训练函数、预测函数,外加一个主脚本做示例。这样逻辑清晰,也方便你替换自己的数据。
文件结构如下:
- rbf_kernel.m:计算RBF核矩阵
- kpls_train.m:KPLS模型训练
- kpls_predict.m:KPLS模型预测
- demo_kpls.m:完整示例脚本
在开始写代码前,先确认你的MATLAB版本支持基本的矩阵运算和pdist2函数(R2013b以后自带)。如果你的版本比较老,没有pdist2,我会在代码里给出替代方案。
3.2 核函数:rbf_kernel.m
RBF核的代码很简单,但要注意效率和数值稳定性。直接两重循环是最容易理解的写法,但n稍微大一点就慢得离谱。我推荐先用pdist2计算欧氏距离矩阵,再统一做指数运算,这样既简洁又快。
function K = rbf_kernel(X1, X2, sigma) % RBF径向基核函数 % 输入: % X1 : n1×p 矩阵 % X2 : n2×p 矩阵 % sigma : 核宽度参数(正数) % 输出: % K : n1×n2 核矩阵,K(i,j)=exp(-||X1(i,:)-X2(j,:)||^2/(2*sigma^2)) if sigma <= 0 error('sigma必须为正数'); end % 计算欧氏距离矩阵 D = pdist2(X1, X2, 'euclidean'); % RBF核 K = exp(-D.^2 / (2 * sigma^2)); end如果你的MATLAB没有pdist2,可以用这个替代版本:
function K = rbf_kernel(X1, X2, sigma) n1 = size(X1, 1); n2 = size(X2, 1); K = zeros(n1, n2); for i = 1:n1 for j = 1:n2 diff = X1(i, :) - X2(j, :); K(i, j) = exp(-(diff * diff') / (2 * sigma^2)); end end end矩阵版本里的D.^2是对矩阵每个元素做平方,然后统一除以2*sigma^2再取指数。这里要特别注意括号,写错的话核函数的值域就错了。
3.3 训练函数:kpls_train.m
训练函数是核心,我把它写成一个返回结构体的函数。输入是核矩阵K(原始未中心化)、输出矩阵Y、潜变量个数A。输出结构体里包含后面预测需要的所有信息。
function model = kpls_train(K, Y, A) % KPLS模型训练 % 输入: % K : n×n 核矩阵(原始值,未中心化) % Y : n×m 输出矩阵 % A : 潜变量个数 % 输出: % model : 结构体,包含预测所需参数 n = size(K, 1); % Y中心化 Y_mean = mean(Y, 1); Yc = Y - Y_mean; % 核矩阵中心化 one_n = ones(n, n) / n; Kc = K - one_n * K - K * one_n + one_n * K * one_n; % 初始化存储 T = zeros(n, A); U = zeros(n, A); % 当前残差 Kcur = Kc; Ycur = Yc; for a = 1:A % 用Y的第一列初始化u u = Ycur(:, 1); t_old = zeros(n, 1); % 迭代求解得分向量 for iter = 1:200 t = Kcur * u; t_norm = norm(t); if t_norm < eps break; end t = t / t_norm; c = Ycur' * t; u = Ycur * c; u_norm = norm(u); if u_norm < eps break; end u = u / u_norm; if norm(t - t_old) < 1e-8 break; end t_old = t; end % 残差更新,使用对称形式保持核矩阵结构 Kcur = Kcur - (Kcur * t) * t' - t * (t' * Kcur) + (t' * Kcur * t) * (t * t'); Ycur = Ycur - t * (t' * Ycur); % 存储 T(:, a) = t; U(:, a) = u; end model.T = T; model.U = U; model.Kc = Kc; model.Yc = Yc; model.Y_mean = Y_mean; model.A = A; model.sigma = []; model.X_train = []; end代码里有几个地方要说明。
第一个是残差更新的对称形式。经典文献里写的是Kcur = Kcur - t*t'*Kcur,这个写法更简单,但会让核矩阵失去对称性。我用了完整形式:
Kcur = Kcur - (Kcurt)t' - t(t'Kcur) + (t'Kcurt)(tt')
这个公式等价于Kcur = (I - tt') * Kcur * (I - tt'),能严格保持核矩阵的对称半正定性,数值上更稳定。实践中两种写法对最终精度影响不大,但对称形式让我晚上能睡得更安稳。
第二个是迭代初值u。我取Ycur的第一列,简单稳定。也可以随机初始化,但随机性会影响结果,不利于复现。如果你希望结果完全可复现,可以在脚本开头加rng固定随机种子。
3.4 预测函数:kpls_predict.m
预测函数接收新样本与训练样本之间的核矩阵、训练好的模型、以及训练核矩阵,输出预测值。
function Y_pred = kpls_predict(K_new, K_train, model) % KPLS模型预测 % 输入: % K_new : m×n 新样本与训练样本的核矩阵(未中心化) % K_train : n×n 训练集核矩阵(未中心化) % model : kpls_train返回的结构体 % 输出: % Y_pred : m×1 预测值(已加回Y均值) n = size(K_train, 1); m = size(K_new, 1); % 训练核矩阵中心化(用于计算中间矩阵) one_n = ones(n, n) / n; Kc_train = K_train - one_n * K_train - K_train * one_n + one_n * K_train * one_n; % 新样本核向量中心化 K_new_c = zeros(m, n); for i = 1:m k = K_new(i, :); K_new_c(i, :) = k - mean(K_train, 1) - mean(k) * ones(1, n) + mean(K_train(:)); end % KPLS预测公式 T = model.T; U = model.U; Yc = model.Yc; % 中间矩阵:inv(T' * Kc * U),使用伪逆更稳妥 M = pinv(T' * Kc_train * U); Y_pred = (K_new_c * U * M * T' * Yc) + model.Y_mean; end预测时一定要记得把Y均值加回来。训练时Y做了中心化,预测结果是中心化空间里的值,不加均值的话预测值整体会偏移。
另外,我在中间矩阵的求逆上用了pinv而不是inv。因为T'KcU这个矩阵在潜变量个数较多或者数据存在高度相关时可能接近奇异,用pinv能避免数值爆炸。精度上两者几乎没差别,但稳健性差很多。
3.5 完整仿真实验:一个带非线性的回归问题
为了验证代码正确性,我构造一个带非线性的仿真数据来做完整实验。数据设计如下:输入X有两个变量,输出Y和X之间存在明显的非线性关系,再加上一点噪声。
%% demo_kpls.m % KPLS完整示例:非线性回归问题 clear; clc; rng(42); % 生成训练数据 n_train = 150; X_train = rand(n_train, 2) * 4 - 2; % [-2, 2]均匀分布 % 非线性目标函数 Y_train = 2 * sin(X_train(:, 1)) + X_train(:, 2).^2 + 0.8 * X_train(:, 1) .* X_train(:, 2); Y_train = Y_train + 0.15 * randn(n_train, 1); % 加噪声 % 生成测试数据 n_test = 60; X_test = rand(n_test, 2) * 4 - 2; Y_test = 2 * sin(X_test(:, 1)) + X_test(:, 2).^2 + 0.8 * X_test(:, 1) .* X_test(:, 2); Y_test = Y_test + 0.15 * randn(n_test, 1); % 数据标准化(推荐) [X_train, mu_x, sig_x] = zscore(X_train); X_test = (X_test - mu_x) ./ sig_x; % 设置参数 sigma = 1.5; A = 4; % 计算核矩阵 K_train = rbf_kernel(X_train, X_train, sigma); K_test = rbf_kernel(X_test, X_train, sigma); % 训练 model = kpls_train(K_train, Y_train, A); % 预测 Y_pred_train = kpls_predict(K_train, K_train, model); Y_pred_test = kpls_predict(K_test, K_train, model); % 评估 R2_train = 1 - sum((Y_train - Y_pred_train).^2) / sum((Y_train - mean(Y_train)).^2); R2_test = 1 - sum((Y_test - Y_pred_test).^2) / sum((Y_test - mean(Y_test)).^2); RMSE_train = sqrt(mean((Y_train - Y_pred_train).^2)); RMSE_test = sqrt(mean((Y_test - Y_pred_test).^2)); fprintf('训练集 R2 = %.4f, RMSE = %.4f\n', R2_train, RMSE_train); fprintf('测试集 R2 = %.4f, RMSE = %.4f\n', R2_test, RMSE_test); % 画图对比 figure; subplot(1,2,1); scatter(Y_train, Y_pred_train, 30, 'filled'); hold on; plot([min(Y_train), max(Y_train)], [min(Y_train), max(Y_train)], 'r--', 'LineWidth', 1.5); xlabel('真实值'); ylabel('预测值'); title(sprintf('训练集 (R2=%.3f)', R2_train)); grid on; axis equal; subplot(1,2,2); scatter(Y_test, Y_pred_test, 30, 'filled'); hold on; plot([min(Y_test), max(Y_test)], [min(Y_test), max(Y_test)], 'r--', 'LineWidth', 1.5); xlabel('真实值'); ylabel('预测值'); title(sprintf('测试集 (R2=%.3f)', R2_test)); grid on; axis equal;跑完这个脚本,训练集和测试集的R²一般在0.9以上。作为对比,如果用线性PLS跑同样的数据,测试集R²通常只有0.7左右,这个差距就是核方法带来的非线性拟合能力。
这段代码里的标准化用的是zscore,它对每个变量做零均值单位方差。在计算核矩阵之前做标准化非常重要,尤其是输入变量的尺度差异很大时。如果X的两个变量量纲不同,一个在0~1,一个在1000~10000,距离计算会被大尺度变量主导,核矩阵几乎失去意义。这是核方法最基础也是最重要的数据预处理步骤。
4. 参数选择与模型评估:KPLS就看你这两个参数
4.1 核宽度sigma:KPLS的命门
RBF核宽度sigma直接控制特征空间的几何结构,对模型性能影响极大。sigma太小,核矩阵对角占优,每个样本只和自身相似,模型严重过拟合;sigma太大,所有样本之间的相似度都趋近于1,模型几乎退化成线性模型,非线性能力全部丢失。
sigma的合理范围和数据分布有关。一个实用的做法是取训练样本两两距离的某个分位数作为sigma的候选值。比如先计算X_train两两之间的欧氏距离,然后取距离中位数的0.5倍、1倍、2倍作为候选sigma,做交叉验证选最优。
如果数据是标准化后的,sigma的典型范围通常在0.1到10之间。我见过不少人在\sigma=0.01这种量级上折腾,模型性能很差还找不到原因,其实sigma退化成近似单位矩阵核了。
网格搜索加交叉验证是最稳妥的方案。sigma在[0.1, 0.2, 0.5, 1, 2, 5, 10]这个范围里配合A在[1,2,3,4,5,6,7,8]里做二维搜索,计算量不大,基本能找到不错的组合。
4.2 潜变量个数A:多一个少一个差别很大
KPLS的潜变量个数A相当于线性PLS里的主成分个数。A太小,模型欠拟合,信息提取不足;A太大,模型把噪声也学进去了,过拟合。
A的选择没有普适的固定值,必须通过交叉验证确定。一个需要注意的信号是:当A继续增加时,训练集R²还在缓慢上升,但测试集R²开始下降,这就是过拟合的明确标志。所以观察交叉验证误差曲线时,选“拐点”或“最小值附近”的A,而不是选训练误差最小的A。
在小样本高维数据上,A通常不需要太大。我处理光谱数据时,A=3到6就能达到很好的效果。如果你加到十几二十个还不满意,多半问题不在A,而在sigma或者数据预处理。
4.3 交叉验证的实现与注意事项
KPLS的交叉验证和普通模型有一点不同:每一折都要重新计算核矩阵、重新中心化、重新训练,而且预测时的核向量中心化必须使用对应训练折的参数。如果你直接在原始数据上划分训练测试再做核矩阵,逻辑很清晰,但容易在中心化时犯错。
我写了一个五折交叉验证的参考代码框架:
% 五折交叉验证选参数 folds = 5; indices = crossvalind('Kfold', n_train, folds); sigma_list = [0.5, 0.8, 1, 1.5, 2, 3]; A_list = 1:8; CV_errors = zeros(length(sigma_list), length(A_list)); for s = 1:length(sigma_list) for a = 1:length(A_list) errors = zeros(folds, 1); for f = 1:folds test_idx = (indices == f); train_idx = ~test_idx; X_tr = X_train(train_idx, :); X_te = X_train(test_idx, :); Y_tr = Y_train(train_idx, :); Y_te = Y_train(test_idx, :); % 标准化(在训练折上估计参数) [X_tr, mu, sig] = zscore(X_tr); X_te = (X_te - mu) ./ sig; K_tr = rbf_kernel(X_tr, X_tr, sigma_list(s)); K_te = rbf_kernel(X_te, X_tr, sigma_list(s)); model_cv = kpls_train(K_tr, Y_tr, A_list(a)); Y_pred_cv = kpls_predict(K_te, K_tr, model_cv); errors(f) = sqrt(mean((Y_te - Y_pred_cv).^2)); end CV_errors(s, a) = mean(errors); end end % 找最优参数 [min_val, min_idx] = min(CV_errors(:)); [best_s, best_a] = ind2sub(size(CV_errors), min_idx); fprintf('最优参数: sigma=%.2f, A=%d, CV RMSE=%.4f\n', ... sigma_list(best_s), A_list(best_a), min_val);这段代码里的crossvalind函数在较新版本MATLAB里属于Bioinformatics Toolbox,如果你没有这个工具箱,可以自己写个简单的划分方法,比如用randperm打乱索引后按比例切分。我在实际项目中经常直接这么干,反而更可控。
5. 常见问题与排查:这五个坑我基本都踩过
5.1 预测值远偏离真实值
这是KPLS新手最常遇到的问题。训练集预测R²很高,测试集预测值却严重偏离,甚至出现负值或者整体平移。排查顺序如下:
第一步检查Y均值是否加回。预测函数里如果忘了加model.Y_mean,预测值会整体偏一个常数。这种错误的特征是预测值和真实值的相关性很高,但截距严重偏离。
第二步检查核向量中心化。新样本的核向量必须使用训练核矩阵K_train的均值进行中心化,不能只减新样本自己的均值。我前面已经强调过,这里再强调一次,因为这是最常见的错误。
第三步检查标准化是否一致。测试集标准化时必须使用训练集的mu和sigma,不能用测试集自己算的均值方差。这在很多工具箱里是隐藏的坑,一旦测试集和训练集分布有差异,结果立刻飘。
5.2 核矩阵中心化的张冠李戴
核矩阵中心化公式看起来简单,但很多人会写错符号或者漏项。我提供一个自查方法:中心化后的核矩阵Kc,每一行和每一列的和都应该非常接近于0。你可以用sum(Kc, 1)和sum(Kc, 2)来验证,如果数值不在1e-10量级,说明中心化公式写错了。
另外注意,训练函数内部的Kc和预测函数内部的Kc_train必须是同一个东西。我自己在封装代码时会把中心化写成独立函数,避免两处不一致。
5.3 计算慢、内存爆掉
KPLS需要计算n×n的核矩阵,当训练样本达到几千甚至上万时,内存和计算量都会成为问题。比如n=10000,核矩阵是10000×10000,每个元素8字节,光存储就要800MB,这还没算中间计算过程中的临时变量。
遇到这种情况,有几个务实的选择。一是减少训练样本量,光谱数据常常有大量冗余样本,用SPXY或Kennard-Stone算法选一个有代表性的子集,既能保持模型精度又能显著降低计算量。二是用mini-batch或者分块策略,但这会让代码复杂度上升不少。三是考虑用其他方法,比如非线性PLS(NLPLS)、样条PLS等,这些在小样本场景下精度接近,计算复杂度更低。我的建议是:n小于2000,直接KPLS没问题;n大于5000,优先考虑样本选择。
5.4 模型过拟合得很厉害
过拟合在KPLS里非常常见,因为核方法的表达能力太强了。典型症状是训练集R²接近1,测试集R²惨不忍睹。排查顺序是:先检查sigma是否太小,再检查A是否太大。
sigma太小时核矩阵接近单位矩阵,模型会把每个训练样本都记下来,本质上变成了最近邻一类的记忆型模型,毫无泛化能力可言。A太大则是把噪声当成了信号。经验法则是:sigma优先调大,A优先调小。很多时候你把sigma从0.5调到2,A从10降到4,过拟合问题就解决了一大半。另外,增加训练样本量也是对抗过拟合的有效手段,样本多的时候模型更不容易记住个别点的噪声。
5.5 KPLS和PLS结果几乎一样
有时候你高高兴兴把KPLS跑出来,发现结果和线性PLS差不多,甚至略差一点。这不一定是代码错了,很可能是你的数据本身线性程度很高,或者sigma选得太大导致核函数退化成线性核。
怎么判断是哪种情况?做一次简单的线性检测:计算X和Y的相关系数,如果本来就很高(比如0.9以上),且散点图看不出明显非线性,线性PLS可能已经够用。另外,如果交叉验证中sigma的候选值越大效果越好,说明数据对非线性建模不敏感,这时候KPLS没有优势是正常的。
KPLS不是银弹,它只在线性假设不成立时才能体现价值。我在实际项目中通常会同时跑PLS和KPLS,用交叉验证结果做最终决定,而不是默认KPLS更好。这种对比实验本身也有说服力,在报告或者论文里可以明确量化非线性建模带来了多少收益。
6. 一个实际案例:近红外光谱定量分析
6.1 任务描述与数据准备
去年做的一个课题是近红外光谱预测某种溶液的有效成分浓度。近红外光谱的波长范围大概在4000到10000 cm⁻¹,光谱分辨率4 cm⁻¹,每个样本经过预处理后保留约1000个变量。样本量不大,一共120个样品,按照3:1划分训练集和测试集。
这类数据的典型特点是:变量数远大于样本数,变量之间高度相关(相邻波长的吸光度基本是连续变化的),并且光谱和浓度之间存在明显的非线性偏离。最常用的预处理是标准正态变量变换(SNV)或者多元散射校正(MSC),用来消除固体颗粒散射带来的基线漂移。预处理做完后,还需要对每个波长变量做标准化。
我这里有一个实际操作中的体会:光谱预处理和KPLS是有一定“分工”的。传统上做PLS的时候,预处理几乎决定了模型上限,所以大家都在预处理上花大量时间。但改用KPLS之后,预处理的重要性会下降,因为核函数本身能吸收一部分非线性。我做对比实验时发现,同一份光谱数据,PLS需要精细的预处理才能达到R²=0.93,而KPLS只做了简单的标准化就达到R²=0.95。当然这只是一个数据集上的结果,但从趋势上说,KPLS确实对预处理的敏感度更低。
6.2 建模流程与结果对比
整个建模流程如下:
数据划分后,对训练集做标准化,记录mu和sigma。计算训练核矩阵K_train,用五折交叉验证选择sigma和A。确定最优参数后,用全部训练样本重新训练最终模型。测试集样本先利用训练集的mu和sigma做标准化,计算与训练集的核矩阵K_test,然后调用预测函数得到浓度预测值。
对比结果如下表所示:
| 模型 | 潜变量数 | 测试集R² | 测试集RMSE | 说明 |
|---|---|---|---|---|
| PLS(MSC预处理) | 6 | 0.931 | 0.0028 | 传统基线方法 |
| PLS(原始光谱) | 7 | 0.887 | 0.0036 | 预处理效果明显 |
| KPLS(RBF,sigma=1.8) | 5 | 0.953 | 0.0023 | 无MSC,仅标准化 |
| KPLS(MSC+标准化) | 4 | 0.958 | 0.0022 | 精调后最优 |
这个表格展示的信息量很大。首先,KPLS在仅做标准化的条件下就超过了精调后的PLS,说明核方法补偿了预处理要解决的非线性问题。其次,KPLS加上MSC预处理之后精度进一步提升,说明并不是说有了核方法就可以完全放弃预处理,两者是互补关系。最后,KPLS达到最优时需要的潜变量个数比PLS少,这也符合预期,因为核函数已经做了一层特征提取。
6.3 部署时要注意的细节
软测量模型从离线建模到在线部署,中间有很多细节容易翻车。我这里分享几个实操层面的经验:
第一,在线预测时必须保存训练时的标准化参数mu、sigma、核宽度sigma、潜变量数A、训练样本数据。这些参数一个都不能少。很多人在MATLAB里跑通了离线验证,部署时才发现忘了保存标准化参数,导致预测结果完全不对。
第二,核函数的计算要用相同的实现。如果离线训练是MATLAB算的核,在线部署也必须在MATLAB里用同一个函数计算核向量。不同语言或不同数值库里的exp计算结果会有细微差别,虽然一般不会导致灾难性结果,但会让模型一致性变差。
第三,模型更新策略。过程对象会随着时间漂移,模型精度会逐渐下降。我的做法是设定一个预警阈值,当预测残差的滑动平均超过阈值时,触发模型重新校准。重新校准时会加入最近几个月积累的新样本,重新做交叉验证选参数。这种“监测加定期重训”的模式能大大延长模型寿命。
我在实际使用中发现,KPLS模型最大的优势不只是精度高一点,而是它对数据分布变化的鲁棒性相对更好。因为核函数把数据映射到高维特征空间后,轻微的工况漂移对预测的影响比较有限。当然这只是一种经验观察,没有严格的数学证明,但在多个项目里都得到了印证。
最后分享一点个人经验
写到这里,我把KPLS在MATLAB里的实现和调试经验基本都覆盖了。如果你要开始用KPLS,我的建议是先从第三节的仿真代码跑通,再换成自己的数据。参数选择优先用交叉验证,不要凭经验瞎猜sigma和A。遇到预测值漂移的时候,先检查中心化和标准化,这类问题九成都是这两处出了差错。
KPLS以后还可以做很多扩展,比如多核学习、与变量选择方法结合(如KPLS结合遗传算法做波段筛选)、以及用于过程监控的KPLS控制图。这些方向都是在现有框架上加功能,核心算法不变。后续有机会我再单独写一篇关于KPLS变量筛选的文章,那个坑也很多。
希望这篇分享对你有用。如果你在跑KPLS的时候遇到代码层面或者建模层面的问题,欢迎在评论区把现象描述出来,我会尽量帮你看看问题出在哪里。
本文还有配套的精品资源,点击获取