第4章:光谱预处理实战:去噪、基线校正与归一化

各位好,我是老张。在光谱分析这个行当摸爬滚打了十几年,我越来越觉得一个道理——光谱分析,三分靠仪器,七分靠预处理。你想想看,原始光谱里藏着多少干扰?噪声、基线漂移、样品厚度不一致……这些乱七八糟的东西不处理干净,后面的模型做得再花哨也是白搭。

今天这一章,咱们就聊聊光谱预处理的几个核心操作。我尽量把每个方法的适用场景、坑点、还有我个人的小习惯都交代清楚。嗯,咱们开始吧。

光谱预处理知识体系 原始光谱 去噪 基线校正 归一化 平滑滤波 移动平均、Savitzky-Golay 小波变换去噪 多项式拟合 不对称最小二乘(ALS) 自适应迭代重加权(airPLS) 面积归一 / 矢量归一 标准正态变量变换(SNV) 多元散射校正(MSC) 干净光谱

4.1 去噪:把毛刺剃干净

光谱噪声这东西,说白了就是仪器给你加的「佐料」。你想想看,光电探测器有暗电流、电路有热噪声、光源有波动……这些都会叠加到真实信号上。我见过不少新手,拿到光谱一看有毛刺就急着建模,结果模型泛化能力一塌糊涂。

4.1.1 平滑滤波

平滑是最直观的去噪方法。我个人最常用的是 Savitzky-Golay 卷积平滑。它不像移动平均那样简单粗暴地取均值,而是在一个滑动窗口内做多项式最小二乘拟合。说白了,它既去噪又保形。

核心参数:

  • 窗口宽度:奇数,通常取 5~21 点。窗口越大,去噪越强,但峰形越容易变矮变宽。
  • 多项式阶数:通常 2~3 阶。阶数太高容易过拟合,把噪声也拟合进去。
# Python 示例:Savitzky-Golay 平滑
from scipy.signal import savgol_filter

# 假设 spectrum 是一维数组
spectrum_smoothed = savgol_filter(spectrum, window_length=11, polyorder=2)

我的小习惯:对于近红外光谱,我一般先用 11 点 2 阶试试。如果峰形明显变钝,就减小窗口到 7 点。记住,宁可去噪不足,也不要过度平滑把特征峰磨平了。

4.1.2 小波变换去噪

小波变换去噪,我觉得是「手术刀」级别的工具。它能把信号分解成不同频率的成分,然后只对高频噪声部分下手。我在处理拉曼光谱时特别喜欢用这个方法,因为拉曼峰通常很尖锐,平滑滤波容易伤到峰形,小波变换就好很多。

具体做法分三步:

  1. 分解:选择小波基(如 db4、sym8),做多层分解。
  2. 阈值处理:对高频细节系数做软阈值或硬阈值处理。
  3. 重构:用处理后的系数重建信号。
# Python 示例:小波去噪
import pywt

def wavelet_denoise(data, wavelet='db4', level=4):
    coeffs = pywt.wavedec(data, wavelet, level=level)
    # 估计噪声标准差
    sigma = np.median(np.abs(coeffs[-1])) / 0.6745
    threshold = sigma * np.sqrt(2 * np.log(len(data)))
    # 软阈值处理
    coeffs_thresh = [coeffs[0]] + [pywt.threshold(c, threshold, mode='soft') for c in coeffs[1:]]
    return pywt.waverec(coeffs_thresh, wavelet)

我曾经踩过的坑:有一次处理一批近红外光谱,我用了 db1(Haar)小波,结果重构出来的光谱出现了阶梯状伪影。后来换成 db4 就好了。所以小波基的选择很重要,别偷懒用默认值。

4.2 基线校正:把「山坡」铲平

基线漂移是光谱分析里最让人头疼的问题之一。你想想看,样品散射、仪器状态变化、温度波动……都会让光谱基线像山坡一样起伏。如果不校正,后续的定量分析误差会非常大。

4.2.1 多项式拟合基线校正

这个方法很直观:在光谱上选一些「没有吸收峰」的点,用多项式拟合出一条基线,然后从原始光谱中减去。我早期做近红外分析时,这个方法用了好几年。

操作要点:

  • 选点要避开吸收峰区域,最好选在峰谷或平坦区。
  • 多项式阶数通常 2~4 阶。阶数太高容易过拟合,把真实峰也拟合进去。
  • 如果基线形状复杂,可以分段拟合。
# Python 示例:多项式拟合基线校正
import numpy as np

