PLS 回归结果负数? Scikit-learn 非负约束实战
2025-04-13 21:28:00
让 PLS 回归结果告别负数:如何在 Scikit-learn 中强制非负约束
搞数据分析,特别是处理像光谱信号这种叠加数据时,偏最小二乘回归(Partial Least Squares Regression, PLS)是个挺好用的工具。它可以处理特征比样本多、特征之间还可能共线性的情况。但是,用 Scikit-learn 里的 PLSRegression
时,有时会碰到个头疼的问题:模型预测出来的某些组分值是负数。在很多实际场景里,比如化学浓度、信号强度,负值是没道理的,不符合物理或化学定律。
那有没有办法让 PLSRegression
老实点,只输出正数或者零呢?
一、遇到问题:PLS 回わりにくわえる? (PLS 回归出现负值?)
具体来说,你可能正用 sklearn.cross_decomposition.PLSRegression
分析你的光谱数据,这些数据由几个已知成分的光谱叠加而成。目标是估计每个成分的贡献(比如浓度或者相对强度)。你期望得到的结果,逻辑上讲,应该是大于等于零的。
可跑完模型一看,pls.predict(X_test)
给出的结果里,某些列(对应某个成分)居然出现了负数。特别是那些信号本身比较弱的成分,更容易出现这种情况。这就让人很困惑,模型是不是哪里没搞对?
import numpy as np
from sklearn.cross_decomposition import PLSRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
# 假设我们有一些模拟的光谱数据
# X 是 (样本数, 光谱点数)
# Y 是 (样本数, 组分数),我们希望 Y >= 0
n_samples, n_features, n_components_true = 100, 500, 3
rng = np.random.RandomState(0)
X = rng.randn(n_samples, n_features)
# 假设真实的组分系数 (大部分是正的)
true_coefs = rng.rand(n_features, n_components_true) * 2 - 0.5 # 可能有少量负值代表噪声?
true_coefs[true_coefs < 0] = 0 # 强制大部分非负
# 模拟真实的 Y (目标组分含量),确保非负
Y_true = np.dot(X, true_coefs) + rng.randn(n_samples, n_components_true) * 0.5
Y_true[Y_true < 0] = 0 # 确保物理意义上的非负性
# 分割数据集
X_train, X_test, Y_train, Y_test = train_test_split(X, Y_true, test_size=0.3, random_state=42)
# 训练一个标准的 PLS 模型
n_pls_components = 10 # 假设我们选择了 10 个 PLS 成分
pls = PLSRegression(n_components=n_pls_components)
pls.fit(X_train, Y_train)
# 进行预测
Y_pred = pls.predict(X_test)
# 检查预测结果中是否有负值
print(f"预测结果 Y_pred 的最小值: {Y_pred.min()}")
if Y_pred.min() < 0:
print("哎呀,PLS 预测结果里出现了负值!")
# (输出可能类似):
# 预测结果 Y_pred 的最小值: -0.8512...
# 哎呀,PLS 预测结果里出现了负值!
看,问题复现了。接下来分析下为啥会这样。
二、为啥 PLS 会搞出负值?
PLS 本身的设计目标是通过寻找 X(输入特征)和 Y(目标变量)之间的潜在变量(Latent Variables, LVs),来最大化它们之间的协方差,同时用这些潜在变量来预测 Y。这个过程本质上还是基于最小二乘法的思想。
标准的最小二乘法,包括 PLS 里的回归步骤,是为了让预测误差的平方和最小。数学优化过程中,它并不关心预测值本身是不是符合某种物理约束(比如非负)。只要某个负的系数或预测值能让整体误差更小,算法就会接受它。
这在以下情况更容易发生:
- 信号弱或噪声大: 对于本身贡献就很小的组分,噪声的干扰或者模型为了拟合其他强信号而产生的微小调整,都可能使其估计值掉到零以下。
- 模型复杂度(成分数
n_components
)选择不当: 如果选择的 PLS 成分数过多,模型可能会过拟合,试图解释数据中的噪声,也可能导致奇怪的负值系数或预测。 - 数据本身存在问题: 比如基线漂移、异常值等没有很好处理,也会干扰模型的学习。
明白了原因,就好对症下药了。可惜的是,Scikit-learn 的 PLSRegression
实现里,并没有直接提供像 positive=True
这样的参数来强制非负约束。所以,我们需要想点别的招。
三、解决方案:给 PLS 上个“紧箍咒”
既然官方没给直路,我们就得绕点弯路或者换个思路。下面是几种可行的办法。
3.1 简单粗暴?后处理截断
这是最直接、最容易想到的方法:先用标准的 PLS 模型进行预测,然后检查预测结果,把所有小于零的值直接改成零。
-
原理:
简单来说,就是模型照常跑,结果出来后加一道“过滤器”。不改变 PLS 算法本身,只修正最终输出。 -
操作步骤:
接上文代码,拿到Y_pred
后:# 对预测结果进行截断 Y_pred_clipped = np.clip(Y_pred, 0, None) # 将小于 0 的值设为 0,上限不限制 print(f"截断后 Y_pred_clipped 的最小值: {Y_pred_clipped.min()}") # (输出): # 截断后 Y_pred_clipped 的最小值: 0.0
-
优缺点与建议:
- 优点: 实现极其简单,一行代码搞定。
- 缺点: 这种做法在数学上可能不太站得住脚。它破坏了原始 PLS 最小二乘优化得到的结果。可能导致最终结果并非在非负约束下的最优解。如果负值很大或者很多,截断后的结果可能跟真实情况偏差较大。
- 建议: 可以作为一种快速尝试或最后的补救措施。如果负值出现的很少且绝对值很小,这种方法影响可能不大。但如果问题严重,或者对模型的理论严谨性要求高,最好考虑下面的方法。
3.2 曲线救国:PLS 降维 + 非负线性回归
这个方法稍微复杂点,但更符合约束优化的思想。核心思路是:利用 PLS 做它最擅长的事——降维和提取与 Y 相关的特征,然后在这个降维后的空间里,使用一个本身就支持非负约束的回归模型来做最后的预测。
Scikit-learn 里的 LinearRegression
提供了一个 positive=True
参数,可以实现非负最小二乘(Non-Negative Least Squares, NNLS)。我们可以把它和 PLS 的变换(transform)功能结合起来。
-
原理:
- 用训练数据 (X_train, Y_train) 拟合一个 PLS 模型。这一步的目的是找到 X 空间中的一组潜在变量(也叫 X-scores),这些变量能最好地解释 X 和 Y 的协方差。
- 使用拟合好的 PLS 模型,将原始的训练数据 X_train 变换到这个低维的潜在变量空间,得到
X_train_scores
。 - 现在,我们把问题转化为:用
X_train_scores
作为新的特征,来预测 Y_train。在这个阶段,我们使用带有非负约束的线性回归模型 (LinearRegression(positive=True)
) 进行拟合。这个模型会确保找到的系数都是非负的,进而使得基于 scores 的预测也倾向于非负(如果 Y 本身是非负的)。 - 对于新的测试数据 X_test,先用同一个 PLS 模型将其变换到潜在变量空间得到
X_test_scores
,然后用训练好的非负线性回归模型基于X_test_scores
进行预测。
-
代码示例:
from sklearn.linear_model import LinearRegression # 1. 训练 PLS 模型 (与之前一样) n_pls_components = 10 # 保持 PLS 成分数不变或重新选择 pls = PLSRegression(n_components=n_pls_components) pls.fit(X_train, Y_train) # 2. 将训练数据 X_train 变换到 PLS 潜在变量空间 X_train_scores = pls.transform(X_train) # 3. 训练非负线性回归模型 # 注意:如果 Y 是多维的 (多个组分), LinearRegression 默认会为每个输出拟合一个模型 nnls_regressor = LinearRegression(positive=True) nnls_regressor.fit(X_train_scores, Y_train) # 4. 对测试数据进行预测 # a. 先将 X_test 变换到 PLS 空间 X_test_scores = pls.transform(X_test) # b. 然后用非负线性回归模型预测 Y_pred_nnls = nnls_regressor.predict(X_test_scores) print(f"PLS + NNLS 预测结果 Y_pred_nnls 的最小值: {Y_pred_nnls.min()}") # 检查非负线性回归的系数是否都是非负的 print(f"NNLS 回归系数的最小值: {nnls_regressor.coef_.min()}") # (输出可能类似): # PLS + NNLS 预测结果 Y_pred_nnls 的最小值: 0.0 # NNLS 回归系数的最小值: 0.0
可以看到,预测结果和回归系数都满足了非负的条件。
-
优缺点与建议:
- 优点: 这种方法在理论上更合理。它将 PLS 的降维能力和非负约束的回归分离开,分别处理。最终的预测是在强制非负的条件下优化得到的(在 PLS 变换后的空间内)。
- 缺点: 实现步骤稍微多一点。需要理解 PLS 的
fit
和transform
的区别。模型的解释性可能稍有变化,因为最后的回归系数是针对 PLS scores 的,而不是原始特征。 - 建议: 这是比较推荐的方法,尤其是当你需要一个数学上更说得通的、带有非负约束的模型时。
-
进阶使用技巧:
- PLS 成分数选择(
n_components
): 无论是直接用 PLS 还是结合 NNLS,PLS 成分数的选择都非常关键。这个数太小可能欠拟合,太大可能过拟合(即使后面加了 NNLS)。通常需要通过交叉验证(Cross-Validation)来确定一个最优的n_components
。你可以评估不同n_components
下,使用“PLS+NNLS”整个流程的预测性能(例如,均方误差 MSE),选择表现最好的那个。 - 多目标回归的处理: 当 Y 有多个维度(比如同时预测多个组分浓度)时,
LinearRegression(positive=True)
能够很好地处理,它会为每个 Y 的维度独立拟合一组非负系数。 - 数据标准化: PLS 对数据的尺度(scale)比较敏感。在应用 PLS 之前,通常需要对 X 和 Y 进行标准化处理(例如,中心化、缩放到单位方差)。Scikit-learn 的
PLSRegression
默认会进行中心化和标准化(可以通过scale=False
关闭)。当你手动组合pls.transform
和LinearRegression
时,要确保数据处理方式的一致性。pls.transform
会使用pls.fit
时学到的均值和标准差来处理新数据。
- PLS 成分数选择(
四、选哪个方案?
一般情况下,推荐使用“PLS 降维 + 非负线性回归” (方法 3.2)。它提供了一种结构化的方式来引入非负约束,结果更可靠,也更容易从模型验证的角度(比如交叉验证选择参数)来评估。
后处理截断(方法 3.1)可以作为一种快速检查或者权宜之计。如果你的负值问题非常轻微(比如接近于零的微小负值,并且很少出现),用它可能也够了。但心里要有数,它可能不是“最优”的非负解。
五、注意事项与一点提醒
- 数据预处理不能忘: 不管用哪种方法,PLS 前的数据预处理(如基线校正、标准化等)都非常重要,能显著影响模型效果。特别是标准化 (
scale=True
inPLSRegression
or usingStandardScaler
),记得要一致地应用到训练和测试数据上。 n_components
的选择是门艺术: 如前所述,用交叉验证等方法仔细选择 PLS 的成分数。一个好的n_components
是模型成功的关键。- 结果的物理解释: 即使模型给出了非负结果,也要结合你的专业知识判断结果是否合理。比如,某个组分的预测值是不是异常地高或低?模型的预测性能(RMSE, R^2 等指标)是否达标?
- 有没有更对口的模型? 如果你的问题本质上更符合“非负矩阵分解”(Non-negative Matrix Factorization, NMF)的应用场景(例如,试图直接从混合信号中分解出非负的源信号和它们的贡献),可以考虑探索
sklearn.decomposition.NMF
。NMF 本身就带有非负约束,但它的原理和目标与 PLS 不同,需要看哪个更适合你的具体问题。
处理模型输出不符合物理约束的情况是数据分析中常遇到的挑战。希望上面介绍的方法,特别是结合 PLS 变换和非负线性回归的策略,能帮你有效地解决 PLS 回归中出现的负值预测问题。