光谱预处理(下):平滑去噪、基线校正与导数光谱
上一讲我们把光谱预处理的“上半场”走完了——异常值剔除、缺失值插补、标准化。但说实话,那些只是开胃菜。真正让光谱分析工程师头疼的,是今天要聊的三个硬骨头:平滑去噪、基线校正和导数光谱。
我入行第三年接过一个近红外项目,测的是谷物水分。原始光谱看着还行,一建模,R² 只有 0.6。折腾了两周,最后发现是 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. 实战流程:一个完整的预处理管线
说了这么多,不如直接看一个完整的流程。这是我个人习惯的管线顺序:
- 原始光谱 → 检查异常值(上一章内容)
- SG 平滑 → 窗口=11,阶数=2
- AsLS 基线校正 → lam=1e5,p=0.01
- 一阶导 → 窗口=11,阶数=2,deriv=1
- 标准化 → 可选,看模型需求
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
最后说一句:预处理没有“万能参数”。每个项目、每种样品、每台仪器,都需要你亲手调参。我的建议是:先跑一遍默认参数,看效果,然后针对问题点微调。别想着一步到位,光谱分析本来就是“试错—改进—再试”的过程。