3. OES信号预处理:去噪滤波、基线校正与归一化

大家好,我是老张。在半导体干法刻蚀这行摸爬滚打了十几年,今天咱们聊聊OES信号预处理。

说实话,很多刚入行的工程师拿到OES数据,直接就开始找特征波长、设阈值。结果呢?噪声太大,终点判断一塌糊涂。我见过太多这样的案例了。

OES信号从光谱仪出来,其实挺「脏」的。有高频噪声、有基线漂移、还有不同工艺腔室间的量级差异。你不处理它,它就给你颜色看。

所以,预处理是终点检测算法的第一步,也是最重要的一步。说白了,就是给信号「洗个澡」,让它干干净净地进入后续分析。

核心观点:预处理做得好,终点检测就成功了一半。我个人的经验是,花在预处理上的时间,至少占整个算法开发的40%。

OES信号预处理知识体系 OES信号预处理 去噪滤波 移动平均 小波去噪 中值滤波 基线校正 多项式拟合 非对称最小二乘 形态学方法 归一化 Min-Max归一化 Z-score标准化 面积归一化 目标:获得干净、稳定、可比的OES信号

3.1 去噪滤波:把信号里的「沙子」筛掉

OES信号里的噪声主要来自哪里?光电探测器的散粒噪声、射频干扰、还有等离子体本身的波动。这些噪声如果不处理,会让你误判终点。

移动平均滤波——最简单,也最常用。我习惯用窗口大小为5~15的滑动平均。窗口太小去噪效果差,窗口太大又会把真实的终点拐点给抹平了。

# 移动平均滤波示例
import numpy as np

def moving_average(signal, window_size=7):
    """移动平均去噪"""
    return np.convolve(signal, np.ones(window_size)/window_size, mode='same')

# 使用示例
raw_oes = np.random.randn(1000) + 5  # 模拟OES信号
filtered = moving_average(raw_oes, window_size=11)

我的经验:移动平均的窗口大小,我一般取工艺时间的1%~2%。比如刻蚀100秒,窗口就设1~2秒。这样既能去噪,又不会丢失终点特征。

小波去噪——这个就高级一些了。小波变换能把信号分解到不同尺度,然后只保留我们关心的频段。

我记得有一次,客户给的OES信号噪声特别大,移动平均根本搞不定。我试了小波去噪,用db4小波基,阈值设为软阈值,效果出奇的好。

# 小波去噪示例
import pywt

def wavelet_denoise(signal, wavelet='db4', level=3):
    """小波去噪"""
    coeffs = pywt.wavedec(signal, wavelet, level=level)
    # 软阈值处理
    sigma = np.median(np.abs(coeffs[-1])) / 0.6745
    threshold = sigma * np.sqrt(2 * np.log(len(signal)))
    coeffs_thresh = [coeffs[0]]  # 保留近似系数
    for i in range(1, len(coeffs)):
        coeffs_thresh.append(pywt.threshold(coeffs[i], threshold, mode='soft'))
    return pywt.waverec(coeffs_thresh, wavelet)

注意:小波去噪的参数调起来比较费劲。我曾经在一个项目中,光调小波基和阈值就花了两天。建议先从db4、sym8这些小波基开始试。

3.2 基线校正:把信号「扶正」

基线漂移是OES信号的老大难问题。腔室温度变化、窗口污染、气体流量波动,都会导致基线慢慢往上飘或往下掉。

你想想看,如果基线一直在漂,终点检测的阈值怎么设?设死了肯定不行。

多项式拟合校正——这是最直观的方法。先用多项式拟合出基线,然后从原始信号中减掉。

# 多项式基线校正
def polynomial_baseline(signal, degree=3):
    """多项式拟合基线校正"""
    x = np.arange(len(signal))
    coeffs = np.polyfit(x, signal, degree)
    baseline = np.polyval(coeffs, x)
    return signal - baseline

非对称最小二乘(AsLS)——这个我强烈推荐。它通过给正负残差不同的权重,能很好地拟合出基线,尤其适合OES这种有尖锐峰值的信号。

# 非对称最小二乘基线校正
def asls_baseline(signal, lam=1e5, p=0.01):
    """非对称最小二乘基线校正"""
    from scipy import sparse
    from scipy.sparse.linalg import spsolve
    
    L = len(signal)
    D = sparse.diags([1, -2, 1], [0, -1, -2], shape=(L, L-2))
    w = np.ones(L)
    
    for _ in range(10):
        W = sparse.spdiags(w, 0, L, L)
        Z = W + lam * D.dot(D.T)
        z = spsolve(Z, w * signal)
        w_new = p * (signal > z) + (1-p) * (signal < z)
        if np.linalg.norm(w_new - w) < 1e-3:
            break
        w = w_new
    
    return signal - z

避坑指南:我曾经在调试AsLS时,lambda参数设得太小,结果基线跟着信号峰一起跑了。后来我把lambda从1e3调到1e5,效果才正常。记住,lambda越大,基线越平滑。

3.3 归一化:让不同腔室的信号「说同一种语言」

归一化解决的是量纲问题。不同腔室、不同工艺条件下,OES信号的绝对强度可能差好几倍。但终点特征往往体现在相对变化上。

Min-Max归一化——把信号映射到[0,1]区间。简单粗暴,但容易受异常值影响。

# Min-Max归一化
def minmax_normalize(signal):
    """Min-Max归一化"""
    return (signal - np.min(signal)) / (np.max(signal) - np.min(signal))

Z-score标准化——减去均值,除以标准差。这个对异常值不那么敏感,我比较常用。

# Z-score标准化
def zscore_normalize(signal):
    """Z-score标准化"""
    return (signal - np.mean(signal)) / np.std(signal)

面积归一化——把整个光谱的面积归一化到1。这个在OES全谱分析中特别有用。

# 面积归一化
def area_normalize(signal):
    """面积归一化"""
    return signal / np.sum(np.abs(signal))

我的建议:如果你只关注单个特征波长,用Z-score就够了。如果你做全谱分析,面积归一化更合适。具体选哪个,取决于你的终点检测算法。

3.4 预处理流程总结

在实际项目中,我一般按这个顺序处理:

  1. 先去噪:用移动平均或小波,把高频噪声干掉
  2. 再校正基线:用AsLS或多项式拟合,把漂移拉回来
  3. 最后归一化:让不同批次的数据具有可比性

这个顺序不能乱。你想想看,如果先归一化再基线校正,基线漂移会被放大,反而更难处理。

预处理步骤 推荐方法 关键参数 我的评分
去噪滤波 移动平均 窗口大小:5~15 ⭐⭐⭐⭐
去噪滤波 小波去噪 小波基:db4,阈值:软阈值 ⭐⭐⭐⭐⭐
基线校正 AsLS lambda:1e5,p:0.01 ⭐⭐⭐⭐⭐
基线校正 多项式拟合 阶数:3~5 ⭐⭐⭐
归一化 Z-score ⭐⭐⭐⭐
归一化 面积归一化 ⭐⭐⭐⭐

好了,这一章的内容就到这里。预处理这块,说白了就是「对症下药」。你的信号有什么问题,就用什么方法。多试几次,找到最适合你工艺的那一套组合拳。

专注资料整理