光谱定量分析模型构建与验证

第1章 光谱数据预处理(下):从原始信号到可用特征

各位好,我是老张。在光谱分析这行摸爬滚打十几年,我最大的体会就是:预处理做得好,模型成功一半。上一章我们聊了基线校正和去噪,今天咱们继续往下走,把剩下的几个硬骨头啃下来。

说实话,很多新手拿到光谱数据,上来就建模。结果呢?模型在训练集上漂亮得很,一上真实样本就崩。为什么?因为原始光谱里藏着太多「脏东西」——散射效应、随机噪声、样本间差异……这些不处理干净,模型学到的全是假规律。

今天的内容,我按实战顺序来讲:导数光谱、散射校正、数据增强,最后聊聊预处理流程怎么设计才靠谱。

1.1 导数光谱:把重叠峰「拆」开

先问个问题:两个峰挨得太近,肉眼分不开怎么办?

嗯,导数光谱就是干这个的。它不改变峰的位置,但能把峰形变尖锐,让重叠的峰「现出原形」。

一阶导数:消除基线偏移

一阶导数说白了就是光谱的斜率。基线平移了,斜率不变。所以它能干掉基线漂移。

公式很简单:

dx(i) = [x(i+1) - x(i-1)] / (2 * Δλ)

这里 Δλ 是波长间隔。我习惯用 Savitzky-Golay 求导,一步到位,既平滑又求导。

举个例子:

from scipy.signal import savgol_filter

# 一阶导数,窗口11点,多项式阶数2
deriv1 = savgol_filter(spectrum, window_length=11, polyorder=2, deriv=1)

我的经验:窗口大小很关键。窗口太小,噪声放大得厉害;窗口太大,峰形细节丢失。我一般从11开始试,看效果调。

二阶导数:分辨重叠峰

二阶导数更狠。它反映的是光谱的「弯曲程度」。两个重叠峰,在一阶导数里可能还糊在一起,到二阶导数里就分开了。

代码几乎一样,改个参数:

# 二阶导数
deriv2 = savgol_filter(spectrum, window_length=15, polyorder=3, deriv=2)

注意:二阶导数会把噪声放大得更厉害。我见过有人用5点窗口做二阶导,结果出来的图跟心电图似的,完全没法用。窗口至少15点起步。

我曾经在一个近红外项目中,用二阶导数把三个重叠的C-H伸缩峰分开了。当时客户看了直呼神奇——其实原理很简单,就是数学。

1.2 散射校正:让光谱「对齐」

散射是固体和液体光谱的噩梦。颗粒大小、装样松紧、温度变化……都会导致散射。同一个样品,测三次,三条线歪歪扭扭。

怎么办?两个主流方法:多元散射校正(MSC)标准正态变量变换(SNV)

多元散射校正(MSC)

MSC 的思路很直白:找一条「理想光谱」做参考,把其他光谱往它上面「掰」。

步骤:

  1. 计算所有光谱的平均值,作为参考光谱
  2. 对每条光谱,用最小二乘法拟合:x_i = a_i * x_ref + b_i
  3. 校正:x_i_corrected = (x_i - b_i) / a_i

代码实现:

def msc_correct(spectra):
    # spectra: 样本×波长 矩阵
    ref = np.mean(spectra, axis=0)
    corrected = np.zeros_like(spectra)
    for i in range(spectra.shape[0]):
        # 线性拟合
        coeff = np.polyfit(ref, spectra[i], deg=1)
        corrected[i] = (spectra[i] - coeff[1]) / coeff[0]
    return corrected

核心要点:MSC 假设散射是线性的。如果你的样本散射是非线性的(比如颗粒度差异极大),MSC 效果会打折扣。

标准正态变量变换(SNV)

SNV 更简单粗暴:对每条光谱,减去均值,除以标准差。

说白了,就是把每条光谱「标准化」到均值为0、方差为1。

def snv_correct(spectrum):
    return (spectrum - np.mean(spectrum)) / np.std(spectrum)

SNV 和 MSC 什么关系?我个人的理解是:SNV 是 MSC 的简化版。MSC 用参考光谱做基准,SNV 用自身统计量做基准。实际效果上,两者经常差不多。

我建议:先试 SNV,因为它快、无参数。如果效果不理想,再上 MSC。

1.3 数据增强:给模型「喂」更多样本

深度学习时代,数据增强是标配。光谱数据也一样——样本不够?造!

常用的三种方法:

方法 操作 适用场景
加噪 添加高斯噪声 模拟仪器波动
平移 沿波长轴左右移动 模拟波长偏移
缩放 乘以随机系数 模拟浓度/厚度变化

代码示例:

def augment_spectrum(spectrum, noise_level=0.01, shift_max=2, scale_range=(0.95, 1.05)):
    # 加噪
    noise = np.random.normal(0, noise_level, spectrum.shape)
    spec = spectrum + noise
    # 平移(整数点)
    shift = np.random.randint(-shift_max, shift_max+1)
    spec = np.roll(spec, shift)
    # 缩放
    scale = np.random.uniform(*scale_range)
    spec = spec * scale
    return spec

避坑指南:我曾经在一个项目中,把噪声加得太大,结果模型学到的全是噪声模式。记住:数据增强是锦上添花,不是雪中送炭。噪声水平控制在原始噪声的1/3以内比较安全。

1.4 预处理流程设计原则

好了,方法都讲完了。但怎么组合?先做哪个后做哪个?

我总结了一个通用流程:

原始光谱 → 去噪 → 基线校正 → 散射校正 → 导数 → 数据增强 → 建模

但这不是铁律。我画了一张图,帮你理清思路:

原始光谱 去噪 基线校正 散射校正 导数光谱 数据增强 建模 可选 可选 可选 散射严重? 光谱预处理流程设计图

几个原则,我这些年总结的:

  • 先粗后细:先做全局校正(去噪、基线),再做局部增强(导数)
  • 能不导就不导:导数会放大噪声,如果基线校正后峰已经能分开,就别用导数
  • 数据增强放最后:所有校正做完,再增强,避免增强后的数据被后续处理破坏
  • 验证集不动:数据增强只对训练集做,验证集和测试集保持原始状态

血的教训:我曾经把数据增强用在了整个数据集上,结果验证集和训练集高度相似,模型评估虚高。后来上线才发现问题,返工了一个月。记住:验证集是「考官」,不能提前泄露题目

好了,今天的内容就到这儿。预处理这块,说白了就是「把脏数据洗干净」。方法很多,但核心思路不变:保留化学信息,去除物理干扰。你想想看,光谱分析的本质不就是从物理信号里提取化学信息吗?

下一章我们聊特征选择,到时候见。


专注资料整理