光谱预处理(下):平滑去噪、基线校正与导数光谱

上一讲我们把光谱预处理的“上半场”走完了——异常值剔除、缺失值插补、标准化。但说实话,那些只是开胃菜。真正让光谱分析工程师头疼的,是今天要聊的三个硬骨头:平滑去噪、基线校正和导数光谱。

我入行第三年接过一个近红外项目,测的是谷物水分。原始光谱看着还行,一建模,R² 只有 0.6。折腾了两周,最后发现是 Savitzky-Golay 滤波的窗口参数设错了。嗯,从那以后,我对预处理参数再也不敢马虎了。

核心观点:预处理不是“按个按钮就完事”的步骤。参数选不对,模型直接报废。你想想看,一个错误的平滑窗口,可能把真实的吸收峰都给抹平了。

光谱预处理(下)知识体系 平滑去噪 基线校正 导数光谱 移动平均 Savitzky-Golay 多项式拟合 不对称最小二乘 一阶导 二阶导 ⚠️ 关键原则 平滑 → 基线校正 → 导数,顺序不能乱 先做平滑再做导数,否则噪声会被放大

1. 平滑去噪:把毛刺剃掉

原始光谱里总有高频噪声。这些噪声来自探测器、光源波动、温度漂移……说白了,就是仪器自己“手抖”了一下。平滑的目的,就是把这些毛刺去掉,保留真实的光谱形状。

1.1 移动平均

最简单的方法。取一个窗口,比如 5 个点,算平均值,然后窗口滑动。我刚开始做光谱时就用这个,因为代码三行就写完。

import numpy as np

def moving_average(spectrum, window_size=5):
    """移动平均平滑"""
    kernel = np.ones(window_size) / window_size
    return np.convolve(spectrum, kernel, mode='same')

我的经验:窗口大小选 3-7 比较安全。窗口太大,峰会被压扁;窗口太小,噪声还在。我一般先试 5,看效果再调。

但移动平均有个硬伤——它会把所有点一视同仁。峰顶和基线噪声用同一个权重,结果就是峰变矮了。说白了,它太“粗暴”了。

1.2 Savitzky-Golay 滤波

这才是专业选手的选择。SG 滤波的思路是:在窗口内用多项式拟合,然后用拟合值代替原始值。它比移动平均聪明的地方在于——它保留了峰的形状。

from scipy.signal import savgol_filter

# SG 滤波:窗口大小 11,多项式阶数 2
spectrum_sg = savgol_filter(spectrum_raw, window_length=11, polyorder=2)
方法 优点 缺点 我常用的场景
移动平均 简单、快 峰形失真 快速预览、高信噪比数据
SG 滤波 保峰形、可调性强 参数敏感、计算稍慢 定量分析、弱峰检测

⚠️ 避坑指南:SG 滤波的窗口长度必须大于多项式阶数+1,且窗口长度必须是奇数。我曾经设过窗口=5、阶数=4,结果报错不说,还浪费了半天排查。

2. 基线校正:把“地板”找平

光谱基线漂移是家常便饭。样品散射、仪器老化、温度变化,都会让基线歪歪扭扭。如果不校正,模型会把基线漂移当成样品信息,结果就是预测值跟着漂。

2.1 多项式拟合

思路很简单:在光谱上选几个“没有吸收”的点,用多项式拟合出一条基线,然后减掉。我早期做拉曼光谱时常用这个方法。

import numpy as np
from numpy.polynomial import polynomial as P

def poly_baseline(x, y, degree=3, baseline_points=None):
    """多项式基线校正"""
    # baseline_points 是手动选的基线点索引
    coeffs = P.polyfit(x[baseline_points], y[baseline_points], degree)
    baseline = P.polyval(x, coeffs)
    return y - baseline

关键:基线点的选择决定了校正质量。选在吸收峰之间的“空白区”,别选在峰上。我见过有人把峰顶当基线点,结果校正完峰变成负的了。

2.2 不对称最小二乘(AsLS)

多项式拟合需要手动选点,太累了。AsLS 可以自动估计基线。它的核心思想是:给正残差和负残差不同的权重。峰是正的,基线是负的,算法会自动区分。

from scipy import sparse
from scipy.sparse.linalg import spsolve

def asls(y, lam=1e5, p=0.01):
    """不对称最小二乘基线校正"""
    L = len(y)
    D = sparse.diags([1, -2, 1], [0, -1, -2], shape=(L-2, L))
    w = np.ones(L)
    for _ in range(10):
        W = sparse.spdiags(w, 0, L, L)
        Z = W + lam * D.T @ D
        z = spsolve(Z, w * y)
        w = p * (y > z) + (1 - p) * (y <= z)
    return z
参数 含义 推荐范围 调参方向
lam 平滑度 1e3 ~ 1e7 越大基线越平滑
p 不对称权重 0.001 ~ 0.1 越小越“相信”负残差

我的习惯:先设 lam=1e5、p=0.01,看基线效果。如果基线穿过了峰,就增大 lam;如果基线太“硬”跟不上漂移,就减小 lam。调两三次基本就能找到合适的值。

3. 导数光谱:放大细节

导数光谱不是“锦上添花”,而是“雪中送炭”。当两个峰靠得太近、或者基线漂移严重时,导数能帮你把重叠峰分开,把基线漂移变成常数。

3.1 一阶导

一阶导数的物理意义是光谱的变化率。基线漂移如果是线性的,一阶导之后变成常数,直接消除。重叠峰在一阶导里会出现“过零点”,可以用来定位峰位。

# 一阶导:直接用 SG 滤波的导数模式
first_deriv = savgol_filter(spectrum, window_length=11, polyorder=2, deriv=1)

3.2 二阶导

二阶导更敏感。它能消除线性基线漂移(变成0),还能把重叠峰“劈开”。但代价是——噪声也被放大了。你想想看,导数本质上是对噪声求差分,噪声频率越高,放大越明显。

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

⚠️ 重要提醒:做导数之前,必须先做平滑。我见过有人直接对原始光谱求二阶导,结果出来的图全是毛刺,根本没法用。顺序是:平滑 → 基线校正 → 导数,一步都不能省。

4. 实战流程:一个完整的预处理管线

说了这么多,不如直接看一个完整的流程。这是我个人习惯的管线顺序:

  1. 原始光谱 → 检查异常值(上一章内容)
  2. SG 平滑 → 窗口=11,阶数=2
  3. AsLS 基线校正 → lam=1e5,p=0.01
  4. 一阶导 → 窗口=11,阶数=2,deriv=1
  5. 标准化 → 可选,看模型需求
def preprocessing_pipeline(spectrum):
    # 1. SG 平滑
    smoothed = savgol_filter(spectrum, 11, 2)
    # 2. AsLS 基线校正
    baseline = asls(smoothed, lam=1e5, p=0.01)
    corrected = smoothed - baseline
    # 3. 一阶导
    deriv = savgol_filter(corrected, 11, 2, deriv=1)
    return deriv

最后说一句:预处理没有“万能参数”。每个项目、每种样品、每台仪器,都需要你亲手调参。我的建议是:先跑一遍默认参数,看效果,然后针对问题点微调。别想着一步到位,光谱分析本来就是“试错—改进—再试”的过程。