# 假设 x 是波数,y 是吸光度
# 手动选择基线点(峰谷位置)
baseline_idx = [10, 50, 100, 200, 350, 500]
coeffs = np.polyfit(x[baseline_idx], y[baseline_idx], deg=3)
baseline = np.polyval(coeffs, x)
y_corrected = y - baseline

我的经验:手动选点虽然麻烦,但对于基线形状稳定的场景,它比自动方法更可靠。我一般会写一个交互式脚本,用鼠标点选基线点,这样效率高很多。

4.2.2 不对称最小二乘(ALS)

ALS 是我现在最常用的基线校正方法。它的核心思想是:给正残差和负残差不同的权重。因为基线通常在光谱下方,所以对负残差(光谱低于基线)给大权重,对正残差(光谱高于基线)给小权重。这样拟合出来的基线会「贴」在光谱的下包络线上。

ALS 有两个关键参数:

  • λ(lambda):平滑度参数。λ 越大,基线越平滑。通常取 10^5 ~ 10^7。
  • p:不对称参数。通常取 0.001 ~ 0.1。p 越小,基线越贴近下包络。
# Python 示例:ALS 基线校正
def als_baseline(y, lam=1e6, p=0.01, niter=10):
    L = len(y)
    D = np.diff(np.eye(L), 2)
    w = np.ones(L)
    for _ in range(niter):
        W = np.diag(w)
        Z = np.linalg.solve(W + lam * D.T @ D, w * y)
        w = p * (y > Z) + (1 - p) * (y < Z)
    return Z

baseline = als_baseline(spectrum)
spectrum_corrected = spectrum - baseline

我曾经踩过的坑:有一次 λ 设得太小(10^4),结果基线跟着噪声一起抖动,校正后的光谱反而多了很多假峰。后来我把 λ 调到 10^6,效果就好多了。记住,λ 宁大勿小。

4.3 归一化:让光谱「站在同一起跑线」

归一化解决的是样品厚度、浓度、光程差异带来的问题。你想想看,同一个样品,测两次因为厚度不同,吸光度可能差一倍。归一化就是把这些差异抹平。

4.3.1 面积归一化

把光谱曲线下的总面积归一化到 1。说白了,就是每个点的吸光度除以总面积。这个方法简单粗暴,适合峰形相似但整体强度不同的情况。

# 面积归一化
area = np.trapz(spectrum, x)  # 梯形积分求面积
spectrum_norm = spectrum / area

4.3.2 矢量归一化

也叫 L2 归一化。把光谱看作一个向量,然后除以它的模长。这样处理后,光谱的「长度」变成了 1,但方向(即峰形比例)保持不变。

# 矢量归一化
norm = np.linalg.norm(spectrum)
spectrum_norm = spectrum / norm

我的选择:对于近红外光谱,我更喜欢矢量归一化。因为面积归一化对基线漂移敏感,而矢量归一化相对鲁棒一些。当然,具体用哪个,还是要看你的数据特点。

4.4 标准正态变量变换(SNV)

SNV 是我个人非常喜欢的一个预处理方法。它本质上是对每条光谱做「标准化」——减去均值,除以标准差。这样处理后,每条光谱的均值为 0,标准差为 1。

SNV 特别适合消除由样品颗粒大小、散射效应引起的基线偏移和斜率变化。我在处理粉末样品的近红外光谱时,几乎必用 SNV。

# SNV 变换
mean = np.mean(spectrum)
std = np.std(spectrum)
spectrum_snv = (spectrum - mean) / std

SNV vs. 其他方法:

方法 消除基线偏移 消除斜率变化 消除散射效应 保留峰形信息
SNV ✅ 强 ✅ 强 ✅ 强 ✅ 好
面积归一 ❌ 弱 ❌ 弱 ❌ 弱 ✅ 好
矢量归一 ✅ 中 ❌ 弱 ✅ 中 ✅ 好
多项式基线校正 ✅ 强 ✅ 强 ❌ 弱 ⚠️ 可能变形

我曾经踩过的坑:有一次我把 SNV 用在了已经做过基线校正的光谱上,结果发现效果反而变差了。后来才意识到,SNV 本身就有一定的基线校正能力,重复使用反而会引入新的误差。所以,预处理流程不是越复杂越好,要懂得「适可而止」。

好了,这一章的内容就到这里。光谱预处理没有银弹,每种方法都有自己的脾气。我的建议是:多试几种组合,用交叉验证的结果来评判好坏。毕竟,实践才是检验真理的唯一标准。


公众号:蓝海资料掘金营,微信deep3321